diff --git a/models/process/src/functions.ts b/models/process/src/functions.ts index 1a6b4c74af..6dc4ac78d8 100644 --- a/models/process/src/functions.ts +++ b/models/process/src/functions.ts @@ -185,6 +185,19 @@ export function defineFunctions (builder: Builder): void { process.function.All ) + builder.createDoc( + process.class.ProcessFunction, + core.space.Model, + { + of: core.class.ArrOf, + category: undefined, + label: process.string.FirstMatchValue, + type: 'reduce', + editor: process.transformEditor.FilterEditor + }, + process.function.FirstMatchValue + ) + builder.createDoc( process.class.ProcessFunction, core.space.Model, @@ -420,6 +433,20 @@ export function defineFunctions (builder: Builder): void { process.function.RemoveLast ) + builder.createDoc( + process.class.ProcessFunction, + core.space.Model, + { + of: core.class.ArrOf, + category: 'array', + label: process.string.Filter, + allowMany: true, + type: 'transform', + editor: process.transformEditor.FilterEditor + }, + process.function.Filter + ) + builder.createDoc( process.class.ProcessFunction, core.space.Model, diff --git a/models/server-process/src/index.ts b/models/server-process/src/index.ts index 0d9c6310fb..0fb5039979 100644 --- a/models/server-process/src/index.ts +++ b/models/server-process/src/index.ts @@ -241,6 +241,14 @@ export function createModel (builder: Builder): void { func: serverProcess.transform.EmptyArray }) + builder.mixin(process.function.FirstMatchValue, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, { + func: serverProcess.transform.FirstMatchValue + }) + + builder.mixin(process.function.Filter, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, { + func: serverProcess.transform.Filter + }) + builder.mixin( process.function.ExecutionEmployeeInitiator, process.class.ProcessFunction, diff --git a/packages/presentation/src/utils.ts b/packages/presentation/src/utils.ts index 10740ca1a0..761cabd422 100644 --- a/packages/presentation/src/utils.ts +++ b/packages/presentation/src/utils.ts @@ -743,7 +743,8 @@ export function findAttributeEditor ( key: KeyedAttribute | string ): AnyComponent | undefined { const hierarchy = client.getHierarchy() - const attribute = typeof key === 'string' ? hierarchy.getAttribute(_class, key) : key.attr + const attribute = typeof key === 'string' ? hierarchy.findAttribute(_class, key) : key.attr + if (attribute === undefined) return if (attribute.type._class === core.class.TypeAny) { const _type: TypeAny = attribute.type as TypeAny diff --git a/plugins/process-assets/lang/cs.json b/plugins/process-assets/lang/cs.json index 1c423e0dea..775e5d8de2 100644 --- a/plugins/process-assets/lang/cs.json +++ b/plugins/process-assets/lang/cs.json @@ -112,7 +112,9 @@ "NewProcessToDo": "Nový Action Item procesu", "EmptyArray": "Prázdné pole", "ExecutionInitiator": "Iniciátor provedení", - "ExecutionStarted": "Provedení spuštěno" + "ExecutionStarted": "Provedení spuštěno", + "Filter": "Filtr", + "FirstMatchValue": "První odpovídající hodnota" }, "error": { "MethodNotFound": "Metoda nenalezena: {methodId}", diff --git a/plugins/process-assets/lang/de.json b/plugins/process-assets/lang/de.json index ec45b94c9e..a4a11a5a2a 100644 --- a/plugins/process-assets/lang/de.json +++ b/plugins/process-assets/lang/de.json @@ -112,7 +112,9 @@ "NewProcessToDo": "Neuer Prozess Action Item", "EmptyArray": "Leeres Array", "ExecutionInitiator": "Ausführungsinitiator", - "ExecutionStarted": "Ausführung gestartet" + "ExecutionStarted": "Ausführung gestartet", + "Filter": "Filter", + "FirstMatchValue": "Erster passender Wert" }, "error": { "MethodNotFound": "Methode nicht gefunden: {methodId}", diff --git a/plugins/process-assets/lang/en.json b/plugins/process-assets/lang/en.json index c318777b9d..07ea656e85 100644 --- a/plugins/process-assets/lang/en.json +++ b/plugins/process-assets/lang/en.json @@ -117,7 +117,9 @@ "NewProcessToDo": "New process Action item", "EmptyArray": "Empty array", "ExecutionInitiator": "Execution initiator", - "ExecutionStarted": "Execution started" + "ExecutionStarted": "Execution started", + "Filter": "Filter", + "FirstMatchValue": "First match value" }, "error": { "MethodNotFound": "Method not found: {methodId}", diff --git a/plugins/process-assets/lang/es.json b/plugins/process-assets/lang/es.json index 5506fc4c3c..f90ddfa1e6 100644 --- a/plugins/process-assets/lang/es.json +++ b/plugins/process-assets/lang/es.json @@ -117,7 +117,9 @@ "NewProcessToDo": "Nueva acción de proceso", "EmptyArray": "Matriz vacía", "ExecutionInitiator": "Iniciador de Ejecución", - "ExecutionStarted": "Ejecución Iniciada" + "ExecutionStarted": "Ejecución Iniciada", + "Filter": "Filtro", + "FirstMatchValue": "Primer valor coincidente" }, "error": { "MethodNotFound": "Método no encontrado: {methodId}", diff --git a/plugins/process-assets/lang/fr.json b/plugins/process-assets/lang/fr.json index 8577746340..a7b6588a82 100644 --- a/plugins/process-assets/lang/fr.json +++ b/plugins/process-assets/lang/fr.json @@ -117,7 +117,9 @@ "NewProcessToDo": "Nouvel Action Item de processus", "EmptyArray": "Tableau vide", "ExecutionInitiator": "Initiateur d'exécution", - "ExecutionStarted": "Exécution démarrée" + "ExecutionStarted": "Exécution démarrée", + "Filter": "Filtrer", + "FirstMatchValue": "Première valeur correspondante" }, "error": { "MethodNotFound": "Méthode introuvable : {methodId}", diff --git a/plugins/process-assets/lang/it.json b/plugins/process-assets/lang/it.json index 15cab6aff7..9ca9109b32 100644 --- a/plugins/process-assets/lang/it.json +++ b/plugins/process-assets/lang/it.json @@ -117,7 +117,9 @@ "NewProcessToDo": "Nuovo Action Item di processo", "EmptyArray": "Array vuoto", "ExecutionInitiator": "Iniziatore dell'esecuzione", - "ExecutionStarted": "Esecuzione avviata" + "ExecutionStarted": "Esecuzione avviata", + "Filter": "Filtro", + "FirstMatchValue": "Primo valore corrispondente" }, "error": { "MethodNotFound": "Metodo non trovato: {methodId}", diff --git a/plugins/process-assets/lang/ja.json b/plugins/process-assets/lang/ja.json index 48eb409c12..0e1bfd1fa0 100644 --- a/plugins/process-assets/lang/ja.json +++ b/plugins/process-assets/lang/ja.json @@ -116,7 +116,9 @@ "NewProcessToDo": "新しいプロセスアクション", "EmptyArray": "空の配列", "ExecutionInitiator": "実行の発起人", - "ExecutionStarted": "実行が開始されました" + "ExecutionStarted": "実行が開始されました", + "Filter": "フィルター", + "FirstMatchValue": "最初の一致する値" }, "error": { "MethodNotFound": "メソッドが見つかりません: {methodId}", diff --git a/plugins/process-assets/lang/pt.json b/plugins/process-assets/lang/pt.json index 8605d2ffdb..93230331c2 100644 --- a/plugins/process-assets/lang/pt.json +++ b/plugins/process-assets/lang/pt.json @@ -117,7 +117,9 @@ "NewProcessToDo": "Nova ação de processo", "EmptyArray": "Array vazio", "ExecutionInitiator": "Iniciador de Execução", - "ExecutionStarted": "Execução Iniciada" + "ExecutionStarted": "Execução Iniciada", + "Filter": "Filtrar", + "FirstMatchValue": "Primeiro Valor Correspondente" }, "error": { "MethodNotFound": "Método não encontrado: {methodId}", diff --git a/plugins/process-assets/lang/ru.json b/plugins/process-assets/lang/ru.json index c714675e54..b219f7545a 100644 --- a/plugins/process-assets/lang/ru.json +++ b/plugins/process-assets/lang/ru.json @@ -117,7 +117,9 @@ "NewProcessToDo": "Новый Action Item процесса", "EmptyArray": "Пустой массив", "ExecutionInitiator": "Инициатор выполнения", - "ExecutionStarted": "Выполнение начато" + "ExecutionStarted": "Выполнение начато", + "Filter": "Фильтр", + "FirstMatchValue": "Первое подходящее значение" }, "error": { "MethodNotFound": "Метод не найден: {methodId}", diff --git a/plugins/process-assets/lang/tr.json b/plugins/process-assets/lang/tr.json index 741c4d673f..0794c24e34 100644 --- a/plugins/process-assets/lang/tr.json +++ b/plugins/process-assets/lang/tr.json @@ -114,7 +114,9 @@ "ProcessRunned": "\"{process}\" süreci başlatıldı", "ProcessStateChanged": "\"{process}\" süreci \"{state}\" durumuna geçti", "ProcessFinished": "\"{process}\" süreci \"{state}\" durumunda tamamlandı", - "NewProcessToDo": "Yeni süreç Eylem öğesi" + "NewProcessToDo": "Yeni süreç Eylem öğesi", + "Filter": "Filtre", + "FirstMatchValue": "İlk eşleşen değer" }, "error": { "MethodNotFound": "Metod bulunamadı: {methodId}", diff --git a/plugins/process-assets/lang/zh.json b/plugins/process-assets/lang/zh.json index 9f30e3c71b..f2fff30ff0 100644 --- a/plugins/process-assets/lang/zh.json +++ b/plugins/process-assets/lang/zh.json @@ -117,7 +117,9 @@ "NewProcessToDo": "新过程待办事项", "EmptyArray": "空数组", "ExecutionInitiator": "执行发起者", - "ExecutionStarted": "执行已启动" + "ExecutionStarted": "执行已启动", + "Filter": "过滤", + "FirstMatchValue": "第一个匹配值" }, "error": { "MethodNotFound": "找不到方法:{methodId}", diff --git a/plugins/process-resources/src/components/attributeEditors/ConfigurePopup.svelte b/plugins/process-resources/src/components/attributeEditors/ConfigurePopup.svelte index 281d9b23e9..563a4236bf 100644 --- a/plugins/process-resources/src/components/attributeEditors/ConfigurePopup.svelte +++ b/plugins/process-resources/src/components/attributeEditors/ConfigurePopup.svelte @@ -136,10 +136,31 @@ } function onFunctionSelect (e: Ref): void { - // if editor is undefined - const func = client.getModel().findAllSync(plugin.class.ProcessFunction, { _id: e })[0] + onFunction(e, {}, addFunction) + } + + function addFunction (func: Func): void { + const arr = contextValue.functions ?? [] + arr.push(func) + contextValue.functions = arr + onChange(contextValue) + } + + function onSourceFunctionSelect (e: Ref): void { + onFunction(e, contextValue.sourceFunction?.props ?? {}, (res) => (contextValue.sourceFunction = res)) + } + + function onSourceFunctionChange (e: Func): void { + contextValue.sourceFunction = e + onChange(contextValue) + } + + function onFunction (_func: Ref, props: Record, cb: (res: Func) => void) { + const func = client.getModel().findAllSync(plugin.class.ProcessFunction, { _id: _func })[0] if (func.editor === undefined) { - addFunction(e, {}) + const res: Func = { func: _func, props: {} } + cb(res) + onChange(contextValue) closeTooltip() } else { showPopup( @@ -149,46 +170,49 @@ process, masterTag: process.masterTag, context, - attribute + attribute, + props }, - elements[functionButtonIndex], + elements[0], (res) => { if (res != null) { - addFunction(e, res) + const result: Func = { func: _func, props: res } + cb(result) + onChange(contextValue) + closeTooltip() } } ) } } - function addFunction (func: Ref, props: Record): void { - const arr = contextValue.functions ?? [] - arr.push({ - func, - props - }) - contextValue.functions = arr - onChange(contextValue) - } - - function onSourceFunctionSelect (e: Ref): void { - contextValue.sourceFunction = { func: e, props: {} } - onChange(contextValue) - } - function onFunctionChange (e: Ref, i: number): void { - if (contextValue.functions === undefined) return - contextValue.functions[i].func = e - contextValue.functions = contextValue.functions - onChange(contextValue) + onFunction(e, contextValue.functions?.[i]?.props ?? {}, (res) => { + if (contextValue.functions === undefined) { + contextValue.functions = [] + } + contextValue.functions[i] = res + contextValue.functions = contextValue.functions + }) } - function getFunctionChange (i: number): (e: Ref) => void { + function getFunctionSelect (i: number): (e: Ref) => void { return (e: Ref) => { onFunctionChange(e, i) } } + function getFunctionChange (i: number): (e: Func) => void { + return (e: Func) => { + if (contextValue.functions === undefined) { + contextValue.functions = [] + } + contextValue.functions[i] = e + contextValue.functions = contextValue.functions + onChange(contextValue) + } + } + function onFunctionRemove (pos: number): void { const arr = contextValue.functions ?? [] if (pos !== -1) { @@ -256,9 +280,15 @@ }} label={sourceFunc.label} props={{ + attribute, + process, + context, + func: contextValue.sourceFunction, availableFunctions: reduceFuncs, - onSelect: onSourceFunctionSelect + onSelect: onSourceFunctionSelect, + onChange: onSourceFunctionChange }} + component={plugin.component.FunctionSubmenu} options={{ component: plugin.component.FunctionSelector }} withHover /> @@ -279,9 +309,15 @@ }} label={getFunction(f.func)?.label} props={{ + func: f, + attribute, + process, + context, availableFunctions: reduceFuncs, - onSelect: getFunctionChange(i) + onSelect: getFunctionSelect(i), + onChange: getFunctionChange(i) }} + component={plugin.component.FunctionSubmenu} options={{ component: plugin.component.FunctionSelector }} withHover /> diff --git a/plugins/process-resources/src/components/criterias/AttributeCriteria.svelte b/plugins/process-resources/src/components/criterias/AttributeCriteria.svelte index 6241641ab2..59e7f7a354 100644 --- a/plugins/process-resources/src/components/criterias/AttributeCriteria.svelte +++ b/plugins/process-resources/src/components/criterias/AttributeCriteria.svelte @@ -14,13 +14,14 @@ -->
diff --git a/plugins/process-resources/src/components/criterias/CriteriasEditor.svelte b/plugins/process-resources/src/components/criterias/CriteriasEditor.svelte index d46ae832ad..f98445d80e 100644 --- a/plugins/process-resources/src/components/criterias/CriteriasEditor.svelte +++ b/plugins/process-resources/src/components/criterias/CriteriasEditor.svelte @@ -14,11 +14,12 @@ --> - + {#if possibleAttrs.length > 0}
+ {/if} + diff --git a/plugins/process-resources/src/index.ts b/plugins/process-resources/src/index.ts index f8550b3d6f..ff09bc6daa 100644 --- a/plugins/process-resources/src/index.ts +++ b/plugins/process-resources/src/index.ts @@ -47,6 +47,8 @@ import ArrayElementEditor from './components/transformEditors/ArrayElementEditor import MultiArrayElementEditor from './components/transformEditors/MultiArrayElementEditor.svelte' import DateOffsetEditor from './components/transformEditors/DateOffsetEditor.svelte' import NumberEditor from './components/transformEditors/NumberEditor.svelte' +import FilterEditor from './components/transformEditors/FilterEditor.svelte' +import FunctionSubmenu from './components/settings/FunctionSubmenu.svelte' import ArraySizeCriteria from './components/criterias/ArraySizeCriteria.svelte' import BaseCriteria from './components/criterias/BaseCriteria.svelte' @@ -135,7 +137,8 @@ export default async (): Promise => ({ AddTagEditor, AddTagPresenter, ExecutionMyToDos, - FieldChangesEditor + FieldChangesEditor, + FunctionSubmenu }, criteriaEditor: { BaseCriteria, @@ -154,7 +157,8 @@ export default async (): Promise => ({ AppendEditor, ReplaceEditor, SplitEditor, - CutEditor + CutEditor, + FilterEditor }, triggerCheck: { MatchCheck: matchCardCheck, diff --git a/plugins/process-resources/src/plugin.ts b/plugins/process-resources/src/plugin.ts index 41f333df5d..7f8cb88068 100644 --- a/plugins/process-resources/src/plugin.ts +++ b/plugins/process-resources/src/plugin.ts @@ -70,7 +70,8 @@ export default mergeIds(processId, process, { TimeEditor: '' as AnyComponent, TimePresenter: '' as AnyComponent, AddTagEditor: '' as AnyComponent, - AddTagPresenter: '' as AnyComponent + AddTagPresenter: '' as AnyComponent, + FunctionSubmenu: '' as AnyComponent }, criteriaEditor: { BaseCriteria: '' as AnyComponent, @@ -85,7 +86,8 @@ export default mergeIds(processId, process, { AppendEditor: '' as AnyComponent, ReplaceEditor: '' as AnyComponent, SplitEditor: '' as AnyComponent, - CutEditor: '' as AnyComponent + CutEditor: '' as AnyComponent, + FilterEditor: '' as AnyComponent }, transformPresenter: { NumberPresenter: '' as AnyComponent, @@ -127,6 +129,8 @@ export default mergeIds(processId, process, { ReplaceAll: '' as IntlString, Split: '' as IntlString, Cut: '' as IntlString, + Filter: '' as IntlString, + FirstMatchValue: '' as IntlString, FirstValue: '' as IntlString, LastValue: '' as IntlString, Random: '' as IntlString, diff --git a/plugins/process/src/dslContext.ts b/plugins/process/src/dslContext.ts index 274d2623ef..830ded0273 100644 --- a/plugins/process/src/dslContext.ts +++ b/plugins/process/src/dslContext.ts @@ -90,12 +90,10 @@ function encodeValue (val: any): string { return val ? 'true' : 'false' case 'string': return `"${val.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` - case 'object': - try { - return JSON.stringify(val) - } catch { - return String(val) - } + case 'object': { + const entries = Object.entries(val).map(([k, v]) => `${k}=${encodeValue(v)}`) + return `{${entries.join(',')}}` + } } return String(val) @@ -384,6 +382,20 @@ function decodeValue (s: string): any { if (inner === '') return [] return inner.split(',').map(decodeValue) } + if (str.startsWith('{') && str.endsWith('}')) { + const inner = str.slice(1, -1).trim() + if (inner === '') return {} + const obj: Record = {} + const entries = splitTopLevel(inner, ',') + for (const entry of entries) { + const eq = entry.indexOf('=') + if (eq === -1) continue + const key = entry.slice(0, eq).trim() + const val = entry.slice(eq + 1).trim() + obj[key] = decodeValue(val) + } + return obj + } return str } diff --git a/plugins/process/src/index.ts b/plugins/process/src/index.ts index c0e29638b6..2db09c4045 100644 --- a/plugins/process/src/index.ts +++ b/plugins/process/src/index.ts @@ -277,6 +277,8 @@ export default plugin(processId, { Time: '' as Asset }, function: { + FirstMatchValue: '' as Ref, + Filter: '' as Ref, FirstValue: '' as Ref, LastValue: '' as Ref, Random: '' as Ref, diff --git a/server-plugins/process-resources/src/index.ts b/server-plugins/process-resources/src/index.ts index af7cd7d991..ca3435c1bf 100644 --- a/server-plugins/process-resources/src/index.ts +++ b/server-plugins/process-resources/src/index.ts @@ -78,7 +78,9 @@ import { UpperCase, EmptyArray, ExecutionInitiator, - ExecutionStarted + ExecutionStarted, + FirstMatchValue, + Filter } from './transform' import { RunSubProcess, @@ -434,7 +436,9 @@ export default async () => ({ RemoveLast, EmptyArray, ExecutionInitiator, - ExecutionStarted + ExecutionStarted, + FirstMatchValue, + Filter }, rollbacks: { ToDoCloseRollback, diff --git a/server-plugins/process-resources/src/transform.ts b/server-plugins/process-resources/src/transform.ts index 1cf861cc1f..f648ec635d 100644 --- a/server-plugins/process-resources/src/transform.ts +++ b/server-plugins/process-resources/src/transform.ts @@ -14,20 +14,21 @@ // import contact, { Employee, Person } from '@hcengineering/contact' -import { Doc, Ref, Timestamp } from '@hcengineering/core' +import core, { Doc, matchQuery, Ref, Timestamp } from '@hcengineering/core' import { Execution, parseContext } from '@hcengineering/process' import { ProcessControl } from '@hcengineering/server-process' import { getContextValue } from './utils' // #region ArrayReduce -export function FirstValue (value: Doc[]): Doc { +export function FirstValue (value: Doc[]): Doc | undefined { if (!Array.isArray(value)) return value return value[0] } -export function LastValue (value: Doc[]): Doc { +export function LastValue (value: Doc[]): Doc | undefined { if (!Array.isArray(value)) return value + if (value.length === 0) return return value[value.length - 1] } @@ -40,16 +41,36 @@ export function All (value: Doc[]): Doc[] { return value } +export async function FirstMatchValue ( + value: any[], + props: Record, + control: ProcessControl +): Promise { + if (value == null) { + return + } + if (!Array.isArray(value)) return value + const { _class, ...otherProps } = props + if (_class == null) return + if (value.length === 0) return + if (typeof value[0] === 'string') { + const docs = await control.client.findAll(_class, { _id: { $in: value } }) + return matchQuery(docs, otherProps, core.class.Doc, control.client.getHierarchy(), true)[0]?._id + } else if (typeof value[0] === 'object') { + return matchQuery(value, otherProps, core.class.Doc, control.client.getHierarchy(), true)[0] + } +} + // #endregion // #region Array export async function Insert ( - value: Doc[], + value: any[], props: Record, control: ProcessControl, execution: Execution -): Promise { +): Promise { if (value == null) { value = [] } @@ -66,11 +87,11 @@ export async function Insert ( } export async function Remove ( - value: Doc[], + value: any[], props: Record, control: ProcessControl, execution: Execution -): Promise { +): Promise { if (value == null) { value = [] } @@ -85,7 +106,7 @@ export async function Remove ( } } -export function RemoveFirst (value: Doc[], props: Record): Doc[] { +export function RemoveFirst (value: any[]): any[] { if (value == null) { value = [] } @@ -93,7 +114,7 @@ export function RemoveFirst (value: Doc[], props: Record): Doc[] { return value.slice(1) } -export function RemoveLast (value: Doc[], props: Record): Doc[] { +export function RemoveLast (value: any[]): any[] { if (value == null) { value = [] } @@ -101,6 +122,23 @@ export function RemoveLast (value: Doc[], props: Record): Doc[] { return value.slice(0, -1) } +export async function Filter (value: any[], props: Record, control: ProcessControl): Promise { + if (value == null) { + return [] + } + if (!Array.isArray(value)) return value + const { _class, ...otherProps } = props + if (_class == null) return value + if (value.length === 0) return value + if (typeof value[0] === 'string') { + const docs = await control.client.findAll(_class, { _id: { $in: value } }) + return matchQuery(docs, otherProps, core.class.Doc, control.client.getHierarchy(), true).map((p) => p._id) + } else if (typeof value[0] === 'object') { + return matchQuery(value, otherProps, core.class.Doc, control.client.getHierarchy(), true) + } + return value +} + // #endregion // #region String diff --git a/server-plugins/process/src/index.ts b/server-plugins/process/src/index.ts index 89efa63e73..ccad74fcba 100644 --- a/server-plugins/process/src/index.ts +++ b/server-plugins/process/src/index.ts @@ -96,6 +96,8 @@ export default plugin(serverProcessId, { CurrentUser: '' as Resource, CurrentDate: '' as Resource, EmptyArray: '' as Resource, + Filter: '' as Resource, + FirstMatchValue: '' as Resource, ExecutionInitiator: '' as Resource, ExecutionStarted: '' as Resource },