Rework todo result (#9893)

This commit is contained in:
Denis Bykhov
2025-09-19 10:41:21 +07:00
committed by GitHub
parent 1e7b531564
commit a7afa2b1dc
34 changed files with 555 additions and 317 deletions
@@ -38,7 +38,7 @@
}
</script>
<div class="grid">
<div class="editor-grid">
{#each keys as key}
<AttributeCriteria
{process}
@@ -54,17 +54,3 @@
/>
{/each}
</div>
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 1fr 3fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
margin-top: 0.5rem;
height: min-content;
}
</style>
@@ -93,7 +93,7 @@
}
</script>
<div class="grid">
<div class="editor-grid">
<span
class="labelOnPanel"
use:tooltip={{
@@ -118,18 +118,3 @@
<Button label={presentation.string.Add} width={'100%'} kind={'link-bordered'} size={'large'} on:click={onAdd} />
</div>
{/if}
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 1fr 1.5fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
width: calc(100% - 4rem);
align-items: center;
margin: 0.25rem 2rem 0.5rems;
row-gap: 0.5rem;
column-gap: 1rem;
height: min-content;
}
</style>
@@ -15,7 +15,7 @@
<script lang="ts">
import core, { Ref, SortingOrder } from '@hcengineering/core'
import { Card, createQuery, getClient } from '@hcengineering/presentation'
import { Process, State, Transition, Trigger, TriggerResult } from '@hcengineering/process'
import { Process, State, Transition, Trigger } from '@hcengineering/process'
import { Component, Dropdown, DropdownIntlItem, DropdownLabelsIntl, Label, ListItem } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
@@ -26,7 +26,6 @@
const dispatch = createEventDispatcher()
let to: Ref<State> | undefined = undefined
let states: State[] = []
let result: TriggerResult | null = null
const query = createQuery()
query.query(
@@ -96,7 +95,6 @@
triggerParams: params,
process: process._id,
rank,
result,
actions: []
})
dispatch('close')
@@ -106,9 +104,6 @@
if (e.detail?.params !== undefined) {
params = e.detail.params
}
if (e.detail?.result !== undefined) {
result = e.detail.result
}
}
</script>
@@ -119,7 +114,7 @@
width={'medium'}
on:close
>
<div class="grid">
<div class="editor-grid">
<Label label={plugin.string.From} />
{#if !withoutFrom}
<Dropdown
@@ -149,7 +144,6 @@
label={plugin.string.Trigger}
on:selected={() => {
params = {}
result = null
}}
justify={'left'}
width={'100%'}
@@ -157,19 +151,6 @@
/>
</div>
{#if triggerValue?.editor}
<Component is={triggerValue.editor} props={{ process, params, result }} on:change={change} />
<Component is={triggerValue.editor} props={{ process, params }} on:change={change} />
{/if}
</Card>
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 1fr 3fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
height: min-content;
}
</style>
@@ -20,12 +20,11 @@
export let transition: Transition
let params = transition.triggerParams ?? {}
let result = transition.result ?? null
const client = getClient()
async function save (): Promise<void> {
await client.update(transition, { triggerParams: params, trigger: selectedTrigger, result })
await client.update(transition, { triggerParams: params, trigger: selectedTrigger })
clearSettingsStore()
}
@@ -38,9 +37,6 @@
if (e.detail?.params !== undefined) {
params = e.detail.params
}
if (e.detail?.result !== undefined) {
result = e.detail.result
}
}
let selectedTrigger: Ref<Trigger> = transition.trigger
@@ -72,7 +68,7 @@
</div>
</div>
{#if trigger !== undefined}
<div class="grid">
<div class="editor-grid">
<Label label={plugin.string.Trigger} />
<DropdownLabelsIntl
items={triggersItems}
@@ -80,7 +76,6 @@
label={plugin.string.Trigger}
on:selected={() => {
params = {}
result = null
}}
justify={'left'}
width={'100%'}
@@ -89,7 +84,7 @@
</div>
{#if trigger.editor !== undefined}
<div class="editor">
<Component is={trigger.editor} props={{ process, params, result, readonly }} on:change={change} />
<Component is={trigger.editor} props={{ process, params, readonly }} on:change={change} />
</div>
{/if}
{/if}
@@ -105,18 +100,6 @@
margin: 0.25rem 2rem 0;
}
.grid {
display: grid;
grid-template-columns: 1fr 3fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.25rem;
column-gap: 1rem;
margin: 0.25rem 2rem 0;
height: min-content;
}
.title {
padding-bottom: 1rem;
}
@@ -24,13 +24,23 @@
export let step: Step<Doc>
$: currentContext = step.context ? process.context[step.context._id] : undefined
$: resultContext =
step.results != null ? step.results.map((r) => process.context[r._id]).filter((ctx) => ctx != null) : []
</script>
{#if currentContext}
<Label label={processPlugin.string.Result} />
<div class="container">
<ProcessContextPresenter context={currentContext} />
</div>
{#if currentContext || resultContext.length > 0}
<Label label={processPlugin.string.Result} />:
{#if currentContext}
<div class="container">
<ProcessContextPresenter context={currentContext} />
</div>
{/if}
{#each resultContext as ctx}
<div class="container">
<ProcessContextPresenter context={ctx} />
</div>
{/each}
{/if}
<style lang="scss">
@@ -13,32 +13,20 @@
// limitations under the License.
-->
<script lang="ts">
import core, { Class, PropertyType, Ref, Type } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { TriggerResult } from '@hcengineering/process'
import setting from '@hcengineering/setting-resources/src/plugin'
import {
AnyComponent,
Component,
DropdownIntlItem,
DropdownLabelsIntl,
EditBox,
Label,
Toggle
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import { deepEqual } from 'fast-equals'
import core, { Type } from '@hcengineering/core'
import { Process, UserResult } from '@hcengineering/process'
import { Button, EditBox, IconClose, Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import { generateContextId } from '../../utils'
import ResultTypeSelector from './ResultTypeSelector.svelte'
export let result: TriggerResult | null
export let result: UserResult | null
export let process: Process
let type: Type<any> | undefined | null = result?.type
let name: string = result?.name ?? ''
let is: AnyComponent | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
let key: string | undefined = result?.key
const dispatch = createEventDispatcher()
function update (): void {
@@ -48,114 +36,34 @@
result = {
_id: generateContextId(),
name,
key,
type
}
}
dispatch('change', result)
}
function getTypes (): DropdownIntlItem[] {
const descendants = hierarchy.getDescendants(core.class.Type)
const res: DropdownIntlItem[] = []
for (const descendant of descendants) {
const _class = hierarchy.getClass(descendant)
if (_class.label !== undefined && hierarchy.hasMixin(_class, view.mixin.ObjectEditor)) {
res.push({
label: _class.label,
id: _class._id
})
}
}
return res
}
const items = getTypes()
let selectedType: Ref<Class<Type<PropertyType>>> | undefined = type?._class
$: selectType(selectedType)
function selectType (type: Ref<Class<Type<PropertyType>>> | undefined): void {
if (type == null) return
const _class = hierarchy.getClass(type)
const editor = hierarchy.as(_class, view.mixin.ObjectEditor)
if (editor.editor !== undefined) {
is = editor.editor
}
}
const handleSelection = (e: { detail: Ref<Class<Type<any>>> }): void => {
selectType(e.detail)
}
const handleChange = (e: any): void => {
if (e.detail?.type === undefined) return
if (!deepEqual(e.detail.type, type)) {
type = e.detail?.type
update()
}
}
function handleNameChange (e: any): void {
function handleNameChange (): void {
if (result != null) {
result.name = name
dispatch('change', result)
}
}
function changeRequired (e: boolean): void {
if (!e) {
type = undefined
} else {
type = null
}
update()
}
</script>
<div class="grid">
<Label label={plugin.string.Result} />
<Toggle
on={result != null}
on:change={(e) => {
changeRequired(e.detail)
}}
/>
{#if type !== undefined}
<span class="label">
<Label label={core.string.Description} />
</span>
<div class="editor-grid">
<span class="label">
<Label label={core.string.Description} />
</span>
<div class="flex-row-center flex-gap-2">
<EditBox bind:value={name} placeholder={core.string.Description} on:change={handleNameChange} kind={'default'} />
<span class="label">
<Label label={setting.string.Type} />
</span>
<DropdownLabelsIntl
label={setting.string.Type}
{items}
size={'large'}
width={'100%'}
bind:selected={selectedType}
on:selected={handleSelection}
<Button
icon={IconClose}
kind="ghost"
on:click={() => {
dispatch('remove')
}}
/>
{#if is}
<Component
{is}
props={{
type,
width: '100%',
isCard: true,
kind: 'regular',
size: 'large'
}}
on:change={handleChange}
/>
{/if}
{/if}
</div>
<ResultTypeSelector {process} bind:key bind:type on:change={update} />
</div>
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 1fr 3fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
height: min-content;
}
</style>
@@ -0,0 +1,80 @@
<!--
// Copyright © 2025 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 { MasterTag } from '@hcengineering/card'
import core, { AnyAttribute, Class, Ref, Type } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Process } from '@hcengineering/process'
import { DropdownLabelsIntl, Label, tooltip } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
export let process: Process
export let key: string | undefined
export let type: Type<any> | null | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
$: setType(key)
function setType (key: string | undefined): void {
const attr = key != null ? hierarchy.findAttribute(process.masterTag, key) : undefined
if (attr != null) {
type = attr.type
} else {
type = null
}
dispatch('change')
}
function getKeys (_class: Ref<Class<MasterTag>>): AnyAttribute[] {
const ignoreKeys = ['_class', 'content', 'parent', 'attachments', 'todos']
const attributes = hierarchy.getAllAttributes(_class, core.class.Doc)
const res: AnyAttribute[] = []
for (const [key, attr] of attributes) {
if (attr.hidden === true) continue
if (ignoreKeys.includes(key)) continue
res.push(attr)
}
return res
}
const allAttrs = getKeys(process.masterTag)
const items = allAttrs.map((attr) => {
return {
id: attr.name,
label: attr.label ?? attr.name
}
})
</script>
<span
class="labelOnPanel"
use:tooltip={{
props: { label: plugin.string.Attribute }
}}
>
<Label label={plugin.string.Attribute} />
</span>
<DropdownLabelsIntl
label={plugin.string.Attribute}
{items}
size={'large'}
width={'100%'}
shouldUpdateUndefined={false}
bind:selected={key}
/>
@@ -0,0 +1,92 @@
<!--
// Copyright © 2025 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 core, { Class, PropertyType, Ref, Type } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import setting from '@hcengineering/setting-resources/src/plugin'
import { AnyComponent, Component, DropdownIntlItem, DropdownLabelsIntl, Label } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { deepEqual } from 'fast-equals'
import { createEventDispatcher } from 'svelte'
export let type: Type<any> | null | undefined
let is: AnyComponent | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
function getTypes (): DropdownIntlItem[] {
const descendants = hierarchy.getDescendants(core.class.Type)
const res: DropdownIntlItem[] = []
for (const descendant of descendants) {
const _class = hierarchy.getClass(descendant)
if (_class.label !== undefined && hierarchy.hasMixin(_class, view.mixin.ObjectEditor)) {
res.push({
label: _class.label,
id: _class._id
})
}
}
return res
}
const items = getTypes()
let selectedType: Ref<Class<Type<PropertyType>>> | undefined = type?._class
$: selectType(selectedType)
function selectType (type: Ref<Class<Type<PropertyType>>> | undefined): void {
if (type == null) return
const _class = hierarchy.getClass(type)
const editor = hierarchy.as(_class, view.mixin.ObjectEditor)
if (editor.editor !== undefined) {
is = editor.editor
}
}
const handleSelection = (e: { detail: Ref<Class<Type<any>>> }): void => {
selectType(e.detail)
}
const handleChange = (e: any): void => {
if (e.detail?.type === undefined) return
if (!deepEqual(e.detail.type, type)) {
type = e.detail?.type
dispatch('change', type)
}
}
</script>
<span class="label">
<Label label={setting.string.Type} />
</span>
<DropdownLabelsIntl
label={setting.string.Type}
{items}
size={'large'}
width={'100%'}
bind:selected={selectedType}
on:selected={handleSelection}
/>
{#if is}
<Component
{is}
props={{
type,
width: '100%',
isCard: true,
kind: 'regular',
size: 'large'
}}
on:change={handleChange}
/>
{/if}
@@ -0,0 +1,54 @@
<!--
// Copyright © 2025 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 } from '@hcengineering/core'
import { Process } from '@hcengineering/process'
import { DropdownIntlItem, DropdownLabelsIntl, Label } from '@hcengineering/ui'
import plugin from '../../plugin'
import ResultFieldTypeEditor from './ResultFieldTypeEditor.svelte'
import ResultTypeEditor from './ResultTypeEditor.svelte'
export let process: Process
export let type: Type<any> | null | undefined
export let key: string | undefined
enum Modes {
Attribute = 'attribute',
Context = 'context'
}
const items: DropdownIntlItem[] = [
{
id: Modes.Attribute,
label: plugin.string.Attribute
},
{
id: Modes.Context,
label: plugin.string.Context
}
]
let selected: Modes = key !== undefined || type == null ? Modes.Attribute : Modes.Context
</script>
<span class="label">
<Label label={plugin.string.For} />
</span>
<DropdownLabelsIntl label={plugin.string.For} {items} size={'large'} width={'100%'} bind:selected />
{#if selected === Modes.Attribute}
<ResultFieldTypeEditor {process} bind:key bind:type on:change />
{:else}
<ResultTypeEditor bind:type on:change />
{/if}
@@ -0,0 +1,64 @@
<!--
// Copyright © 2025 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 { Process, UserResult } from '@hcengineering/process'
import { Button } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import ResultEditor from './ResultEditor.svelte'
export let process: Process
export let result: UserResult[] | undefined
const dispatch = createEventDispatcher()
let items: (UserResult | null)[] = result ?? []
function handleChange (e: CustomEvent<UserResult>, index: number) {
if (e.detail !== undefined) {
items[index === -1 ? items.length : index] = e.detail
result = items.filter((r) => r != null)
dispatch('change', result)
}
}
function onAdd (): void {
items.push(null)
items = items
}
function remove (index: number): void {
items.splice(index, 1)
items = items
result = items.filter((r) => r != null)
dispatch('change', result)
}
</script>
{#each items as item, i}
<ResultEditor
{process}
result={item}
on:change={(e) => {
handleChange(e, i)
}}
on:remove={() => {
remove(i)
}}
/>
{/each}
<div class="flex-center mt-4">
<Button label={plugin.string.AddResult} width={'100%'} kind={'link-bordered'} size={'large'} on:click={onAdd} />
</div>
@@ -70,7 +70,7 @@
const dispatch = createEventDispatcher()
</script>
<div class="grid">
<div class="editor-grid">
<ProcessAttribute
{process}
masterTag={process.masterTag}
@@ -88,17 +88,3 @@
}}
/>
</div>
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 1fr 3fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
margin-top: 0.5rem;
height: min-content;
}
</style>
@@ -13,12 +13,12 @@
// limitations under the License.
-->
<script lang="ts">
import { Process, ProcessToDo, Step } from '@hcengineering/process'
import { Process, ProcessToDo, Step, UserResult } from '@hcengineering/process'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import ParamsEditor from './ParamsEditor.svelte'
import ResultEditor from './ResultEditor.svelte'
import { Label, Toggle } from '@hcengineering/ui'
import ResultsEditor from './ResultsEditor.svelte'
export let process: Process
export let step: Step<ProcessToDo>
@@ -35,6 +35,13 @@
}
}
function changeResults (e: CustomEvent<UserResult[]>): void {
if (e.detail !== undefined) {
step.results = e.detail
dispatch('change', step)
}
}
const keys = ['title', 'user', 'dueDate']
</script>
@@ -53,6 +60,8 @@
}}
/>
</div>
<div class="divider" />
<ResultsEditor {process} result={step.results} on:change={changeResults} />
<style lang="scss">
.divider {
@@ -14,18 +14,16 @@
-->
<script lang="ts">
import { Process, TriggerResult } from '@hcengineering/process'
import { Process } from '@hcengineering/process'
import { Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import ToDoContextSelector from '../contextEditors/ToDoContextSelector.svelte'
import ResultEditor from './ResultEditor.svelte'
export let readonly: boolean
export let process: Process
export let params: Record<string, any>
export let skipRollback: boolean = false
export let result: TriggerResult | null = null
const dispatch = createEventDispatcher()
@@ -34,39 +32,9 @@
params._id = e.detail
dispatch('change', { params })
}
function changeResult (e: CustomEvent<any>): void {
if (e.detail !== undefined) {
result = e.detail
dispatch('change', { result })
}
}
</script>
<div class="grid">
<div class="editor-grid">
<Label label={plugin.string.ToDo} />
<ToDoContextSelector {readonly} {skipRollback} {process} value={params._id} on:change={change} />
</div>
{#if !skipRollback}
<div class="divider" />
<ResultEditor {result} on:change={changeResult} />
{/if}
<style lang="scss">
.divider {
border-bottom: 1px solid var(--divider-color);
margin: 1rem 0;
}
.grid {
display: grid;
grid-template-columns: 1fr 3fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
margin-top: 0.5rem;
height: min-content;
}
</style>