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
+2
View File
@@ -1021,6 +1021,7 @@ export function createModel (builder: Builder): void {
label: process.string.OnToDoDone,
icon: process.icon.ToDo,
editor: process.component.ToDoCloseEditor,
presenter: process.component.ToDoSettingPresenter,
requiredParams: ['_id'],
checkFunction: process.triggerCheck.ToDo,
init: false
@@ -1035,6 +1036,7 @@ export function createModel (builder: Builder): void {
label: process.string.OnToDoCancelled,
icon: process.icon.ToDoRemove,
editor: process.component.ToDoRemoveEditor,
presenter: process.component.ToDoSettingPresenter,
requiredParams: ['_id'],
checkFunction: process.triggerCheck.ToDo,
init: false
-1
View File
@@ -40,7 +40,6 @@ export default mergeIds(processId, process, {
ContinueExecution: '' as Ref<Action<Doc, any>>
},
string: {
Done: '' as IntlString,
ConfigLabel: '' as IntlString,
ConfigDescription: '' as IntlString,
LogAction: '' as IntlString
+4 -2
View File
@@ -20,6 +20,7 @@
import ErrorPresenter from './ErrorPresenter.svelte'
import Loading from './Loading.svelte'
import ErrorBoundary from './internal/ErrorBoundary'
import { clone } from '@hcengineering/core'
// Reference to rendered component instance
export let innerRef: SvelteComponent | undefined = undefined
@@ -31,7 +32,6 @@
export let disabled: boolean = false
let _is: AnyComponent | AnySvelteComponent = is
let _props: any = props
// See https://github.com/sveltejs/svelte/issues/4068
// When passing undefined prop value, then Svelte uses default value only first time when
@@ -39,9 +39,11 @@
// Here we filter out undefined values from props on updates to ensure we don't overwrite them.
const filterDefaultUndefined = (pnew: any, pold: any): any =>
pnew != null
? Object.fromEntries(Object.entries(pnew).filter(([k, v]) => v !== undefined || pold?.[k] !== undefined))
? Object.fromEntries(Object.entries(clone(pnew)).filter(([k, v]) => v !== undefined || pold?.[k] !== undefined))
: pnew
let _props: any = filterDefaultUndefined(props, props)
$: if (!deepEqual(_is, is)) {
_is = is
}
+2 -1
View File
@@ -95,7 +95,8 @@
"Insert": "Vložit",
"Remove": "Odstranit",
"RemoveFirst": "Odstranit první",
"RemoveLast": "Odstranit poslední"
"RemoveLast": "Odstranit poslední",
"Cancelled": "Zrušeno"
},
"error": {
"MethodNotFound": "Metoda nenalezena: {methodId}",
+2 -1
View File
@@ -95,7 +95,8 @@
"Insert": "Einfügen",
"Remove": "Entfernen",
"RemoveFirst": "Erstes entfernen",
"RemoveLast": "Letztes entfernen"
"RemoveLast": "Letztes entfernen",
"Cancelled": "Abgebrochen"
},
"error": {
"MethodNotFound": "Methode nicht gefunden: {methodId}",
+2 -1
View File
@@ -100,7 +100,8 @@
"Insert": "Insert",
"Remove": "Remove",
"RemoveFirst": "Remove first",
"RemoveLast": "Remove last"
"RemoveLast": "Remove last",
"Cancelled": "Cancelled"
},
"error": {
"MethodNotFound": "Method not found: {methodId}",
+2 -1
View File
@@ -100,7 +100,8 @@
"ArraySizeGt": "Tamaño mayor que",
"ArraySizeGte": "Tamaño mayor o igual que",
"ArraySizeLt": "Tamaño menor que",
"ArraySizeLte": "Tamaño menor o igual que"
"ArraySizeLte": "Tamaño menor o igual que",
"Cancelled": "Cancelado"
},
"error": {
"MethodNotFound": "Método no encontrado: {methodId}",
+2 -1
View File
@@ -100,7 +100,8 @@
"ArraySizeGt": "Taille supérieure à",
"ArraySizeGte": "Taille supérieure ou égale à",
"ArraySizeLt": "Taille inférieure à",
"ArraySizeLte": "Taille inférieure ou égale à"
"ArraySizeLte": "Taille inférieure ou égale à",
"Cancelled": "Annulé"
},
"error": {
"MethodNotFound": "Méthode introuvable : {methodId}",
+2 -1
View File
@@ -100,7 +100,8 @@
"ArraySizeGt": "Dimensione maggiore di",
"ArraySizeGte": "Dimensione maggiore o uguale a",
"ArraySizeLt": "Dimensione minore di",
"ArraySizeLte": "Dimensione minore o uguale a"
"ArraySizeLte": "Dimensione minore o uguale a",
"Cancelled": "Annullato"
},
"error": {
"MethodNotFound": "Metodo non trovato: {methodId}",
+2 -1
View File
@@ -99,7 +99,8 @@
"ArraySizeGt": "サイズがより大きい",
"ArraySizeGte": "サイズが以上",
"ArraySizeLt": "サイズがより小さい",
"ArraySizeLte": "サイズが以下"
"ArraySizeLte": "サイズが以下",
"Cancelled": "キャンセル済み"
},
"error": {
"MethodNotFound": "メソッドが見つかりません: {methodId}",
+2 -1
View File
@@ -100,7 +100,8 @@
"ArraySizeGt": "Tamanho maior que",
"ArraySizeGte": "Tamanho maior ou igual a",
"ArraySizeLt": "Tamanho menor que",
"ArraySizeLte": "Tamanho menor ou igual a"
"ArraySizeLte": "Tamanho menor ou igual a",
"Cancelled": "Cancelado"
},
"error": {
"MethodNotFound": "Método não encontrado: {methodId}",
+6 -5
View File
@@ -96,11 +96,12 @@
"Each": "Каждый",
"CreateCard": "Создать карточку",
"OnCardUpdate": "При обновлении карточки",
"ArraySizeEquals": "Размер равен",
"ArraySizeGt": "Размер больше чем",
"ArraySizeGte": "Размер больше или равен",
"ArraySizeLt": "Размер меньше чем",
"ArraySizeLte": "Размер меньше или равен"
"ArraySizeEquals": "Количество равно",
"ArraySizeGt": "Количество больше",
"ArraySizeGte": "Количество больше или равно",
"ArraySizeLt": "Количество меньше",
"ArraySizeLte": "Количество меньше или равно",
"Cancelled": "Отменено"
},
"error": {
"MethodNotFound": "Метод не найден: {methodId}",
+2 -1
View File
@@ -100,7 +100,8 @@
"ArraySizeGt": "大小大于",
"ArraySizeGte": "大小大于等于",
"ArraySizeLt": "大小小于",
"ArraySizeLte": "大小小于等于"
"ArraySizeLte": "大小小于等于",
"Cancelled": "已取消"
},
"error": {
"MethodNotFound": "找不到方法:{methodId}",
@@ -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>
+5 -1
View File
@@ -65,9 +65,12 @@ import RefCriteria from './components/criterias/RefCriteria.svelte'
import EnumCriteria from './components/criterias/EnumCriteria.svelte'
import ArrayCriteria from './components/criterias/ArrayCriteria.svelte'
import CardUpdatePresenter from './components/settings/CardUpdatePresenter.svelte'
import ToDoSettingPresenter from './components/settings/ToDoPresenter.svelte'
import { ProcessMiddleware } from './middleware'
import { continueExecution, showDoneQuery, todoTranstionCheck, updateCardTranstionCheck } from './utils'
export * from './query'
export default async (): Promise<Resources> => ({
actionImpl: {
ContinueExecution: continueExecution
@@ -110,7 +113,8 @@ export default async (): Promise<Resources> => ({
CreateCardPresenter,
AddRelationEditor,
AddRelationPresenter,
CardUpdatePresenter
CardUpdatePresenter,
ToDoSettingPresenter
},
criteriaEditor: {
DateCriteria,
+10 -2
View File
@@ -62,7 +62,8 @@ export default mergeIds(processId, process, {
AddRelationEditor: '' as AnyComponent,
AddRelationPresenter: '' as AnyComponent,
CardUpdateEditor: '' as AnyComponent,
CardUpdatePresenter: '' as AnyComponent
CardUpdatePresenter: '' as AnyComponent,
ToDoSettingPresenter: '' as AnyComponent
},
criteriaEditor: {
StringCriteria: '' as AnyComponent,
@@ -173,6 +174,13 @@ export default mergeIds(processId, process, {
End: '' as IntlString,
Started: '' as IntlString,
Each: '' as IntlString,
CreateCard: '' as IntlString
CreateCard: '' as IntlString,
Done: '' as IntlString,
Cancelled: '' as IntlString,
ArraySizeEquals: '' as IntlString,
ArraySizeGt: '' as IntlString,
ArraySizeGte: '' as IntlString,
ArraySizeLt: '' as IntlString,
ArraySizeLte: '' as IntlString
}
})
+174
View File
@@ -0,0 +1,174 @@
// 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.
import { type QuerySelector } from '@hcengineering/core'
import { getEmbeddedLabel, type IntlString } from '@hcengineering/platform'
import view from '@hcengineering/view-resources/src/plugin'
export interface Mode {
id: string
label: IntlString
query: QuerySelector<any> | string
parse?: (value: any) => any
}
const Equal: Mode = {
id: 'equals',
label: getEmbeddedLabel('='),
query: '$val'
}
const NotEqual: Mode = {
id: 'notEquals',
label: getEmbeddedLabel('≠'),
query: { $ne: '$val' }
}
const GT: Mode = {
id: 'greaterThan',
label: getEmbeddedLabel('>'),
query: { $gt: '$val' as any }
}
const LT: Mode = {
id: 'lessThan',
label: getEmbeddedLabel('<'),
query: { $lt: '$val' as any }
}
const StringContains: Mode = {
id: 'contains',
label: view.string.Contains,
query: { $like: '%$val%' },
parse: (value: any): any => {
if (typeof value !== 'string') return value
return value.startsWith('%') && value.endsWith('%') ? value.slice(1, -1) : value
}
}
const ArrayAll: Mode = {
id: 'arrayAll',
label: view.string.FilterArrayAll,
query: { $all: '$val' }
}
const ArrayAny: Mode = {
id: 'arrayAny',
label: view.string.FilterArrayAny,
query: { $in: '$val' as any }
}
const ArrayNotIncludes: Mode = {
id: 'arrayNotIncludes',
label: view.string.NotContains,
query: { $nin: '$val' as any }
}
const ArraySizeEquals: Mode = {
id: 'sizeEquals',
label: getEmbeddedLabel('='),
query: { $size: '$val' as any }
}
const ArraySizeGt: Mode = {
id: 'sizeGt',
label: getEmbeddedLabel('>'),
query: { $size: { $gt: '$val' as any } }
}
const ArraySizeGte: Mode = {
id: 'sizeGte',
label: getEmbeddedLabel('≥'),
query: { $size: { $gte: '$val' as any } }
}
const ArraySizeLt: Mode = {
id: 'sizeLt',
label: getEmbeddedLabel('<'),
query: { $size: { $lt: '$val' as any } }
}
const ArraySizeLte: Mode = {
id: 'sizeLte',
label: getEmbeddedLabel('≤'),
query: { $size: { $lte: '$val' as any } }
}
export const Modes: Record<string, Mode> = {
Equal,
NotEqual,
StringContains,
GT,
LT,
ArrayAll,
ArrayAny,
ArrayNotIncludes,
ArraySizeEquals,
ArraySizeGt,
ArraySizeGte,
ArraySizeLt,
ArraySizeLte
} as const
export function parseValue (modes: Mode[], value: any): [any, Mode] {
if (value == null) {
return [value, modes[0]]
}
for (const mode of modes) {
if (typeof mode.query === 'string' && typeof value !== 'object') {
return [value, mode]
}
let obj1: any = mode.query
let obj2: any = value
while (typeof obj1 === 'object' && typeof obj2 === 'object' && Object.keys(obj1)[0] === Object.keys(obj2)[0]) {
const key1 = Object.keys(obj1)[0]
const key2 = Object.keys(obj2)[0]
if (typeof obj1[key1] === 'string' && (typeof obj2[key2] !== 'object' || Array.isArray(obj2[key2]))) {
if (mode.parse !== undefined) {
return [mode.parse(obj2[key2]), mode]
}
return [obj2[key2], mode]
}
obj1 = obj1[key1]
obj2 = obj2[key2]
}
}
return [value, modes[0]]
}
export function buildResult (mode: Mode, value: any): any {
let q = mode.query
if (typeof q !== 'object') return value
const result: any = {}
let res = result
while (true) {
if (typeof q === 'object') {
const key = Object.keys(q)[0]
const v: any = (q as any)[key]
if (typeof v !== 'object') {
if (typeof value === 'string') {
res[key] = v.replace('$val', value)
} else {
res[key] = value
}
return result
}
q = v
res[key] = {}
res = res[key]
} else {
break
}
}
return result
}
+2 -2
View File
@@ -107,8 +107,8 @@
"Between": "je mezi",
"ShowColors": "Použít barvy",
"Show": "Zobrazit",
"FilterArrayAll": "zahrnuje vše",
"FilterArrayAny": "zahrnuje některé",
"FilterArrayAll": "vše",
"FilterArrayAny": "některé",
"Or": "Nebo",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "Kopírovat do schránky",
+2 -2
View File
@@ -107,8 +107,8 @@
"Between": "ist zwischen",
"ShowColors": "Farben verwenden",
"Show": "Anzeigen",
"FilterArrayAll": "enthält alle",
"FilterArrayAny": "enthält beliebige",
"FilterArrayAll": "alle",
"FilterArrayAny": "beliebige",
"Or": "Oder",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "In die Zwischenablage kopieren",
+2 -2
View File
@@ -107,8 +107,8 @@
"Between": "is between",
"ShowColors": "Use colors",
"Show": "Show",
"FilterArrayAll": "includes all",
"FilterArrayAny": "includes any",
"FilterArrayAll": "all",
"FilterArrayAny": "any",
"Or": "Or",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "Copy to clipboard",
+2 -2
View File
@@ -103,8 +103,8 @@
"Between": "está entre",
"ShowColors": "Usar colores",
"Show": "Mostrar",
"FilterArrayAll": "incluye todos",
"FilterArrayAny": "incluye alguno",
"FilterArrayAll": "todos",
"FilterArrayAny": "alguno",
"Or": "O",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "Copiar al portapapeles",
+2 -2
View File
@@ -103,8 +103,8 @@
"Between": "est entre",
"ShowColors": "Utiliser les couleurs",
"Show": "Afficher",
"FilterArrayAll": "inclut tous",
"FilterArrayAny": "inclut n'importe lequel",
"FilterArrayAll": "tous",
"FilterArrayAny": "n'importe lequel",
"Or": "Ou",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "Copier dans le presse-papiers",
+2 -2
View File
@@ -103,8 +103,8 @@
"Between": "è tra",
"ShowColors": "Usa colori",
"Show": "Mostra",
"FilterArrayAll": "include tutti",
"FilterArrayAny": "include qualsiasi",
"FilterArrayAll": "tutti",
"FilterArrayAny": "qualsiasi",
"Or": "O",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "Copia negli appunti",
+2 -2
View File
@@ -103,8 +103,8 @@
"Between": "está entre",
"ShowColors": "Usar cores",
"Show": "Mostrar",
"FilterArrayAll": "inclui todos",
"FilterArrayAny": "inclui qualquer",
"FilterArrayAll": "todos",
"FilterArrayAny": "qualquer",
"Or": "Ou",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "Copiar para área de transferência",
+2 -2
View File
@@ -103,8 +103,8 @@
"Between": "между",
"ShowColors": "Использовать цвета",
"Show": "Отображение",
"FilterArrayAll": "содержит все",
"FilterArrayAny": "содержит любое из",
"FilterArrayAll": "все",
"FilterArrayAny": "любое из",
"Or": "Или",
"HyperlinkPlaceholder": "https://jappleseed.com",
"CopyToClipboard": "Скопировать в буфер обмена",
@@ -37,11 +37,7 @@
}
</script>
<span
class="{oneLine ? 'overflow-label' : 'lines-limit-2'} select-text"
class:fs-bold={accent}
use:tooltip={tooltipParams}
>
<span class={oneLine ? 'overflow-label' : 'lines-limit-2'} class:fs-bold={accent} use:tooltip={tooltipParams}>
{#if Array.isArray(value)}
{#each value as str, i}
<span class:ml-1={i !== 0}><LinkWrapper text={str} /></span>
-5
View File
@@ -64,11 +64,6 @@ export default mergeIds(viewId, view, {
FilterUpdated: '' as IntlString,
FilterLessThan: '' as IntlString,
FilterGreaterThan: '' as IntlString,
ArraySizeEquals: '' as IntlString,
ArraySizeGt: '' as IntlString,
ArraySizeGte: '' as IntlString,
ArraySizeLt: '' as IntlString,
ArraySizeLte: '' as IntlString,
Before: '' as IntlString,
After: '' as IntlString,
Apply: '' as IntlString,
+57 -35
View File
@@ -304,34 +304,33 @@ export function getAttrTypePresenter (hierarchy: Hierarchy, type: Type<any>): An
}
}
export async function getAttributePresenter (
export function findAttributePresenter (
client: Client,
_class: Ref<Class<Obj>>,
key: string,
preserveKey: BuildModelKey,
mixinClass?: Ref<Mixin<CollectionPresenter>>,
mixinClass: Ref<Mixin<CollectionPresenter>> = view.mixin.CollectionPresenter,
_category?: AttributeCategory
): Promise<AttributeModel> {
const actualMixinClass = mixinClass ?? view.mixin.AttributePresenter
): AnyComponent | undefined {
const model = client.getModel()
const hierarchy = client.getHierarchy()
const attribute = hierarchy.getAttribute(_class, key)
let { attrClass, category } = getAttributePresenterClass(hierarchy, attribute.type)
if (_category !== undefined) {
category = _category
}
let overridedPresenter = await client
.getModel()
.findOne(view.class.AttrPresenter, { objectClass: _class, attribute: attribute._id, category })
const attribute = hierarchy.findAttribute(_class, key)
if (attribute === undefined) return
const { attrClass, category } = getAttributePresenterClass(hierarchy, attribute.type)
let overridedPresenter = model.findAllSync(view.class.AttrPresenter, {
objectClass: _class,
attribute: attribute._id,
category
})[0]
if (overridedPresenter === undefined) {
overridedPresenter = await client
.getModel()
.findOne(view.class.AttrPresenter, { attribute: attribute._id, category })
overridedPresenter = model.findAllSync(view.class.AttrPresenter, { attribute: attribute._id, category })[0]
}
if (overridedPresenter !== undefined) {
return overridedPresenter.component
}
const isCollectionAttr = category === 'collection'
const mixin = isCollectionAttr ? view.mixin.CollectionPresenter : actualMixinClass
const mixin = isCollectionAttr ? view.mixin.CollectionPresenter : mixinClass
let presenterMixin: AttributePresenter | CollectionPresenter | undefined = hierarchy.classHierarchyMixin(
attrClass,
@@ -342,28 +341,51 @@ export async function getAttributePresenter (
presenterMixin = hierarchy.classHierarchyMixin(attrClass, view.mixin.AttributePresenter)
}
let presenter: AnySvelteComponent | undefined
const attributePresenter = presenterMixin as AttributePresenter
if (category === 'array' && attributePresenter.arrayPresenter !== undefined) {
return attributePresenter.arrayPresenter
} else if (presenterMixin?.presenter !== undefined) {
return presenterMixin.presenter
} else if (attrClass === core.class.TypeAny) {
const typeAny = attribute.type as TypeAny
return typeAny.presenter
}
}
if (overridedPresenter !== undefined) {
presenter = await getResource(overridedPresenter.component)
export async function getAttributePresenter (
client: Client,
_class: Ref<Class<Obj>>,
key: string,
preserveKey: BuildModelKey,
mixinClass?: Ref<Mixin<CollectionPresenter>>,
_category?: AttributeCategory
): Promise<AttributeModel> {
const actualMixinClass = mixinClass ?? view.mixin.AttributePresenter
const hierarchy = client.getHierarchy()
const attribute = hierarchy.getAttribute(_class, key)
let { attrClass, category } = getAttributePresenterClass(hierarchy, attribute.type)
if (_category !== undefined) {
category = _category
}
if (presenter === undefined) {
const attributePresenter = presenterMixin as AttributePresenter
if (category === 'array' && attributePresenter.arrayPresenter !== undefined) {
presenter = await getResource(attributePresenter.arrayPresenter)
} else if (presenterMixin?.presenter !== undefined) {
presenter = await getResource(presenterMixin.presenter)
} else if (attrClass === core.class.TypeAny) {
const typeAny = attribute.type as TypeAny
presenter = await getResource(typeAny.presenter)
}
}
if (presenter === undefined) {
const presenterRef = findAttributePresenter(client, _class, key, actualMixinClass, category)
if (presenterRef === undefined) {
throw new Error('attribute presenter not found for ' + JSON.stringify(preserveKey))
}
const isCollectionAttr = category === 'collection'
const mixin = isCollectionAttr ? view.mixin.CollectionPresenter : actualMixinClass
let presenterMixin: AttributePresenter | CollectionPresenter | undefined = hierarchy.classHierarchyMixin(
attrClass,
mixin
)
if (presenterMixin?.presenter === undefined && mixinClass != null && mixin === mixinClass) {
presenterMixin = hierarchy.classHierarchyMixin(attrClass, view.mixin.AttributePresenter)
}
const resultKey = preserveKey.sortingKey ?? preserveKey.key
const sortingKey = Array.isArray(resultKey)
? resultKey
@@ -376,7 +398,7 @@ export async function getAttributePresenter (
sortingKey,
_class: attrClass,
label: preserveKey.label ?? attribute.shortLabel ?? attribute.label,
presenter,
presenter: await getResource(presenterRef),
props: preserveKey.props,
displayProps: preserveKey.displayProps,
icon: presenterMixin?.icon,