Sortable actions (#9888)

This commit is contained in:
Denis Bykhov
2025-09-18 10:20:11 +07:00
committed by GitHub
parent ffff8c804b
commit 4e18cf4eca
6 changed files with 155 additions and 155 deletions
@@ -17,11 +17,11 @@
import { translate } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Process, State } from '@hcengineering/process'
import { makeRank } from '@hcengineering/rank'
import { Button, IconAdd, Label } from '@hcengineering/ui'
import { SortableDocList } from '@hcengineering/view-resources'
import plugin from '../../plugin'
import StateInlineEditor from './StateInlineEditor.svelte'
import { makeRank } from '@hcengineering/rank'
import { SortableList } from '@hcengineering/view-resources'
export let process: Process
export let states: State[]
@@ -49,11 +49,11 @@
<div class="header w-full p-4">
<Label label={plugin.string.States} />
</div>
<SortableList _class={plugin.class.State} query={{ process: process._id }} isAddButtonHidden>
<SortableDocList _class={plugin.class.State} query={{ process: process._id }}>
<svelte:fragment slot="object" let:value>
<StateInlineEditor value={toState(value)} />
</svelte:fragment>
</SortableList>
</SortableDocList>
{#if !readonly}
<Button kind={'ghost'} width={'100%'} icon={IconAdd} label={plugin.string.AddState} on:click={addState} />
{/if}
@@ -41,6 +41,7 @@
import Navigator from './Navigator.svelte'
import TransitionPresenter from './TransitionPresenter.svelte'
import TriggerPresenter from './TriggerPresenter.svelte'
import { SortableList } from '@hcengineering/view-resources'
export let _id: Ref<Transition>
export let visibleSecondNav: boolean = true
@@ -136,6 +137,11 @@
function edit (): void {
$settingsStore = { id: _id, component: AsideTransitionEditor, props: { process, transition: value } }
}
async function moveHadler (): Promise<void> {
if (value === undefined) return
await client.update(value, { actions: value.actions })
}
</script>
<div class="hulyComponent-content__container columns">
@@ -169,20 +175,22 @@
<div class="label w-full p-4">
<Label label={plugin.string.Actions} />
</div>
{#each value.actions as action}
<Button
justify="left"
kind="ghost"
width="100%"
on:click={() => {
editAction(action)
}}
>
<svelte:fragment slot="content">
<ActionPresenter {action} {process} readonly={false} />
</svelte:fragment>
</Button>
{/each}
<SortableList bind:items={value.actions} on:move={moveHadler}>
<svelte:fragment slot="object" let:value>
<Button
justify="left"
kind="ghost"
width="100%"
on:click={() => {
editAction(value)
}}
>
<svelte:fragment slot="content">
<ActionPresenter action={value} {process} readonly={false} />
</svelte:fragment>
</Button>
</svelte:fragment>
</SortableList>
<Button
kind={'ghost'}
width={'100%'}
@@ -20,7 +20,7 @@
import AddTransitionPopup from './AddTransitionPopup.svelte'
import TransitionPresenter from './TransitionPresenter.svelte'
import TriggerPresenter from './TriggerPresenter.svelte'
import { SortableList } from '@hcengineering/view-resources'
import { SortableDocList } from '@hcengineering/view-resources'
export let process: Process
export let readonly: boolean
@@ -47,7 +47,7 @@
<div class="header w-full p-4">
<Label label={plugin.string.Transitions} />
</div>
<SortableList _class={plugin.class.Transition} query={{ process: process._id }} isAddButtonHidden>
<SortableDocList _class={plugin.class.Transition} query={{ process: process._id }}>
<svelte:fragment slot="object" let:value>
{@const transition = toTransition(value)}
<Button
@@ -66,7 +66,7 @@
</svelte:fragment>
</Button>
</svelte:fragment>
</SortableList>
</SortableDocList>
{#if !readonly}
<Button kind={'ghost'} width={'100%'} icon={IconAdd} label={plugin.string.AddTransition} on:click={addTransition} />
{/if}
@@ -0,0 +1,107 @@
<!--
// Copyright © 2022 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 { Class, Doc, DocumentQuery, FindOptions, FindResult, Ref, SortingOrder } from '@hcengineering/core'
import { Asset, getResource, IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { DocWithRank, makeRank } from '@hcengineering/task'
import { IconSize } from '@hcengineering/ui'
import { SvelteComponent } from 'svelte'
import { getListItemPresenter, getObjectPresenter } from '../../utils'
import SortableList from './SortableList.svelte'
export let _class: Ref<Class<Doc>>
export let label: IntlString | undefined = undefined
export let query: DocumentQuery<Doc> = {}
export let queryOptions: FindOptions<Doc> | undefined = undefined
export let presenterProps: Record<string, any> = {}
export let direction: 'row' | 'column' = 'column'
export let flipDuration = 200
export let itemsCount = 0
export let icon: Asset | undefined = undefined
export let iconSize: IconSize = 'small'
const SORTING_ORDER = SortingOrder.Ascending
const client = getClient()
const hierarchy = client.getHierarchy()
const itemsQuery = createQuery()
let isPresenterLoading = false
let areItemsloading = true
let areItemsSorting = false
let presenter: typeof SvelteComponent | undefined
let items: Doc[] = []
async function updatePresenter (classRef: Ref<Class<Doc>>) {
try {
isPresenterLoading = true
const listItemPresenter = await getListItemPresenter(client, classRef)
if (listItemPresenter) {
presenter = await getResource(listItemPresenter)
return
}
const objectModel = await getObjectPresenter(client, classRef, { key: '' })
if (objectModel?.presenter) {
presenter = objectModel.presenter
}
} finally {
isPresenterLoading = false
}
}
function updateItems (newItems: FindResult<Doc>): void {
items = newItems
areItemsloading = false
}
$: !$$slots.object && updatePresenter(_class)
$: itemsQuery.query(_class, query, updateItems, {
...(isSortable ? { sort: { rank: SORTING_ORDER } } : {}),
...(queryOptions ?? {}),
limit: Math.max(queryOptions?.limit ?? 0, 200)
})
$: isSortable = hierarchy.getAllAttributes(_class).has('rank')
async function handleMove (
e: CustomEvent<{ item: DocWithRank, prev: DocWithRank | undefined, next: DocWithRank | undefined }>
): Promise<void> {
if (!isSortable) return
const { item, prev, next } = e.detail
const sortingOrder = queryOptions?.sort?.rank ?? SORTING_ORDER
const rank =
sortingOrder === SortingOrder.Ascending ? makeRank(prev?.rank, next?.rank) : makeRank(next?.rank, prev?.rank)
try {
areItemsSorting = true
await client.update(item, { rank })
} finally {
areItemsSorting = false
}
}
</script>
<SortableList {items} {label} {direction} {flipDuration} {icon} {iconSize} bind:itemsCount on:move={handleMove}>
<svelte:fragment slot="object" let:value>
{#if $$slots.object}
<slot name="object" {value} />
{:else if presenter}
<svelte:component this={presenter} {...presenterProps} {value} />
{/if}
</svelte:fragment>
</SortableList>
@@ -13,92 +13,24 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, DocumentQuery, FindOptions, FindResult, Ref, SortingOrder } from '@hcengineering/core'
import { Asset, getResource, IntlString } from '@hcengineering/platform'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { DocWithRank, makeRank } from '@hcengineering/task'
import { Button, Component, Icon, IconAdd, IconSize, Label, Loading } from '@hcengineering/ui'
import view, { ObjectFactory } from '@hcengineering/view'
import { Asset, IntlString } from '@hcengineering/platform'
import { Icon, IconSize, Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { flip } from 'svelte/animate'
import { getListItemPresenter, getObjectPresenter } from '../../utils'
import { SvelteComponent } from 'svelte'
/*
How to use:
We must add presenter for the "_class" via "ListItemPresenter" / "AttributePresenter"
mixins or render the "object" slot to be able display the rows list.
To create a new items, we should add "ObjectFactory" mixin also.
We can create a custom list items or editor based on "SortableListItem"
Important: the "ObjectFactory" component must emit the "close" event
*/
export let _class: Ref<Class<Doc>>
export let items: any[]
export let label: IntlString | undefined = undefined
export let query: DocumentQuery<Doc> = {}
export let queryOptions: FindOptions<Doc> | undefined = undefined
export let presenterProps: Record<string, any> = {}
export let direction: 'row' | 'column' = 'column'
export let flipDuration = 200
export let isAddButtonHidden = false
export let isAddButtonDisabled = false
export let itemsCount = 0
export let icon: Asset | undefined = undefined
export let iconSize: IconSize = 'small'
const SORTING_ORDER = SortingOrder.Ascending
const client = getClient()
const hierarchy = client.getHierarchy()
const itemsQuery = createQuery()
let isPresenterLoading = false
let areItemsloading = true
let areItemsSorting = false
let presenter: typeof SvelteComponent | undefined
let objectFactory: ObjectFactory | undefined
let items: Doc[] = []
const areItemsSorting = false
let draggingIndex: number | null = null
let hoveringIndex: number | null = null
let isCreating = false
async function updatePresenter (classRef: Ref<Class<Doc>>) {
try {
isPresenterLoading = true
const listItemPresenter = await getListItemPresenter(client, classRef)
if (listItemPresenter) {
presenter = await getResource(listItemPresenter)
return
}
const objectModel = await getObjectPresenter(client, classRef, { key: '' })
if (objectModel?.presenter) {
presenter = objectModel.presenter
}
} finally {
isPresenterLoading = false
}
}
function updateObjectFactory (objectFactoryClassRef: Ref<Class<Doc>>) {
const objectFactoryClass = hierarchy.getClass(objectFactoryClassRef)
if (hierarchy.hasMixin(objectFactoryClass, view.mixin.ObjectFactory)) {
objectFactory = hierarchy.as(objectFactoryClass, view.mixin.ObjectFactory)
}
}
function updateItems (newItems: FindResult<Doc>): void {
items = newItems
areItemsloading = false
}
function handleDragStart (ev: DragEvent, itemIndex: number) {
if (ev.dataTransfer) {
ev.dataTransfer.effectAllowed = 'move'
@@ -114,63 +46,38 @@
hoveringIndex = itemIndex
}
const dispatch = createEventDispatcher()
async function handleDrop (itemIndex: number) {
if (isSortable && items && draggingIndex !== null && draggingIndex !== itemIndex) {
const item = items[draggingIndex] as DocWithRank
if (draggingIndex !== null && draggingIndex !== itemIndex) {
const item = items[draggingIndex]
const [prev, next] = [
items[draggingIndex < itemIndex ? itemIndex : itemIndex - 1] as DocWithRank,
items[draggingIndex < itemIndex ? itemIndex + 1 : itemIndex] as DocWithRank
items[draggingIndex < itemIndex ? itemIndex : itemIndex - 1],
items[draggingIndex < itemIndex ? itemIndex + 1 : itemIndex]
]
const sortingOrder = queryOptions?.sort?.rank ?? SORTING_ORDER
const rank =
sortingOrder === SortingOrder.Ascending ? makeRank(prev?.rank, next?.rank) : makeRank(next?.rank, prev?.rank)
items.splice(itemIndex, 0, items.splice(draggingIndex, 1)[0])
try {
areItemsSorting = true
await client.update(item, { rank })
} finally {
areItemsSorting = false
}
dispatch('move', { item, prev, next, items })
}
resetDrag()
}
async function create () {
if (objectFactory?.create) {
const createFn = await getResource(objectFactory.create)
await createFn(query)
return
}
isCreating = true
}
function resetDrag () {
draggingIndex = null
hoveringIndex = null
}
$: !$$slots.object && updatePresenter(_class)
$: updateObjectFactory(_class)
$: itemsQuery.query(_class, query, updateItems, {
...(isSortable ? { sort: { rank: SORTING_ORDER } } : {}),
...(queryOptions ?? {}),
limit: Math.max(queryOptions?.limit ?? 0, 200)
})
$: isLoading = isPresenterLoading || areItemsloading
$: isSortable = hierarchy.getAllAttributes(_class).has('rank')
$: itemsCount = items?.length ?? 0
$: isVertical = direction === 'column'
$: isDraggable = isSortable && items.length > 1 && !areItemsSorting
$: isDraggable = items.length > 1 && !areItemsSorting
</script>
<div class="flex-col" class:w-full={isVertical}>
{#if label || !isAddButtonHidden}
{#if label}
<div class="flex mb-4">
{#if icon}
<div class="mr-2 flex-center">
@@ -184,25 +91,10 @@
</span>
</div>
{/if}
{#if !isAddButtonHidden}
<div class="ml-auto">
<Button
showTooltip={{ label: presentation.string.Add }}
disabled={isAddButtonDisabled || isLoading || !objectFactory}
width="min-content"
icon={IconAdd}
size="small"
kind="ghost"
on:click={create}
/>
</div>
{/if}
</div>
{/if}
{#if isLoading}
<Loading />
{:else if ($$slots.object ?? presenter) && items}
{#if $$slots.object && items}
<div class="flex-gap-1" class:flex-col={isVertical} class:flex={!isVertical} class:flex-wrap={!isVertical}>
{#each items as item, index (item._id)}
<!-- svelte-ignore a11y-no-static-element-interactions -->
@@ -225,18 +117,9 @@
}}
on:dragend={resetDrag}
>
{#if $$slots.object}
<slot name="object" value={item} {isDraggable} />
{:else if presenter}
<svelte:component this={presenter} {isDraggable} {...presenterProps} value={item} />
{/if}
<slot name="object" value={item} {isDraggable} />
</div>
{/each}
{#if objectFactory?.component && isCreating}
<!-- Important: the "close" event must be specified -->
<Component is={objectFactory.component} props={query} showLoading on:close={() => (isCreating = false)} />
{/if}
</div>
{/if}
</div>
+2
View File
@@ -90,6 +90,7 @@ import YoutubePresenter from './components/linkPresenters/YoutubePresenter.svelt
import DividerPresenter from './components/list/DividerPresenter.svelte'
import GrowPresenter from './components/list/GrowPresenter.svelte'
import ListView from './components/list/ListView.svelte'
import SortableDocList from './components/list/SortableDocList.svelte'
import SortableList from './components/list/SortableList.svelte'
import SortableListItem from './components/list/SortableListItem.svelte'
import TreeElement from './components/navigator/TreeElement.svelte'
@@ -236,6 +237,7 @@ export {
ObjectIcon,
ObjectMention,
SortableList,
SortableDocList,
SortableListItem,
SpaceHeader,
SpacePresenter,