mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
Sortable actions (#9888)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user