Add MoveCard action (#1375)

Signed-off-by: Dvinyanin Alexandr <dvinyanin.alexandr@gmail.com>
This commit is contained in:
Alex 2022-04-18 14:44:15 +07:00 committed by GitHub
parent 5cd7771257
commit f6f59f04f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 293 additions and 7 deletions

View File

@ -206,6 +206,10 @@ export function createModel (builder: Builder): void {
presenter: board.component.CardPresenter
})
builder.mixin(board.class.Board, core.class.Class, view.mixin.AttributePresenter, {
presenter: board.component.BoardPresenter
})
builder.createDoc(
task.class.KanbanTemplateSpace,
core.space.Model,

View File

@ -56,15 +56,20 @@
"AddACard": "Add a card",
"AddCard": "Add card",
"CardTitlePlaceholder": "Enter a title for this card...",
"MoveCard": "Move card",
"SelectDestination": "Select destination",
"Create": "Create",
"CreateDescription": "If you want, we can create a card for every new line ({number}). You can also create one card with a long title.",
"CreateSingle": "Just one card",
"CreateMultiple": "Create {number} cards",
"Cancel": "Cancel",
"List": "List",
"Position": "Position",
"Current": "{label} (current)",
"StartDate": "Start date",
"DueDate": "Due date",
"Save": "Save",
"Remove": "Remove",
"Cancel": "Cancel",
"NullDate": "M/D/YYYY"
}
}

View File

@ -60,11 +60,16 @@
"CreateDescription": "Можно создать отдельные карточки для каждой строки ({number}) или одну с длинным названием.",
"CreateSingle": "Создать одну",
"CreateMultiple": "Создать несколько ({number})",
"MoveCard": "Переместить карточку",
"SelectDestination": "Выберите список",
"Cancel": "Отмена",
"List": "Список",
"Position": "Позиция",
"Current": "{label} (текущий)",
"StartDate": "Начало",
"DueDate": "Срок",
"Save": "Сохранить",
"Remove": "Удалить",
"Cancel": "Закрыть",
"NullDate": "М/Д/ГГГГ"
}
}

View File

@ -0,0 +1,37 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Board } from '@anticrm/board'
import { getPanelURI, Icon } from '@anticrm/ui'
import view from '@anticrm/view'
import board from '../plugin'
export let value: Board
export let inline: boolean = false
</script>
{#if value}
<a
class="flex-presenter"
class:inline-presenter={inline}
href="#{getPanelURI(view.component.EditDoc, value._id, value._class, 'full')}"
>
<div class="icon">
<Icon icon={board.icon.Board} size={'small'} />
</div>
<span class="label">{value.name}</span>
</a>
{/if}

View File

@ -0,0 +1,100 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Label, Button, Status as StatusControl } from '@anticrm/ui'
import { getClient } from '@anticrm/presentation'
import { Class, Client, Doc, DocumentUpdate, Ref } from '@anticrm/core'
import { getResource, OK, Resource, Status } from '@anticrm/platform'
import { Card } from '@anticrm/board';
import view from '@anticrm/view'
import board from '../../plugin'
import SpaceSelect from '../selectors/SpaceSelect.svelte'
import StateSelect from '../selectors/StateSelect.svelte'
import RankSelect from '../selectors/RankSelect.svelte'
export let object: Card
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
let status: Status = OK
const selected = {
space: object.space,
state: object.state,
rank: object.rank
}
function move () {
const update: DocumentUpdate<Card> = {}
if (selected.space !== object.space) update.space = selected.space
if (selected.state !== object.state) update.state = selected.state
if (selected.rank !== object.rank) update.rank = selected.rank
client.update(object, update)
dispatch('close')
}
async function invokeValidate (
action: Resource<<T extends Doc>(doc: T, client: Client) => Promise<Status>>
): Promise<Status> {
const impl = await getResource(action)
return await impl(object, client)
}
async function validate (doc: Doc, _class: Ref<Class<Doc>>): Promise<void> {
const clazz = hierarchy.getClass(_class)
const validatorMixin = hierarchy.as(clazz, view.mixin.ObjectValidator)
if (validatorMixin?.validator != null) {
status = await invokeValidate(validatorMixin.validator)
} else if (clazz.extends != null) {
await validate(doc, clazz.extends)
} else {
status = OK
}
}
$: validate({...object, ...selected}, object._class)
</script>
<div class="antiPopup antiPopup-withHeader antiPopup-withTitle antiPopup-withCategory w-85">
<div class="ap-space"/>
<div class="fs-title ap-header flex-row-center">
<Label label={board.string.MoveCard}/>
</div>
<div class="ap-space bottom-divider"/>
<StatusControl {status} />
<div class="ap-title">
<Label label={board.string.SelectDestination} />
</div>
<div class="ap-category">
<div class="categoryItem w-full border-radius-2 p-2 background-button-bg-enabled">
<SpaceSelect label={board.string.Board} {object} bind:selected={selected.space} />
</div>
</div>
<div class="ap-category flex-gap-3">
<div class="categoryItem w-full border-radius-2 p-2 background-button-bg-enabled">
{#key selected.space}
<StateSelect label={board.string.List} {object} space={selected.space} bind:selected={selected.state} />
{/key}
</div>
<div class="categoryItem w-full border-radius-2 p-2 background-button-bg-enabled">
{#key selected.state}
<RankSelect label={board.string.Position} {object} state={selected.state} bind:selected={selected.rank} />
{/key}
</div>
</div>
<div class="ap-footer">
<Button
size={'small'}
label={board.string.Cancel}
on:click={() => {
dispatch('close')
}}
/>
<Button
label={board.string.Move}
size={'small'}
disabled={status !== OK || object.state === selected.state && object.rank === selected.rank}
kind={'primary'}
on:click={move}
/>
</div>
</div>

View File

@ -0,0 +1,41 @@
<script lang="ts">
import { IntlString, translate } from '@anticrm/platform'
import { createQuery } from '@anticrm/presentation';
import { DropdownLabels } from '@anticrm/ui'
import { Ref, SortingOrder } from '@anticrm/core'
import { DropdownTextItem } from '@anticrm/ui/src/types';
import { calcRank, State } from '@anticrm/task';
import { Card } from '@anticrm/board'
import board from '../../plugin'
export let object: Card
export let label: IntlString
export let state: Ref<State>
export let selected: string
let ranks: DropdownTextItem[] = []
const tasksQuery = createQuery()
tasksQuery.query(
board.class.Card, { state },
async result => {
[ranks] = [...result.filter(t => t._id !== object._id), undefined]
.reduce<[DropdownTextItem[], Card | undefined]>(
([arr, prev], next) => [[...arr, {id: calcRank(prev, next), label:`${arr.length+1}`}], next],
[[], undefined]
);
[{ id: selected = object.rank }] = ranks.slice(-1)
if (object.state === state){
const index = result.findIndex(t => t._id === object._id)
ranks[index] = {
id: object.rank,
label: await translate(board.string.Current, {label: ranks[index].label})
}
selected = object.rank
}
},
{ sort: { rank: SortingOrder.Ascending } }
)
</script>
<DropdownLabels items={ranks} label={label} bind:selected={selected}/>

View File

@ -0,0 +1,26 @@
<script lang="ts">
import { IntlString, translate } from '@anticrm/platform'
import { createQuery } from '@anticrm/presentation';
import { DropdownLabels } from '@anticrm/ui'
import { Ref } from '@anticrm/core'
import { DropdownTextItem } from '@anticrm/ui/src/types';
import { Board, Card } from '@anticrm/board'
import board from '../../plugin'
export let object: Card
export let label: IntlString
export let selected: Ref<Board>
let spaces: DropdownTextItem[] = []
const spacesQuery = createQuery()
spacesQuery.query(
board.class.Board, {},
async result => {
spaces = result.map(({_id, name}) => ({id: _id, label: name}))
const index = spaces.findIndex(({id}) => id === object.space)
spaces[index].label = await translate(board.string.Current, {label: spaces[index].label})
}
)
</script>
<DropdownLabels items={spaces} label={label} bind:selected/>

View File

@ -0,0 +1,34 @@
<script lang="ts">
import { IntlString, translate } from '@anticrm/platform'
import { createQuery } from '@anticrm/presentation';
import { DropdownLabels } from '@anticrm/ui'
import { Ref, SortingOrder, Space } from '@anticrm/core'
import { DropdownTextItem } from '@anticrm/ui/src/types';
import task, { State } from '@anticrm/task';
import { Card } from '@anticrm/board'
import board from '../../plugin'
export let object: Card
export let label: IntlString
export let selected: Ref<State>
export let space: Ref<Space>
let states: DropdownTextItem[] = []
const statesQuery = createQuery()
statesQuery.query(
task.class.State, { space },
async result => {
if(!result) return
states = result.map(({_id, title }) => ({id: _id, label: title}));
[{ _id: selected }] = result
if (object.space === space) {
const index = states.findIndex(({id}) => id === object.state)
states[index].label = await translate(board.string.Current, {label: states[index].label})
selected = object.state
}
},
{ sort: { rank: SortingOrder.Ascending } }
)
</script>
<DropdownLabels items={states} label={label} bind:selected/>

View File

@ -14,19 +14,25 @@
// limitations under the License.
//
import { Resources } from '@anticrm/platform'
import { showPopup } from '@anticrm/ui'
import { Card } from '@anticrm/board'
import { Resources } from '@anticrm/platform'
import CardPresenter from './components/CardPresenter.svelte'
import BoardPresenter from './components/BoardPresenter.svelte'
import CreateBoard from './components/CreateBoard.svelte'
import CreateCard from './components/CreateCard.svelte'
import EditCard from './components/EditCard.svelte'
import KanbanCard from './components/KanbanCard.svelte'
import TemplatesIcon from './components/TemplatesIcon.svelte'
import KanbanView from './components/KanbanView.svelte'
import MoveView from './components/popups/MoveCard.svelte'
import DateRangePicker from './components/popups/DateRangePicker.svelte'
import { addCurrentUser, canAddCurrentUser, isArchived, isUnarchived } from './utils/CardUtils'
async function showMoveCardPopup (object: Card): Promise<void> {
showPopup(MoveView, { object })
}
async function showDatePickerPopup (object: Card): Promise<void> {
showPopup(DateRangePicker, { object })
}
@ -38,10 +44,12 @@ export default async (): Promise<Resources> => ({
KanbanCard,
CardPresenter,
TemplatesIcon,
KanbanView
KanbanView,
BoardPresenter
},
cardActionHandler: {
Join: addCurrentUser,
Move: showMoveCardPopup,
Dates: showDatePickerPopup
},
cardActionSupportedHandler: {

View File

@ -81,16 +81,22 @@ export default mergeIds(boardId, board, {
CreateDescription: '' as IntlString,
CreateSingle: '' as IntlString,
CreateMultiple: '' as IntlString,
MoveCard: '' as IntlString,
Cancel: '' as IntlString,
SelectDestination: '' as IntlString,
List: '' as IntlString,
Position: '' as IntlString,
Current: '' as IntlString,
StartDate: '' as IntlString,
DueDate: '' as IntlString,
Save: '' as IntlString,
Remove: '' as IntlString,
Cancel: '' as IntlString,
NullDate: '' as IntlString
},
component: {
CreateCustomer: '' as AnyComponent,
CardsPresenter: '' as AnyComponent,
BoardPresenter: '' as AnyComponent,
Boards: '' as AnyComponent,
EditCard: '' as AnyComponent,
Members: '' as AnyComponent,

View File

@ -337,6 +337,25 @@ class TServerStorage implements ServerStorage {
return result
}
async processMove (ctx: MeasureContext, tx: Tx): Promise<Tx[]> {
const actualTx = this.extractTx(tx)
if (!this.hierarchy.isDerived(actualTx._class, core.class.TxUpdateDoc)) return []
const rtx = actualTx as TxUpdateDoc<Doc>
if (rtx.operations.space === undefined || rtx.operations.space === rtx.objectSpace) return []
const result: Tx[] = []
const factory = new TxFactory(core.account.System)
for (const [, attribute] of this.hierarchy.getAllAttributes(rtx.objectClass)) {
if (!this.hierarchy.isDerived(attribute.type._class, core.class.Collection)) continue
const collection = attribute.type as Collection<AttachedDoc>
const allAttached = await this.findAll(ctx, collection.of, { attachedTo: rtx.objectId, space: rtx.objectSpace })
const allTx = allAttached.map(({ _class, space, _id }) =>
factory.createTxUpdateDoc(_class, space, _id, { space: rtx.operations.space })
)
result.push(...allTx)
}
return result
}
async tx (ctx: MeasureContext, tx: Tx): Promise<[TxResult, Tx[]]> {
// store tx
const _class = txClass(tx)
@ -367,6 +386,7 @@ class TServerStorage implements ServerStorage {
derived = [
...(await ctx.with('process-collection', { _class }, () => this.processCollection(ctx, tx))),
...(await ctx.with('process-remove', { _class }, () => this.processRemove(ctx, tx))),
...(await ctx.with('process-move', { _class }, () => this.processMove(ctx, tx))),
...(await ctx.with('process-triggers', {}, (ctx) =>
this.triggers.apply(tx.modifiedBy, tx, {
fx: triggerFx.fx,