mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 19:11:33 +03:00
Add MoveCard action (#1375)
Signed-off-by: Dvinyanin Alexandr <dvinyanin.alexandr@gmail.com>
This commit is contained in:
parent
5cd7771257
commit
f6f59f04f5
@ -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,
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,11 +60,16 @@
|
||||
"CreateDescription": "Можно создать отдельные карточки для каждой строки ({number}) или одну с длинным названием.",
|
||||
"CreateSingle": "Создать одну",
|
||||
"CreateMultiple": "Создать несколько ({number})",
|
||||
"MoveCard": "Переместить карточку",
|
||||
"SelectDestination": "Выберите список",
|
||||
"Cancel": "Отмена",
|
||||
"List": "Список",
|
||||
"Position": "Позиция",
|
||||
"Current": "{label} (текущий)",
|
||||
"StartDate": "Начало",
|
||||
"DueDate": "Срок",
|
||||
"Save": "Сохранить",
|
||||
"Remove": "Удалить",
|
||||
"Cancel": "Закрыть",
|
||||
"NullDate": "М/Д/ГГГГ"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
37
plugins/board-resources/src/components/BoardPresenter.svelte
Normal file
37
plugins/board-resources/src/components/BoardPresenter.svelte
Normal 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}
|
100
plugins/board-resources/src/components/popups/MoveCard.svelte
Normal file
100
plugins/board-resources/src/components/popups/MoveCard.svelte
Normal 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>
|
@ -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}/>
|
@ -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/>
|
@ -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/>
|
@ -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: {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user