Add FirstMatchValue and Filter functions (#10042)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2025-10-07 23:17:04 +07:00
committed by GitHub
parent 0a682d03e3
commit f8205e57a1
29 changed files with 423 additions and 68 deletions
@@ -136,10 +136,31 @@
}
function onFunctionSelect (e: Ref<ProcessFunction>): 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<ProcessFunction>): void {
onFunction(e, contextValue.sourceFunction?.props ?? {}, (res) => (contextValue.sourceFunction = res))
}
function onSourceFunctionChange (e: Func): void {
contextValue.sourceFunction = e
onChange(contextValue)
}
function onFunction (_func: Ref<ProcessFunction>, props: Record<string, any>, 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<ProcessFunction>, props: Record<string, any>): void {
const arr = contextValue.functions ?? []
arr.push({
func,
props
})
contextValue.functions = arr
onChange(contextValue)
}
function onSourceFunctionSelect (e: Ref<ProcessFunction>): void {
contextValue.sourceFunction = { func: e, props: {} }
onChange(contextValue)
}
function onFunctionChange (e: Ref<ProcessFunction>, 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<ProcessFunction>) => void {
function getFunctionSelect (i: number): (e: Ref<ProcessFunction>) => void {
return (e: Ref<ProcessFunction>) => {
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
/>
@@ -14,13 +14,14 @@
-->
<script lang="ts">
import { Doc, DocumentQuery } from '@hcengineering/core'
import { Class, Doc, DocumentQuery, Ref } from '@hcengineering/core'
import { getAttributePresenterClass, getClient } from '@hcengineering/presentation'
import { Process } from '@hcengineering/process'
import { Component, Label, tooltip } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { getCirteriaEditor, getContext } from '../../utils'
export let _class: Ref<Class<Doc>>
export let readonly: boolean
export let process: Process
export let key: string
@@ -30,7 +31,7 @@
const client = getClient()
const hierarchy = client.getHierarchy()
$: attribute = hierarchy.getAttribute(process.masterTag, key)
$: attribute = hierarchy.getAttribute(_class, key)
$: presenterClass = getAttributePresenterClass(hierarchy, attribute.type)
$: updateCriteria = getCirteriaEditor(presenterClass.attrClass, presenterClass.category)
@@ -60,8 +60,7 @@
const hierarchy = client.getHierarchy()
$: presenterClass = getAttributePresenterClass(hierarchy, attribute.type)
$: baseEditor = findAttributeEditor(client, process.masterTag, attribute.name)
$: baseEditor = findAttributeEditor(client, attribute.attributeOf, attribute.name)
</script>
<div class="text-input" class:context={contextValue}>
@@ -14,11 +14,12 @@
-->
<script lang="ts">
import { Doc, DocumentQuery } from '@hcengineering/core'
import { Class, Doc, DocumentQuery, Ref } from '@hcengineering/core'
import { Process } from '@hcengineering/process'
import { createEventDispatcher } from 'svelte'
import AttributeCriteria from './AttributeCriteria.svelte'
export let _class: Ref<Class<Doc>>
export let readonly: boolean
export let process: Process
export let keys: string[]
@@ -41,6 +42,7 @@
<div class="editor-grid">
{#each keys as key}
<AttributeCriteria
{_class}
{process}
{key}
{params}
@@ -93,7 +93,15 @@
}
</script>
<CriteriasEditor {keys} {readonly} {process} {params} on:remove={remove} on:change={change} />
<CriteriasEditor
_class={process.masterTag}
{keys}
{readonly}
{process}
{params}
on:remove={remove}
on:change={change}
/>
{#if possibleAttrs.length > 0}
<div class="flex-center mt-4">
<Button label={presentation.string.Add} width={'100%'} kind={'link-bordered'} size={'large'} on:click={onAdd} />
@@ -90,7 +90,15 @@
</script>
{#if key !== undefined}
<CriteriasEditor keys={[key]} {readonly} {process} {params} on:remove={remove} on:change={change} />
<CriteriasEditor
keys={[key]}
_class={process.masterTag}
{readonly}
{process}
{params}
on:remove={remove}
on:change={change}
/>
{:else}
<div class="flex-center mt-4">
<Button label={view.string.Select} width={'100%'} kind={'link-bordered'} size={'large'} on:click={onSelect} />
@@ -0,0 +1,65 @@
<!--
// 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, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Context, Func, Process, ProcessFunction } from '@hcengineering/process'
import { ButtonIcon, eventToHTMLElement, IconSettings, Label, showPopup } from '@hcengineering/ui'
import plugin from '../../plugin'
export let attribute: AnyAttribute
export let process: Process
export let context: Context
export let func: Func
export let onChange: (func: Func) => void
const client = getClient()
function getFunction (_id: Ref<ProcessFunction>): ProcessFunction {
return client.getModel().findAllSync(plugin.class.ProcessFunction, { _id })[0]
}
function onConfigure (e: MouseEvent): void {
const f = getFunction(func.func)
if (f.editor === undefined) return
showPopup(
f.editor,
{
func: f,
masterTag: process.masterTag,
process,
context,
attribute,
props: func.props
},
eventToHTMLElement(e),
(res) => {
if (res != null) {
func.props = res
onChange(func)
}
}
)
}
</script>
<div class="flex-row-center flex-gap-2 pr-1 flex-shrink">
<span class="overflow-label">
<Label label={getFunction(func.func).label} />
</span>
{#if getFunction(func.func).editor}
<ButtonIcon icon={IconSettings} size="small" kind="tertiary" on:click={onConfigure} />
{/if}
</div>
@@ -0,0 +1,112 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import core, { AnyAttribute } from '@hcengineering/core'
import presentation, { Card, getAttributePresenterClass, getClient } from '@hcengineering/presentation'
import { Process, ProcessFunction } from '@hcengineering/process'
import { Button, SelectPopup, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { getCirteriaEditor } from '../../utils'
import CriteriasEditor from '../criterias/CriteriasEditor.svelte'
export let func: ProcessFunction
export let process: Process
export let attribute: AnyAttribute
export let props: Record<string, any> = {}
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
const presenterClass = getAttributePresenterClass(hierarchy, attribute.type)
function save (): void {
dispatch('close', {
...props,
_class: presenterClass.attrClass
})
}
function getKeys (): AnyAttribute[] {
const attributes = hierarchy.getAllAttributes(presenterClass.attrClass, core.class.Doc)
const res: AnyAttribute[] = []
for (const [key, attr] of attributes) {
if (key === '_id' || key === '_class') continue
if (attr.hidden === true) continue
const presenterClass = getAttributePresenterClass(hierarchy, attr.type)
const updateCriteria = getCirteriaEditor(presenterClass.attrClass, presenterClass.category)
const editor = updateCriteria?.editor
if (editor == null) continue
res.push(attr)
}
return res
}
let keys = Object.keys(props).filter((p) => p !== '_class')
const allAttrs = getKeys()
$: possibleAttrs = allAttrs.filter((attr) => !keys.includes(attr.name))
function onAdd (e: MouseEvent): void {
showPopup(
SelectPopup,
{
value: possibleAttrs.map((p) => {
return { id: p.name, label: p.label }
})
},
eventToHTMLElement(e),
(res) => {
if (res != null) {
addKey(res)
}
}
)
}
function addKey (key: string): void {
keys = [...keys, key]
}
function remove (e: CustomEvent<any>): void {
if (e.detail !== undefined) {
const key = e.detail.key
keys = keys.filter((k) => k !== key)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (props as any)[key]
}
}
function change (e: CustomEvent<any>): void {
if (e.detail == null) return
props = e.detail
}
</script>
<Card on:close width={'x-small'} label={func.label} canSave okAction={save} okLabel={presentation.string.Save}>
<CriteriasEditor
_class={presenterClass.attrClass}
readonly={false}
{keys}
{process}
params={props}
on:remove={remove}
on:change={change}
/>
{#if possibleAttrs.length > 0}
<div class="flex-center mt-4">
<Button label={presentation.string.Add} width={'100%'} kind={'link-bordered'} size={'large'} on:click={onAdd} />
</div>
{/if}
</Card>