Create card process (#9522)

This commit is contained in:
Denis Bykhov
2025-07-11 09:11:58 +07:00
committed by GitHub
parent 881a12cc6e
commit cd3396cf9d
48 changed files with 1005 additions and 467 deletions
@@ -13,10 +13,11 @@
// limitations under the License.
-->
<script lang="ts">
import { Execution } from '@hcengineering/process'
import { Doc, WithLookup } from '@hcengineering/core'
import presentation, { ActionContext, Card, createQuery } from '@hcengineering/presentation'
import presentation, { ActionContext, createQuery } from '@hcengineering/presentation'
import { Execution } from '@hcengineering/process'
import { Modal, registerFocus } from '@hcengineering/ui'
import view, { Viewlet, ViewletPreference, ViewOptions } from '@hcengineering/view'
import {
List,
ListSelectionProvider,
@@ -26,7 +27,6 @@
} from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import process from '../plugin'
import view, { Viewlet, ViewletPreference, ViewOptions } from '@hcengineering/view'
export let execution: Execution
@@ -43,7 +43,7 @@
}
)
let docs: Doc[] = []
function select () {
function select (): void {
listProvider.update(docs)
listProvider.updateFocus(docs[0])
list?.select(0, undefined)
@@ -68,6 +68,7 @@
let preference: ViewletPreference | undefined = undefined
const query = createQuery()
const viewletId = process.viewlet.ExecutionLogList
$: query.query(
view.class.Viewlet,
@@ -86,8 +87,6 @@
const preferenceQuery = createQuery()
const viewletId = process.viewlet.ExecutionLogList
$: if (viewlet != null) {
preferenceQuery.query(
view.class.ViewletPreference,
@@ -13,8 +13,9 @@
// limitations under the License.
-->
<script lang="ts">
import { MasterTag, Tag } from '@hcengineering/card'
import { AnyAttribute, Class, Doc, Ref } from '@hcengineering/core'
import { Context, parseContext, SelectedContext, Process } from '@hcengineering/process'
import { Context, parseContext, Process, SelectedContext } from '@hcengineering/process'
import {
AnySvelteComponent,
Button,
@@ -29,7 +30,6 @@
import { createEventDispatcher } from 'svelte'
import ContextSelectorPopup from './attributeEditors/ContextSelectorPopup.svelte'
import ContextValue from './attributeEditors/ContextValue.svelte'
import { MasterTag, Tag } from '@hcengineering/card'
export let process: Process
export let masterTag: Ref<MasterTag | Tag>
@@ -61,6 +61,7 @@
masterTag,
context,
attribute,
forbidValue,
onSelect
},
eventToHTMLElement(e)
@@ -73,69 +74,68 @@
}
</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
{process}
{masterTag}
{contextValue}
{context}
{attribute}
{allowArray}
category={presenterClass.category}
attrClass={presenterClass.attrClass}
on:update={(e) => {
onSelect(e.detail)
}}
/>
{:else}
<div class="w-full">
{#if !forbidValue}
<svelte:component
this={editor}
label={attribute?.label}
placeholder={attribute?.label}
kind={'ghost'}
size={'large'}
width={'100%'}
justify={'left'}
type={attribute?.type}
{value}
{onChange}
{focus}
/>
{/if}
</div>
{/if}
<div class="button flex-row-center">
<Button
icon={IconAdd}
kind="ghost"
on:click={(e) => {
selectContext(e)
}}
/>
{#if allowRemove}
<Button
icon={IconClose}
kind="ghost"
on:click={() => {
dispatch('remove', { key: attribute.name })
}}
<span
class="labelOnPanel"
use:tooltip={{
props: { label: attribute.label }
}}
>
<Label label={attribute.label} />
</span>
<div class="text-input" class:context={contextValue}>
{#if contextValue}
<ContextValue
{process}
{masterTag}
{contextValue}
{context}
{attribute}
{allowArray}
category={presenterClass.category}
attrClass={presenterClass.attrClass}
{forbidValue}
on:update={(e) => {
onSelect(e.detail)
}}
/>
{:else}
<div class="w-full">
{#if !forbidValue && editor}
<svelte:component
this={editor}
label={attribute?.label}
placeholder={attribute?.label}
kind={'ghost'}
size={'large'}
width={'100%'}
justify={'left'}
type={attribute?.type}
{value}
{onChange}
{focus}
/>
{/if}
</div>
{/if}
<div class="button flex-row-center">
<Button
icon={IconAdd}
kind="ghost"
on:click={(e) => {
selectContext(e)
}}
/>
{#if allowRemove}
<Button
icon={IconClose}
kind="ghost"
on:click={() => {
dispatch('remove', { key: attribute.name })
}}
/>
{/if}
</div>
{/if}
</div>
<style lang="scss">
.text-input {
@@ -13,6 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { MasterTag, Tag } from '@hcengineering/card'
import { AnyAttribute, Class, Doc, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Context, Func, Process, ProcessFunction, SelectedContext } from '@hcengineering/process'
@@ -33,7 +34,6 @@
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import FallbackEditor from '../contextEditors/FallbackEditor.svelte'
import { MasterTag, Tag } from '@hcengineering/card'
export let process: Process
export let masterTag: Ref<MasterTag | Tag>
@@ -44,6 +44,7 @@
export let category: AttributeCategory
export let onChange: (contextValue: SelectedContext) => void
export let allowArray: boolean = false
export let forbidValue: boolean = false
const client = getClient()
@@ -321,50 +322,52 @@
{/if}
<div class="menu-separator" />
{/if}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
bind:this={elements[functionButtonIndex + 1]}
on:keydown={(event) => {
keyDown(event, functionButtonIndex + 1)
}}
on:mouseover={() => {
elements[functionButtonIndex + 1]?.focus()
}}
on:click={onFallbackChange}
class="menu-item flex-gap-2 fallback"
>
<div>
<div class="label">
<Label label={plugin.string.Required} />
</div>
<div class="text-sm">
<Label label={plugin.string.FallbackValueError} />
</div>
</div>
<CheckBox
on:click={onFallbackChange}
checked={contextValue.fallbackValue === undefined}
size={'medium'}
kind={'primary'}
/>
</button>
{#if contextValue.fallbackValue !== undefined}
{#if !forbidValue}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
bind:this={elements[functionButtonIndex + 2]}
bind:this={elements[functionButtonIndex + 1]}
on:keydown={(event) => {
keyDown(event, functionButtonIndex + 2)
keyDown(event, functionButtonIndex + 1)
}}
on:mouseover={() => {
elements[functionButtonIndex + 2]?.focus()
elements[functionButtonIndex + 1]?.focus()
}}
on:click={onFallback}
class="menu-item"
on:click={onFallbackChange}
class="menu-item flex-gap-2 fallback"
>
<span class="overflow-label pr-1">
<Label label={plugin.string.FallbackValue} />
</span>
<div>
<div class="label">
<Label label={plugin.string.Required} />
</div>
<div class="text-sm">
<Label label={plugin.string.FallbackValueError} />
</div>
</div>
<CheckBox
on:click={onFallbackChange}
checked={contextValue.fallbackValue === undefined}
size={'medium'}
kind={'primary'}
/>
</button>
{#if contextValue.fallbackValue !== undefined}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
bind:this={elements[functionButtonIndex + 2]}
on:keydown={(event) => {
keyDown(event, functionButtonIndex + 2)
}}
on:mouseover={() => {
elements[functionButtonIndex + 2]?.focus()
}}
on:click={onFallback}
class="menu-item"
>
<span class="overflow-label pr-1">
<Label label={plugin.string.FallbackValue} />
</span>
</button>
{/if}
{/if}
</Scroller>
<div class="menu-space" />
@@ -18,7 +18,6 @@
import { getClient } from '@hcengineering/presentation'
import {
Context,
ContextId,
Process,
ProcessContext,
ProcessFunction,
@@ -28,7 +27,7 @@
import { Label, resizeObserver, Scroller, Submenu } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
import { generateContextId, getRelationObjectReduceFunc, getValueReduceFunc, isTypeEqual } from '../../utils'
import { generateContextId, getRelationObjectReduceFunc, getValueReduceFunc } from '../../utils'
import ExecutionContextPresenter from './ExecutionContextPresenter.svelte'
export let process: Process
@@ -36,6 +35,7 @@
export let context: Context
export let attribute: AnyAttribute
export let onSelect: (val: SelectedContext | null) => void
export let forbidValue: boolean = false
const dispatch = createEventDispatcher()
@@ -58,18 +58,7 @@
})
}
$: processContext = getProcessContext(process, attribute)
function getProcessContext (process: Process, attribute: AnyAttribute): ProcessContext[] {
const res: ProcessContext[] = []
for (const key in process?.context ?? {}) {
const ctx = process.context[key as ContextId]
if (ctx._class === attribute.type._class && isTypeEqual(ctx.type, attribute.type)) {
res.push(ctx)
}
}
return res
}
$: processContext = Object.values(context.executionContext)
$: nested = Object.values(context.nested)
$: relations = Object.entries(context.relations)
@@ -236,17 +225,19 @@
{/each}
<div class="menu-separator" />
{/if}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
on:click={() => {
onCustom()
}}
class="menu-item"
>
<span class="overflow-label pr-1">
<Label label={plugin.string.CustomValue} />
</span>
</button>
{#if !forbidValue}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
on:click={() => {
onCustom()
}}
class="menu-item"
>
<span class="overflow-label pr-1">
<Label label={plugin.string.CustomValue} />
</span>
</button>
{/if}
</Scroller>
<div class="menu-space" />
</div>
@@ -13,14 +13,14 @@
// limitations under the License.
-->
<script lang="ts">
import { SelectedContext, Context, Process } from '@hcengineering/process'
import { MasterTag, Tag } from '@hcengineering/card'
import { AnyAttribute, Class, Doc, Ref } from '@hcengineering/core'
import { Context, Process, SelectedContext } from '@hcengineering/process'
import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
import ConfigurePopup from './ConfigurePopup.svelte'
import { Ref, Class, Doc, AnyAttribute } from '@hcengineering/core'
import ContextValuePresenter from './ContextValuePresenter.svelte'
import { AttributeCategory } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import { MasterTag, Tag } from '@hcengineering/card'
import ConfigurePopup from './ConfigurePopup.svelte'
import ContextValuePresenter from './ContextValuePresenter.svelte'
export let process: Process
export let masterTag: Ref<MasterTag | Tag>
@@ -30,6 +30,7 @@
export let attrClass: Ref<Class<Doc>>
export let category: AttributeCategory
export let allowArray: boolean = false
export let forbidValue: boolean = false
const dispatch = createEventDispatcher()
@@ -50,7 +51,7 @@
}
showPopup(
ConfigurePopup,
{ contextValue, attrClass, masterTag, category, attribute, context, onChange, allowArray },
{ contextValue, attrClass, masterTag, category, attribute, context, onChange, allowArray, forbidValue },
eventToHTMLElement(e)
)
}
@@ -1,12 +0,0 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large' | 'full'
const fill: string = 'currentColor'
</script>
<svg xmlns="http://www.w3.org/2000/svg" class="svg-{size}" viewBox="0 0 16 45" fill="none">
<path d="M8 0L8 43" stroke={fill} />
<path
d="M12.295 37.295L8.5 41.085V31.5C8.5 31.22386 8.27614 31 8 31C7.72386 31 7.5 31.22386 7.5 31.5V41.085L3.705 37.295L3 38L8 43L13 38L12.295 37.295Z"
{fill}
/>
</svg>
@@ -1,11 +0,0 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large' | 'full'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" viewBox="0 0 6 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M3 0.333333C1.52724 0.333333 0.333333 1.52724 0.333333 3C0.333333 4.47276 1.52724 5.66667 3 5.66667C4.47276 5.66667 5.66667 4.47276 5.66667 3C5.66667 1.52724 4.47276 0.333333 3 0.333333ZM2.5 3L2.5 48L3.5 48L3.5 3L2.5 3Z"
{fill}
/>
</svg>
@@ -1,33 +0,0 @@
<script lang="ts">
import { ColorDefinition, IconSize, getPlatformColorDef, themeStore } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
export let size: IconSize = 'small'
export let fill: number = 21
const dispatch = createEventDispatcher()
const dispatchAccentColor = (color?: ColorDefinition) => dispatch('accent-color', color)
$: color = getPlatformColorDef(fill, $themeStore.dark)
$: dispatchAccentColor(color)
onMount(() => {
dispatchAccentColor(color)
})
</script>
<svg
class="svg-{size}"
fill={color?.icon ?? 'currentColor'}
style:flex-shrink={0}
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.97975 1.07389C3.92356 1.52014 1.50831 3.94118 1.0708 7.0002H2.08287C2.50337 4.49345 4.47822 2.51374 6.9825 2.08599L6.97975 1.07389ZM8.98249 2.08014L8.97974 1.06812C12.055 1.49885 14.4896 3.92773 14.9291 7.0002H13.917C13.4945 4.48183 11.5033 2.4954 8.98249 2.08014ZM9.01467 13.9146C11.5201 13.4879 13.4962 11.5077 13.917 9.00019H14.929C14.4913 12.06 12.0748 14.4815 9.01742 14.9267L9.01467 13.9146ZM2.0829 9.0002C2.50529 11.5176 4.49522 13.5034 7.01468 13.9196L7.01743 14.9317C3.94351 14.4999 1.51022 12.0717 1.07083 9.0002H2.0829Z"
/>
</svg>
@@ -1,34 +0,0 @@
<script lang="ts">
import { ColorDefinition, IconSize, getPlatformColorDef, themeStore } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
export let size: IconSize = 'small'
export let fill: number = 17
const dispatch = createEventDispatcher()
const dispatchAccentColor = (color?: ColorDefinition) => dispatch('accent-color', color)
$: color = getPlatformColorDef(fill, $themeStore.dark)
$: dispatchAccentColor(color)
onMount(() => {
dispatchAccentColor(color)
})
</script>
<svg
class="svg-{size}"
fill={color?.icon ?? 'currentColor'}
style:flex-shrink={0}
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8,1C4.1,1,1,4.1,1,8c0,3.9,3.1,7,7,7c3.9,0,7-3.1,7-7C15,4.1,11.9,1,8,1z M8,14c-3.3,0-6-2.7-6-6s2.7-6,6-6s6,2.7,6,6S11.3,14,8,14z"
/>
<path
d="M10.6,6.1L7.5,9.2L5.9,7.6c-0.2-0.2-0.6-0.2-0.8,0s-0.2,0.6,0,0.8l2,2c0.1,0.1,0.3,0.2,0.4,0.2s0.3-0.1,0.4-0.2l3.5-3.5c0.2-0.2,0.2-0.6,0-0.8S10.8,5.8,10.6,6.1z"
/>
</svg>
@@ -1,53 +0,0 @@
<script lang="ts">
import { ColorDefinition, IconSize, getPlatformColorDef, themeStore } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
export let size: IconSize = 'small'
export let fill: number = 11
const dispatch = createEventDispatcher()
const dispatchAccentColor = (color?: ColorDefinition) => dispatch('accent-color', color)
export let count: number | undefined = undefined
export let index: number | undefined = undefined
$: color = getPlatformColorDef(fill, $themeStore.dark)
$: dispatchAccentColor(color)
onMount(() => {
dispatchAccentColor(color)
})
</script>
<svg
class="svg-{size}"
fill={color?.icon ?? 'currentColor'}
style:flex-shrink={0}
style:transform={'rotate(-90deg)'}
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14ZM8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15Z"
/>
{#if count !== undefined && index !== undefined}
<path
d="M 4.5 4.5 L 9 4.5 A 4.5 4.5 0 {index > (count - 1) / 2 ? 1 : 0} 1 {Math.cos(
((2 * Math.PI) / count) * index - 0.01
) *
4.5 +
4.5} {Math.sin(((2 * Math.PI) / count) * index - 0.01) * 4.5 + 4.5} Z"
transform="translate(3.5,3.5)"
/>
{:else}
<path
d="M 4.5 4.5 L 9 4.5 A 4.5 4.5 0 1 1 {Math.cos(Math.PI - 0.01) * 4.5 + 4.5} {Math.sin(Math.PI - 0.01) * 4.5 +
4.5} Z"
transform="translate(3.5,3.5)"
/>
{/if}
</svg>
@@ -0,0 +1,62 @@
<!--
// 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/card'
import { Association, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { MethodParams, parseContext, Process, Step } from '@hcengineering/process'
import { Label } from '@hcengineering/ui'
import plugin from '../../plugin'
import { getContext } from '../../utils'
import ContextValuePresenter from '../attributeEditors/ContextValuePresenter.svelte'
export let step: Step<Card>
export let process: Process
export let params: MethodParams<Card>
const client = getClient()
$: method = client.getModel().findAllSync(plugin.class.Method, { _id: step.methodId })[0]
$: association = params.association as Ref<Association> | undefined
$: selected = association && client.getModel().findObject(association)
$: direction = params.direction as 'A' | 'B' | undefined
$: assoc = association && client.getModel().findObject(association)
$: targetClass = assoc && direction ? (direction === 'A' ? assoc.classA : assoc.classB) : undefined
$: contextValue = params._id !== undefined ? parseContext(params._id) : undefined
$: context = targetClass !== undefined ? getContext(client, process, targetClass, 'object') : undefined
</script>
<div class="flex-row-center flex-gap-1">
<Label label={method.label} />:
<div class="title">
{#if selected && direction}
{direction === 'A' ? selected.nameA : selected.nameB}
{/if}
</div>
{#if contextValue !== undefined && context !== undefined}
<ContextValuePresenter {contextValue} {context} {process} />
{/if}
</div>
<style lang="scss">
.title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--theme-caption-color);
}
</style>
@@ -0,0 +1,71 @@
<!--
// 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, { Card, MasterTag } from '@hcengineering/card'
import core, { Ref } from '@hcengineering/core'
import { getClient, IconWithEmoji } from '@hcengineering/presentation'
import { MethodParams, parseContext, Process, Step } from '@hcengineering/process'
import { Icon, Label, tooltip } from '@hcengineering/ui'
import view from '@hcengineering/view'
import plugin from '../../plugin'
import { getContext } from '../../utils'
import ContextValuePresenter from '../attributeEditors/ContextValuePresenter.svelte'
export let step: Step<Card>
export let process: Process
export let params: MethodParams<Card>
const client = getClient()
$: method = client.getModel().findAllSync(plugin.class.Method, { _id: step.methodId })[0]
$: contextValue = params.title !== undefined ? parseContext(params.title) : undefined
$: context = getContext(client, process, core.class.TypeString, 'attribute')
$: _class = params?._class
? (client.getHierarchy().getClass(params._class as Ref<MasterTag>) as MasterTag)
: undefined
$: icon = _class?.icon
</script>
<div class="flex-row-center flex-gap-1">
<Label label={method.label} />:
{#if icon}
<div class="icon" use:tooltip={{ label: _class?.label ?? card.string.Card }}>
<Icon
icon={icon === view.ids.IconWithEmoji ? IconWithEmoji : icon ?? card.icon.Card}
iconProps={{ icon: _class?.color }}
size={'small'}
/>
</div>
{/if}
{#if params.title}
<div class="title">
{#if contextValue}
<ContextValuePresenter {contextValue} {context} {process} />
{:else}
{params.title}
{/if}
</div>
{/if}
</div>
<style lang="scss">
.title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--theme-caption-color);
}
</style>
@@ -0,0 +1,120 @@
<!--
// 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, Association, Doc, generateId, Ref, RefTo, Relation } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Process, Step } from '@hcengineering/process'
import { Label, tooltip } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { getContext } from '../../utils'
import ProcessAttribute from '../ProcessAttribute.svelte'
import AssociationSelector from './AssociationSelector.svelte'
export let process: Process
export let step: Step<Relation>
const dispatch = createEventDispatcher()
const params = step.params
let association = params.association as Ref<Association> | undefined
let direction = params.direction as 'A' | 'B' | undefined
const _id = params._id
const client = getClient()
$: assoc = association && client.getModel().findObject(association)
$: targetClass = assoc && direction ? (direction === 'A' ? assoc.classA : assoc.classB) : undefined
function changeAssociation (e: CustomEvent<any>): void {
if (e.detail !== undefined) {
association = e.detail.association
direction = e.detail.direction
params.association = association
params.direction = direction
;(step.params as any) = params
dispatch('change', step)
}
}
function changeParam (e: CustomEvent<any>): void {
if (e.detail !== undefined) {
params._id = e.detail
;(step.params as any)._id = e.detail
dispatch('change', step)
}
}
let attribute: AnyAttribute
$: attribute = {
attributeOf: process.masterTag,
name: '',
type: {
label: core.string.Ref,
_class: core.class.RefTo,
to: targetClass
},
_id: generateId(),
space: core.space.Model,
modifiedOn: 0,
modifiedBy: core.account.System,
_class: core.class.Attribute,
label: core.string.Object
}
$: context = targetClass && getContext(client, process, targetClass, 'object')
</script>
<div class="grid">
<span
class="labelOnPanel"
use:tooltip={{
props: { label: core.string.Relation }
}}
>
<Label label={core.string.Relation} />
</span>
<AssociationSelector {process} {association} {direction} on:change={changeAssociation} />
{#if targetClass && context && attribute}
<ProcessAttribute
value={_id}
{process}
{attribute}
{context}
masterTag={process.masterTag}
editor={undefined}
presenterClass={{
attrClass: targetClass,
category: 'object'
}}
forbidValue
on:change={changeParam}
/>
{/if}
</div>
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 1fr 1.5fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
margin: 0.25rem 2rem 0;
width: calc(100% - 4rem);
height: min-content;
}
</style>
@@ -0,0 +1,84 @@
<!--
// 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, { Association, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Process } from '@hcengineering/process'
import { Button, eventToHTMLElement, Label, SelectPopup, SelectPopupValueType, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
export let process: Process
export let association: Ref<Association> | undefined = undefined
export let direction: 'A' | 'B' | undefined = undefined
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
function open (e: MouseEvent): void {
const descendants = hierarchy.getDescendants(process.masterTag)
const leftAssociations = client.getModel().findAllSync(core.class.Association, { classA: { $in: descendants } })
const rightAssociations = client.getModel().findAllSync(core.class.Association, { classB: { $in: descendants } })
const items: SelectPopupValueType[] = []
rightAssociations.forEach((a) => {
items.push({
id: 'A_' + a._id,
text: a.nameA,
isSelected: association !== undefined && association === a._id && direction === 'A'
})
})
leftAssociations.forEach((a) => {
items.push({
id: 'B_' + a._id,
text: a.nameB,
isSelected: association !== undefined && association === a._id && direction === 'B'
})
})
showPopup(
SelectPopup,
{
value: items
},
eventToHTMLElement(e),
(res) => {
if (res !== undefined) {
const [dir, id] = res.split('_')
association = id as Ref<Association>
direction = dir as 'A' | 'B'
} else {
association = undefined
direction = undefined
}
dispatch('change', {
association,
direction
})
}
)
}
$: selected = association !== undefined && client.getModel().findObject(association)
</script>
<Button on:click={open} width={'100%'}>
<div slot="content">
{#if selected && direction !== undefined}
{direction === 'A' ? selected.nameA : selected.nameB}
{:else}
<Label label={core.string.Relation} />
{/if}
</div>
</Button>
@@ -27,12 +27,12 @@
const client = getClient()
async function save () {
async function save (): Promise<void> {
await client.update(process, { context: process.context })
clearSettingsStore()
}
function onNameChange (ev: Event, _id: string, ctx: ProcessContext) {
function onNameChange (ev: Event, _id: string, ctx: ProcessContext): void {
const value = (ev.target as HTMLInputElement).value?.trim()
ctx.name = value
process.context[_id as ContextId] = ctx
@@ -0,0 +1,170 @@
<!--
// 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 cardPlugin, { Card, MasterTag } from '@hcengineering/card'
import { TypeSelector } from '@hcengineering/card-resources'
import core, { AnyAttribute, Class, Ref } from '@hcengineering/core'
import { translateCB } from '@hcengineering/platform'
import presentation, { getClient } from '@hcengineering/presentation'
import { MethodParams, Process, Step } from '@hcengineering/process'
import { Button, eventToHTMLElement, Label, SelectPopup, showPopup, tooltip } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import ParamsEditor from './ParamsEditor.svelte'
export let process: Process
export let step: Step<Card>
const dispatch = createEventDispatcher()
step.params.title = step.params.title ?? ''
step.params._class = step.params._class ?? process.masterTag
let params = step.params
let _class: Ref<Class<MasterTag>> = (params._class as Ref<Class<MasterTag>>) ?? process.masterTag
const client = getClient()
const hierarchy = client.getHierarchy()
translateCB(cardPlugin.string.Card, {}, undefined, (res) => {
if (params.title === undefined || params.title === '') {
params.title = res
;(step.params as any) = params
dispatch('change', step)
}
})
function change (e: CustomEvent<any>): void {
if (e.detail !== undefined) {
params = e.detail
;(step.params as any) = e.detail
dispatch('change', step)
}
}
function getKeys (_class: Ref<Class<MasterTag>>): AnyAttribute[] {
const ignoreKeys = ['_class', 'content', 'parent', 'attachments', 'todos']
const attributes = hierarchy.getAllAttributes(_class, core.class.Doc)
const res: AnyAttribute[] = []
for (const [key, attr] of attributes) {
if (attr.hidden === true) continue
if (ignoreKeys.includes(key)) continue
res.push(attr)
}
return res
}
let keys = Object.keys(params).filter((key) => {
return key !== '_class'
})
$: allAttrs = getKeys(_class)
$: possibleAttrs = allAttrs.filter((attr) => !keys.includes(attr.name))
function addKey (key: string): void {
keys = [...keys, key]
}
function onAdd (e: MouseEvent): void {
showPopup(
SelectPopup,
{
value: possibleAttrs.map((p) => {
return { id: p.name, label: p.label }
})
},
eventToHTMLElement(e),
(res) => {
if (res != null) {
addKey(res)
}
}
)
}
function remove (e: CustomEvent<any>): void {
if (e.detail !== undefined) {
const key = e.detail.key
if (key === 'title') return
keys = keys.filter((k) => k !== key)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (params as any)[key]
;(step.params as any) = params
dispatch('change', step)
}
}
function typeChange (e: CustomEvent<Ref<Class<MasterTag>>>): void {
if (e.detail === undefined || e.detail === _class) return
allAttrs = getKeys(e.detail)
const oldParams = { ...params }
params = {}
for (const attr of allAttrs) {
const key = attr.name
if (oldParams[key] !== undefined) {
;(params as any)[key] = oldParams[key]
}
}
_class = e.detail
keys = Object.keys(params)
params._class = _class
step.params = params
if (step.context != null) {
step.context._class = _class
}
dispatch('change', step)
}
</script>
<div class="grid">
<span
class="labelOnPanel"
use:tooltip={{
props: { label: cardPlugin.string.MasterTag }
}}
>
<Label label={cardPlugin.string.MasterTag} />
</span>
<TypeSelector value={_class} width={'100%'} on:change={typeChange} />
</div>
<div class="divider" />
{#key _class}
<ParamsEditor {_class} {process} {keys} {params} allowRemove on:remove={remove} on:change={change} />
{/key}
{#if possibleAttrs.length > 0}
<div class="flex-center mt-4">
<Button label={presentation.string.Add} width={'100%'} kind={'link-bordered'} size={'large'} on:click={onAdd} />
</div>
{/if}
<style lang="scss">
.divider {
border-bottom: 1px solid var(--divider-color);
margin: 1rem 0;
}
.grid {
display: grid;
grid-template-columns: 1fr 1.5fr;
grid-auto-rows: minmax(2rem, max-content);
justify-content: start;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
margin: 0.25rem 2rem 0;
width: calc(100% - 4rem);
height: min-content;
}
</style>
@@ -14,7 +14,7 @@
-->
<script lang="ts">
import { Class, Doc, Ref } from '@hcengineering/core'
import { MethodParams, Process, State } from '@hcengineering/process'
import { MethodParams, Process } from '@hcengineering/process'
import { createEventDispatcher } from 'svelte'
import ProcessAttributeEditor from './ProcessAttributeEditor.svelte'
@@ -15,7 +15,7 @@
<script lang="ts">
import core, { Class, Doc, PropertyType, Ref, Type } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { ContextId, Step } from '@hcengineering/process'
import { Step } from '@hcengineering/process'
import setting from '@hcengineering/setting-resources/src/plugin'
import {
AnyComponent,
@@ -24,13 +24,13 @@
const client = getClient()
const query = createQuery()
$: from = transition.from && client.getModel().findObject(transition.from)
$: to = client.getModel().findObject(transition.to)
$: query.query(plugin.class.State, { process: transition.process }, () => {
from = transition.from && client.getModel().findObject(transition.from)
to = client.getModel().findObject(transition.to)
})
$: from = transition.from && client.getModel().findObject(transition.from)
$: to = client.getModel().findObject(transition.to)
</script>
<span>
@@ -14,7 +14,6 @@
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Process, Transition } from '@hcengineering/process'
import { Button, getCurrentLocation, IconAdd, Label, navigate, showPopup } from '@hcengineering/ui'
import plugin from '../../plugin'
@@ -25,8 +24,6 @@
export let transitions: Transition[]
export let readonly: boolean
const client = getClient()
function addTransition (): void {
showPopup(AddTransitionPopup, { process }, 'top')
}
@@ -16,10 +16,10 @@
import { Card, MasterTag } from '@hcengineering/card'
import core, { AnyAttribute, Class, Ref } from '@hcengineering/core'
import presentation, { getClient } from '@hcengineering/presentation'
import { Process, State, Step } from '@hcengineering/process'
import { Process, Step } from '@hcengineering/process'
import { Button, eventToHTMLElement, SelectPopup, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import ParamsEditor from './ParamsEditor.svelte'
import { Button, eventToHTMLElement, SelectPopup, showPopup } from '@hcengineering/ui'
export let process: Process
export let step: Step<Card>