Card version settings (#10843)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2026-05-17 14:53:34 +05:00
committed by GitHub
parent fa2930d54e
commit 23faf14cca
17 changed files with 401 additions and 159 deletions
@@ -162,6 +162,23 @@ export class Hierarchy {
return Array.from(resultSet)
}
getAllPossibleMixins (_class: Ref<Class<Doc>>, to: Ref<Class<Doc>> = core.class.Doc): Ref<Mixin<Doc>>[] {
const result = new Set<Ref<Mixin<Doc>>>()
let c = this.getClass(_class)
while (c._id !== to && c.extends !== undefined) {
const _descendants = this.descendants.get(c._id)
for (const d of _descendants ?? []) {
if (this.isMixin(d) && this.getBaseClass(d) === c._id) {
result.add(d)
}
}
c = this.getClass(c.extends)
}
return [...result]
}
isMixin (_class: Ref<Class<Doc>>): boolean {
const data = this.classifiers.get(_class)
return data !== undefined && this._isMixin(data)
@@ -1,4 +1,4 @@
import { Class, Doc, PersonId, Ref } from './classes'
import { Class, Doc, Mixin, PersonId, Ref } from './classes'
export interface VersionableDoc extends Doc {
baseId?: Ref<Doc>
@@ -10,4 +10,7 @@ export interface VersionableDoc extends Doc {
export interface VersionableClass extends Class<Doc> {
enabled: boolean
excludedProperties?: string[]
excludedRelations?: string[] // ${associationId}_${a|b}
excludeMixins?: Ref<Mixin<Doc>>[]
}
+3
View File
@@ -447,4 +447,7 @@ export class TCollaborator extends TAttachedDoc implements Collaborator {
@MMixin(core.mixin.VersionableClass, core.class.Class)
export class TVersionableClass extends TClass implements VersionableClass {
enabled!: boolean
excludedProperties?: string[]
excludedRelations?: string[] // ${associationId}_${a|b}
excludeMixins?: Ref<Mixin<Doc>>[]
}
+13
View File
@@ -600,6 +600,19 @@ export function defineFunctions (builder: Builder): void {
process.function.StringFromNumber
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
{
of: core.class.TypeIdentifier,
to: core.class.TypeString,
category: 'attribute',
label: process.string.TextFromIdentifier,
type: 'convert'
},
process.function.StringFromIdentifier
)
builder.createDoc(
process.class.ProcessFunction,
core.space.Model,
+4
View File
@@ -207,6 +207,10 @@ export function createModel (builder: Builder): void {
func: serverProcess.transform.DateFromNumber
})
builder.mixin(process.function.StringFromIdentifier, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.StringFromIdentifier
})
builder.mixin(process.function.NumberFromString, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.NumberFromString
})
@@ -30,7 +30,8 @@
export let label: IntlString = ui.string.DropdownDefaultLabel
export let params: Record<string, any> = {}
export let items: DropdownIntlItem[]
export let selected: DropdownIntlItem['id'] | undefined = undefined
export let multiselect: boolean = false
export let selected: DropdownIntlItem['id'] | Array<DropdownIntlItem['id']> | undefined = multiselect ? [] : undefined
export let disabled: boolean = false
export let kind: ButtonKind = 'regular'
export let size: ButtonSize = 'small'
@@ -48,9 +49,11 @@
let container: HTMLElement
let opened: boolean = false
$: selectedItem = items.find((x) => x.id === selected)
$: if (shouldUpdateUndefined && selected === undefined && items[0] !== undefined) {
selected = items[0].id
$: selectedItem = multiselect
? (items ?? []).filter((p) => (selected as Array<DropdownIntlItem['id']>)?.includes(p.id))
: (items ?? []).find((x) => x.id === selected)
$: if (shouldUpdateUndefined && selected === undefined && items?.[0] !== undefined) {
selected = multiselect ? [items[0].id] : items[0].id
dispatch('selected', selected)
}
@@ -59,13 +62,24 @@
function openPopup () {
if (!opened) {
opened = true
showPopup(DropdownLabelsPopupIntl, { items, selected, params, withSearch }, container, (result) => {
if (result) {
selected = result
dispatch('selected', result)
showPopup(
DropdownLabelsPopupIntl,
{ items, selected, params, withSearch, multiselect },
container,
(result) => {
if (result) {
selected = result
dispatch('selected', result)
}
opened = false
},
(result) => {
if (result != null) {
selected = result
dispatch('selected', result)
}
}
opened = false
})
)
}
}
@@ -98,10 +112,21 @@
on:click={openPopup}
>
<span slot="content" class="overflow-label disabled flex-grow text-left mr-2">
<Label
label={selectedItem ? selectedItem.label : label}
params={selectedItem ? (selectedItem.params ?? params) : params}
/>
{#if Array.isArray(selectedItem)}
{#if selectedItem.length > 0}
{#each selectedItem as item}
<span class="step-row">
<Label label={item.label} params={item.params ?? params} />
</span>
{/each}
{:else}
<Label {label} {params} />
{/if}
{:else if selectedItem}
<Label label={selectedItem.label} params={selectedItem.params ?? params} />
{:else}
<Label {label} {params} />
{/if}
</span>
<svelte:fragment slot="iconRight">
<DropdownIcon
@@ -111,3 +136,22 @@
</svelte:fragment>
</Button>
</div>
<style lang="scss">
.step-row + .step-row {
position: relative;
margin-left: 0.75rem;
&::before {
position: absolute;
content: '';
top: 50%;
left: -0.5rem;
width: 0.25rem;
height: 0.25rem;
background-color: var(--dark-color);
border-radius: 50%;
transform: translateY(-50%);
}
}
</style>
@@ -23,12 +23,24 @@
import ui from '../plugin'
export let items: DropdownIntlItem[]
export let selected: DropdownIntlItem['id'] | undefined = undefined
export let selected: DropdownIntlItem['id'] | Array<DropdownIntlItem['id']> | undefined = undefined
export let multiselect: boolean = false
export let params: Record<string, any> = {}
export let withSearch: boolean = false
export let searchPlaceholder: IntlString = ui.string.Search
const dispatch = createEventDispatcher()
function isSelected (
selected: DropdownIntlItem['id'] | Array<DropdownIntlItem['id']> | undefined,
item: DropdownIntlItem
): boolean {
if (Array.isArray(selected)) {
return selected.includes(item.id)
} else {
return item.id === selected
}
}
let btns: HTMLButtonElement[] = []
const keyDown = (ev: KeyboardEvent, n?: number): void => {
@@ -100,7 +112,18 @@
keyDown(ev, i)
}}
on:click={() => {
dispatch('close', item.id)
if (multiselect && Array.isArray(selected)) {
const index = selected.indexOf(item.id)
if (index !== -1) {
selected.splice(index, 1)
selected = selected
} else {
selected = selected === undefined ? [item.id] : [...selected, item.id]
}
dispatch('update', selected)
} else {
dispatch('close', item.id)
}
}}
>
<div class="flex-grow caption-color nowrap flex-presenter flex-gap-2">
@@ -110,7 +133,7 @@
<Label label={item.label} params={item.params ?? params} />
</div>
<div class="check">
{#if item.id === selected}<IconCheck size={'small'} />{/if}
{#if isSelected(selected, item)}<IconCheck size={'small'} />{/if}
</div>
</button>
{/each}
@@ -20,7 +20,7 @@
import { createQuery, getClient } from '@hcengineering/presentation'
import { Button, DropdownLabels, DropdownTextItem, getCurrentLocation, navigate, showPopup } from '@hcengineering/ui'
import card from '../plugin'
import NewVersionPopup from './NewVersionPopup.svelte'
import { createNewVersion } from '../utils'
export let value: Card
@@ -70,10 +70,12 @@
}
}
function newVersion (): void {
showPopup(NewVersionPopup, {
value
})
async function newVersion (): Promise<void> {
const _id = await createNewVersion(value)
const loc = getCurrentLocation()
loc.path[2] = cardId
loc.path[3] = _id
navigate(loc)
}
</script>
@@ -1,106 +0,0 @@
<!--
//
// Copyright © 2026 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 { cardId, Card as CardType } from '@hcengineering/card'
import core from '@hcengineering/core'
import { Card, getClient } from '@hcengineering/presentation'
import { getCurrentLocation, Label, navigate, Toggle } from '@hcengineering/ui'
import plugin from '../plugin'
import { createNewVersion } from '../utils'
export let value: CardType
const client = getClient()
const hierarchy = client.getHierarchy()
const ancestors = hierarchy.getAncestors(value._class)
const mixins = hierarchy.findAllMixins(value)
const targets = new Set([...ancestors, ...mixins])
const associations = client.getModel().findAllSync(core.class.Association, {})
const relationsA = associations.filter((it) => targets.has(it.classB))
const relationsB = associations.filter((it) => targets.has(it.classA))
let relationsToCopy = new Set<string>([
...relationsA.map((p) => `${p._id}_a`),
...relationsB.map((p) => `${p._id}_b`)
])
async function create (): Promise<void> {
const _id = await createNewVersion(value, relationsToCopy)
const loc = getCurrentLocation()
loc.path[2] = cardId
loc.path[3] = _id
navigate(loc)
}
</script>
<Card label={plugin.string.NewVersion} canSave={true} width={'small'} okAction={create} on:close>
<div class="flex-col flex-gap-2">
<Label label={plugin.string.NewVersionConfirmation} />
{#if relationsA.length > 0 || relationsB.length > 0}
<div>
<Label label={plugin.string.RelationCopyDescr} />
</div>
<div class="grid">
{#each relationsB as assoc}
{@const id = `${assoc._id}_b`}
<span>{assoc.nameB}</span>
<Toggle
on={relationsToCopy.has(id)}
on:change={(e) => {
if (e.detail === true) {
relationsToCopy.add(id)
} else {
relationsToCopy.delete(id)
}
relationsToCopy = relationsToCopy
}}
/>
{/each}
{#each relationsA as assoc}
{@const id = `${assoc._id}_a`}
<span>{assoc.nameA}</span>
<Toggle
on={relationsToCopy.has(id)}
on:change={(e) => {
if (e.detail === true) {
relationsToCopy.add(id)
} else {
relationsToCopy.delete(id)
}
relationsToCopy = relationsToCopy
}}
/>
{/each}
</div>
{/if}
</div>
</Card>
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 3fr 1fr;
grid-auto-rows: minmax(1rem, max-content);
justify-content: start;
width: 100%;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
height: min-content;
}
</style>
@@ -26,10 +26,13 @@
getCurrentLocation,
getPlatformColorDef,
IconDelete,
IconSettings,
Label,
ModernEditbox,
navigate,
showPopup,
themeStore,
Toggle,
ToggleWithLabel
} from '@hcengineering/ui'
import view from '@hcengineering/view'
@@ -37,6 +40,7 @@
import { exportModule } from '../../exporter'
import card from '../../plugin'
import { deleteMasterTag } from '../../utils'
import VersioningSetting from './VersioningSetting.svelte'
export let masterTag: MasterTag
@@ -143,6 +147,12 @@
})
}
function versioningSetting (): void {
showPopup(VersioningSetting, {
masterTag: masterTag._id
})
}
let versioningEnabled = h.classHierarchyMixin(masterTag._id, core.mixin.VersionableClass)?.enabled
$: versioningEnabled = h.classHierarchyMixin(masterTag._id, core.mixin.VersionableClass)?.enabled
</script>
@@ -191,15 +201,16 @@
{/if}
</div>
{#if !h.isMixin(masterTag._id)}
<div class="mx-2">
<ToggleWithLabel
label={card.string.Versioning}
on={versioningEnabled}
disabled={versioningEnabled}
on:change={enableVersioning}
/>
<div class="mx-2 flex-between items-center">
<Label label={card.string.Versioning} />
<div class="flex items-center gap-1">
{#if versioningEnabled}
<ButtonIcon icon={setting.icon.Setting} size="extra-small" on:click={versioningSetting} />
{/if}
<Toggle on={versioningEnabled} disabled={versioningEnabled} on:change={enableVersioning} />
</div>
</div>
<div class="mx-2">
<div class="mx-2 pt-2">
<ToggleWithLabel
label={card.string.SingleColumn}
on={masterTag.singleColumn}
@@ -0,0 +1,206 @@
<!--
//
// Copyright © 2026 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 { MasterTag } from '@hcengineering/card'
import core, { Doc, Mixin, Ref } from '@hcengineering/core'
import { Card, getClient } from '@hcengineering/presentation'
import { DropdownLabels, DropdownLabelsIntl, Label, Toggle } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import card from '../../plugin'
import setting from '@hcengineering/setting'
export let masterTag: Ref<MasterTag>
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
const ancestors = hierarchy.getAncestors(masterTag)
const mixins = hierarchy.getAllPossibleMixins(masterTag)
const current = hierarchy.classHierarchyMixin(masterTag, core.mixin.VersionableClass)
const mixinsItems = mixins.map((it) => {
const cl = hierarchy.getClass(it)
return { label: cl.label, id: it }
})
let excludedRelations = new Set<string>(current?.excludedRelations ?? [])
let excludedProperties = new Set<string>(current?.excludedProperties ?? [])
let excludeMixins = new Set<Ref<Mixin<Doc>>>(current?.excludeMixins ?? [])
$: selectedMixins = mixins.filter((it) => !excludeMixins.has(it))
const systemFields = [
'_class',
'id',
'createdOn',
'modifiedOn',
'modifiedBy',
'createdBy',
'createdOn',
'rank',
'title',
'space',
'version',
'icon',
'color',
'todos',
'comments'
]
const allProperties = hierarchy
.getAllAttributes(masterTag, core.class.Doc)
.values()
.toArray()
.filter((it) => {
if (systemFields.includes(it.name)) return false
return true
})
.map((p) => {
return { label: p.label, id: p.name }
})
$: selectedProperties = allProperties.filter((it) => !excludedProperties.has(it.id)).map((it) => it.id)
const targets = new Set([...ancestors, ...mixins])
const associations = client.getModel().findAllSync(core.class.Association, {})
const relationsA = associations.filter((it) => targets.has(it.classB))
const relationsB = associations.filter((it) => targets.has(it.classA))
async function save (): Promise<void> {
if (current?._id === masterTag) {
await client.updateMixin(masterTag, card.class.MasterTag, core.space.Model, core.mixin.VersionableClass, {
excludedRelations: [...excludedRelations],
excludedProperties: [...excludedProperties],
excludeMixins: [...excludeMixins],
enabled: true
})
} else {
await client.createMixin(masterTag, card.class.MasterTag, core.space.Model, core.mixin.VersionableClass, {
excludedRelations: [...excludedRelations],
excludedProperties: [...excludedProperties],
excludeMixins: [...excludeMixins],
enabled: true
})
}
dispatch('close')
}
function propertiesSelected (event: CustomEvent): void {
const selectedProperties = event.detail as string[]
allProperties.forEach((it) => {
if (!selectedProperties.includes(it.id)) {
excludedProperties.add(it.id)
} else {
excludedProperties.delete(it.id)
}
})
excludedProperties = excludedProperties
}
function mixinsSelected (event: CustomEvent): void {
const selectedMixins = event.detail as string[]
mixins.forEach((it) => {
if (!selectedMixins.includes(it)) {
excludeMixins.add(it)
} else {
excludeMixins.delete(it)
}
})
excludeMixins = excludeMixins
}
</script>
<Card label={card.string.NewVersion} canSave={true} width={'medium'} okAction={save} on:close>
<div class="flex-col flex-gap-2">
<div class="flex-between">
<Label label={setting.string.Properties} />
<div class="max-w-40">
<DropdownLabelsIntl
width="100%"
items={allProperties}
multiselect
selected={selectedProperties}
on:selected={propertiesSelected}
/>
</div>
</div>
<div class="flex-between">
<Label label={card.string.Tags} />
<div class="max-w-40">
<DropdownLabelsIntl
width="100%"
items={mixinsItems}
multiselect
selected={selectedMixins}
on:selected={mixinsSelected}
/>
</div>
</div>
{#if relationsA.length > 0 || relationsB.length > 0}
<div class="divider" />
<div>
<Label label={card.string.RelationCopyDescr} />
</div>
<div class="grid">
{#each relationsB as assoc}
{@const id = `${assoc._id}_b`}
<span>{assoc.nameB}</span>
<Toggle
on={!excludedRelations.has(id)}
on:change={(e) => {
if (e.detail === true) {
excludedRelations.delete(id)
} else {
excludedRelations.add(id)
}
excludedRelations = excludedRelations
}}
/>
{/each}
{#each relationsA as assoc}
{@const id = `${assoc._id}_a`}
<span>{assoc.nameA}</span>
<Toggle
on={excludedRelations.has(id)}
on:change={(e) => {
if (e.detail === true) {
excludedRelations.delete(id)
} else {
excludedRelations.add(id)
}
excludedRelations = excludedRelations
}}
/>
{/each}
</div>
{/if}
</div>
</Card>
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 3fr 1fr;
grid-auto-rows: minmax(1rem, max-content);
justify-content: start;
width: 100%;
align-items: center;
row-gap: 0.5rem;
column-gap: 1rem;
height: min-content;
}
</style>
+35 -22
View File
@@ -39,6 +39,7 @@ import core, {
type Space,
toRank,
type TxOperations,
type VersionableClass,
type WithLookup
} from '@hcengineering/core'
import login from '@hcengineering/login'
@@ -74,6 +75,7 @@ import CreateSpace from './components/navigator/CreateSpace.svelte'
import card from './plugin'
import { type NavigatorConfig } from './types'
import { writable } from 'svelte/store'
import { makeRank } from '@hcengineering/rank'
export async function deleteMasterTag (tag: MasterTag | undefined, onDelete?: () => void): Promise<void> {
if (tag !== undefined) {
@@ -280,7 +282,7 @@ export async function createTypePermissions (masterTag: MasterTag | Tag): Promis
async function cloneCard (
origin: Card,
overrideProps: Record<string, any>,
relationToCopy: Set<string> | 'all',
versionableClass?: VersionableClass,
copyIds: boolean = false
): Promise<Ref<Card>> {
const client = getClient()
@@ -292,8 +294,12 @@ async function cloneCard (
const skipClasses = copyIds
? [core.class.TypeCollaborativeDoc]
: [core.class.TypeCollaborativeDoc, core.class.TypeIdentifier]
const systemFields = ['_class', 'id', 'createdOn', 'modifiedOn', 'modifiedBy', 'createdBy', 'createdOn', 'rank']
for (const [key, attr] of attrs) {
if (versionableClass?.excludedProperties?.includes(key) === true || systemFields.includes(key)) {
continue
}
if (attr.type._class === core.class.Collection) {
;(props as any)[key] = 0
} else if (!skipClasses.includes(attr.type._class)) {
@@ -303,19 +309,25 @@ async function cloneCard (
for (const [k, v] of Object.entries(overrideProps)) {
;(props as any)[k] = v
}
props.rank = makeRank(origin.rank, undefined)
const targetId = generateId<Card>()
const relationsA = await client.findAll(core.class.Relation, { docA: origin._id })
const relationsB = await client.findAll(core.class.Relation, { docB: origin._id })
const markup = await getMarkup(makeDocCollabId(origin, 'content'), origin.content)
if (!isEmptyMarkup(markup)) {
const collabId = makeCollabId(base, targetId, 'content')
props.content = await createMarkup(collabId, markup)
if (versionableClass?.excludedProperties?.includes('content') !== true) {
const markup = await getMarkup(makeDocCollabId(origin, 'content'), origin.content)
if (!isEmptyMarkup(markup)) {
const collabId = makeCollabId(base, targetId, 'content')
props.content = await createMarkup(collabId, markup)
}
}
const ops = client.apply(`Duplicate_card_${origin._id}`)
await ops.createDoc(base, origin.space, props, targetId)
for (const mixin of mixins) {
if (versionableClass?.excludeMixins?.includes(mixin) === true) {
continue
}
const mixinAttrs = h.getOwnAttributes(mixin)
const as = h.as(origin, mixin)
const attributes: Partial<Data<Doc>> = {}
@@ -326,7 +338,7 @@ async function cloneCard (
}
for (const rel of relationsA) {
if (relationToCopy === 'all' || relationToCopy.has(`${rel.association}_b`)) {
if (versionableClass?.excludedRelations?.includes(`${rel.association}_b`) !== true) {
await ops.createDoc(core.class.Relation, core.space.Workspace, {
docA: targetId,
docB: rel.docB,
@@ -335,7 +347,7 @@ async function cloneCard (
}
}
for (const rel of relationsB) {
if (relationToCopy === 'all' || relationToCopy.has(`${rel.association}_a`)) {
if (versionableClass?.excludedRelations?.includes(`${rel.association}_a`) !== true) {
await ops.createDoc(core.class.Relation, core.space.Workspace, {
docA: rel.docA,
docB: targetId,
@@ -345,25 +357,23 @@ async function cloneCard (
}
await ops.commit()
const attachments = await client.findAll(attachment.class.Attachment, { attachedTo: origin._id })
const attachmentOps = client.apply(`Duplicate_attachments_${origin._id}`)
for (const att of attachments) {
const { _id, modifiedBy, modifiedOn, attachedTo, attachedToClass, collection, space, ...props } = att
await attachmentOps.addCollection(attachment.class.Attachment, origin.space, targetId, base, 'attachments', props)
if (versionableClass?.excludedProperties?.includes('attachments') !== true) {
const attachments = await client.findAll(attachment.class.Attachment, { attachedTo: origin._id })
const attachmentOps = client.apply(`Duplicate_attachments_${origin._id}`)
for (const att of attachments) {
const { _id, modifiedBy, modifiedOn, attachedTo, attachedToClass, collection, space, ...props } = att
await attachmentOps.addCollection(attachment.class.Attachment, origin.space, targetId, base, 'attachments', props)
}
await attachmentOps.commit()
}
await attachmentOps.commit()
return targetId
}
export async function duplicateCard (origin: Card): Promise<void> {
const targetId = await cloneCard(
origin,
{
title: `${origin.title} (Copy)`
},
'all'
)
const targetId = await cloneCard(origin, {
title: `${origin.title} (Copy)`
})
const loc = getCurrentLocation()
loc.path[2] = cardId
@@ -513,14 +523,17 @@ export async function cardFactory (props: Record<string, any> = {}): Promise<Ref
return await createCard(_class, space, props.data, props.content)
}
export async function createNewVersion (card: Card, relationsToCopy: Set<string>): Promise<Ref<Card>> {
export async function createNewVersion (card: Card): Promise<Ref<Card>> {
const client = getClient()
const mixin = client.getHierarchy().classHierarchyMixin(card._class, core.mixin.VersionableClass)
return await cloneCard(
card,
{
baseId: card.baseId,
docCreatedBy: card.docCreatedBy ?? card.createdBy ?? card.modifiedBy
},
relationsToCopy,
mixin,
true
)
}
+1
View File
@@ -267,6 +267,7 @@ export default mergeIds(processId, process, {
UnlockField: '' as IntlString,
Export: '' as IntlString,
Import: '' as IntlString,
TextFromIdentifier: '' as IntlString,
TextFromNumber: '' as IntlString,
TextFromDate: '' as IntlString,
TextFromCheckbox: '' as IntlString,
+1
View File
@@ -378,6 +378,7 @@ export default plugin(processId, {
EmptyValue: '' as Ref<ProcessFunction>,
EmptyArray: '' as Ref<ProcessFunction>,
CurrentDate: '' as Ref<ProcessFunction>,
StringFromIdentifier: '' as Ref<ProcessFunction>,
StringFromNumber: '' as Ref<ProcessFunction>,
StringFromDate: '' as Ref<ProcessFunction>,
StringFromMarkup: '' as Ref<ProcessFunction>,
@@ -107,6 +107,7 @@ import {
LastValue,
LowerCase,
MarkupFromString,
StringFromIdentifier,
Max,
Min,
Modulo,
@@ -800,6 +801,7 @@ export default async () => ({
Max,
StringFromMarkup,
MarkupFromString,
StringFromIdentifier,
StringFromEnum,
EnumFromString
},
@@ -582,6 +582,10 @@ export function MarkupFromString (value: string): string {
return value
}
export function StringFromIdentifier (value: string): string {
return value
}
export function StringFromEnum (value: string): string {
if (value == null) return ''
return String(value)
+1
View File
@@ -126,6 +126,7 @@ export default plugin(serverProcessId, {
NumberFromString: '' as Resource<TransformFunc>,
DateFromString: '' as Resource<TransformFunc>,
MarkupFromString: '' as Resource<TransformFunc>,
StringFromIdentifier: '' as Resource<TransformFunc>,
YearFromDate: '' as Resource<TransformFunc>,
MonthFromDate: '' as Resource<TransformFunc>,
DayFromDate: '' as Resource<TransformFunc>,