mirror of
https://github.com/hcengineering/platform.git
synced 2025-01-03 08:57:14 +03:00
UBER-1080 improve selection (#3857)
Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
parent
20fe438e7e
commit
7fdfaaeece
@ -38,7 +38,6 @@
|
|||||||
groupBy,
|
groupBy,
|
||||||
ListSelectionProvider,
|
ListSelectionProvider,
|
||||||
SelectDirection,
|
SelectDirection,
|
||||||
selectionStore,
|
|
||||||
setGroupByValues
|
setGroupByValues
|
||||||
} from '@hcengineering/view-resources'
|
} from '@hcengineering/view-resources'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
@ -100,6 +99,8 @@
|
|||||||
;(document.activeElement as HTMLElement)?.blur()
|
;(document.activeElement as HTMLElement)?.blur()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selection = listProvider.selection
|
||||||
|
|
||||||
const showMenu = async (ev: MouseEvent, object: Doc): Promise<void> => {
|
const showMenu = async (ev: MouseEvent, object: Doc): Promise<void> => {
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
if (object._class !== board.class.Card) {
|
if (object._class !== board.class.Card) {
|
||||||
@ -156,7 +157,7 @@
|
|||||||
}}
|
}}
|
||||||
{groupByDocs}
|
{groupByDocs}
|
||||||
{getUpdateProps}
|
{getUpdateProps}
|
||||||
checked={$selectionStore ?? []}
|
checked={$selection ?? []}
|
||||||
on:check={(evt) => {
|
on:check={(evt) => {
|
||||||
listProvider.updateSelection(evt.detail.docs, evt.detail.value)
|
listProvider.updateSelection(evt.detail.docs, evt.detail.value)
|
||||||
}}
|
}}
|
||||||
|
@ -56,7 +56,6 @@
|
|||||||
Menu,
|
Menu,
|
||||||
noCategory,
|
noCategory,
|
||||||
SelectDirection,
|
SelectDirection,
|
||||||
selectionStore,
|
|
||||||
setGroupByValues
|
setGroupByValues
|
||||||
} from '@hcengineering/view-resources'
|
} from '@hcengineering/view-resources'
|
||||||
import view from '@hcengineering/view-resources/src/plugin'
|
import view from '@hcengineering/view-resources/src/plugin'
|
||||||
@ -113,6 +112,8 @@
|
|||||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
||||||
kanbanUI?.select(offset, of, dir)
|
kanbanUI?.select(offset, of, dir)
|
||||||
})
|
})
|
||||||
|
const selection = listProvider.selection
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
;(document.activeElement as HTMLElement)?.blur()
|
;(document.activeElement as HTMLElement)?.blur()
|
||||||
})
|
})
|
||||||
@ -248,7 +249,7 @@
|
|||||||
listProvider.updateFocus(evt.detail)
|
listProvider.updateFocus(evt.detail)
|
||||||
}}
|
}}
|
||||||
selection={listProvider.current($focusStore)}
|
selection={listProvider.current($focusStore)}
|
||||||
checked={$selectionStore ?? []}
|
checked={$selection ?? []}
|
||||||
on:check={(evt) => {
|
on:check={(evt) => {
|
||||||
listProvider.updateSelection(evt.detail.docs, evt.detail.value)
|
listProvider.updateSelection(evt.detail.docs, evt.detail.value)
|
||||||
}}
|
}}
|
||||||
|
@ -66,7 +66,6 @@
|
|||||||
noCategory,
|
noCategory,
|
||||||
openDoc,
|
openDoc,
|
||||||
SelectDirection,
|
SelectDirection,
|
||||||
selectionStore,
|
|
||||||
setGroupByValues
|
setGroupByValues
|
||||||
} from '@hcengineering/view-resources'
|
} from '@hcengineering/view-resources'
|
||||||
import view from '@hcengineering/view-resources/src/plugin'
|
import view from '@hcengineering/view-resources/src/plugin'
|
||||||
@ -149,6 +148,8 @@
|
|||||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
||||||
kanbanUI?.select(offset, of, dir)
|
kanbanUI?.select(offset, of, dir)
|
||||||
})
|
})
|
||||||
|
const selection = listProvider.selection
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
;(document.activeElement as HTMLElement)?.blur()
|
;(document.activeElement as HTMLElement)?.blur()
|
||||||
})
|
})
|
||||||
@ -288,7 +289,7 @@
|
|||||||
listProvider.updateFocus(evt.detail)
|
listProvider.updateFocus(evt.detail)
|
||||||
}}
|
}}
|
||||||
selection={listProvider.current($focusStore)}
|
selection={listProvider.current($focusStore)}
|
||||||
checked={$selectionStore ?? []}
|
checked={$selection ?? []}
|
||||||
on:check={(evt) => {
|
on:check={(evt) => {
|
||||||
listProvider.updateSelection(evt.detail.docs, evt.detail.value)
|
listProvider.updateSelection(evt.detail.docs, evt.detail.value)
|
||||||
}}
|
}}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!--
|
<!--
|
||||||
// Copyright © 2022 Hardcore Engineering Inc.
|
// Copyright © 2022, 2023 Hardcore Engineering Inc.
|
||||||
//
|
//
|
||||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
// 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
|
// you may not use this file except in compliance with the License. You may
|
||||||
@ -19,7 +19,7 @@
|
|||||||
import { Issue } from '@hcengineering/tracker'
|
import { Issue } from '@hcengineering/tracker'
|
||||||
import { AnyComponent, AnySvelteComponent, registerFocus } from '@hcengineering/ui'
|
import { AnyComponent, AnySvelteComponent, registerFocus } from '@hcengineering/ui'
|
||||||
import { ViewOptions, Viewlet, ViewletPreference } from '@hcengineering/view'
|
import { ViewOptions, Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||||
import { List, ListSelectionProvider, SelectDirection, selectionStore } from '@hcengineering/view-resources'
|
import { List, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import tracker from '../../../plugin'
|
import tracker from '../../../plugin'
|
||||||
|
|
||||||
@ -47,6 +47,7 @@
|
|||||||
listProvider.updateFocus(docs[0])
|
listProvider.updateFocus(docs[0])
|
||||||
list?.select(0, undefined)
|
list?.select(0, undefined)
|
||||||
}
|
}
|
||||||
|
const selection = listProvider.selection
|
||||||
|
|
||||||
// Focusable control with index
|
// Focusable control with index
|
||||||
let focused = false
|
let focused = false
|
||||||
@ -88,7 +89,8 @@
|
|||||||
{createItemDialog}
|
{createItemDialog}
|
||||||
{createItemDialogProps}
|
{createItemDialogProps}
|
||||||
{createItemLabel}
|
{createItemLabel}
|
||||||
selectedObjectIds={$selectionStore ?? []}
|
{listProvider}
|
||||||
|
selectedObjectIds={$selection ?? []}
|
||||||
{compactMode}
|
{compactMode}
|
||||||
on:row-focus={(event) => {
|
on:row-focus={(event) => {
|
||||||
listProvider.updateFocus(event.detail ?? undefined)
|
listProvider.updateFocus(event.detail ?? undefined)
|
||||||
|
@ -14,7 +14,14 @@ import {
|
|||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import MoveView from './components/Move.svelte'
|
import MoveView from './components/Move.svelte'
|
||||||
import view from './plugin'
|
import view from './plugin'
|
||||||
import { FocusSelection, SelectDirection, focusStore, previewDocument, selectionStore } from './selection'
|
import {
|
||||||
|
FocusSelection,
|
||||||
|
SelectDirection,
|
||||||
|
SelectionStore,
|
||||||
|
focusStore,
|
||||||
|
previewDocument,
|
||||||
|
selectionStore
|
||||||
|
} from './selection'
|
||||||
import { deleteObjects, getObjectLinkFragment } from './utils'
|
import { deleteObjects, getObjectLinkFragment } from './utils'
|
||||||
import contact from '@hcengineering/contact'
|
import contact from '@hcengineering/contact'
|
||||||
|
|
||||||
@ -107,6 +114,11 @@ contextStore.subscribe((it) => {
|
|||||||
$contextStore = it
|
$contextStore = it
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let $selectionStore: SelectionStore
|
||||||
|
selectionStore.subscribe((it) => {
|
||||||
|
$selectionStore = it
|
||||||
|
})
|
||||||
|
|
||||||
export function select (
|
export function select (
|
||||||
evt: Event | undefined,
|
evt: Event | undefined,
|
||||||
offset: 1 | -1 | 0,
|
offset: 1 | -1 | 0,
|
||||||
@ -128,8 +140,9 @@ export function select (
|
|||||||
|
|
||||||
function SelectItem (doc: Doc | Doc[] | undefined, evt: Event): void {
|
function SelectItem (doc: Doc | Doc[] | undefined, evt: Event): void {
|
||||||
const focus = $focusStore.focus
|
const focus = $focusStore.focus
|
||||||
|
const provider = $selectionStore.provider ?? $focusStore.provider
|
||||||
if (focus !== undefined) {
|
if (focus !== undefined) {
|
||||||
selectionStore.update((selection) => {
|
provider?.selection.update((selection) => {
|
||||||
const ind = selection.findIndex((it) => it._id === focus._id)
|
const ind = selection.findIndex((it) => it._id === focus._id)
|
||||||
if (ind === -1) {
|
if (ind === -1) {
|
||||||
selection.push(focus)
|
selection.push(focus)
|
||||||
@ -142,15 +155,21 @@ function SelectItem (doc: Doc | Doc[] | undefined, evt: Event): void {
|
|||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
}
|
}
|
||||||
function SelectItemNone (doc: Doc | undefined, evt: Event): void {
|
function SelectItemNone (doc: Doc | undefined, evt: Event): void {
|
||||||
selectionStore.set([])
|
const provider = $selectionStore.provider ?? $focusStore.provider
|
||||||
previewDocument.set(undefined)
|
if (provider !== undefined) {
|
||||||
evt.preventDefault()
|
provider.selection.set([])
|
||||||
|
previewDocument.set(undefined)
|
||||||
|
evt.preventDefault()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function SelectItemAll (doc: Doc | undefined, evt: Event): void {
|
function SelectItemAll (doc: Doc | undefined, evt: Event): void {
|
||||||
const docs = $focusStore.provider?.docs() ?? []
|
const provider = $selectionStore.provider ?? $focusStore.provider
|
||||||
selectionStore.set(docs)
|
if (provider !== undefined) {
|
||||||
previewDocument.set(undefined)
|
const docs = provider.docs() ?? []
|
||||||
evt.preventDefault()
|
provider.selection.set(docs)
|
||||||
|
previewDocument.set(undefined)
|
||||||
|
evt.preventDefault()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const MoveUp = (doc: Doc | undefined, evt: Event): void => select(evt, -1, $focusStore.focus, 'vertical')
|
const MoveUp = (doc: Doc | undefined, evt: Event): void => select(evt, -1, $focusStore.focus, 'vertical')
|
||||||
|
@ -27,17 +27,17 @@ import core, {
|
|||||||
import { getResource } from '@hcengineering/platform'
|
import { getResource } from '@hcengineering/platform'
|
||||||
import { Action, ActionGroup, ViewAction, ViewActionInput, ViewContextType } from '@hcengineering/view'
|
import { Action, ActionGroup, ViewAction, ViewActionInput, ViewContextType } from '@hcengineering/view'
|
||||||
import view from './plugin'
|
import view from './plugin'
|
||||||
import { FocusSelection } from './selection'
|
import { FocusSelection, SelectionStore } from './selection'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export function getSelection (focusStore: FocusSelection, selectionStore: Doc[]): Doc[] {
|
export function getSelection (focus: FocusSelection, selection: SelectionStore): Doc[] {
|
||||||
let docs: Doc[] = []
|
let docs: Doc[] = []
|
||||||
if (selectionStore.length > 0) {
|
if (selection.docs.length > 0) {
|
||||||
docs = selectionStore
|
docs = selection.docs
|
||||||
} else if (focusStore.focus !== undefined) {
|
} else if (focus.focus !== undefined) {
|
||||||
docs = [focusStore.focus]
|
docs = [focus.focus]
|
||||||
}
|
}
|
||||||
return docs
|
return docs
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
import { Action, ViewContextType } from '@hcengineering/view'
|
import { Action, ViewContextType } from '@hcengineering/view'
|
||||||
import { fly } from 'svelte/transition'
|
import { fly } from 'svelte/transition'
|
||||||
import { getContextActions, getSelection } from '../actions'
|
import { getContextActions, getSelection } from '../actions'
|
||||||
import { focusStore, previewDocument, selectionStore } from '../selection'
|
import { ListSelectionProvider, SelectionStore, focusStore, previewDocument, selectionStore } from '../selection'
|
||||||
import { getObjectPreview } from '../utils'
|
import { getObjectPreview } from '../utils'
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
@ -28,8 +28,9 @@
|
|||||||
addTxListener((tx) => {
|
addTxListener((tx) => {
|
||||||
if (tx._class === core.class.TxRemoveDoc) {
|
if (tx._class === core.class.TxRemoveDoc) {
|
||||||
const docId = (tx as TxRemoveDoc<Doc>).objectId
|
const docId = (tx as TxRemoveDoc<Doc>).objectId
|
||||||
if ($selectionStore.find((it) => it._id === docId) !== undefined) {
|
const provider = ListSelectionProvider.Find(docId)
|
||||||
selectionStore.update((old) => {
|
if (provider !== undefined) {
|
||||||
|
provider.selection.update((old) => {
|
||||||
return old.filter((it) => it._id !== docId)
|
return old.filter((it) => it._id !== docId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -47,13 +48,13 @@
|
|||||||
application?: Ref<Doc>
|
application?: Ref<Doc>
|
||||||
},
|
},
|
||||||
focus: Doc | undefined | null,
|
focus: Doc | undefined | null,
|
||||||
selection: Doc[]
|
selection: SelectionStore
|
||||||
): Promise<Action[]> {
|
): Promise<Action[]> {
|
||||||
let docs: Doc | Doc[] = []
|
let docs: Doc | Doc[] = []
|
||||||
if (selection.find((it) => it._id === focus?._id) === undefined && focus != null) {
|
if (selection.docs.find((it) => it._id === focus?._id) === undefined && focus != null) {
|
||||||
docs = focus
|
docs = focus
|
||||||
} else {
|
} else {
|
||||||
docs = selection
|
docs = selection.docs
|
||||||
}
|
}
|
||||||
|
|
||||||
return await getContextActions(client, docs, context)
|
return await getContextActions(client, docs, context)
|
||||||
|
@ -193,11 +193,11 @@
|
|||||||
on:keydown={onKeydown}
|
on:keydown={onKeydown}
|
||||||
use:resizeObserver={() => dispatch('changeContent')}
|
use:resizeObserver={() => dispatch('changeContent')}
|
||||||
>
|
>
|
||||||
{#if $selectionStore.length > 0 || $focusStore.focus !== undefined || (activeAction && activeAction?.actionPopup !== undefined)}
|
{#if $selectionStore.docs.length > 0 || $focusStore.focus !== undefined || (activeAction && activeAction?.actionPopup !== undefined)}
|
||||||
<div class="mt-2 ml-2 flex-between flex-no-shrink">
|
<div class="mt-2 ml-2 flex-between flex-no-shrink">
|
||||||
{#if $selectionStore.length > 0}
|
{#if $selectionStore.docs.length > 0}
|
||||||
<div class="item-box">
|
<div class="item-box">
|
||||||
<Label label={view.string.NumberItems} params={{ count: $selectionStore.length }} />
|
<Label label={view.string.NumberItems} params={{ count: $selectionStore.docs.length }} />
|
||||||
</div>
|
</div>
|
||||||
{:else if $focusStore.focus !== undefined}
|
{:else if $focusStore.focus !== undefined}
|
||||||
<div class="item-box">
|
<div class="item-box">
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
import { Scroller, tableSP, FadeOptions } from '@hcengineering/ui'
|
import { Scroller, tableSP, FadeOptions } from '@hcengineering/ui'
|
||||||
import { BuildModelKey } from '@hcengineering/view'
|
import { BuildModelKey } from '@hcengineering/view'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '../selection'
|
import { focusStore, ListSelectionProvider, SelectDirection } from '../selection'
|
||||||
import { LoadingProps } from '../utils'
|
import { LoadingProps } from '../utils'
|
||||||
import SourcePresenter from './inference/SourcePresenter.svelte'
|
import SourcePresenter from './inference/SourcePresenter.svelte'
|
||||||
import Table from './Table.svelte'
|
import Table from './Table.svelte'
|
||||||
@ -48,6 +48,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
const selection = listProvider.selection
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
;(document.activeElement as HTMLElement)?.blur()
|
;(document.activeElement as HTMLElement)?.blur()
|
||||||
@ -101,7 +102,7 @@
|
|||||||
highlightRows={true}
|
highlightRows={true}
|
||||||
{enableChecking}
|
{enableChecking}
|
||||||
showFooter
|
showFooter
|
||||||
checked={$selectionStore ?? []}
|
checked={$selection ?? []}
|
||||||
{prefferedSorting}
|
{prefferedSorting}
|
||||||
{tableId}
|
{tableId}
|
||||||
selection={listProvider.current($focusStore)}
|
selection={listProvider.current($focusStore)}
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
|
import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui'
|
||||||
import { BuildModelKey, ViewOptionModel, ViewOptions, ViewQueryOption, Viewlet } from '@hcengineering/view'
|
import { BuildModelKey, ViewOptionModel, ViewOptions, ViewQueryOption, Viewlet } from '@hcengineering/view'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
import { SelectionFocusProvider } from '../../selection'
|
||||||
import { buildConfigLookup } from '../../utils'
|
import { buildConfigLookup } from '../../utils'
|
||||||
import ListCategories from './ListCategories.svelte'
|
import ListCategories from './ListCategories.svelte'
|
||||||
|
|
||||||
@ -40,6 +41,7 @@
|
|||||||
export let props: Record<string, any> = {}
|
export let props: Record<string, any> = {}
|
||||||
export let selection: number | undefined = undefined
|
export let selection: number | undefined = undefined
|
||||||
export let compactMode: boolean = false
|
export let compactMode: boolean = false
|
||||||
|
export let listProvider: SelectionFocusProvider
|
||||||
|
|
||||||
const limiter = new RateLimitter(() => ({ rate: 10 }))
|
const limiter = new RateLimitter(() => ({ rate: 10 }))
|
||||||
|
|
||||||
@ -189,6 +191,7 @@
|
|||||||
{viewOptionsConfig}
|
{viewOptionsConfig}
|
||||||
{selectedObjectIds}
|
{selectedObjectIds}
|
||||||
{limiter}
|
{limiter}
|
||||||
|
{listProvider}
|
||||||
level={0}
|
level={0}
|
||||||
groupPersistKey={''}
|
groupPersistKey={''}
|
||||||
{createItemDialog}
|
{createItemDialog}
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
ViewOptions
|
ViewOptions
|
||||||
} from '@hcengineering/view'
|
} from '@hcengineering/view'
|
||||||
import { createEventDispatcher, onDestroy, SvelteComponentTyped } from 'svelte'
|
import { createEventDispatcher, onDestroy, SvelteComponentTyped } from 'svelte'
|
||||||
|
import { SelectionFocusProvider } from '../../selection'
|
||||||
import {
|
import {
|
||||||
buildModel,
|
buildModel,
|
||||||
concatCategories,
|
concatCategories,
|
||||||
@ -83,6 +84,7 @@
|
|||||||
export let resultQuery: DocumentQuery<Doc>
|
export let resultQuery: DocumentQuery<Doc>
|
||||||
export let resultOptions: FindOptions<Doc>
|
export let resultOptions: FindOptions<Doc>
|
||||||
export let limiter: RateLimitter
|
export let limiter: RateLimitter
|
||||||
|
export let listProvider: SelectionFocusProvider
|
||||||
|
|
||||||
$: groupByKey = viewOptions.groupBy[level] ?? noCategory
|
$: groupByKey = viewOptions.groupBy[level] ?? noCategory
|
||||||
let categories: CategoryType[] = []
|
let categories: CategoryType[] = []
|
||||||
@ -380,6 +382,7 @@
|
|||||||
{resultQuery}
|
{resultQuery}
|
||||||
{resultOptions}
|
{resultOptions}
|
||||||
{limiter}
|
{limiter}
|
||||||
|
{listProvider}
|
||||||
on:check
|
on:check
|
||||||
on:uncheckAll
|
on:uncheckAll
|
||||||
on:row-focus
|
on:row-focus
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
import { AttributeModel, BuildModelKey, ViewOptionModel, ViewOptions, Viewlet } from '@hcengineering/view'
|
import { AttributeModel, BuildModelKey, ViewOptionModel, ViewOptions, Viewlet } from '@hcengineering/view'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import { fade } from 'svelte/transition'
|
import { fade } from 'svelte/transition'
|
||||||
import { FocusSelection, focusStore } from '../../selection'
|
import { FocusSelection, SelectionFocusProvider, focusStore } from '../../selection'
|
||||||
import Menu from '../Menu.svelte'
|
import Menu from '../Menu.svelte'
|
||||||
import ListHeader from './ListHeader.svelte'
|
import ListHeader from './ListHeader.svelte'
|
||||||
import ListItem from './ListItem.svelte'
|
import ListItem from './ListItem.svelte'
|
||||||
@ -87,6 +87,7 @@
|
|||||||
export let resultOptions: FindOptions<Doc>
|
export let resultOptions: FindOptions<Doc>
|
||||||
export let parentCategories: number = 0
|
export let parentCategories: number = 0
|
||||||
export let limiter: RateLimitter
|
export let limiter: RateLimitter
|
||||||
|
export let listProvider: SelectionFocusProvider
|
||||||
|
|
||||||
$: lastLevel = level + 1 >= viewOptions.groupBy.length
|
$: lastLevel = level + 1 >= viewOptions.groupBy.length
|
||||||
|
|
||||||
@ -432,6 +433,7 @@
|
|||||||
limited={lastLevel ? limited.length : itemProj.length}
|
limited={lastLevel ? limited.length : itemProj.length}
|
||||||
itemsProj={itemProj}
|
itemsProj={itemProj}
|
||||||
items={limited}
|
items={limited}
|
||||||
|
{listProvider}
|
||||||
{headerComponent}
|
{headerComponent}
|
||||||
{createItemDialog}
|
{createItemDialog}
|
||||||
{createItemDialogProps}
|
{createItemDialogProps}
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
import { AttributeModel, ViewOptions } from '@hcengineering/view'
|
import { AttributeModel, ViewOptions } from '@hcengineering/view'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import view from '../../plugin'
|
import view from '../../plugin'
|
||||||
import { selectionStore, selectionStoreMap } from '../../selection'
|
import { SelectionFocusProvider } from '../../selection'
|
||||||
import { noCategory } from '../../viewOptions'
|
import { noCategory } from '../../viewOptions'
|
||||||
|
|
||||||
export let groupByKey: string
|
export let groupByKey: string
|
||||||
@ -50,6 +50,7 @@
|
|||||||
export let collapsed = false
|
export let collapsed = false
|
||||||
export let lastCat = false
|
export let lastCat = false
|
||||||
export let level: number
|
export let level: number
|
||||||
|
export let listProvider: SelectionFocusProvider
|
||||||
|
|
||||||
export let createItemDialog: AnyComponent | AnySvelteComponent | undefined
|
export let createItemDialog: AnyComponent | AnySvelteComponent | undefined
|
||||||
export let createItemDialogProps: Record<string, any> | undefined
|
export let createItemDialogProps: Record<string, any> | undefined
|
||||||
@ -82,7 +83,10 @@
|
|||||||
}
|
}
|
||||||
let mouseOver = false
|
let mouseOver = false
|
||||||
|
|
||||||
$: selected = items.filter((it) => $selectionStoreMap.has(it._id))
|
const selection = listProvider.selection
|
||||||
|
|
||||||
|
$: selectionIds = new Set($selection.map((it) => it._id))
|
||||||
|
$: selected = items.filter((it) => selectionIds.has(it._id))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if headerComponent || groupByKey === noCategory}
|
{#if headerComponent || groupByKey === noCategory}
|
||||||
@ -177,18 +181,18 @@
|
|||||||
kind={'ghost'}
|
kind={'ghost'}
|
||||||
showTooltip={{ label: view.string.Select }}
|
showTooltip={{ label: view.string.Select }}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
let newSelection = [...$selectionStore]
|
let newSelection = [...$selection]
|
||||||
if (selected.length > 0) {
|
if (selected.length > 0) {
|
||||||
const smap = new Map(selected.map((it) => [it._id, it]))
|
const smap = new Map(selected.map((it) => [it._id, it]))
|
||||||
newSelection = newSelection.filter((it) => !smap.has(it._id))
|
newSelection = newSelection.filter((it) => !smap.has(it._id))
|
||||||
} else {
|
} else {
|
||||||
for (const s of items) {
|
for (const s of items) {
|
||||||
if (!$selectionStoreMap.has(s._id)) {
|
if (!selectionIds.has(s._id)) {
|
||||||
newSelection.push(s)
|
newSelection.push(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
selectionStore.set(newSelection)
|
listProvider.selection.set(newSelection)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,3 +1,17 @@
|
|||||||
|
<!--
|
||||||
|
// Copyright © 2023 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">
|
<script lang="ts">
|
||||||
import { Class, Doc, DocumentQuery, FindOptions, Ref, Space } from '@hcengineering/core'
|
import { Class, Doc, DocumentQuery, FindOptions, Ref, Space } from '@hcengineering/core'
|
||||||
import { IntlString } from '@hcengineering/platform'
|
import { IntlString } from '@hcengineering/platform'
|
||||||
@ -5,7 +19,7 @@
|
|||||||
import { BuildModelKey, Viewlet, ViewOptions } from '@hcengineering/view'
|
import { BuildModelKey, Viewlet, ViewOptions } from '@hcengineering/view'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { ActionContext } from '@hcengineering/presentation'
|
import { ActionContext } from '@hcengineering/presentation'
|
||||||
import { ListSelectionProvider, SelectDirection, focusStore, selectionStore } from '../..'
|
import { ListSelectionProvider, SelectDirection, focusStore } from '../..'
|
||||||
|
|
||||||
import List from './List.svelte'
|
import List from './List.svelte'
|
||||||
|
|
||||||
@ -37,6 +51,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
const selection = listProvider.selection
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
;(document.activeElement as HTMLElement)?.blur()
|
;(document.activeElement as HTMLElement)?.blur()
|
||||||
@ -70,9 +85,10 @@
|
|||||||
{createItemLabel}
|
{createItemLabel}
|
||||||
{viewOptions}
|
{viewOptions}
|
||||||
{props}
|
{props}
|
||||||
|
{listProvider}
|
||||||
compactMode={listWidth <= 800}
|
compactMode={listWidth <= 800}
|
||||||
viewOptionsConfig={viewlet.viewOptions?.other}
|
viewOptionsConfig={viewlet.viewOptions?.other}
|
||||||
selectedObjectIds={$selectionStore ?? []}
|
selectedObjectIds={$selection ?? []}
|
||||||
selection={listProvider.current($focusStore)}
|
selection={listProvider.current($focusStore)}
|
||||||
on:row-focus={(event) => {
|
on:row-focus={(event) => {
|
||||||
listProvider.updateFocus(event.detail ?? undefined)
|
listProvider.updateFocus(event.detail ?? undefined)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Doc, Ref } from '@hcengineering/core'
|
import { Doc, Ref } from '@hcengineering/core'
|
||||||
import { panelstore } from '@hcengineering/ui'
|
import { panelstore } from '@hcengineering/ui'
|
||||||
import { onDestroy } from 'svelte'
|
import { onDestroy } from 'svelte'
|
||||||
import { Unsubscriber, derived, writable } from 'svelte/store'
|
import { Unsubscriber, Writable, writable } from 'svelte/store'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -32,7 +32,11 @@ export interface SelectionFocusProvider {
|
|||||||
|
|
||||||
// Return all selectable documents
|
// Return all selectable documents
|
||||||
docs: () => Doc[]
|
docs: () => Doc[]
|
||||||
|
|
||||||
|
// All selected documents
|
||||||
|
selection: Writable<Doc[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
@ -46,6 +50,18 @@ export interface FocusSelection {
|
|||||||
provider?: SelectionFocusProvider
|
provider?: SelectionFocusProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*
|
||||||
|
* Define document selection inside platform.
|
||||||
|
*/
|
||||||
|
export interface SelectionStore {
|
||||||
|
// Selected documents
|
||||||
|
docs: Doc[]
|
||||||
|
// Provider where documents are selected
|
||||||
|
provider?: SelectionFocusProvider
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -54,18 +70,17 @@ export const focusStore = writable<FocusSelection>({})
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export const selectionStore = writable<Doc[]>([])
|
export const selectionStore = writable<SelectionStore>({ docs: [] })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export const selectionStoreMap = derived(selectionStore, (it) => new Set(it.map((it) => it._id)))
|
|
||||||
|
|
||||||
export const previewDocument = writable<Doc | undefined>()
|
export const previewDocument = writable<Doc | undefined>()
|
||||||
|
|
||||||
panelstore.subscribe((val) => {
|
panelstore.subscribe((val) => {
|
||||||
previewDocument.set(undefined)
|
previewDocument.set(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -88,17 +103,23 @@ export function updateFocus (selection?: FocusSelection): void {
|
|||||||
|
|
||||||
return cur
|
return cur
|
||||||
})
|
})
|
||||||
|
|
||||||
// We need to clear selection items not belong to passed provider.
|
|
||||||
if (selection?.provider !== undefined) {
|
|
||||||
const docs = new Set(selection?.provider.docs().map((it) => it._id))
|
|
||||||
selectionStore.update((old) => {
|
|
||||||
return old.filter((it) => docs.has(it._id))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const providers: ListSelectionProvider[] = []
|
interface ProviderSelection {
|
||||||
|
docs: Doc[]
|
||||||
|
provider: ListSelectionProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
const providers: ProviderSelection[] = []
|
||||||
|
|
||||||
|
function updateSelection (selection: ProviderSelection): void {
|
||||||
|
const index = providers.findIndex((p) => p.provider === selection.provider)
|
||||||
|
if (index !== -1) {
|
||||||
|
providers[index] = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionStore.set(selection)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -108,15 +129,27 @@ const providers: ListSelectionProvider[] = []
|
|||||||
export class ListSelectionProvider implements SelectionFocusProvider {
|
export class ListSelectionProvider implements SelectionFocusProvider {
|
||||||
private _docs: Doc[] = []
|
private _docs: Doc[] = []
|
||||||
_current?: FocusSelection
|
_current?: FocusSelection
|
||||||
private readonly unsubscribe: Unsubscriber
|
selection: Writable<Doc[]> = writable([])
|
||||||
|
private readonly unsubscribe: Unsubscriber[]
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly delegate: (offset: 1 | -1 | 0, of?: Doc, direction?: SelectDirection, noScroll?: boolean) => void,
|
private readonly delegate: (offset: 1 | -1 | 0, of?: Doc, direction?: SelectDirection, noScroll?: boolean) => void,
|
||||||
autoDestroy = true
|
autoDestroy = true
|
||||||
) {
|
) {
|
||||||
this.unsubscribe = focusStore.subscribe((doc) => {
|
this.unsubscribe = [
|
||||||
this._current = doc
|
// keep track of current focus
|
||||||
})
|
focusStore.subscribe((focus) => {
|
||||||
providers.push(this)
|
this._current = focus
|
||||||
|
}),
|
||||||
|
// update global selection when current changes
|
||||||
|
this.selection.subscribe((docs) => {
|
||||||
|
updateSelection({ docs, provider: this })
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
providers.push({ docs: [], provider: this })
|
||||||
|
selectionStore.set({ docs: [], provider: this })
|
||||||
|
|
||||||
if (autoDestroy) {
|
if (autoDestroy) {
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
this.destroy()
|
this.destroy()
|
||||||
@ -125,9 +158,11 @@ export class ListSelectionProvider implements SelectionFocusProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Find (_id: Ref<Doc>): ListSelectionProvider | undefined {
|
static Find (_id: Ref<Doc>): ListSelectionProvider | undefined {
|
||||||
for (const provider of providers) {
|
for (const { provider } of providers) {
|
||||||
if (provider.docs().findIndex((p) => p._id === _id) !== -1) {
|
if (provider !== undefined) {
|
||||||
return provider
|
if (provider.docs().findIndex((p) => p._id === _id) !== -1) {
|
||||||
|
return provider
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -135,23 +170,35 @@ export class ListSelectionProvider implements SelectionFocusProvider {
|
|||||||
static Pop (): void {
|
static Pop (): void {
|
||||||
if (providers.length === 0) return
|
if (providers.length === 0) return
|
||||||
const last = providers[providers.length - 1]
|
const last = providers[providers.length - 1]
|
||||||
last.destroy()
|
last.provider.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy (): void {
|
destroy (): void {
|
||||||
const thisIndex = providers.findIndex((p) => p === this)
|
const thisIndex = providers.findIndex((p) => p.provider === this)
|
||||||
providers.splice(thisIndex, 1)
|
providers.splice(thisIndex, 1)
|
||||||
|
|
||||||
|
// switch selection to the last provider if lost selection
|
||||||
if (thisIndex === providers.length) {
|
if (thisIndex === providers.length) {
|
||||||
if (providers.length > 0) {
|
if (providers.length > 0) {
|
||||||
const current = providers[providers.length - 1]
|
const next = providers[providers.length - 1].provider
|
||||||
const index = current.current()
|
const index = next.current()
|
||||||
const target = index !== undefined ? current.docs()[index] : undefined
|
const target = index !== undefined ? next.docs()[index] : undefined
|
||||||
updateFocus({ focus: target, provider: current })
|
updateFocus({ focus: target, provider: next })
|
||||||
} else {
|
} else {
|
||||||
updateFocus()
|
updateFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.unsubscribe()
|
|
||||||
|
// switch selection to the last provider if lost selection
|
||||||
|
selectionStore.update((selection) => {
|
||||||
|
if (selection.provider === this) {
|
||||||
|
const next = providers[providers.length - 1]
|
||||||
|
return next ?? { docs: selection.docs }
|
||||||
|
}
|
||||||
|
return selection
|
||||||
|
})
|
||||||
|
|
||||||
|
this.unsubscribe.forEach((p) => p())
|
||||||
}
|
}
|
||||||
|
|
||||||
select (offset: 1 | -1 | 0, of?: Doc, direction?: SelectDirection, noScroll?: boolean): void {
|
select (offset: 1 | -1 | 0, of?: Doc, direction?: SelectDirection, noScroll?: boolean): void {
|
||||||
@ -162,7 +209,8 @@ export class ListSelectionProvider implements SelectionFocusProvider {
|
|||||||
update (docs: Doc[]): void {
|
update (docs: Doc[]): void {
|
||||||
this._docs = docs
|
this._docs = docs
|
||||||
|
|
||||||
selectionStore.update((docs) => {
|
// remove missing documents from selection
|
||||||
|
this.selection.update((docs) => {
|
||||||
const ids = new Set(docs.map((it) => it._id))
|
const ids = new Set(docs.map((it) => it._id))
|
||||||
return this._docs.filter((it) => ids.has(it._id))
|
return this._docs.filter((it) => ids.has(it._id))
|
||||||
})
|
})
|
||||||
@ -174,6 +222,7 @@ export class ListSelectionProvider implements SelectionFocusProvider {
|
|||||||
// Check if we don't have object, we need to select first one.
|
// Check if we don't have object, we need to select first one.
|
||||||
this.delegate(0, this._current?.focus, 'vertical', true)
|
this.delegate(0, this._current?.focus, 'vertical', true)
|
||||||
}
|
}
|
||||||
|
// focus current provider if nothing focused
|
||||||
if (this._current?.focus === undefined) {
|
if (this._current?.focus === undefined) {
|
||||||
updateFocus({ focus: this._current?.focus, provider: this })
|
updateFocus({ focus: this._current?.focus, provider: this })
|
||||||
}
|
}
|
||||||
@ -189,7 +238,7 @@ export class ListSelectionProvider implements SelectionFocusProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateSelection (docs: Doc[], value: boolean): void {
|
updateSelection (docs: Doc[], value: boolean): void {
|
||||||
selectionStore.update((selection) => {
|
this.selection.update((selection) => {
|
||||||
const docsSet = new Set(docs.map((it) => it._id))
|
const docsSet = new Set(docs.map((it) => it._id))
|
||||||
const noDocs = selection.filter((it) => !docsSet.has(it._id))
|
const noDocs = selection.filter((it) => !docsSet.has(it._id))
|
||||||
return value ? [...noDocs, ...docs] : noDocs
|
return value ? [...noDocs, ...docs] : noDocs
|
||||||
|
Loading…
Reference in New Issue
Block a user