Improve process ui (#9645)

This commit is contained in:
Denis Bykhov
2025-08-06 17:19:12 +07:00
committed by GitHub
parent 2021e27379
commit 29257e9fc8
43 changed files with 738 additions and 1303 deletions
@@ -26,6 +26,7 @@
ViewletsSettingButton
} from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import NextTriggers from './NextTriggers.svelte'
import process from '../plugin'
export let execution: Execution
@@ -128,6 +129,7 @@
showCancelButton={false}
on:close
>
<NextTriggers {execution} />
<div class="flex-row-reverse">
<ViewletsSettingButton bind:viewOptions viewletQuery={{ _id: viewletId }} kind={'tertiary'} bind:viewlet />
</div>
@@ -19,7 +19,7 @@
import ErrorPresenter from './ErrorPresenter.svelte'
import { continueExecution } from '../utils'
import { showPopup } from '@hcengineering/ui'
import ExecutionLogs from './ExecutionLogs.svelte'
import ExecutionDetails from './ExecutionDetails.svelte'
export let value: WithLookup<Execution>
@@ -29,7 +29,7 @@
function open (): void {
showPopup(
ExecutionLogs,
ExecutionDetails,
{
execution: value
},
@@ -0,0 +1,48 @@
<!--
// 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 { getClient } from '@hcengineering/presentation'
import plugin from '../plugin'
import { Execution, ExecutionStatus } from '@hcengineering/process'
import TransitionPresenter from './settings/TransitionPresenter.svelte'
import TriggerPresenter from './settings/TriggerPresenter.svelte'
import { Label } from '@hcengineering/ui'
export let execution: Execution
const client = getClient()
const model = client.getModel()
$: transitions = model.findAllSync(plugin.class.Transition, {
process: execution.process,
from: execution.currentState
})
$: process = model.findObject(execution.process)
</script>
{#if execution.status === ExecutionStatus.Done}
<Label label={plugin.string.Done} />
{:else if execution.status === ExecutionStatus.Cancelled}
<Label label={plugin.string.Cancelled} />
{:else if process !== undefined}
{#each transitions as transition}
<div class="flex-row-center flex-gap-2">
<TransitionPresenter {transition} direction={'from'} />
<TriggerPresenter value={transition.trigger} {process} params={transition.triggerParams} withLabel={true} />
</div>
{/each}
{/if}
@@ -21,7 +21,6 @@
AnyComponent,
Button,
Component,
DropdownIntlItem,
DropdownLabelsIntl,
eventToHTMLElement,
IconAdd,
@@ -30,9 +29,11 @@
} from '@hcengineering/ui'
import view from '@hcengineering/view-resources/src/plugin'
import { createEventDispatcher } from 'svelte'
import { buildResult, Mode, Modes, parseValue } from '../../query'
import { getContext } from '../../utils'
import ContextSelectorPopup from '../attributeEditors/ContextSelectorPopup.svelte'
import ContextValue from '../attributeEditors/ContextValue.svelte'
import is from 'date-fns/locale/is'
export let readonly: boolean
export let value: any
@@ -41,46 +42,30 @@
export let attribute: AnyAttribute
export let baseEditor: AnyComponent | undefined
const modes: Mode[] = [
Modes.ArrayAll,
Modes.ArrayAny,
Modes.ArrayNotIncludes,
Modes.ArraySizeEquals,
Modes.ArraySizeGt,
Modes.ArraySizeGte,
Modes.ArraySizeLt,
Modes.ArraySizeLte
]
const client = getClient()
$: numberContext = getContext(client, process, core.class.TypeNumber, 'attribute')
const modes: DropdownIntlItem[] = [
{ id: 'all', label: view.string.FilterArrayAll },
{ id: 'any', label: view.string.FilterArrayAny },
{ id: 'notall', label: view.string.NotContains },
{ id: 'size', label: view.string.ArraySizeEquals },
{ id: 'sizeGt', label: view.string.ArraySizeGt },
{ id: 'sizeGte', label: view.string.ArraySizeGte },
{ id: 'sizeLt', label: view.string.ArraySizeLt },
{ id: 'sizeLte', label: view.string.ArraySizeLte }
]
let [val, selectedMode] = parseValue(modes, value)
let mode =
value == null
? 'all'
: value.$all !== undefined
? 'all'
: value.$in !== undefined
? 'any'
: value.$nin !== undefined
? 'notIncludes'
: value.$size !== undefined
? typeof value.$size === 'object'
? Object.keys(value.$size)[0] === '$gt'
? 'sizeGt'
: Object.keys(value.$size)[0] === '$gte'
? 'sizeGte'
: Object.keys(value.$size)[0] === '$lt'
? 'sizeLt'
: 'sizeLte'
: 'size'
: 'all'
$: mode = selectedMode.id
const dispatch = createEventDispatcher()
let contextValue: SelectedContext | undefined = undefined
let val: any = undefined
$: [val, selectedMode] = parseValue(modes, value)
$: contextValue = parseContext(val)
function selectContext (e: MouseEvent): void {
showPopup(
@@ -96,62 +81,22 @@
)
}
$: parseValue(value)
function parseValue (value: any) {
if (value?.$all !== undefined) {
mode = 'all'
val = value.$all
} else if (value?.$in !== undefined) {
mode = 'any'
val = value.$in
} else if (value?.$nin !== undefined) {
mode = 'notIncludes'
val = value.$nin
} else if (value?.$size !== undefined) {
if (typeof value.$size === 'object') {
const operator = Object.keys(value.$size)[0]
val = value.$size[operator]
mode =
operator === '$gt' ? 'sizeGt' : operator === '$gte' ? 'sizeGte' : operator === '$lt' ? 'sizeLt' : 'sizeLte'
} else {
mode = 'size'
val = value.$size
}
} else {
mode = 'all'
val = undefined
}
contextValue = parseContext(val)
}
function onSelect (res: SelectedContext | null): void {
buildResult(res === null ? undefined : '$' + JSON.stringify(res))
changeResult(res === null ? undefined : '$' + JSON.stringify(res))
}
function onChange (value: any | undefined): void {
buildResult(value)
changeResult(value)
}
function buildResult (val: any | undefined) {
if (val === undefined || val === '') {
function changeResult (val: any | undefined, allowEmpty: boolean = false): void {
if ((!allowEmpty && val === undefined) || val === '') {
dispatch('change', null)
return
}
let result: any
if (mode === 'all') {
result = { $all: val }
} else if (mode === 'any') {
result = { $in: val }
} else if (mode === 'notIncludes') {
result = { $nin: val }
} else if (mode === 'size') {
result = { $size: val }
} else if (mode.startsWith('size')) {
const operator = mode === 'sizeGt' ? '$gt' : mode === 'sizeGte' ? '$gte' : mode === 'sizeLt' ? '$lt' : '$lte'
result = { $size: { [operator]: val } }
}
const result = buildResult(selectedMode, val)
value = result
dispatch('change', result)
}
@@ -171,7 +116,12 @@
width={'100%'}
on:selected={(e) => {
mode = e.detail
buildResult(val)
if ((isSize(mode) && !isSize(selectedMode.id)) || (!isSize(mode) && isSize(selectedMode.id))) {
val = undefined
contextValue = undefined
}
selectedMode = modes.find((m) => m.id === mode) ?? modes[0]
changeResult(val, true)
}}
/>
@@ -0,0 +1,183 @@
<!--
// 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 { AnyAttribute } from '@hcengineering/core'
import { getAttributePresenterClass, getClient } from '@hcengineering/presentation'
import { Context, parseContext, Process, SelectedContext } from '@hcengineering/process'
import {
AnyComponent,
Button,
Component,
DropdownLabelsIntl,
eventToHTMLElement,
IconAdd,
IconClose,
showPopup
} from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { buildResult, Mode, Modes, parseValue } from '../../query'
import ContextSelectorPopup from '../attributeEditors/ContextSelectorPopup.svelte'
import ContextValue from '../attributeEditors/ContextValue.svelte'
export let readonly: boolean
export let value: any
export let process: Process
export let context: Context
export let attribute: AnyAttribute
export let baseEditor: AnyComponent | undefined
export let modes: Mode[] = []
let [val, selectedMode] = parseValue(modes, value)
$: mode = selectedMode.id
const dispatch = createEventDispatcher()
$: [val, selectedMode] = parseValue(modes, value)
$: contextValue = parseContext(val)
function selectContext (e: MouseEvent): void {
showPopup(
ContextSelectorPopup,
{
process,
masterTag: process.masterTag,
context,
attribute,
onSelect
},
eventToHTMLElement(e)
)
}
function onSelect (res: SelectedContext | null): void {
val = res === null ? undefined : '$' + JSON.stringify(res)
changeResult()
}
function onChange (value: any | undefined): void {
val = value
changeResult()
}
const client = getClient()
const hierarchy = client.getHierarchy()
$: presenterClass = getAttributePresenterClass(hierarchy, attribute.type)
function changeResult () {
if (val === undefined || val === '') {
dispatch('change', null)
return
}
const result = buildResult(selectedMode, val)
dispatch('change', result)
}
</script>
<div class="flex-row-center flex-gap-4">
<DropdownLabelsIntl
items={modes}
selected={mode}
disabled={readonly}
minW0={false}
kind={'no-border'}
width={'100%'}
on:selected={(e) => {
mode = e.detail
selectedMode = modes.find((m) => m.id === mode) ?? modes[0]
changeResult()
}}
/>
<div class="text-input" class:context={contextValue}>
{#if contextValue}
<ContextValue
{process}
masterTag={process.masterTag}
{contextValue}
{context}
{attribute}
category={presenterClass.category}
attrClass={presenterClass.attrClass}
on:update={(e) => {
onSelect(e.detail)
}}
/>
{:else}
<div class="w-full">
{#if baseEditor}
<Component
is={baseEditor}
props={{
label: attribute?.label,
placeholder: attribute?.label,
kind: 'ghost',
size: 'large',
width: '100%',
justify: 'left',
readonly,
type: attribute?.type,
value: val,
onChange,
focus
}}
/>
{/if}
</div>
{/if}
<div class="button flex-row-center">
<Button
icon={IconAdd}
kind="ghost"
on:click={(e) => {
selectContext(e)
}}
/>
<Button
icon={IconClose}
kind="ghost"
on:click={() => {
dispatch('delete')
}}
/>
</div>
</div>
</div>
<style lang="scss">
.text-input {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border: 1px solid var(--theme-refinput-border);
border-radius: 0.375rem;
max-width: 100%;
width: 100%;
.button {
flex-shrink: 0;
}
&.context {
background: #3575de33;
padding-left: 0.75rem;
border-color: var(--primary-button-default);
}
}
</style>
@@ -14,23 +14,11 @@
-->
<script lang="ts">
import core, { AnyAttribute } from '@hcengineering/core'
import { Context, parseContext, Process, SelectedContext } from '@hcengineering/process'
import {
AnyComponent,
Button,
Component,
DropdownIntlItem,
DropdownLabelsIntl,
eventToHTMLElement,
IconAdd,
IconClose,
showPopup
} from '@hcengineering/ui'
import view from '@hcengineering/view-resources/src/plugin'
import { createEventDispatcher } from 'svelte'
import ContextSelectorPopup from '../attributeEditors/ContextSelectorPopup.svelte'
import ContextValue from '../attributeEditors/ContextValue.svelte'
import { AnyAttribute } from '@hcengineering/core'
import { Context, Process } from '@hcengineering/process'
import { AnyComponent } from '@hcengineering/ui'
import { Mode, Modes } from '../../query'
import BaseCriteria from './BaseCriteria.svelte'
export let readonly: boolean
export let value: any
@@ -39,164 +27,7 @@
export let attribute: AnyAttribute
export let baseEditor: AnyComponent | undefined
const modes: DropdownIntlItem[] = [
{ id: 'equals', label: view.string.FilterIsEither },
{ id: 'notEquals', label: view.string.FilterIsNot }
]
let mode = value == null || typeof value === 'boolean' ? 'equals' : 'notEquals'
const dispatch = createEventDispatcher()
let contextValue: SelectedContext | undefined = undefined
let val: any = undefined
function selectContext (e: MouseEvent): void {
showPopup(
ContextSelectorPopup,
{
process,
masterTag: process.masterTag,
context,
attribute,
onSelect
},
eventToHTMLElement(e)
)
}
$: parseValue(value)
function parseValue (value: any) {
if (typeof value === 'boolean' || value == null) {
mode = 'equals'
val = value
} else {
mode = 'notEquals'
if (value?.$ne !== undefined) {
val = value.$ne
} else {
val = undefined
}
}
contextValue = parseContext(val)
}
function onSelect (res: SelectedContext | null): void {
val = res === null ? undefined : '$' + JSON.stringify(res)
buildResult()
}
function onChange (value: any | undefined): void {
val = value
buildResult()
}
function buildResult () {
if (val === undefined) {
dispatch('change', null)
return
}
let result: any
if (mode === 'equals') {
result = val
} else {
result = { $ne: val }
}
dispatch('change', result)
}
const modes: Mode[] = [Modes.Equal, Modes.NotEqual]
</script>
<div class="flex-row-center flex-gap-4">
<DropdownLabelsIntl
items={modes}
selected={mode}
disabled={readonly}
minW0={false}
kind={'no-border'}
width={'100%'}
on:selected={(e) => {
mode = e.detail
buildResult()
}}
/>
<div class="text-input" class:context={contextValue}>
{#if contextValue}
<ContextValue
{process}
masterTag={process.masterTag}
{contextValue}
{context}
{attribute}
category={'attribute'}
attrClass={core.class.TypeBoolean}
on:update={(e) => {
onSelect(e.detail)
}}
/>
{:else}
<div class="w-full">
{#if baseEditor}
<Component
is={baseEditor}
props={{
label: attribute?.label,
placeholder: attribute?.label,
kind: 'ghost',
size: 'large',
width: '100%',
justify: 'left',
readonly,
type: attribute?.type,
value: val,
onChange,
focus
}}
/>
{/if}
</div>
{/if}
<div class="button flex-row-center">
<Button
icon={IconAdd}
kind="ghost"
on:click={(e) => {
selectContext(e)
}}
/>
<Button
icon={IconClose}
kind="ghost"
on:click={() => {
dispatch('delete')
}}
/>
</div>
</div>
</div>
<style lang="scss">
.text-input {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border: 1px solid var(--theme-refinput-border);
border-radius: 0.375rem;
max-width: 100%;
width: 100%;
.button {
flex-shrink: 0;
}
&.context {
background: #3575de33;
padding-left: 0.75rem;
border-color: var(--primary-button-default);
}
}
</style>
<BaseCriteria {readonly} {value} {process} {context} {attribute} {baseEditor} {modes} on:change on:delete />
@@ -14,23 +14,11 @@
-->
<script lang="ts">
import core, { AnyAttribute } from '@hcengineering/core'
import { Context, parseContext, Process, SelectedContext } from '@hcengineering/process'
import {
AnyComponent,
Button,
Component,
DropdownIntlItem,
DropdownLabelsIntl,
eventToHTMLElement,
IconAdd,
IconClose,
showPopup
} from '@hcengineering/ui'
import view from '@hcengineering/view-resources/src/plugin'
import { createEventDispatcher } from 'svelte'
import ContextSelectorPopup from '../attributeEditors/ContextSelectorPopup.svelte'
import ContextValue from '../attributeEditors/ContextValue.svelte'
import { AnyAttribute } from '@hcengineering/core'
import { Context, Process } from '@hcengineering/process'
import { AnyComponent } from '@hcengineering/ui'
import { Mode, Modes } from '../../query'
import BaseCriteria from './BaseCriteria.svelte'
export let readonly: boolean
export let value: any
@@ -39,174 +27,7 @@
export let attribute: AnyAttribute
export let baseEditor: AnyComponent | undefined
const modes: DropdownIntlItem[] = [
{ id: 'equals', label: view.string.FilterIsEither },
{ id: 'greaterThan', label: view.string.After },
{ id: 'lessThan', label: view.string.Before }
]
let mode =
value == null || typeof value === 'number' ? 'equals' : value?.$gt !== undefined ? 'greaterThan' : 'lessThan'
const dispatch = createEventDispatcher()
let contextValue: SelectedContext | undefined = undefined
let val: any = undefined
function selectContext (e: MouseEvent): void {
showPopup(
ContextSelectorPopup,
{
process,
masterTag: process.masterTag,
context,
attribute,
onSelect
},
eventToHTMLElement(e)
)
}
$: parseValue(value)
function parseValue (value: any) {
if (typeof value === 'number') {
mode = 'equals'
val = value
} else if (value?.$gt !== undefined) {
mode = 'greaterThan'
val = value.$gt
} else if (value?.$lt !== undefined) {
mode = 'lessThan'
val = value.$lt
} else {
mode = 'equals'
val = undefined
}
contextValue = parseContext(val)
}
function onSelect (res: SelectedContext | null): void {
val = res === null ? undefined : '$' + JSON.stringify(res)
buildResult()
}
function onChange (value: any | undefined): void {
val = value
buildResult()
}
function buildResult () {
if (val === undefined || val === '') {
dispatch('change', null)
return
}
let result: any
switch (mode) {
case 'equals':
result = val
break
case 'greaterThan':
result = { $gt: val }
break
case 'lessThan':
result = { $lt: val }
break
}
dispatch('change', result)
}
const modes: Mode[] = [Modes.Equal, Modes.GT, Modes.LT]
</script>
<div class="flex-row-center flex-gap-4">
<DropdownLabelsIntl
items={modes}
selected={mode}
disabled={readonly}
minW0={false}
kind={'no-border'}
width={'100%'}
on:selected={(e) => {
mode = e.detail
buildResult()
}}
/>
<div class="text-input" class:context={contextValue}>
{#if contextValue}
<ContextValue
{process}
masterTag={process.masterTag}
{contextValue}
{context}
{attribute}
category={'attribute'}
attrClass={core.class.TypeDate}
on:update={(e) => {
onSelect(e.detail)
}}
/>
{:else}
<div class="w-full">
{#if baseEditor}
<Component
is={baseEditor}
props={{
label: attribute?.label,
placeholder: attribute?.label,
kind: 'ghost',
size: 'large',
width: '100%',
justify: 'left',
readonly,
type: attribute?.type,
value: val,
onChange,
focus
}}
/>
{/if}
</div>
{/if}
<div class="button flex-row-center">
<Button
icon={IconAdd}
kind="ghost"
on:click={(e) => {
selectContext(e)
}}
/>
<Button
icon={IconClose}
kind="ghost"
on:click={() => {
dispatch('delete')
}}
/>
</div>
</div>
</div>
<style lang="scss">
.text-input {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border: 1px solid var(--theme-refinput-border);
border-radius: 0.375rem;
max-width: 100%;
width: 100%;
.button {
flex-shrink: 0;
}
&.context {
background: #3575de33;
padding-left: 0.75rem;
border-color: var(--primary-button-default);
}
}
</style>
<BaseCriteria {readonly} {value} {process} {context} {attribute} {baseEditor} {modes} on:change on:delete />
@@ -14,23 +14,11 @@
-->
<script lang="ts">
import core, { AnyAttribute } from '@hcengineering/core'
import { Context, parseContext, Process, SelectedContext } from '@hcengineering/process'
import {
AnyComponent,
Button,
Component,
DropdownIntlItem,
DropdownLabelsIntl,
eventToHTMLElement,
IconAdd,
IconClose,
showPopup
} from '@hcengineering/ui'
import view from '@hcengineering/view-resources/src/plugin'
import { createEventDispatcher } from 'svelte'
import ContextSelectorPopup from '../attributeEditors/ContextSelectorPopup.svelte'
import ContextValue from '../attributeEditors/ContextValue.svelte'
import { AnyAttribute } from '@hcengineering/core'
import { Context, Process } from '@hcengineering/process'
import { AnyComponent } from '@hcengineering/ui'
import { Mode, Modes } from '../../query'
import BaseCriteria from './BaseCriteria.svelte'
export let readonly: boolean
export let value: any
@@ -39,164 +27,7 @@
export let attribute: AnyAttribute
export let baseEditor: AnyComponent | undefined
const modes: DropdownIntlItem[] = [
{ id: 'equals', label: view.string.FilterIsEither },
{ id: 'notEquals', label: view.string.FilterIsNot }
]
let mode = value == null || typeof value === 'string' ? 'equals' : 'notEquals'
const dispatch = createEventDispatcher()
let contextValue: SelectedContext | undefined = undefined
let val: any = undefined
function selectContext (e: MouseEvent): void {
showPopup(
ContextSelectorPopup,
{
process,
masterTag: process.masterTag,
context,
attribute,
onSelect
},
eventToHTMLElement(e)
)
}
$: parseValue(value)
function parseValue (value: any) {
if (typeof value === 'string' || value == null) {
mode = 'equals'
val = value
} else {
mode = 'notEquals'
if (value?.$ne !== undefined) {
val = value.$ne
} else {
val = undefined
}
}
contextValue = parseContext(val)
}
function onSelect (res: SelectedContext | null): void {
val = res === null ? undefined : '$' + JSON.stringify(res)
buildResult()
}
function onChange (value: any | undefined): void {
val = value
buildResult()
}
function buildResult () {
if (val === undefined || val === '') {
dispatch('change', null)
return
}
let result: any
if (mode === 'equals') {
result = val
} else {
result = { $ne: val }
}
dispatch('change', result)
}
const modes: Mode[] = [Modes.Equal, Modes.NotEqual]
</script>
<div class="flex-row-center flex-gap-4">
<DropdownLabelsIntl
items={modes}
selected={mode}
disabled={readonly}
minW0={false}
kind={'no-border'}
width={'100%'}
on:selected={(e) => {
mode = e.detail
buildResult()
}}
/>
<div class="text-input" class:context={contextValue}>
{#if contextValue}
<ContextValue
{process}
masterTag={process.masterTag}
{contextValue}
{context}
{attribute}
category={'attribute'}
attrClass={core.class.EnumOf}
on:update={(e) => {
onSelect(e.detail)
}}
/>
{:else}
<div class="w-full">
{#if baseEditor}
<Component
is={baseEditor}
props={{
label: attribute?.label,
placeholder: attribute?.label,
kind: 'ghost',
size: 'large',
width: '100%',
justify: 'left',
readonly,
type: attribute?.type,
value: val,
onChange,
focus
}}
/>
{/if}
</div>
{/if}
<div class="button flex-row-center">
<Button
icon={IconAdd}
kind="ghost"
on:click={(e) => {
selectContext(e)
}}
/>
<Button
icon={IconClose}
kind="ghost"
on:click={() => {
dispatch('delete')
}}
/>
</div>
</div>
</div>
<style lang="scss">
.text-input {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border: 1px solid var(--theme-refinput-border);
border-radius: 0.375rem;
max-width: 100%;
width: 100%;
.button {
flex-shrink: 0;
}
&.context {
background: #3575de33;
padding-left: 0.75rem;
border-color: var(--primary-button-default);
}
}
</style>
<BaseCriteria {readonly} {value} {process} {context} {attribute} {baseEditor} {modes} on:change on:delete />
@@ -14,23 +14,11 @@
-->
<script lang="ts">
import core, { AnyAttribute } from '@hcengineering/core'
import { Context, parseContext, Process, SelectedContext } from '@hcengineering/process'
import {
AnyComponent,
Button,
Component,
DropdownIntlItem,
DropdownLabelsIntl,
eventToHTMLElement,
IconAdd,
IconClose,
showPopup
} from '@hcengineering/ui'
import view from '@hcengineering/view-resources/src/plugin'
import { createEventDispatcher } from 'svelte'
import ContextSelectorPopup from '../attributeEditors/ContextSelectorPopup.svelte'
import ContextValue from '../attributeEditors/ContextValue.svelte'
import { AnyAttribute } from '@hcengineering/core'
import { Context, Process } from '@hcengineering/process'
import { AnyComponent } from '@hcengineering/ui'
import { Mode, Modes } from '../../query'
import BaseCriteria from './BaseCriteria.svelte'
export let readonly: boolean
export let value: any
@@ -39,173 +27,7 @@
export let attribute: AnyAttribute
export let baseEditor: AnyComponent | undefined
const modes: DropdownIntlItem[] = [
{ id: 'equals', label: view.string.FilterIsEither },
{ id: 'greaterThan', label: view.string.FilterGreaterThan },
{ id: 'lessThan', label: view.string.FilterLessThan }
]
let mode =
value == null || typeof value === 'number' ? 'equals' : value?.$gt !== undefined ? 'greaterThan' : 'lessThan'
const dispatch = createEventDispatcher()
let contextValue: SelectedContext | undefined = undefined
let val: any = undefined
function selectContext (e: MouseEvent): void {
showPopup(
ContextSelectorPopup,
{
process,
masterTag: process.masterTag,
context,
attribute,
onSelect
},
eventToHTMLElement(e)
)
}
$: parseValue(value)
function parseValue (value: any) {
if (typeof value === 'number' || value == null) {
mode = 'equals'
val = value
} else if (value?.$gt !== undefined) {
mode = 'greaterThan'
val = value.$gt
} else if (value?.$lt !== undefined) {
mode = 'lessThan'
val = value.$lt
} else {
val = undefined
}
contextValue = parseContext(val)
}
function onSelect (res: SelectedContext | null): void {
val = res === null ? undefined : '$' + JSON.stringify(res)
buildResult()
}
function onChange (value: any | undefined): void {
val = value
buildResult()
}
function buildResult () {
if (val === undefined || val === '') {
dispatch('change', null)
return
}
let result: any
switch (mode) {
case 'equals':
result = val
break
case 'greaterThan':
result = { $gt: val }
break
case 'lessThan':
result = { $lt: val }
break
}
dispatch('change', result)
}
const modes: Mode[] = [Modes.Equal, Modes.GT, Modes.LT]
</script>
<div class="flex-row-center flex-gap-4">
<DropdownLabelsIntl
items={modes}
selected={mode}
disabled={readonly}
minW0={false}
kind={'no-border'}
width={'100%'}
on:selected={(e) => {
mode = e.detail
buildResult()
}}
/>
<div class="text-input" class:context={contextValue}>
{#if contextValue}
<ContextValue
{process}
masterTag={process.masterTag}
{contextValue}
{context}
{attribute}
category={'attribute'}
attrClass={core.class.TypeNumber}
on:update={(e) => {
onSelect(e.detail)
}}
/>
{:else}
<div class="w-full">
{#if baseEditor}
<Component
is={baseEditor}
props={{
label: attribute?.label,
placeholder: attribute?.label,
kind: 'ghost',
size: 'large',
width: '100%',
justify: 'left',
readonly,
type: attribute?.type,
value: val,
onChange,
focus
}}
/>
{/if}
</div>
{/if}
<div class="button flex-row-center">
<Button
icon={IconAdd}
kind="ghost"
on:click={(e) => {
selectContext(e)
}}
/>
<Button
icon={IconClose}
kind="ghost"
on:click={() => {
dispatch('delete')
}}
/>
</div>
</div>
</div>
<style lang="scss">
.text-input {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border: 1px solid var(--theme-refinput-border);
border-radius: 0.375rem;
max-width: 100%;
width: 100%;
.button {
flex-shrink: 0;
}
&.context {
background: #3575de33;
padding-left: 0.75rem;
border-color: var(--primary-button-default);
}
}
</style>
<BaseCriteria {readonly} {value} {process} {context} {attribute} {baseEditor} {modes} on:change on:delete />
@@ -14,24 +14,11 @@
-->
<script lang="ts">
import core, { AnyAttribute } from '@hcengineering/core'
import { Context, parseContext, Process, SelectedContext } from '@hcengineering/process'
import {
AnyComponent,
Button,
Component,
DropdownIntlItem,
DropdownLabelsIntl,
eventToHTMLElement,
IconAdd,
IconClose,
showPopup
} from '@hcengineering/ui'
import view from '@hcengineering/view-resources/src/plugin'
import { createEventDispatcher } from 'svelte'
import ContextSelectorPopup from '../attributeEditors/ContextSelectorPopup.svelte'
import ContextValue from '../attributeEditors/ContextValue.svelte'
import { getAttributePresenterClass, getClient } from '@hcengineering/presentation'
import { AnyAttribute } from '@hcengineering/core'
import { Context, Process } from '@hcengineering/process'
import { AnyComponent } from '@hcengineering/ui'
import { Mode, Modes } from '../../query'
import BaseCriteria from './BaseCriteria.svelte'
export let readonly: boolean
export let value: any
@@ -40,169 +27,7 @@
export let attribute: AnyAttribute
export let baseEditor: AnyComponent | undefined
const modes: DropdownIntlItem[] = [
{ id: 'equals', label: view.string.FilterIsEither },
{ id: 'notEquals', label: view.string.FilterIsNot }
]
let mode = value == null || typeof value === 'string' ? 'equals' : 'notEquals'
const dispatch = createEventDispatcher()
let contextValue: SelectedContext | undefined = undefined
let val: any = undefined
function selectContext (e: MouseEvent): void {
showPopup(
ContextSelectorPopup,
{
process,
masterTag: process.masterTag,
context,
attribute,
onSelect
},
eventToHTMLElement(e)
)
}
$: parseValue(value)
function parseValue (value: any) {
if (typeof value === 'string' || value == null) {
mode = 'equals'
val = value
} else {
mode = 'notEquals'
if (value?.$ne !== undefined) {
val = value.$ne
} else {
val = undefined
}
}
contextValue = parseContext(val)
}
function onSelect (res: SelectedContext | null): void {
val = res === null ? undefined : '$' + JSON.stringify(res)
buildResult()
}
function onChange (value: any | undefined): void {
val = value
buildResult()
}
const client = getClient()
const hierarchy = client.getHierarchy()
$: presenterClass = getAttributePresenterClass(hierarchy, attribute.type)
function buildResult () {
if (val === undefined || val === '') {
dispatch('change', null)
return
}
let result: any
if (mode === 'equals') {
result = val
} else {
result = { $ne: val }
}
dispatch('change', result)
}
const modes: Mode[] = [Modes.Equal, Modes.NotEqual]
</script>
<div class="flex-row-center flex-gap-4">
<DropdownLabelsIntl
items={modes}
selected={mode}
disabled={readonly}
minW0={false}
kind={'no-border'}
width={'100%'}
on:selected={(e) => {
mode = e.detail
buildResult()
}}
/>
<div class="text-input" class:context={contextValue}>
{#if contextValue}
<ContextValue
{process}
masterTag={process.masterTag}
{contextValue}
{context}
{attribute}
category={presenterClass.category}
attrClass={presenterClass.attrClass}
on:update={(e) => {
onSelect(e.detail)
}}
/>
{:else}
<div class="w-full">
{#if baseEditor}
<Component
is={baseEditor}
props={{
label: attribute?.label,
placeholder: attribute?.label,
kind: 'ghost',
size: 'large',
width: '100%',
justify: 'left',
readonly,
type: attribute?.type,
value: val,
onChange,
focus
}}
/>
{/if}
</div>
{/if}
<div class="button flex-row-center">
<Button
icon={IconAdd}
kind="ghost"
on:click={(e) => {
selectContext(e)
}}
/>
<Button
icon={IconClose}
kind="ghost"
on:click={() => {
dispatch('delete')
}}
/>
</div>
</div>
</div>
<style lang="scss">
.text-input {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border: 1px solid var(--theme-refinput-border);
border-radius: 0.375rem;
max-width: 100%;
width: 100%;
.button {
flex-shrink: 0;
}
&.context {
background: #3575de33;
padding-left: 0.75rem;
border-color: var(--primary-button-default);
}
}
</style>
<BaseCriteria {readonly} {value} {process} {context} {attribute} {baseEditor} {modes} on:change on:delete />
@@ -14,23 +14,11 @@
-->
<script lang="ts">
import core, { AnyAttribute } from '@hcengineering/core'
import { Context, parseContext, Process, SelectedContext } from '@hcengineering/process'
import {
AnyComponent,
Button,
Component,
DropdownIntlItem,
DropdownLabelsIntl,
eventToHTMLElement,
IconAdd,
IconClose,
showPopup
} from '@hcengineering/ui'
import view from '@hcengineering/view-resources/src/plugin'
import { createEventDispatcher } from 'svelte'
import ContextSelectorPopup from '../attributeEditors/ContextSelectorPopup.svelte'
import ContextValue from '../attributeEditors/ContextValue.svelte'
import { AnyAttribute } from '@hcengineering/core'
import { Context, Process } from '@hcengineering/process'
import { AnyComponent } from '@hcengineering/ui'
import { Mode, Modes } from '../../query'
import BaseCriteria from './BaseCriteria.svelte'
export let readonly: boolean
export let value: any
@@ -39,164 +27,7 @@
export let attribute: AnyAttribute
export let baseEditor: AnyComponent | undefined
const modes: DropdownIntlItem[] = [
{ id: 'equals', label: view.string.FilterIsEither },
{ id: 'contains', label: view.string.Contains }
]
let mode = value == null || typeof value === 'string' ? 'equals' : 'contains'
const dispatch = createEventDispatcher()
let contextValue: SelectedContext | undefined = undefined
let val: any = undefined
function selectContext (e: MouseEvent): void {
showPopup(
ContextSelectorPopup,
{
process,
masterTag: process.masterTag,
context,
attribute,
onSelect
},
eventToHTMLElement(e)
)
}
$: parseValue(value)
function parseValue (value: any) {
if (typeof value === 'string' || value == null) {
mode = 'equals'
val = value
} else {
mode = 'contains'
if (value?.$like !== undefined) {
val = value.$like.startsWith('%') && value.$like.endsWith('%') ? value.$like.slice(1, -1) : value.$like
} else {
val = undefined
}
}
contextValue = parseContext(val)
}
function onSelect (res: SelectedContext | null): void {
val = res === null ? undefined : '$' + JSON.stringify(res)
buildResult()
}
function onChange (value: any | undefined): void {
val = value
buildResult()
}
function buildResult () {
if (val === undefined || val === '') {
dispatch('change', null)
return
}
let result: any
if (mode === 'equals') {
result = val
} else {
result = { $like: '%' + val + '%' }
}
dispatch('change', result)
}
const modes: Mode[] = [Modes.Equal, Modes.StringContains]
</script>
<div class="flex-row-center flex-gap-4">
<DropdownLabelsIntl
items={modes}
selected={mode}
disabled={readonly}
minW0={false}
kind={'no-border'}
width={'100%'}
on:selected={(e) => {
mode = e.detail
buildResult()
}}
/>
<div class="text-input" class:context={contextValue}>
{#if contextValue}
<ContextValue
{process}
masterTag={process.masterTag}
{contextValue}
{context}
{attribute}
category={'attribute'}
attrClass={core.class.TypeString}
on:update={(e) => {
onSelect(e.detail)
}}
/>
{:else}
<div class="w-full">
{#if baseEditor}
<Component
is={baseEditor}
props={{
label: attribute?.label,
placeholder: attribute?.label,
kind: 'ghost',
size: 'large',
width: '100%',
justify: 'left',
readonly,
type: attribute?.type,
value: val,
onChange,
focus
}}
/>
{/if}
</div>
{/if}
<div class="button flex-row-center">
<Button
icon={IconAdd}
kind="ghost"
on:click={(e) => {
selectContext(e)
}}
/>
<Button
icon={IconClose}
kind="ghost"
on:click={() => {
dispatch('delete')
}}
/>
</div>
</div>
</div>
<style lang="scss">
.text-input {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border: 1px solid var(--theme-refinput-border);
border-radius: 0.375rem;
max-width: 100%;
width: 100%;
.button {
flex-shrink: 0;
}
&.context {
background: #3575de33;
padding-left: 0.75rem;
border-color: var(--primary-button-default);
}
}
</style>
<BaseCriteria {readonly} {value} {process} {context} {attribute} {baseEditor} {modes} on:change on:delete />
@@ -0,0 +1,83 @@
<!--
// 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 { getAttributePresenterClass, getClient } from '@hcengineering/presentation'
import { parseContext, Process } from '@hcengineering/process'
import { AnyComponent, Component, Label } from '@hcengineering/ui'
import { AttributeCategory } from '@hcengineering/view'
import { findAttributePresenter } from '@hcengineering/view-resources'
import view from '@hcengineering/view-resources/src/plugin'
import { Mode, Modes, parseValue } from '../../query'
import ContextValuePresenter from '../attributeEditors/ContextValuePresenter.svelte'
import { getContext } from '../../utils'
import core from '@hcengineering/core'
export let process: Process
export let key: string
export let value: any
const client = getClient()
const hierarchy = client.getHierarchy()
$: attribute = client.getHierarchy().findAttribute(process.masterTag, key)
$: [val, selectedMode] = parseValue(Object.values(Modes), value)
$: presenterClass = attribute !== undefined ? getAttributePresenterClass(hierarchy, attribute.type) : undefined
function isArraySize (category: AttributeCategory | undefined, selectedMode: Mode): boolean {
return category === 'array' && selectedMode.id.startsWith('size')
}
function getPresenter (key: string): AnyComponent | undefined {
const isArraySizeCriteria = isArraySize(presenterClass?.category, selectedMode)
if (isArraySizeCriteria) {
return view.component.NumberPresenter
}
const res = findAttributePresenter(client, process.masterTag, key)
return res
}
$: presenter = getPresenter(key)
$: contextValue = parseContext(val)
$: context =
presenterClass !== undefined
? isArraySize(presenterClass.category, selectedMode)
? getContext(client, process, core.class.TypeNumber, 'attribute')
: getContext(client, process, presenterClass.attrClass, presenterClass.category)
: undefined
</script>
{#if attribute}
<div class="title">
<Label label={attribute.label} />
</div>
<Label label={selectedMode.label} />
{#if presenter}
{#if contextValue && context}
<ContextValuePresenter {contextValue} {context} {process} />
{:else}
<Component is={presenter} props={{ value: val, readonly: true }} />
{/if}
{/if}
{/if}
<style lang="scss">
.title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--theme-caption-color);
}
</style>
@@ -14,11 +14,12 @@
-->
<script lang="ts">
import { Card } from '@hcengineering/card'
import { IntlString } from '@hcengineering/platform'
import { AnyAttribute } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { MethodParams, Process, Step } from '@hcengineering/process'
import { Icon, IconError, Label, tooltip } from '@hcengineering/ui'
import plugin from '../../plugin'
import UpdateAttributePresenter from './UpdateAttributePresenter.svelte'
export let step: Step<Card>
export let process: Process
@@ -26,36 +27,19 @@
const client = getClient()
$: method = client.getModel().findAllSync(plugin.class.Method, { _id: step.methodId })[0]
$: attributes = getAttributes(params, process)
function getAttributes (params: Record<string, any>, process: Process): IntlString[] {
const res: IntlString[] = []
for (const key in params) {
if (params[key] !== undefined) {
const attr = client.getHierarchy().findAttribute(process.masterTag, key)
if (attr?.label !== undefined) {
res.push(attr.label)
}
}
}
return res
}
</script>
{#if attributes.length === 0}
{#if Object.keys(params).length === 0}
<div class="mr-2" use:tooltip={{ label: plugin.string.NoAttributesForUpdate }}>
<Icon icon={IconError} size="medium" />
</div>
{/if}
<div class="flex-row-center flex-gap-1">
<Label label={method.label} />
{#if attributes.length > 0}
{#if Object.keys(params).length > 0}
:
{#each attributes as attr}
<div class="title">
<Label label={attr} />
</div>
{#each Object.entries(params) as [key, value]}
<UpdateAttributePresenter {process} {key} {value} />
{/each}
{/if}
</div>
@@ -14,47 +14,16 @@
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { Trigger, Process } from '@hcengineering/process'
import { Label } from '@hcengineering/ui'
import { Process } from '@hcengineering/process'
import UpdateAttributePresenter from '../presenters/UpdateAttributePresenter.svelte'
export let process: Process
export let params: Record<string, any>
const client = getClient()
$: attributes = getAttributes(params, process)
function getAttributes (params: Record<string, any>, process: Process): IntlString[] {
const res: IntlString[] = []
for (const key in params) {
if (params[key] !== undefined) {
const attr = client.getHierarchy().findAttribute(process.masterTag, key)
if (attr?.label !== undefined) {
res.push(attr.label)
}
}
}
return res
}
</script>
{#if attributes.length > 0}
{#if Object.keys(params).length > 0}
:
{#each attributes as attr}
<div class="title">
<Label label={attr} />
</div>
{#each Object.entries(params) as [key, value]}
<UpdateAttributePresenter {process} {key} {value} />
{/each}
{/if}
<style lang="scss">
.title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--theme-caption-color);
}
</style>
@@ -0,0 +1,43 @@
<!--
// 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 { parseContext, Process, SelectedContext, SelectedExecutonContext } from '@hcengineering/process'
import ui, { Label } from '@hcengineering/ui'
import ExecutionContextPresenter from '../attributeEditors/ExecutionContextPresenter.svelte'
export let process: Process
export let params: Record<string, any>
$: context = getContext(params._id)
function getContext (value: string | undefined): SelectedExecutonContext | undefined {
if (value === undefined) return
const context = parseContext(value)
if (context !== undefined && isExecutionContext(context)) {
return context
}
}
function isExecutionContext (context: SelectedContext): context is SelectedExecutonContext {
return context.type === 'context'
}
</script>
{#if context === undefined}
<Label label={ui.string.NotSelected} />
{:else}
<ExecutionContextPresenter {process} contextValue={context} />
{/if}
@@ -53,12 +53,12 @@
</script>
{#if trigger}
<div class="flex flex-gap-2" use:tooltip={{ label: trigger.label }}>
<div class="flex-row-center flex-gap-1" use:tooltip={{ label: trigger.label }}>
<Icon icon={error ? IconError : trigger.icon} size={'medium'} />
{#if withLabel}
<Label label={trigger.label} />
{#if trigger.presenter}
<Component is={trigger.presenter} props={{ value, process, params }} />
<Component is={trigger.presenter} props={{ process, params }} />
{/if}
{/if}
</div>