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
@@ -227,6 +227,7 @@ export interface Attribute<T extends PropertyType> extends Doc, UXObject {
defaultValue?: any
automationOnly?: boolean
rank?: Rank
required?: boolean
// Extra customization properties
[key: string]: any
+1
View File
@@ -247,6 +247,7 @@ export class TAttribute extends TDoc implements AnyAttribute {
isCustom?: boolean
defaultValue?: any
automationOnly?: boolean
required?: boolean
}
@Model(core.class.Type, core.class.Obj, DOMAIN_MODEL)
@@ -14,9 +14,10 @@
// limitations under the License.
-->
<script lang="ts">
import { type Class, type Doc, type Ref } from '@hcengineering/core'
import core, { type Class, type Doc, type Ref } from '@hcengineering/core'
import type { AnySvelteComponent, ButtonKind, ButtonSize } from '@hcengineering/ui'
import { Icon, Label, tooltip } from '@hcengineering/ui'
import { isEmptyMarkup } from '@hcengineering/text'
import view from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import { getAttribute, KeyedAttribute, updateAttribute } from '../attributes'
@@ -70,12 +71,19 @@
$: isReadonly = readonly || (attribute.readonly ?? false)
$: icon = attribute?.icon ?? attribute?.type?.icon
$: value = getAttribute(client, object, { key: attributeKey, attr: attribute })
$: isRequiredAndEmpty =
(attribute?.required ?? false) &&
(attribute?.type?._class === core.class.TypeMarkup
? isEmptyMarkup(value)
: value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0))
</script>
{#if editor}
{#if showHeader}
<span
class="labelOnPanel"
class:required-empty={isRequiredAndEmpty}
use:tooltip={{
component: Label,
props: { label: attribute.automationOnly ? view.string.AutomationOnly : attribute.label }
@@ -85,9 +93,15 @@
<div class="flex flex-gap-1 items-center">
<Icon icon={icon ?? view.icon.Setting} size="small" />
<Label label={attribute.label} />
{#if attribute.required}
<span class="required-asterisk">*</span>
{/if}
</div>
{:else}
<Label label={attribute.label} />
{#if attribute.required}
<span class="required-asterisk">*</span>
{/if}
{/if}
</span>
<div class="flex flex-grow min-w-0">
@@ -106,7 +120,7 @@
{maxWidth}
{attribute}
{attributeKey}
value={getAttribute(client, object, { key: attributeKey, attr: attribute })}
{value}
space={object.space}
{onChange}
{focus}
@@ -114,13 +128,13 @@
/>
</div>
{:else}
<div style="grid-column: 1/3;">
<div style="grid-column: 1/3;" class:required-empty={isRequiredAndEmpty}>
<svelte:component
this={editor}
type={attribute?.type}
{maxWidth}
{attributeKey}
value={getAttribute(client, object, { key: attributeKey, attr: attribute })}
{value}
readonly={isReadonly}
disabled={isReadonly}
space={object.space}
@@ -133,3 +147,13 @@
</div>
{/if}
{/if}
<style lang="scss">
.labelOnPanel.required-empty {
color: var(--theme-error-color, #eb5757) !important;
}
.required-asterisk {
color: var(--theme-error-color, #eb5757);
margin-left: 2px;
}
</style>
@@ -19,6 +19,7 @@
import core, { Blob, Class, Doc, Ref, toRank } from '@hcengineering/core'
import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { getClient, KeyedAttribute, updateAttribute } from '@hcengineering/presentation'
import { isEmptyMarkup } from '@hcengineering/text'
import { Label } from '@hcengineering/ui'
import { MarkupEditor } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
@@ -75,17 +76,34 @@
</script>
{#each keys as key}
{@const val = getValue(doc, key.key)}
{@const isRequiredAndEmpty = (key.attr.required ?? false) && isEmptyMarkup(val)}
<div class="w-full mt-2">
<span>
<span class:required-empty-label={isRequiredAndEmpty}>
<Label label={key.attr.label} />
{#if key.attr.required}
<span class="required-asterisk">*</span>
{/if}
</span>
<MarkupEditor
value={getValue(doc, key.key)}
onChange={(value) => {
onChange(value, key)
}}
{readonly}
{attachFile}
/>
<div>
<MarkupEditor
value={val}
onChange={(value) => {
onChange(value, key)
}}
{readonly}
{attachFile}
/>
</div>
</div>
{/each}
<style lang="scss">
.required-empty-label {
color: var(--theme-error-color, #eb5757) !important;
}
.required-asterisk {
color: var(--theme-error-color, #eb5757);
margin-left: 2px;
}
</style>
+2 -1
View File
@@ -176,7 +176,8 @@
"ExportWithSlots": "Exportovat se sloty",
"ExportWithoutSlots": "Exportovat bez slotů",
"RequiredSlots": "Požadované sloty",
"Bindings": "Vazby"
"Bindings": "Vazby",
"AskRequired": "zeptat se na prázdná povinná pole"
},
"error": {
"MethodNotFound": "Metoda nenalezena: {methodId}",
+2 -1
View File
@@ -176,7 +176,8 @@
"ExportWithSlots": "Slot-Export",
"ExportWithoutSlots": "Slot-freier Export",
"RequiredSlots": "Erforderliche Slots",
"Bindings": "Bindungen"
"Bindings": "Bindungen",
"AskRequiredFields": "Erforderliche Felder erfragen"
},
"error": {
"MethodNotFound": "Methode nicht gefunden: {methodId}",
+1
View File
@@ -181,6 +181,7 @@
"TextFromSelect": "Text from select",
"SelectFromText": "Select from text",
"AskSubclass": "Ask subclass",
"AskRequired": "Ask all required fields",
"TextFromIdentifier": "Text from identifier"
},
"error": {
+2 -1
View File
@@ -181,7 +181,8 @@
"ExportWithSlots": "Exportar con slots",
"ExportWithoutSlots": "Exportar sin slots",
"RequiredSlots": "Slots requeridos",
"Bindings": "Bindings"
"Bindings": "Bindings",
"AskRequired": "Preguntar por campos requeridos"
},
"error": {
"MethodNotFound": "Método no encontrado: {methodId}",
+2 -1
View File
@@ -181,7 +181,8 @@
"ExportWithSlots": "Exporter avec slots",
"ExportWithoutSlots": "Exporter sans slots",
"RequiredSlots": "Slots requis",
"Bindings": "Bindings"
"Bindings": "Bindings",
"AskRequired": "Demander les champs requis"
},
"error": {
"MethodNotFound": "Méthode introuvable : {methodId}",
+2 -1
View File
@@ -181,7 +181,8 @@
"ExportWithSlots": "Esporta con slot",
"ExportWithoutSlots": "Esporta senza slot",
"RequiredSlots": "Slot richiesti",
"Bindings": "Binding"
"Bindings": "Binding",
"AskRequired": "Richiedi campi"
},
"error": {
"MethodNotFound": "Metodo non trovato: {methodId}",
+2 -1
View File
@@ -180,7 +180,8 @@
"ExportWithSlots": "スロット付きでエクスポート",
"ExportWithoutSlots": "スロットなしでエクスポート",
"RequiredSlots": "必須スロット",
"Bindings": "バインディング"
"Bindings": "バインディング",
"AskRequired": "必須フィールドを尋ねる"
},
"error": {
"MethodNotFound": "メソッドが見つかりません: {methodId}",
+2 -1
View File
@@ -181,7 +181,8 @@
"ExportWithSlots": "슬롯 포함 내보내기",
"ExportWithoutSlots": "슬롯 없이 내보내기",
"RequiredSlots": "필수 슬롯",
"Bindings": "바인딩"
"Bindings": "바인딩",
"AskRequired": "필수 필드 질문"
},
"error": {
"MethodNotFound": "메서드를 찾을 수 없습니다: {methodId}",
+2 -1
View File
@@ -181,7 +181,8 @@
"TextFromSelect": "Tekst z wyboru",
"SelectFromText": "Wybór z tekstu",
"AskSubclass": "Poproś o podklasę",
"TextFromIdentifier": "Tekst z identyfikatora"
"TextFromIdentifier": "Tekst z identyfikatora",
"AskRequired": "Poproś o wymagane pola"
},
"error": {
"MethodNotFound": "Nie odnaleziono metody: {methodId}",
+2 -1
View File
@@ -169,7 +169,8 @@
"ExportWithSlots": "Exportar com slots",
"ExportWithoutSlots": "Exportar sem slots",
"RequiredSlots": "Slots necessários",
"Bindings": "Vinculações"
"Bindings": "Vinculações",
"AskRequired": "Perguntar por campos requeridos"
},
"error": {
"MethodNotFound": "Método não encontrado: {methodId}",
+2 -1
View File
@@ -181,7 +181,8 @@
"ExportWithSlots": "Exportar com slots",
"ExportWithoutSlots": "Exportar sem slots",
"RequiredSlots": "Slots necessários",
"Bindings": "Vinculações"
"Bindings": "Vinculações",
"AskRequired": "Perguntar por campos requeridos"
},
"error": {
"MethodNotFound": "Método não encontrado: {methodId}",
+1
View File
@@ -181,6 +181,7 @@
"TextFromSelect": "Текст из выбора",
"SelectFromText": "Выбор из текста",
"AskSubclass": "Спросить подкласс",
"AskRequired": "Запросить все обязательные поля",
"TextFromIdentifier": "Текст из идентификатора"
},
"error": {
+2 -1
View File
@@ -176,7 +176,8 @@
"ExportWithSlots": "Slotlu dışa aktar",
"ExportWithoutSlots": "Slotsuz dışa aktar",
"RequiredSlots": "Gerekli slotlar",
"Bindings": "Bağlamalar"
"Bindings": "Bağlamalar",
"AskRequired": "Gerekli alanları sor"
},
"error": {
"MethodNotFound": "Metod bulunamadı: {methodId}",
+2 -1
View File
@@ -181,7 +181,8 @@
"ExportWithSlots": "导出包含槽",
"ExportWithoutSlots": "导出不含槽",
"RequiredSlots": "必需槽",
"Bindings": "绑定"
"Bindings": "绑定",
"AskRequired": "询问必需字段"
},
"error": {
"MethodNotFound": "找不到方法:{methodId}",
@@ -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, {})))
+2 -1
View File
@@ -238,6 +238,7 @@
"TwoFactorAuthDisabled": "Dvoufaktorové ověřování je zakázáno",
"ShowQRCode": "Zobrazit QR kód",
"EnterVerificationCode": "Zadejte ověřovací kód",
"OverrideAttribute": "Přepsat atribut"
"OverrideAttribute": "Přepsat atribut",
"Required": "Požadované"
}
}
+2 -1
View File
@@ -240,6 +240,7 @@
"TwoFactorAuthDisabled": "Zweistufige Authentifizierung ist deaktiviert",
"ShowQRCode": "QR-Code anzeigen",
"EnterVerificationCode": "Verifizierungscode eingeben",
"OverrideAttribute": "Überschreibattribut"
"OverrideAttribute": "Überschreibattribut",
"Required": "Pflichtfeld"
}
}
+2 -1
View File
@@ -240,6 +240,7 @@
"TwoFactorAuthDisabled": "Two-factor authentication is disabled",
"ShowQRCode": "Show QR code",
"EnterVerificationCode": "Enter verification code",
"OverrideAttribute": "Override attribute"
"OverrideAttribute": "Override attribute",
"Required": "Required"
}
}
+2 -1
View File
@@ -231,6 +231,7 @@
"TwoFactorAuthDisabled": "La autenticación de dos factores está deshabilitada",
"ShowQRCode": "Mostrar código QR",
"EnterVerificationCode": "Introducir código de verificación",
"OverrideAttribute": "Sobreescribir atributo"
"OverrideAttribute": "Sobreescribir atributo",
"Required": "Requerido"
}
}
+2 -1
View File
@@ -240,6 +240,7 @@
"TwoFactorAuthDisabled": "L'authentification à deux facteurs est désactivée",
"ShowQRCode": "Afficher le code QR",
"EnterVerificationCode": "Entrer le code de vérification",
"OverrideAttribute": "Surcharger l'attribut"
"OverrideAttribute": "Surcharger l'attribut",
"Required": "Requis"
}
}
+2 -1
View File
@@ -240,6 +240,7 @@
"TwoFactorAuthDisabled": "L'autenticazione a due fattori è disabilitata",
"ShowQRCode": "Mostra codice QR",
"EnterVerificationCode": "Inserisci codice di verifica",
"OverrideAttribute": "Sovrascrivi attributo"
"OverrideAttribute": "Sovrascrivi attributo",
"Required": "Richiesto"
}
}
+2 -1
View File
@@ -240,6 +240,7 @@
"TwoFactorAuthDisabled": "二要素認証は無効です",
"ShowQRCode": "QRコードを表示",
"EnterVerificationCode": "確認コードを入力",
"OverrideAttribute": "属性を上書き"
"OverrideAttribute": "属性を上書き",
"Required": "必須"
}
}
+2 -1
View File
@@ -240,6 +240,7 @@
"TwoFactorAuthDisabled": "2단계 인증이 비활성화됨",
"ShowQRCode": "QR 코드 표시",
"EnterVerificationCode": "인증 코드 입력",
"OverrideAttribute": "속성 재정의"
"OverrideAttribute": "속성 재정의",
"Required": "필수"
}
}
+2 -1
View File
@@ -240,6 +240,7 @@
"TwoFactorAuthDisabled": "Uwierzytelnienie dwuskładnikowe jest wyłączone",
"ShowQRCode": "Pokaż kod QR",
"EnterVerificationCode": "Wpisz kod weryfikacyjny",
"OverrideAttribute": "Nadpisz atrybut"
"OverrideAttribute": "Nadpisz atrybut",
"Required": "Obowiązkowe"
}
}
+2 -1
View File
@@ -231,6 +231,7 @@
"TwoFactorAuthDisabled": "Autenticação de dois fatores está desativada",
"ShowQRCode": "Mostrar código QR",
"EnterVerificationCode": "Inserir código de verificação",
"OverrideAttribute": "Sobrescrever atributo"
"OverrideAttribute": "Sobrescrever atributo",
"Required": "Obrigatório"
}
}
+2 -1
View File
@@ -231,6 +231,7 @@
"TwoFactorAuthDisabled": "Autenticação de dois fatores está desativada",
"ShowQRCode": "Mostrar código QR",
"EnterVerificationCode": "Inserir código de verificação",
"OverrideAttribute": "Sobrescrever atributo"
"OverrideAttribute": "Sobrescrever atributo",
"Required": "Obrigatório"
}
}
+2 -1
View File
@@ -240,6 +240,7 @@
"TwoFactorAuthDisabled": "Двухфакторная аутентификация отключена",
"ShowQRCode": "Показать QR-код",
"EnterVerificationCode": "Введите код подтверждения",
"OverrideAttribute": "Переопределить атрибут"
"OverrideAttribute": "Переопределить атрибут",
"Required": "Обязательный"
}
}
+3 -2
View File
@@ -240,6 +240,7 @@
"TwoFactorAuthDisabled": "İki faktörlü kimlik doğrulama devre dışı",
"ShowQRCode": "QR kodu göster",
"EnterVerificationCode": "Doğrulama kodunu gir",
"OverrideAttribute": "Özniteliği geçersiz kıl"
"OverrideAttribute": "Özniteliği geçersiz kıl",
"Required": "Zorunlu"
}
}
}
+2 -1
View File
@@ -240,6 +240,7 @@
"TwoFactorAuthDisabled": "双因素认证已禁用",
"ShowQRCode": "显示QR码",
"EnterVerificationCode": "输入验证码",
"OverrideAttribute": "覆盖属性"
"OverrideAttribute": "覆盖属性",
"Required": "必须"
}
}
@@ -57,6 +57,7 @@
let extra: Record<string, any> = {}
let is: AnyComponent | undefined
let readonly: boolean = false
let required: boolean = false
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -72,7 +73,8 @@
type,
defaultValue,
readonly,
automationOnly: readonly
automationOnly: readonly,
required
}
if (index !== undefined) {
data.index = index
@@ -196,6 +198,10 @@
<Label label={view.string.AutomationOnly} />
</span>
<Toggle bind:on={readonly} />
<span class="label">
<Label label={setting.string.Required} />
</span>
<Toggle bind:on={required} />
</div>
</Modal>
@@ -56,6 +56,7 @@
let defaultValue: any | undefined = attribute.defaultValue
let icon: Asset | undefined = attribute.icon
let automationOnly = attribute.automationOnly ?? false
let required = attribute.required ?? false
let extra: Record<string, any> = {}
let is: AnyComponent | undefined
@@ -94,6 +95,9 @@
update.readonly = automationOnly
update.automationOnly = automationOnly
}
if (required !== attribute.required) {
update.required = required
}
for (const [k, v] of Object.entries(extra)) {
if (attribute[k] === v) continue
update[k] = v
@@ -308,6 +312,10 @@
<Label label={view.string.AutomationOnly} />
</span>
<Toggle bind:on={automationOnly} disabled={attribute.isCustom !== true} />
<span class="label">
<Label label={setting.string.Required} />
</span>
<Toggle bind:on={required} {disabled} />
<span class="label">
<Label label={setting.string.Restricted} />
</span>
+2 -1
View File
@@ -167,6 +167,7 @@ export default mergeIds(settingId, setting, {
SpaceMembersOnly: '' as IntlString,
LastOwnerLeaveTitle: '' as IntlString,
LastOwnerLeaveMessage: '' as IntlString,
OverrideAttribute: '' as IntlString
OverrideAttribute: '' as IntlString,
Required: '' as IntlString
}
})
@@ -353,14 +353,24 @@ export async function AddTag (
execution: Execution,
control: ProcessControl
): Promise<ExecuteResult> {
const { _id, props } = params
const { _id, props, requiredProperties } = params
if (_id === undefined) throw processError(process.error.RequiredParamsNotProvided, { params: '_id' })
const tagId = _id as Ref<Tag>
const res: Tx[] = []
const _process = control.client.getModel().findObject(execution.process)
if (_process === undefined) throw processError(process.error.ObjectNotFound, { _id: execution.process })
// todo fill default for tag and set parent tags
const tx = control.client.txFactory.createTxMixin(execution.card, _process.masterTag, execution.space, tagId, props)
const mergedProps = { ...props }
if (requiredProperties !== undefined && typeof requiredProperties === 'object' && requiredProperties !== null) {
Object.assign(mergedProps, requiredProperties)
}
const tx = control.client.txFactory.createTxMixin(
execution.card,
_process.masterTag,
execution.space,
tagId,
mergedProps
)
res.push(tx)
const card = control.cache.get(execution.card)
if (card === undefined) throw processError(process.error.ObjectNotFound, { _id: execution.card })
@@ -843,7 +853,7 @@ export async function CreateCard (
execution: Execution,
control: ProcessControl
): Promise<ExecuteResult> {
const { _class, title, content, ...attrs } = params
const { _class, title, content, requiredFields, ...attrs } = params
for (const key in { _class, title }) {
const val = (params as any)[key]
if (isEmpty(val)) {
@@ -854,6 +864,9 @@ export async function CreateCard (
if (_process === undefined) {
throw processError(process.error.RequiredParamsNotProvided, {})
}
if (requiredFields !== undefined && typeof requiredFields === 'object' && requiredFields !== null) {
Object.assign(attrs, requiredFields)
}
const resolvedAttrs: Record<string, any> = {}
for (const key in attrs) {
resolvedAttrs[resolveAttributeId(_process, key)] = (attrs as any)[key]