From ea6d9d1517590f4f0d74c83fc79657684d450e67 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Thu, 21 Apr 2022 14:40:15 +0700 Subject: [PATCH] Kanban keyboard shortcuts (#1469) Signed-off-by: Andrey Sobolev --- packages/kanban/src/components/Kanban.svelte | 216 ++++++++++++++---- .../kanban/src/components/KanbanPanel.svelte | 78 ------- packages/theme/styles/_colors.scss | 2 + .../src/components/KanbanView.svelte | 58 ++++- .../src/components/KanbanCard.svelte | 1 - .../src/components/kanban/KanbanView.svelte | 108 +++++++-- .../src/components/issues/Board.svelte | 71 +++++- plugins/view-resources/src/actionImpl.ts | 7 +- .../src/components/ActionHandler.svelte | 25 +- .../src/components/Table.svelte | 3 +- .../src/components/TableBrowser.svelte | 6 + plugins/view-resources/src/selection.ts | 1 + plugins/view-resources/src/utils.ts | 4 +- tests/install-elastic-plugin.sh | 3 +- 14 files changed, 420 insertions(+), 163 deletions(-) delete mode 100644 packages/kanban/src/components/KanbanPanel.svelte diff --git a/packages/kanban/src/components/Kanban.svelte b/packages/kanban/src/components/Kanban.svelte index 2a40506a48..3496a62359 100644 --- a/packages/kanban/src/components/Kanban.svelte +++ b/packages/kanban/src/components/Kanban.svelte @@ -16,15 +16,15 @@ import core, { AttachedDoc, Class, Doc, DocumentQuery, DocumentUpdate, FindOptions, Ref, Space } from '@anticrm/core' import { createQuery, getClient } from '@anticrm/presentation' import { getPlatformColor, ScrollBox } from '@anticrm/ui' + import { createEventDispatcher, tick } from 'svelte' import { slide } from 'svelte/transition' import { DocWithRank } from '../types' import { calcRank } from '../utils' - import KanbanPanel from './KanbanPanel.svelte' type StateType = any type Item = DocWithRank & { state: StateType; doneState: StateType | null } type TypeState = { _id: StateType; title: string; color: number } - type ExtItem = { prev?: Item; it: Item; next?: Item } + type ExtItem = { prev?: Item; it: Item; next?: Item, pos: number } type CardDragEvent = DragEvent & { currentTarget: EventTarget & HTMLDivElement } export let _class: Ref> @@ -35,6 +35,10 @@ export let query: DocumentQuery = {} export let fieldName: string export let rankFieldName: string + export let selection: number | undefined = undefined + export let checked: Doc[] = [] + + const dispatch = createEventDispatcher() let objects: Item[] = [] @@ -54,6 +58,8 @@ } ) + $: dispatch('content', objects) + function getStateObjects ( objects: Item[], state: TypeState, @@ -61,7 +67,7 @@ ): ExtItem[] { const stateCards = objects.filter((it) => (it as any)[fieldName] === state._id) stateCards.sort((a, b) => (a as any)[rankFieldName]?.localeCompare((b as any)[rankFieldName])) - return stateCards.map((it, idx, arr) => ({ it, prev: arr[idx - 1], next: arr[idx + 1] })) + return stateCards.map((it, idx, arr) => ({ it, prev: arr[idx - 1], next: arr[idx + 1], pos: objects.findIndex(pi => pi._id === it._id) })) } async function updateItem (item: Item, update: DocumentUpdate) { @@ -162,6 +168,7 @@ dragCardInitialRank = (object.it as any)[rankFieldName] dragCard = object.it isDragging = true + dispatch('obj-focus', object.it) } // eslint-disable-next-line let dragged: boolean = false @@ -172,54 +179,143 @@ // eslint-disable-next-line no-unused-vars let stateObjects: ExtItem[] + + const stateRefs: HTMLElement[] = [] + + $: stateRefs.length = states.length + + function scrollInto (statePos: number): void { + stateRefs[statePos].scrollIntoView({ behavior: 'auto', block: 'nearest' }) + } + + export function selectStatePosition (pos: number, direction: 'up' | 'down' | 'left' | 'right'): void { + const obj = objects[pos] + if (obj === undefined) { + return + } + const fState = (obj as any)[fieldName] + let objState = states.findIndex((it) => it._id === fState) + if (objState === -1) { + return + } + const stateObjs = getStateObjects(objects, states[objState]) + const statePos = stateObjs.findIndex((it) => it.it._id === obj._id) + if (statePos === undefined) { + return + } + switch (direction) { + case 'up': + scrollInto(objState) + dispatch('obj-focus', (stateObjs[statePos - 1] ?? stateObjs[0]).it) + break + case 'down': + scrollInto(objState) + dispatch('obj-focus', (stateObjs[statePos + 1] ?? stateObjs[stateObjs.length - 1]).it) + break + case 'left': + while (objState > 0) { + objState-- + const nstateObjs = getStateObjects(objects, states[objState]) + if (nstateObjs.length > 0) { + scrollInto(objState) + dispatch('obj-focus', (nstateObjs[statePos] ?? nstateObjs[nstateObjs.length - 1]).it) + break + } + } + break + case 'right': + while (objState < states.length - 1) { + objState++ + const nstateObjs = getStateObjects(objects, states[objState]) + if (nstateObjs.length > 0) { + scrollInto(objState) + dispatch('obj-focus', (nstateObjs[statePos] ?? nstateObjs[nstateObjs.length - 1]).it) + break + } + } + break + } + } + + $: checkedSet = new Set>(checked.map(it => it._id)) + + export function check (docs: Doc[], value: boolean) { + dispatch('check', { docs, value }) + } + const showMenu = async (evt: MouseEvent, object: ExtItem): Promise => { + selection = object.pos + if (!checkedSet.has(object.it._id)) { + check(objects, false) + checked = [] + } + dispatch('contextmenu', { evt: evt, objects: checked.length > 0 ? checked : object.it }) + }
- {#each states as state} + {#each states as state, si} {@const stateObjects = getStateObjects(objects, state, dragCard)} - panelDragOver(event, state)} on:drop={() => { move(state._id) isDragging = false }} - customHeader={$$slots.header !== undefined} - > - - - - - {#each stateObjects as object} - {@const dragged = isDragging && object.it._id === dragCard?._id} -
cardDragOver(evt, object)} - on:drop|preventDefault={(evt) => cardDrop(evt, object)} - > -
onDragStart(object, state)} - on:dragend={() => { - isDragging = false - }} - > - + > + {#if $$slots.header !== undefined} + + {:else} +
+
+
+
+ {state.title} +
- {/each} - - + {/if} +
+ + + {#each stateObjects as object} + {@const dragged = isDragging && object.it._id === dragCard?._id} +
cardDragOver(evt, object)} + on:drop|preventDefault={(evt) => cardDrop(evt, object)} + > +
dispatch('obj-focus', object.it)} + on:focus={() => {}} + on:contextmenu={(evt) => showMenu(evt, object)} + draggable={true} + class:draggable={true} + on:dragstart + on:dragend + class:dragged + on:dragstart={() => onDragStart(object, state)} + on:dragend={() => { + isDragging = false + }} + > + +
+
+ {/each} + +
+
+
{/each}
@@ -247,12 +343,18 @@ } .card-container { background-color: var(--board-card-bg-color); - border-radius: .25rem; + border-radius: 0.25rem; user-select: none; - &:hover { - background-color: var(--board-card-bg-hover); + &.checked { + background-color: var(--theme-bg-checked); } + + &.selection { + background-color: var(--theme-bg-checked-hover); + } + + &.draggable { cursor: grab; } @@ -260,4 +362,38 @@ background-color: var(--board-bg-color); } } + .panel-container { + display: flex; + flex-direction: column; + align-items: stretch; + width: 20rem; + height: 100%; + background-color: transparent; + border: 1px solid transparent; + border-radius: 0.25rem; + + .header { + display: flex; + flex-direction: column; + height: 4rem; + min-height: 4rem; + user-select: none; + + .bar { + height: 0.375rem; + border-radius: 0.25rem; + } + .label { + padding: 0 0.5rem 0 1rem; + height: 100%; + font-weight: 500; + color: var(--theme-caption-color); + } + } + + .scroll { + min-height: 0; + height: 100%; + } + } diff --git a/packages/kanban/src/components/KanbanPanel.svelte b/packages/kanban/src/components/KanbanPanel.svelte deleted file mode 100644 index f0a20490ac..0000000000 --- a/packages/kanban/src/components/KanbanPanel.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - - -
- {#if customHeader} - - {:else} -
-
-
-
- {label} -
-
-
- {/if} -
- - - -
-
- - diff --git a/packages/theme/styles/_colors.scss b/packages/theme/styles/_colors.scss index 4d389fe56d..a4da04e21a 100644 --- a/packages/theme/styles/_colors.scss +++ b/packages/theme/styles/_colors.scss @@ -106,6 +106,8 @@ --theme-bg-color: #18181e; --theme-bg-selection: #282830; + --theme-bg-checked: #262b39; + --theme-bg-checked-hover: #2d3344; --theme-menu-color: #111117; --theme-menu-selection: #1D1D23; --theme-menu-divider: rgba(255, 255, 255, .05); diff --git a/plugins/board-resources/src/components/KanbanView.svelte b/plugins/board-resources/src/components/KanbanView.svelte index 216632ea0c..d023ec91ca 100644 --- a/plugins/board-resources/src/components/KanbanView.svelte +++ b/plugins/board-resources/src/components/KanbanView.svelte @@ -15,19 +15,23 @@ --> + { + listProvider.update(evt.detail) + }} + on:obj-focus={(evt) => { + listProvider.updateFocus(evt.detail) + }} + checked={$selectionStore ?? []} + on:check={(evt) => { + listProvider.updateSelection(evt.detail.docs, evt.detail.value) + }} + on:contextmenu={(evt) => showMenu(evt.detail.evt, evt.detail.objects)} + selection={listProvider.current($focusStore)} > diff --git a/plugins/recruit-resources/src/components/KanbanCard.svelte b/plugins/recruit-resources/src/components/KanbanCard.svelte index 9319644864..b9848e8256 100644 --- a/plugins/recruit-resources/src/components/KanbanCard.svelte +++ b/plugins/recruit-resources/src/components/KanbanCard.svelte @@ -52,7 +52,6 @@
- {/if}
diff --git a/plugins/task-resources/src/components/kanban/KanbanView.svelte b/plugins/task-resources/src/components/kanban/KanbanView.svelte index 828b8b0b7c..0334c5dedb 100644 --- a/plugins/task-resources/src/components/kanban/KanbanView.svelte +++ b/plugins/task-resources/src/components/kanban/KanbanView.svelte @@ -13,14 +13,17 @@ // See the License for the specific language governing permissions and // limitations under the License. --> - {#await cardPresenter then presenter} - - - + + + { + listProvider.update(evt.detail) + }} + on:obj-focus={(evt) => { + listProvider.updateFocus(evt.detail) + }} + checked={$selectionStore ?? []} + on:check={(evt) => { + listProvider.updateSelection(evt.detail.docs, evt.detail.value) + }} + on:contextmenu={(evt) => showMenu(evt.detail.evt, evt.detail.objects)} + selection={listProvider.current($focusStore)} + > + + // eslint-disable-next-line no-undef - - { - // eslint-disable-next-line no-undef - onDone({ doneState: e.detail._id }) - }} /> + + { + // eslint-disable-next-line no-undef + onDone({ doneState: e.detail._id }) + }} + /> {/await} diff --git a/plugins/tracker-resources/src/components/issues/Board.svelte b/plugins/tracker-resources/src/components/issues/Board.svelte index ce340cfc96..d44e958876 100644 --- a/plugins/tracker-resources/src/components/issues/Board.svelte +++ b/plugins/tracker-resources/src/components/issues/Board.svelte @@ -1,16 +1,23 @@ {#if currentTeam} +
Board
{ + listProvider.update(evt.detail) + }} + on:obj-focus={(evt) => { + listProvider.updateFocus(evt.detail) + }} + selection={listProvider.current($focusStore)} + + checked={$selectionStore ?? []} + on:check={(evt) => { + listProvider.updateSelection(evt.detail.docs, evt.detail.value) + }} + on:contextmenu={(evt) => showMenu(evt.detail.evt, evt.detail.objects)} >
@@ -100,26 +158,25 @@ }} /> -
{@const issue = toIssue(object)} -
+
{#if issue.$lookup?.assignee} - + {/if}
{object.title} +
+ +
diff --git a/plugins/view-resources/src/actionImpl.ts b/plugins/view-resources/src/actionImpl.ts index a47e7f4d0a..5adc72a35e 100644 --- a/plugins/view-resources/src/actionImpl.ts +++ b/plugins/view-resources/src/actionImpl.ts @@ -95,7 +95,12 @@ function ShowActions (doc: Doc | Doc[] | undefined, evt: Event): void { } function ShowPreview (doc: Doc | undefined, evt: Event): void { - previewDocument.set(doc) + previewDocument.update(old => { + if (old?._id === doc?._id) { + return undefined + } + return doc + }) evt.preventDefault() } /** diff --git a/plugins/view-resources/src/components/ActionHandler.svelte b/plugins/view-resources/src/components/ActionHandler.svelte index 81a1de82c5..678134bb65 100644 --- a/plugins/view-resources/src/components/ActionHandler.svelte +++ b/plugins/view-resources/src/components/ActionHandler.svelte @@ -1,12 +1,13 @@ {#if $previewDocument !== undefined && presenter } -
+
diff --git a/plugins/view-resources/src/components/Table.svelte b/plugins/view-resources/src/components/Table.svelte index 126adfb8c5..f6e1d3fd64 100644 --- a/plugins/view-resources/src/components/Table.svelte +++ b/plugins/view-resources/src/components/Table.svelte @@ -93,8 +93,9 @@ selection = row if (!checkedSet.has(object._id)) { check(objects, false) + checked = [] } - const items = $selectionStore.length > 0 ? $selectionStore : object + const items = checked.length > 0 ? checked : object showPopup(Menu, { object: items, baseMenuClass }, { getBoundingClientRect: () => DOMRect.fromRect({ width: 1, height: 1, x: ev.clientX, y: ev.clientY }) }, () => { diff --git a/plugins/view-resources/src/components/TableBrowser.svelte b/plugins/view-resources/src/components/TableBrowser.svelte index 8f920f26cf..036cf29c44 100644 --- a/plugins/view-resources/src/components/TableBrowser.svelte +++ b/plugins/view-resources/src/components/TableBrowser.svelte @@ -15,6 +15,7 @@ + { if (old !== undefined) { return selection?.focus diff --git a/plugins/view-resources/src/utils.ts b/plugins/view-resources/src/utils.ts index b31dc9c229..c67e470e7b 100644 --- a/plugins/view-resources/src/utils.ts +++ b/plugins/view-resources/src/utils.ts @@ -64,14 +64,12 @@ export async function getObjectPresenter ( /** * @public */ -export async function getObjectPreview (client: Client, _class: Ref>): Promise { +export async function getObjectPreview (client: Client, _class: Ref>): Promise { const clazz = client.getHierarchy().getClass(_class) const presenterMixin = client.getHierarchy().as(clazz, view.mixin.PreviewPresenter) if (presenterMixin.presenter === undefined) { if (clazz.extends !== undefined) { return await getObjectPreview(client, clazz.extends) - } else { - throw new Error('object presenter not found for ' + _class) } } return presenterMixin?.presenter diff --git a/tests/install-elastic-plugin.sh b/tests/install-elastic-plugin.sh index 9be2eb94b7..be983c3ae7 100755 --- a/tests/install-elastic-plugin.sh +++ b/tests/install-elastic-plugin.sh @@ -3,8 +3,9 @@ # Usage # ./install-elastic-plugin.sh sanity_elastic_1 +curdir=$(dirname $0) # Direct download, or use VPN. #wget https://artifacts.elastic.co/downloads/elasticsearch-plugins/ingest-attachment/ingest-attachment-7.14.2.zip -docker cp ./ingest-attachment-7.14.2.zip $1:/ingest-attachment-7.14.2.zip +docker cp $curdir/ingest-attachment-7.14.2.zip $1:/ingest-attachment-7.14.2.zip docker exec -ti $1 ./bin/elasticsearch-plugin install file:///ingest-attachment-7.14.2.zip docker restart $1 \ No newline at end of file