Required attributes (#10918)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2026-06-22 12:53:11 +05:00
committed by GitHub
parent 059cc38928
commit b32ff0f46a
40 changed files with 303 additions and 51 deletions
@@ -14,13 +14,15 @@
-->
<script lang="ts">
import cardPlugin, { Tag } from '@hcengineering/card'
import { Process, Step } from '@hcengineering/process'
import { Process, Step, createContext } from '@hcengineering/process'
import ParamsEditor from './ParamsEditor.svelte'
import { Ref } from '@hcengineering/core'
import presentation, { getClient } from '@hcengineering/presentation'
import { Button, eventToHTMLElement, Label, SelectPopup, showPopup, tooltip } from '@hcengineering/ui'
import { generateContextId } from '../../utils'
import { Button, eventToHTMLElement, Label, SelectPopup, showPopup, Toggle, tooltip } from '@hcengineering/ui'
import TagSelector from './TagSelector.svelte'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
export let process: Process
export let step: Step<Tag>
@@ -41,6 +43,16 @@
params._id = _id
props = {}
params.props = props
if (params.askRequired) {
params.requiredProperties = createContext({
type: 'userRequest',
id: generateContextId(),
_class: _id,
key: 'requiredProperties'
})
} else {
delete params.requiredProperties
}
step.params = params
if (step.context != null) {
step.context._class = _id
@@ -91,6 +103,27 @@
step.params = params
dispatch('change', step)
}
let askRequired = params.askRequired ?? false
$: askRequired = params.askRequired ?? false
function changeAskRequired (e: CustomEvent<boolean>): void {
if (e.detail !== undefined) {
params.askRequired = e.detail
if (e.detail) {
params.requiredProperties = createContext({
type: 'userRequest',
id: generateContextId(),
_class: _id,
key: 'requiredProperties'
})
} else {
delete params.requiredProperties
}
step.params = params
dispatch('change', step)
}
}
</script>
<div class="flex-col flex-gap-2">
@@ -104,6 +137,17 @@
<Label label={cardPlugin.string.Tag} />
</span>
<TagSelector {process} tag={_id} on:change={changeTag} />
<span
class="labelOnPanel"
use:tooltip={{
props: { label: plugin.string.AskRequired }
}}
>
<Label label={plugin.string.AskRequired} />
</span>
<div>
<Toggle on={askRequired} on:change={changeAskRequired} />
</div>
</div>
<ParamsEditor
{process}
@@ -75,7 +75,7 @@
}
let keys = Object.keys(params).filter((key) => {
return key !== '_class' && key !== 'targetClass'
return key !== '_class' && key !== 'targetClass' && key !== 'askRequired' && key !== 'requiredFields'
})
$: allAttrs = getKeys(targetClass)
@@ -116,7 +116,7 @@
function typeChange (_id: Ref<Class<MasterTag>>): void {
if (_id === undefined) return
const attrKeys = ['_class', 'targetClass', ...getKeys(_id).map((p) => p.name)]
const attrKeys = ['_class', 'targetClass', 'askRequired', 'requiredFields', ...getKeys(_id).map((p) => p.name)]
const oldParams = { ...params }
params = {}
for (const key of attrKeys) {
@@ -125,12 +125,22 @@
}
}
keys = Object.keys(params).filter((key) => {
return key !== '_class' && key !== 'targetClass'
return key !== '_class' && key !== 'targetClass' && key !== 'askRequired' && key !== 'requiredFields'
})
if (params.targetClass !== _id) {
params._class = _id
params.targetClass = _id
}
if (params.askRequired) {
params.requiredFields = createContext({
type: 'userRequest',
id: generateContextId(),
_class: _id,
key: 'requiredFields'
})
} else {
delete params.requiredFields
}
step.params = params
if (step.context != null) {
step.context._class = _id
@@ -158,6 +168,27 @@
dispatch('change', step)
}
}
let askRequired = params.askRequired ?? false
$: askRequired = params.askRequired ?? false
function changeAskRequired (e: CustomEvent<boolean>): void {
if (e.detail !== undefined) {
params.askRequired = e.detail
if (e.detail) {
params.requiredFields = createContext({
type: 'userRequest',
id: generateContextId(),
_class: targetClass,
key: 'requiredFields'
})
} else {
delete params.requiredFields
}
step.params = params
dispatch('change', step)
}
}
</script>
<div class="grid">
@@ -191,6 +222,17 @@
<Toggle on={askSubclass} on:change={changeAskSubclass} />
</div>
{/if}
<span
class="labelOnPanel"
use:tooltip={{
props: { label: plugin.string.AskRequired }
}}
>
<Label label={plugin.string.AskRequired} />
</span>
<div>
<Toggle on={askRequired} on:change={changeAskRequired} />
</div>
</div>
<div class="divider" />
{#key targetClass}
+1
View File
@@ -286,6 +286,7 @@ export default mergeIds(processId, process, {
TextFromSelect: '' as IntlString,
SelectFromText: '' as IntlString,
AskSubclass: '' as IntlString,
AskRequired: '' as IntlString,
RequiredSlots: '' as IntlString,
Bindings: '' as IntlString
},
+66 -1
View File
@@ -543,10 +543,59 @@ export async function getTransitionUserInput (
skipExisting: boolean = false
): Promise<ExecutionContext | undefined> {
let changed = false
const client = getClient()
const hierarchy = client.getHierarchy()
for (const action of transition.actions) {
if (action == null) continue
const inputs: SelectedUserRequest[] = []
// Map of: tempInputId -> { attrName, virtualContextId }
const dynamicMappings: Record<string, { attrName: string, virtualContextId: string }> = {}
// Check for virtual required attributes
let virtualContext: SelectedUserRequest | undefined
let virtualKey = ''
if (action.params.requiredFields !== undefined) {
virtualContext = parseContext(action.params.requiredFields) as SelectedUserRequest
virtualKey = 'requiredFields'
} else if (action.params.requiredProperties !== undefined) {
virtualContext = parseContext(action.params.requiredProperties) as SelectedUserRequest
virtualKey = 'requiredProperties'
}
if (virtualContext !== undefined && virtualContext.type === 'userRequest') {
const classId = virtualContext._class
const allAttributes =
virtualKey === 'requiredFields'
? Array.from(hierarchy.getAllAttributes(classId, core.class.Doc).values())
: Array.from(hierarchy.getOwnAttributes(classId).values())
for (const attr of allAttributes) {
if (virtualKey === 'requiredFields' && attr.name === 'title') continue
if (attr.hidden === true) continue
const isConfigured =
virtualKey === 'requiredFields'
? action.params[attr.name] !== undefined
: action.params.props !== undefined && action.params.props[attr.name] !== undefined
if (attr.required === true && !isConfigured) {
const tempInputId = generateContextId()
dynamicMappings[tempInputId] = { attrName: attr.name, virtualContextId: virtualContext.id }
inputs.push({
type: 'userRequest',
id: tempInputId,
key: attr.name,
_class: classId
})
}
}
}
for (const key in action.params) {
if (key === 'requiredFields' || key === 'requiredProperties') continue
const value = (action.params as any)[key]
if (typeof value === 'string') {
const context = parseContext(value)
@@ -564,6 +613,7 @@ export async function getTransitionUserInput (
}
}
}
if (inputs.length > 0) {
const promise = new Promise<void>((resolve, reject) => {
showPopup(
@@ -580,9 +630,24 @@ export async function getTransitionUserInput (
const isComplete = res?.value !== undefined && inputs.every((input) => res.value[input.id] != null)
if (isComplete) {
changed = true
const groupedValues: Record<string, Record<string, any>> = {}
for (const [key, value] of Object.entries(res.value)) {
userContext[key as ContextId] = value
const mapping = dynamicMappings[key]
if (mapping !== undefined) {
if (groupedValues[mapping.virtualContextId] === undefined) {
groupedValues[mapping.virtualContextId] = {}
}
groupedValues[mapping.virtualContextId][mapping.attrName] = value
} else {
userContext[key as ContextId] = value
}
}
for (const [virtualId, obj] of Object.entries(groupedValues)) {
userContext[virtualId as ContextId] = obj
}
resolve()
} else {
reject(new PlatformError(new Status(Severity.ERROR, process.error.ResultNotProvided, {})))