Date process funcs (#8441)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2025-04-03 00:22:53 +07:00
committed by GitHub
parent 241d08fd89
commit 073017ae67
48 changed files with 798 additions and 195 deletions
+3 -1
View File
@@ -58,6 +58,7 @@ import { cardOperation } from '@hcengineering/model-card'
import { aiBotId, aiBotOperation } from '@hcengineering/model-ai-bot'
import { chatId, chatOperation } from '@hcengineering/model-chat'
import { inboxId, inboxOperation } from '@hcengineering/model-inbox'
import { processId, processOperation } from '@hcengineering/model-process'
export const migrateOperations: [string, MigrateOperation][] = [
['core', coreOperation],
@@ -104,5 +105,6 @@ export const migrateOperations: [string, MigrateOperation][] = [
['survey', surveyOperation],
[aiBotId, aiBotOperation],
[chatId, chatOperation],
[inboxId, inboxOperation]
[inboxId, inboxOperation],
[processId, processOperation]
]
+57 -1
View File
@@ -139,10 +139,14 @@ export class TState extends TDoc implements State {
@Model(process.class.ProcessFunction, core.class.Doc, DOMAIN_MODEL)
export class TProcessFunction extends TDoc implements ProcessFunction {
of!: Ref<Class<Doc<Space>>>
category?: AttributeCategory | undefined
category: AttributeCategory | undefined
label!: IntlString
editor?: AnyComponent
allowMany?: boolean
}
export * from './migration'
export function createModel (builder: Builder): void {
builder.createModel(TProcess, TExecution, TProcessToDo, TMethod, TState, TProcessFunction)
@@ -218,6 +222,7 @@ export function createModel (builder: Builder): void {
core.space.Model,
{
of: core.class.ArrOf,
category: undefined,
label: process.string.FirstValue
},
process.function.FirstValue
@@ -228,6 +233,7 @@ export function createModel (builder: Builder): void {
core.space.Model,
{
of: core.class.ArrOf,
category: undefined,
label: process.string.LastValue
},
process.function.LastValue
@@ -238,11 +244,61 @@ export function createModel (builder: Builder): void {
core.space.Model,
{
of: core.class.ArrOf,
category: undefined,
label: process.string.Random
},
process.function.Random
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Add,
allowMany: true,
editor: process.component.NumberOffsetEditor
},
process.function.Add
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeNumber,
category: 'attribute',
label: process.string.Subtract,
allowMany: true,
editor: process.component.NumberOffsetEditor
},
process.function.Subtract
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeDate,
category: 'attribute',
label: process.string.Offset,
editor: process.component.DateOffsetEditor
},
process.function.Offset
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeDate,
category: 'attribute',
label: process.string.FirstWorkingDayAfter
},
process.function.FirstWorkingDayAfter
)
builder.mixin(process.class.Process, core.class.Class, view.mixin.AttributePresenter, {
presenter: process.component.ProcessPresenter
})
+93
View File
@@ -0,0 +1,93 @@
//
// 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 core, { type Ref, TxOperations } from '@hcengineering/core'
import {
tryUpgrade,
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient
} from '@hcengineering/model'
import { type Func, parseContext, type ProcessFunction } from '@hcengineering/process'
import { processId } from '.'
import process from './plugin'
export const processOperation: MigrateOperation = {
async migrate (client: MigrationClient, mode): Promise<void> {},
async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>, mode): Promise<void> {
await tryUpgrade(mode, state, client, processId, [
{
state: 'migrateStateFuncs',
func: migrateStateFuncs
}
])
}
}
function getContext (value: string): string | undefined {
const context = parseContext(value)
if (context !== undefined) {
let contextChanged = false
if (context.functions !== undefined) {
for (let i = 0; i < context.functions.length; i++) {
const func = context.functions[i] as any
if (typeof func === 'string') {
const res: Func = {
func: func as Ref<ProcessFunction>,
props: {}
}
context.functions[i] = res
contextChanged = true
}
}
}
if (contextChanged) {
return '$' + JSON.stringify(context)
}
}
}
async function migrateStateFuncs (client: MigrationUpgradeClient): Promise<void> {
const txOp = new TxOperations(client, core.account.System)
const states = await client.findAll(process.class.State, {})
for (const state of states) {
let changed = false
const actions = state.actions
for (const action of actions) {
for (const key of Object.keys(action.params)) {
const value = (action.params as any)[key]
const context = getContext(value)
if (context !== undefined) {
;(action.params as any)[key] = context
changed = true
}
}
}
const endAction = state.endAction
if (endAction != null) {
for (const key of Object.keys(endAction.params)) {
const value = (endAction.params as any)[key]
const context = getContext(value)
if (context !== undefined) {
;(endAction.params as any)[key] = context
changed = true
}
}
}
if (changed) {
await txOp.updateDoc(state._class, state.space, state._id, { actions, endAction })
}
}
}
+16
View File
@@ -75,6 +75,22 @@ export function createModel (builder: Builder): void {
func: serverProcess.transform.Trim
})
builder.mixin(process.function.Add, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.Add
})
builder.mixin(process.function.Subtract, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.Subtract
})
builder.mixin(process.function.Offset, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.Offset
})
builder.mixin(process.function.FirstWorkingDayAfter, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.FirstWorkingDayAfter
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverProcess.trigger.OnProcessRemove,
txMatch: {
+4 -1
View File
@@ -111,6 +111,9 @@
"UseMaxWidth": "Maximální šířka",
"Sidebar": "Postranní panel",
"FontSize": "Velikost písma",
"Language": "Jazyk"
"Language": "Jazyk",
"DaysWOValue": "{days, plural, =1 {den} other {dny}}",
"WeeksWOValue": "{weeks, plural, =1 {týden} other {týdny}}",
"MonthsWOValue": "{months, plural, =1 {měsíc} other {měsíce}}"
}
}
+4 -1
View File
@@ -111,6 +111,9 @@
"UseMaxWidth": "Maximale Breite",
"Sidebar": "Seitenleiste",
"FontSize": "Schriftgröße",
"Language": "Sprache"
"Language": "Sprache",
"DaysWOValue": "{days, plural, =1 {Tag} other {Tage}}",
"WeeksWOValue": "{weeks, plural, =1 {Woche} other {Wochen}}",
"MonthsWOValue": "{months, plural, =1 {Monat} other {Monate}}"
}
}
+4 -1
View File
@@ -112,6 +112,9 @@
"UseMaxWidth": "Max width",
"Sidebar": "Sidebar",
"FontSize": "Font size",
"Language": "Language"
"Language": "Language",
"DaysWOValue": "{days, plural, =1 {day} other {days}}",
"WeeksWOValue": "{weeks, plural, =1 {week} other {weeks}}",
"MonthsWOValue": "{months, plural, =1 {month} other {months}}"
}
}
+4 -1
View File
@@ -112,6 +112,9 @@
"UseMaxWidth": "Ancho máximo",
"Sidebar": "Barra lateral",
"FontSize": "Tamaño de fuente",
"Language": "Idioma"
"Language": "Idioma",
"DaysWOValue": "{days, plural, =1 {día} other {días}}",
"WeeksWOValue": "{weeks, plural, =1 {semana} other {semanas}}",
"MonthsWOValue": "{months, plural, =1 {mes} other {meses}}"
}
}
+4 -1
View File
@@ -112,6 +112,9 @@
"UseMaxWidth": "Largeur maximale",
"Sidebar": "Barre latérale",
"FontSize": "Taille de police",
"Language": "Langue"
"Language": "Langue",
"DaysWOValue": "{days, plural, =1 {jour} other {jours}}",
"WeeksWOValue": "{weeks, plural, =1 {semaine} other {semaines}}",
"MonthsWOValue": "{months, plural, =1 {mois} other {mois}}"
}
}
+4 -1
View File
@@ -112,6 +112,9 @@
"UseMaxWidth": "Larghezza massima",
"Sidebar": "Barra laterale",
"FontSize ": "Dimensione del carattere",
"Language": "Lingua"
"Language": "Lingua",
"DaysWOValue": "{days, plural, =1 {giorno} other {giorni}}",
"WeeksWOValue": "{weeks, plural, =1 {settimana} other {settimane}}",
"MonthsWOValue": "{months, plural, =1 {mese} other {mesi}}"
}
}
+4 -1
View File
@@ -112,6 +112,9 @@
"UseMaxWidth": "Largura máxima",
"Sidebar": "Barra lateral",
"FontSize": "Tamanho da fonte",
"Language": "Idioma"
"Language": "Idioma",
"DaysWOValue": "{days, plural, =1 {dia} other {dias}}",
"WeeksWOValue": "{weeks, plural, =1 {semana} other {semanas}}",
"MonthsWOValue": "{months, plural, =1 {mês} other {meses}}"
}
}
+4 -1
View File
@@ -112,6 +112,9 @@
"UseMaxWidth": "Максимальная ширина",
"Sidebar": "Боковая панель",
"FontSize": "Размер шрифта",
"Language": "Язык"
"Language": "Язык",
"DaysWOValue": "{days, plural, one {день} few {дня} other {дней}}",
"WeeksWOValue": "{weeks, plural, one {неделя} few {недели} other {недель}}",
"MonthsWOValue": "{months, plural, one {месяц} few {месяца} other {месяцев}}"
}
}
+4 -1
View File
@@ -112,6 +112,9 @@
"UseMaxWidth": "使用最大宽度",
"Sidebar": "侧边栏",
"FontSize": "字体大小",
"Language": "语言"
"Language": "语言",
"DaysWOValue": "{days, plural, =1 {天} other {天}}",
"WeeksWOValue": "{weeks, plural, =1 {周} other {周}}",
"MonthsWOValue": "{months, plural, =1 {月} other {月}}"
}
}
+4 -1
View File
@@ -139,7 +139,10 @@ export const uis = plugin(uiId, {
UseMaxWidth: '' as IntlString,
Sidebar: '' as IntlString,
FontSize: '' as IntlString,
Language: '' as IntlString
Language: '' as IntlString,
DaysWOValue: '' as IntlString,
WeeksWOValue: '' as IntlString,
MonthsWOValue: '' as IntlString
},
metadata: {
DefaultApplication: '' as Metadata<AnyComponent>,
-3
View File
@@ -55,9 +55,6 @@ export default mergeIds(calendarId, calendar, {
Ends: '' as IntlString,
Never: '' as IntlString,
After: '' as IntlString,
Day: '' as IntlString,
Week: '' as IntlString,
Month: '' as IntlString,
Year: '' as IntlString,
MondayShort: '' as IntlString,
TuesdayShort: '' as IntlString,
+4 -1
View File
@@ -234,7 +234,10 @@ const calendarPlugin = plugin(calendarId, {
ScheduleTitlePlaceholder: '' as IntlString,
ScheduleUnavailable: '' as IntlString,
MeetingDuration: '' as IntlString,
MeetingInterval: '' as IntlString
MeetingInterval: '' as IntlString,
Day: '' as IntlString,
Week: '' as IntlString,
Month: '' as IntlString
},
handler: {
DisconnectHandler: '' as Handler
@@ -13,23 +13,12 @@
// limitations under the License.
-->
<script lang="ts">
import { Card, MasterTag } from '@hcengineering/card'
import { Card } from '@hcengineering/card'
import { AnyAttribute, Ref, RefTo } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import {
Button,
ButtonKind,
ButtonSize,
eventToHTMLElement,
IconWithEmoji,
Label,
showPopup
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import { ButtonKind, ButtonSize } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import card from '../plugin'
import CardsPopup from './CardsPopup.svelte'
import CardSelector from './CardSelector.svelte'
import CardPresenter from './CardPresenter.svelte'
@@ -52,10 +41,10 @@
onChange(val)
}
const _class = (attribute.type as RefTo<Card>).to
const _class = (attribute?.type as RefTo<Card>)?.to ?? card.class.Card
</script>
{#if readonly || attribute.readonly}
{#if readonly || attribute?.readonly}
<CardPresenter {value} noUnderline />
{:else}
<CardSelector
@@ -66,6 +66,7 @@
selectedSubObjectId = undefined
subEditorTitle = undefined
subEditor = undefined
subEditorTitle = undefined
}
}
+5 -1
View File
@@ -36,6 +36,10 @@
"Random": "Náhodné",
"AllProcesses": "Všechny procesy",
"MyProcesses": "Moje procesy",
"ShowDone": "Zobrazit hotovo"
"ShowDone": "Zobrazit hotovo",
"Add": "Přidat",
"Subtract": "Odečíst",
"Offset": "Posunout",
"FirstWorkingDayAfter": "První pracovní den po"
}
}
+5 -1
View File
@@ -36,6 +36,10 @@
"Random": "Zufällig",
"AllProcesses": "Alle Prozesse",
"MyProcesses": "Meine Prozesse",
"ShowDone": "Erledigt anzeigen"
"ShowDone": "Erledigt anzeigen",
"Add": "Hinzufügen",
"Subtract": "Subtrahieren",
"Offset": "Verschieben",
"FirstWorkingDayAfter": "Erste Arbeitstag nach"
}
}
+5 -1
View File
@@ -36,6 +36,10 @@
"Random": "Random",
"AllProcesses": "All processes",
"MyProcesses": "My processes",
"ShowDone": "Show done"
"ShowDone": "Show done",
"Add": "Add",
"Subtract": "Subtract",
"Offset": "Offset",
"FirstWorkingDayAfter": "First working day after"
}
}
+5 -1
View File
@@ -36,6 +36,10 @@
"Random": "Aleatorio",
"AllProcesses": "Todos los procesos",
"MyProcesses": "Mis procesos",
"ShowDone": "Mostrar hecho"
"ShowDone": "Mostrar hecho",
"Add": "Agregar",
"Subtract": "Restar",
"Offset": "Desplazar",
"FirstWorkingDayAfter": "Primer día de trabajo después"
}
}
+5 -1
View File
@@ -36,6 +36,10 @@
"Random": "Aléatoire",
"AllProcesses": "Tous les processus",
"MyProcesses": "Mes processus",
"ShowDone": "Afficher terminé"
"ShowDone": "Afficher terminé",
"Add": "Ajouter",
"Subtract": "Soustraire",
"Offset": "Décalage",
"FirstWorkingDayAfter": "Premier jour de travail après"
}
}
+5 -1
View File
@@ -36,6 +36,10 @@
"Random": "Casuale",
"AllProcesses": "Tutti i processi",
"MyProcesses": "I miei processi",
"ShowDone": "Mostra completato"
"ShowDone": "Mostra completato",
"Add": "Aggiungi",
"Subtract": "Sottrai",
"Offset": "Offset",
"FirstWorkingDayAfter": "Primo giorno lavorativo dopo"
}
}
+5 -1
View File
@@ -36,6 +36,10 @@
"Random": "Aleatório",
"AllProcesses": "Todos os Processos",
"MyProcesses": "Meus Processos",
"ShowDone": "Mostrar Concluído"
"ShowDone": "Mostrar Concluído",
"Add": "Adicionar",
"Subtract": "Subtrair",
"Offset": "Deslocar",
"FirstWorkingDayAfter": "Primeiro Dia de Trabalho Após"
}
}
+5 -1
View File
@@ -37,6 +37,10 @@
"Random": "Случайное",
"AllProcesses": "Все процессы",
"MyProcesses": "Мои процессы",
"ShowDone": "Показать выполненные"
"ShowDone": "Показать выполненные",
"Add": "Добавить",
"Subtract": "Вычесть",
"Offset": "Смещение",
"FirstWorkingDayAfter": "Первый рабочий день после"
}
}
+5 -1
View File
@@ -36,6 +36,10 @@
"Random": "随机",
"AllProcesses": "所有过程",
"MyProcesses": "我的过程",
"ShowDone": "显示已完成"
"ShowDone": "显示已完成",
"Add": "添加",
"Subtract": "减去",
"Offset": "偏移",
"FirstWorkingDayAfter": "在之后的第一个工作日"
}
}
+1
View File
@@ -45,6 +45,7 @@
"@hcengineering/view": "^0.6.13",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/card-resources": "^0.6.0",
"@hcengineering/calendar": "^0.6.24",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/card": "^0.6.0",
"@hcengineering/workbench-resources": "^0.6.1",
@@ -0,0 +1,131 @@
<!--
// 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, Class, Doc, Ref } from '@hcengineering/core'
import { Context, parseContext, SelectedContext } from '@hcengineering/process'
import { AnySvelteComponent, Button, eventToHTMLElement, IconAdd, Label, showPopup, tooltip } from '@hcengineering/ui'
import { AttributeCategory } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import ContextSelectorPopup from './attributeEditors/ContextSelectorPopup.svelte'
import ContextValue from './attributeEditors/ContextValue.svelte'
export let value: any
export let context: Context
export let presenterClass: {
attrClass: Ref<Class<Doc>>
category: AttributeCategory
}
export let attribute: AnyAttribute
export let editor: AnySvelteComponent | undefined
const dispatch = createEventDispatcher()
$: contextValue = parseContext(value)
function onChange (value: any | undefined): void {
dispatch('change', value)
}
function selectContext (e: MouseEvent): void {
showPopup(
ContextSelectorPopup,
{
context,
attribute,
onSelect
},
eventToHTMLElement(e)
)
}
function onSelect (res: SelectedContext | null): void {
value = res === null ? undefined : '$' + JSON.stringify(res)
dispatch('change', value)
}
</script>
{#if editor}
<span
class="labelOnPanel"
use:tooltip={{
props: { label: attribute.label }
}}
>
<Label label={attribute.label} />
</span>
<div class="text-input" class:context={contextValue}>
{#if contextValue}
<ContextValue
{contextValue}
{context}
{attribute}
category={presenterClass.category}
attrClass={presenterClass.attrClass}
on:update={(e) => {
onSelect(e.detail)
}}
/>
{:else}
<div class="w-full">
<svelte:component
this={editor}
label={attribute?.label}
placeholder={attribute?.label}
kind={'ghost'}
size={'large'}
width={'100%'}
justify={'left'}
type={attribute?.type}
{value}
{onChange}
{focus}
/>
</div>
{/if}
<div class="button">
<Button
icon={IconAdd}
kind="ghost"
on:click={(e) => {
selectContext(e)
}}
/>
</div>
</div>
{/if}
<style lang="scss">
.text-input {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
min-height: 2.5rem;
border: 0.0625rem 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>
@@ -15,11 +15,10 @@
<script lang="ts">
import { Class, Doc, Ref } from '@hcengineering/core'
import { getAttributeEditor, getAttributePresenterClass, getClient } from '@hcengineering/presentation'
import { parseContext, Process, SelectedContext, State } from '@hcengineering/process'
import { AnySvelteComponent, Button, eventToHTMLElement, IconAdd, Label, showPopup, tooltip } from '@hcengineering/ui'
import { Process, State } from '@hcengineering/process'
import { AnySvelteComponent } from '@hcengineering/ui'
import { getContext } from '../utils'
import ContextSelectorPopup from './attributeEditors/ContextSelectorPopup.svelte'
import ContextValue from './attributeEditors/ContextValue.svelte'
import ProcessAttribute from './ProcessAttribute.svelte'
export let process: Process
export let state: State
@@ -54,96 +53,15 @@
}
$: getBaseEditor(_class, key)
$: contextValue = parseContext(value)
function selectContext (e: MouseEvent): void {
showPopup(
ContextSelectorPopup,
{
context,
attribute,
onSelect
},
eventToHTMLElement(e)
)
}
function onSelect (res: SelectedContext | null): void {
value = res === null ? undefined : '$' + JSON.stringify(res)
onChange(value)
}
</script>
{#if editor}
<span
class="labelOnPanel"
use:tooltip={{
props: { label: attribute.label }
}}
>
<Label label={attribute.label} />
</span>
<div class="text-input" class:context={contextValue}>
{#if contextValue}
<ContextValue
{contextValue}
{context}
category={presenterClass.category}
attrClass={presenterClass.attrClass}
on:update={(e) => {
onSelect(e.detail)
}}
/>
{:else}
<div class="w-full">
<svelte:component
this={editor}
label={attribute?.label}
placeholder={attribute?.label}
kind={'ghost'}
size={'large'}
width={'100%'}
justify={'left'}
type={attribute?.type}
{value}
{onChange}
{focus}
/>
</div>
{/if}
<div class="button">
<Button
icon={IconAdd}
kind="ghost"
on:click={(e) => {
selectContext(e)
}}
/>
</div>
</div>
{/if}
<style lang="scss">
.text-input {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
min-height: 2.5rem;
border: 0.0625rem 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>
<ProcessAttribute
{context}
{editor}
{attribute}
{presenterClass}
{value}
on:change={(e) => {
onChange(e.detail)
}}
/>
@@ -13,16 +13,28 @@
// limitations under the License.
-->
<script lang="ts">
import { ButtonIcon, IconClose, Label, resizeObserver, Scroller, Submenu } from '@hcengineering/ui'
import {
ButtonIcon,
closeTooltip,
eventToHTMLElement,
IconClose,
IconSettings,
Label,
resizeObserver,
Scroller,
showPopup,
Submenu
} from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import core, { Class, Doc, Ref } from '@hcengineering/core'
import { Context, ProcessFunction, SelectedContext } from '@hcengineering/process'
import core, { AnyAttribute, Class, Doc, Ref } from '@hcengineering/core'
import { Context, Func, ProcessFunction, SelectedContext } from '@hcengineering/process'
import { getClient } from '@hcengineering/presentation'
import { AttributeCategory } from '@hcengineering/view'
export let contextValue: SelectedContext
export let context: Context
export let attribute: AnyAttribute
export let attrClass: Ref<Class<Doc>>
export let category: AttributeCategory
export let onChange: (contextValue: SelectedContext) => void
@@ -58,14 +70,14 @@
function getAvailableFunctions (
context: Context,
functions: Ref<ProcessFunction>[] | undefined,
functions: Func[] | undefined,
attrClass: Ref<Class<Doc>>,
category: AttributeCategory
): Ref<ProcessFunction>[] {
const result: Ref<ProcessFunction>[] = []
const allFunctions = client.getModel().findAllSync(plugin.class.ProcessFunction, { of: attrClass, category })
for (const f of allFunctions) {
if (functions === undefined || functions.findIndex((p) => p === f._id) === -1) {
if (functions === undefined || f.allowMany === true || functions.findIndex((p) => p.func === f._id) === -1) {
result.push(f._id)
}
}
@@ -74,9 +86,47 @@
function onFallback (): void {}
$: funcs = client
.getModel()
.findAllSync(plugin.class.ProcessFunction, { _id: { $in: contextValue.functions?.map((it) => it.func) } })
$: sourceFunc =
contextValue.sourceFunction !== undefined
? client.getModel().findAllSync(plugin.class.ProcessFunction, { _id: contextValue.sourceFunction })[0]
: undefined
$: functionButtonIndex = functionsLength + (sourceFunc !== undefined ? 1 : 0)
function onFunctionSelect (e: Ref<ProcessFunction>): void {
// if editor is undefined
const func = client.getModel().findAllSync(plugin.class.ProcessFunction, { _id: e })[0]
if (func.editor === undefined) {
addFunction(e, {})
closeTooltip()
} else {
showPopup(
func.editor,
{
func,
context,
attribute
},
elements[functionButtonIndex],
(res) => {
if (res != null) {
addFunction(e, res)
}
}
)
}
}
function addFunction (func: Ref<ProcessFunction>, props: Record<string, any>): void {
const arr = contextValue.functions ?? []
arr.push(e)
arr.push({
func,
props
})
contextValue.functions = arr
onChange(contextValue)
}
@@ -88,7 +138,7 @@
function onFunctionChange (e: Ref<ProcessFunction>, i: number): void {
if (contextValue.functions === undefined) return
contextValue.functions[i] = e
contextValue.functions[i].func = e
contextValue.functions = contextValue.functions
onChange(contextValue)
}
@@ -99,9 +149,8 @@
}
}
function onFunctionRemove (id: Ref<ProcessFunction>): void {
function onFunctionRemove (pos: number): void {
const arr = contextValue.functions ?? []
const pos = arr.findIndex((p) => p === id)
if (pos !== -1) {
arr.splice(pos, 1)
}
@@ -109,12 +158,33 @@
onChange(contextValue)
}
$: funcs = client.getModel().findAllSync(plugin.class.ProcessFunction, { _id: { $in: contextValue.functions } })
$: sourceFunc =
contextValue.sourceFunction !== undefined
? client.getModel().findAllSync(plugin.class.ProcessFunction, { _id: contextValue.sourceFunction })[0]
: undefined
function onConfigure (e: MouseEvent, func: ProcessFunction, pos: number): void {
if (contextValue.functions === undefined || func.editor === undefined) return
const val = contextValue.functions[pos]
showPopup(
func.editor,
{
func,
context,
attribute,
props: val?.props ?? {}
},
eventToHTMLElement(e),
(res) => {
if (res != null) {
if (contextValue.functions !== undefined) {
const func = contextValue.functions[pos]
if (func === undefined) return
func.props = res
contextValue.functions[pos] = func
contextValue.functions = contextValue.functions
console.log(contextValue.functions)
onChange(contextValue)
}
}
}
)
}
</script>
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
@@ -175,12 +245,22 @@
<Label label={f.label} />
</div>
<div>
{#if f.editor}
<ButtonIcon
icon={IconSettings}
size="small"
kind="tertiary"
on:click={(e) => {
onConfigure(e, f, i)
}}
/>
{/if}
<ButtonIcon
icon={IconClose}
size="small"
kind="tertiary"
on:click={() => {
onFunctionRemove(f._id)
onFunctionRemove(i)
}}
/>
</div>
@@ -189,12 +269,12 @@
{/each}
{#if availableFunctions.length > 0}
<Submenu
bind:element={elements[functionsLength + (sourceFunc !== undefined ? 1 : 0)]}
bind:element={elements[functionButtonIndex]}
on:keydown={(event) => {
keyDown(event, functionsLength + (sourceFunc !== undefined ? 1 : 0))
keyDown(event, functionButtonIndex)
}}
on:mouseover={() => {
elements[functionsLength + (sourceFunc !== undefined ? 1 : 0)]?.focus()
elements[functionButtonIndex]?.focus()
}}
label={plugin.string.Functions}
props={{
@@ -57,7 +57,7 @@
onClick({
type: 'attribute',
key: val.name,
functions: valueFunc !== undefined ? [valueFunc] : []
functions: valueFunc !== undefined ? [{ func: valueFunc, props: {} }] : []
})
}
@@ -16,13 +16,14 @@
import { SelectedContext, Context } from '@hcengineering/process'
import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
import ConfigurePopup from './ConfigurePopup.svelte'
import { Ref, Class, Doc } from '@hcengineering/core'
import { Ref, Class, Doc, AnyAttribute } from '@hcengineering/core'
import ContextValuePresenter from './ContextValuePresenter.svelte'
import { AttributeCategory } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
export let contextValue: SelectedContext
export let context: Context
export let attribute: AnyAttribute
export let attrClass: Ref<Class<Doc>>
export let category: AttributeCategory
@@ -40,7 +41,11 @@
dispatch('update', res)
}
}
showPopup(ConfigurePopup, { contextValue, attrClass, category, context, onChange }, eventToHTMLElement(e))
showPopup(
ConfigurePopup,
{ contextValue, attrClass, category, attribute, context, onChange },
eventToHTMLElement(e)
)
}
</script>
@@ -48,7 +48,7 @@
type: 'nested',
key: attr.name,
path: context.attribute.name,
functions: valueFunc !== undefined ? [valueFunc] : [],
functions: valueFunc !== undefined ? [{ func: valueFunc, props: {} }] : [],
sourceFunction: pathReduce
})
}
@@ -53,7 +53,7 @@
association: context.association,
direction: context.direction,
name: context.name,
functions: valueFunc !== undefined ? [valueFunc] : [],
functions: valueFunc !== undefined ? [{ func: valueFunc, props: {} }] : [],
sourceFunction: reduceFunc
})
}
@@ -0,0 +1,61 @@
<!--
// 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 { Card } from '@hcengineering/presentation'
import { createEventDispatcher } from 'svelte'
import process from '../../plugin'
import ui, { DropdownIntlItem, DropdownLabelsIntl, NumberInput } from '@hcengineering/ui'
export const props: Record<string, any> = {}
let offset = props?.offset
let offsetType = props?.offsetType ?? 'days'
let direction = props?.direction ?? 'after'
const items: DropdownIntlItem[] = [
{ id: 'days', label: ui.string.DaysWOValue },
{ id: 'weeks', label: ui.string.WeeksWOValue },
{ id: 'months', label: ui.string.MonthsWOValue }
]
const directions: DropdownIntlItem[] = [
{ id: 'after', label: ui.string.After },
{ id: 'before', label: ui.string.Before }
]
const dispatch = createEventDispatcher()
function save (): void {
dispatch('close', {
offset,
offsetType,
direction
})
}
$: params = {
days: offset,
weeks: offset,
months: offset
}
</script>
<Card on:close width={'small'} label={process.string.Offset} canSave okAction={save}>
<div class="flex-row-center flex-gap-2">
<DropdownLabelsIntl items={directions} bind:selected={direction} />
<NumberInput bind:value={offset} autoFocus focusable maxDigitsAfterPoint={0} />
<DropdownLabelsIntl {items} bind:selected={offsetType} {params} />
</div></Card
>
@@ -0,0 +1,74 @@
<!--
// 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 { getResource } from '@hcengineering/platform'
import presentation, { Card, getClient } from '@hcengineering/presentation'
import { Context, ProcessFunction } from '@hcengineering/process'
import { AnySvelteComponent } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import ProcessAttribute from '../ProcessAttribute.svelte'
export let func: ProcessFunction
export let context: Context
export let attribute: AnyAttribute
export let props: Record<string, any> = {}
let value: number = props?.offset ?? 0
const dispatch = createEventDispatcher()
const client = getClient()
const h = client.getHierarchy()
function save (): void {
dispatch('close', { offset: value })
}
let editor: AnySvelteComponent | undefined
function getEditor (): void {
try {
const inlineEditor = h.as(h.getClass(core.class.TypeNumber), view.mixin.AttributeEditor).inlineEditor
void getResource(inlineEditor)
.then((p) => {
editor = p
})
.catch((e) => {
console.error(e)
})
} catch (e) {
console.error(e)
}
}
getEditor()
</script>
<Card on:close width={'menu'} label={func.label} canSave okAction={save} okLabel={presentation.string.Save}>
<ProcessAttribute
{context}
{attribute}
presenterClass={{
attrClass: core.class.TypeNumber,
category: 'attribute'
}}
{value}
{editor}
on:change={(e) => {
value = e.detail
}}
/>
</Card>
+5 -1
View File
@@ -31,6 +31,8 @@ import FunctionSelector from './components/attributeEditors/FunctionSelector.sve
import Main from './components/Main.svelte'
import RunProcessCardPopup from './components/RunProcessCardPopup.svelte'
import { showDoneQuery } from './utils'
import DateOffsetEditor from './components/contextEditors/DateOffsetEditor.svelte'
import NumberOffsetEditor from './components/contextEditors/NumberOffsetEditor.svelte'
export default async (): Promise<Resources> => ({
component: {
@@ -51,7 +53,9 @@ export default async (): Promise<Resources> => ({
RelatedContextSelector,
FunctionSelector,
Main,
RunProcessCardPopup
RunProcessCardPopup,
DateOffsetEditor,
NumberOffsetEditor
},
function: {
ShowDoneQuery: showDoneQuery
+11 -2
View File
@@ -40,7 +40,9 @@ export default mergeIds(processId, process, {
NestedContextSelector: '' as AnyComponent,
RelatedContextSelector: '' as AnyComponent,
FunctionSelector: '' as AnyComponent,
RunProcessCardPopup: '' as AnyComponent
RunProcessCardPopup: '' as AnyComponent,
DateOffsetEditor: '' as AnyComponent,
NumberOffsetEditor: '' as AnyComponent
},
function: {
ShowDoneQuery: '' as ViewQueryAction
@@ -76,6 +78,13 @@ export default mergeIds(processId, process, {
Random: '' as IntlString,
MyProcesses: '' as IntlString,
AllProcesses: '' as IntlString,
ShowDone: '' as IntlString
ShowDone: '' as IntlString,
Increment: '' as IntlString,
Decrement: '' as IntlString,
Add: '' as IntlString,
Subtract: '' as IntlString,
Offset: '' as IntlString,
Value: '' as IntlString,
FirstWorkingDayAfter: '' as IntlString
}
})
+1 -1
View File
@@ -51,7 +51,7 @@ export function getContext (
attr?: Ref<AnyAttribute>
): Context {
let attributes = getClassAttributes(client, process.masterTag, target, category)
if (attr !== undefined) {
if (attr !== undefined && category === 'object') {
attributes = attributes.filter((it) => it._id !== attr)
}
const nested: Record<string, NestedContext> = {}
+8 -3
View File
@@ -72,10 +72,11 @@ export interface Method<T extends Doc> extends Doc {
presenter?: AnyComponent
}
// add impl in server
export interface ProcessFunction extends Doc {
of: Ref<Class<Doc>>
category?: AttributeCategory
editor?: AnyComponent
category: AttributeCategory | undefined
allowMany?: boolean
label: IntlString
}
@@ -113,6 +114,10 @@ export default plugin(processId, {
Random: '' as Ref<ProcessFunction>,
UpperCase: '' as Ref<ProcessFunction>,
LowerCase: '' as Ref<ProcessFunction>,
Trim: '' as Ref<ProcessFunction>
Trim: '' as Ref<ProcessFunction>,
Add: '' as Ref<ProcessFunction>,
Subtract: '' as Ref<ProcessFunction>,
Offset: '' as Ref<ProcessFunction>,
FirstWorkingDayAfter: '' as Ref<ProcessFunction>
}
})
+6 -1
View File
@@ -19,6 +19,11 @@ export interface RelatedContext {
attributes: AnyAttribute[]
}
export interface Func {
func: Ref<ProcessFunction>
props: Record<string, any>
}
interface BaseSelectedContext {
type: 'attribute' | 'relation' | 'nested'
// attribute key
@@ -28,7 +33,7 @@ interface BaseSelectedContext {
sourceFunction?: Ref<ProcessFunction>
// process one by one
functions?: Array<Ref<ProcessFunction>>
functions?: Func[]
fallbackValue?: any
}
+1 -1
View File
@@ -5,7 +5,7 @@ export function parseContext (value: any): SelectedContext | undefined {
if (typeof value !== 'string') return
const index = value.indexOf('${')
if (index === -1) return
const endIndex = value.indexOf('}')
const endIndex = value.lastIndexOf('}')
if (endIndex === -1) return
const content = value.substring(index + 1, endIndex + 1)
try {
@@ -15,7 +15,7 @@
-->
<script lang="ts">
import type { IntlString } from '@hcengineering/platform'
import type { ButtonSize } from '@hcengineering/ui'
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
import { EditBox, Label, showPopup, eventToHTMLElement, Button } from '@hcengineering/ui'
import EditBoxPopup from './EditBoxPopup.svelte'
@@ -24,7 +24,7 @@
export let autoFocus: boolean = false
// export let maxWidth: string = '10rem'
export let onChange: (value: number | undefined) => void
export let kind: 'no-border' | 'link' | 'button' | 'list' = 'no-border'
export let kind: ButtonKind | undefined = undefined
export let readonly = false
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
@@ -40,9 +40,9 @@
}
</script>
{#if kind === 'button' || kind === 'link' || kind === 'list'}
{#if kind}
<Button
kind={kind === 'button' ? 'regular' : kind}
{kind}
{size}
{justify}
{width}
@@ -20,7 +20,7 @@
export let value: number | undefined
export let label: IntlString
export let onChange: ((value: number | undefined) => void) | undefined = undefined
export let kind: 'no-border' | 'link' | 'button' | 'list' = 'link'
export let kind: 'no-border' | 'link' | 'list' = 'link'
export let readonly = false
</script>
+98 -20
View File
@@ -22,6 +22,7 @@ import core, {
Ref,
RefTo,
SortingOrder,
Timestamp,
Tx,
TxCreateDoc,
TxProcessor,
@@ -162,19 +163,24 @@ async function executeAction<T extends Doc> (
}
}
async function fillValue (value: any, context: SelectedContext, control: TriggerControl): Promise<any> {
async function fillValue (
value: any,
context: SelectedContext,
control: TriggerControl,
execution: Execution
): Promise<any> {
if (value === undefined) {
// we should add error to execution
throw new Error('Value not found')
}
for (const func of context.functions ?? []) {
try {
const transform = control.modelDb.findObject(func)
const transform = control.modelDb.findObject(func.func)
if (transform === undefined) continue
if (!control.hierarchy.hasMixin(transform, serverProcess.mixin.FuncImpl)) continue
const funcImpl = control.hierarchy.as(transform, serverProcess.mixin.FuncImpl)
const f = await getResource(funcImpl.func)
value = f(value)
value = await f(value, func.props, control, execution)
} catch (err: any) {
control.ctx.error(err)
}
@@ -211,7 +217,7 @@ async function getNestedValue (control: TriggerControl, execution: Execution, co
if (!control.hierarchy.hasMixin(transform, serverProcess.mixin.FuncImpl)) return
const funcImpl = control.hierarchy.as(transform, serverProcess.mixin.FuncImpl)
const f = await getResource(funcImpl.func)
const reduced = f(target)
const reduced = await f(target, {}, control, execution)
return getObjectValue(context.key, reduced)
}
return getObjectValue(context.key, target[0])
@@ -238,7 +244,7 @@ async function getRelationValue (
if (!control.hierarchy.hasMixin(transform, serverProcess.mixin.FuncImpl)) return
const funcImpl = control.hierarchy.as(transform, serverProcess.mixin.FuncImpl)
const f = await getResource(funcImpl.func)
const reduced = f(target)
const reduced = await f(target, {}, control, execution)
return getObjectValue(context.key, reduced)
}
return getObjectValue(context.key, target[0])
@@ -252,24 +258,29 @@ async function fillParams<T extends Doc> (
const res: MethodParams<T> = {}
for (const key in params) {
const value = (params as any)[key]
const context = parseContext(value)
if (context !== undefined) {
let value = context.fallbackValue
if (context.type === 'attribute') {
value = await getAttributeValue(control, execution, context)
} else if (context.type === 'relation') {
value = await getRelationValue(control, execution, context)
} else if (context.type === 'nested') {
value = await getNestedValue(control, execution, context)
}
;(res as any)[key] = await fillValue(value === undefined ? context.fallbackValue : value, context, control)
} else {
;(res as any)[key] = value
}
const valueResult = await getContextValue(value, control, execution)
;(res as any)[key] = valueResult
}
return res
}
async function getContextValue (value: any, control: TriggerControl, execution: Execution): Promise<any> {
const context = parseContext(value)
if (context !== undefined) {
let value = context.fallbackValue
if (context.type === 'attribute') {
value = await getAttributeValue(control, execution, context)
} else if (context.type === 'relation') {
value = await getRelationValue(control, execution, context)
} else if (context.type === 'nested') {
value = await getNestedValue(control, execution, context)
}
return await fillValue(value === undefined ? context.fallbackValue : value, context, control, execution)
} else {
return value
}
}
async function changeState (
execution: Execution,
state: State,
@@ -479,6 +490,69 @@ export function Trim (value: string): string {
return value.trim()
}
export async function Add (
value: number,
props: Record<string, any>,
control: TriggerControl,
execution: Execution
): Promise<number> {
const context = parseContext(props.offset)
if (context !== undefined) {
if (context.type === 'attribute') {
const offset = await getAttributeValue(control, execution, context)
return value + offset
}
} else if (typeof value === 'number') {
return value + props.offset
}
return value
}
export async function Subtract (
value: number,
props: Record<string, any>,
control: TriggerControl,
execution: Execution
): Promise<number> {
const context = parseContext(props.offset)
if (context !== undefined) {
if (context.type === 'attribute') {
const offset = await getAttributeValue(control, execution, context)
return value - offset
}
} else if (typeof value === 'number') {
return value - props.offset
}
return value
}
export function Offset (val: Timestamp, props: Record<string, any>): Timestamp {
if (typeof val !== 'number') return val
const value = new Date(val)
const offset = props.offset * (props.direction === 'after' ? 1 : -1)
switch (props.offsetType) {
case 'days':
return value.setDate(value.getDate() + offset)
case 'weeks':
return value.setDate(value.getDate() + 7 * offset)
case 'months':
return value.setMonth(value.getMonth() + offset)
}
return val
}
export function FirstWorkingDayAfter (val: Timestamp): Timestamp {
if (typeof val !== 'number') return val
const value = new Date(val)
const day = value.getUTCDay()
if (day === 6 || day === 0) {
const date = value.getDate() + (day === 6 ? 2 : 1)
const res = value.setDate(date)
return res
}
return val
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
func: {
@@ -492,7 +566,11 @@ export default async () => ({
Random,
UpperCase,
LowerCase,
Trim
Trim,
Add,
Subtract,
Offset,
FirstWorkingDayAfter
},
trigger: {
OnExecutionCreate,
+5 -1
View File
@@ -39,7 +39,11 @@ export default plugin(serverProcessId, {
Random: '' as Resource<TransformFunc>,
UpperCase: '' as Resource<TransformFunc>,
LowerCase: '' as Resource<TransformFunc>,
Trim: '' as Resource<TransformFunc>
Trim: '' as Resource<TransformFunc>,
Add: '' as Resource<TransformFunc>,
Subtract: '' as Resource<TransformFunc>,
Offset: '' as Resource<TransformFunc>,
FirstWorkingDayAfter: '' as Resource<TransformFunc>
},
trigger: {
OnExecutionCreate: '' as Resource<TriggerFunc>,
+6 -1
View File
@@ -13,4 +13,9 @@ export interface ExecuteResult {
rollback: Tx[] | undefined
}
export type TransformFunc = (value: any) => any
export type TransformFunc = (
value: any,
props: Record<string, any>,
control: TriggerControl,
execution: Execution
) => Promise<any>