mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-10 11:47:42 +02:00
Improve process ui (#9645)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -99,7 +99,8 @@
|
||||
"ArraySizeGt": "サイズがより大きい",
|
||||
"ArraySizeGte": "サイズが以上",
|
||||
"ArraySizeLt": "サイズがより小さい",
|
||||
"ArraySizeLte": "サイズが以下"
|
||||
"ArraySizeLte": "サイズが以下",
|
||||
"Cancelled": "キャンセル済み"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "メソッドが見つかりません: {methodId}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -96,11 +96,12 @@
|
||||
"Each": "Каждый",
|
||||
"CreateCard": "Создать карточку",
|
||||
"OnCardUpdate": "При обновлении карточки",
|
||||
"ArraySizeEquals": "Размер равен",
|
||||
"ArraySizeGt": "Размер больше чем",
|
||||
"ArraySizeGte": "Размер больше или равен",
|
||||
"ArraySizeLt": "Размер меньше чем",
|
||||
"ArraySizeLte": "Размер меньше или равен"
|
||||
"ArraySizeEquals": "Количество равно",
|
||||
"ArraySizeGt": "Количество больше",
|
||||
"ArraySizeGte": "Количество больше или равно",
|
||||
"ArraySizeLt": "Количество меньше",
|
||||
"ArraySizeLte": "Количество меньше или равно",
|
||||
"Cancelled": "Отменено"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Метод не найден: {methodId}",
|
||||
|
||||
@@ -100,7 +100,8 @@
|
||||
"ArraySizeGt": "大小大于",
|
||||
"ArraySizeGte": "大小大于等于",
|
||||
"ArraySizeLt": "大小小于",
|
||||
"ArraySizeLte": "大小小于等于"
|
||||
"ArraySizeLte": "大小小于等于",
|
||||
"Cancelled": "已取消"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "找不到方法:{methodId}",
|
||||
|
||||
+2
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user