diff --git a/.github/workflows/deploy-examples.yml b/.github/workflows/deploy-examples.yml index 34327d4..311c221 100644 --- a/.github/workflows/deploy-examples.yml +++ b/.github/workflows/deploy-examples.yml @@ -33,9 +33,9 @@ jobs: - name: Build run: yarn build - name: Upload artifact - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@v3 with: path: demos - name: Deploy id: deployment - uses: actions/deploy-pages@v1 + uses: actions/deploy-pages@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 04e67aa..979cb3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,56 @@ +## O.14.8 + +This PR adds support for the internationalization of options in the choice value model [#53](https://github.com/nocode-js/sequential-workflow-editor/issues/53). + +## O.14.7 + +Added an `index` argument to the `itemComponentFactory` callback in the `dynamicListComponent` function. + +## O.14.6 + +Added comments to describe the `BranchedStepModelBuilder`, `SequentialStepModelBuilder` and `RootModelBuilder` classes. + +## O.14.5 + +Added comments to describe the `EditorProvider` class. + +## 0.14.4 + +This version exposes the `ToolboxGroup` interface in the `sequential-workflow-editor` package [#46](https://github.com/nocode-js/sequential-workflow-editor/issues/46#issuecomment-2439817733). + +## 0.14.3 + +This version provides the ability to sort the steps in the toolbox in a custom way. By default, the steps are sorted alphabetically. + +```ts +EditorProvider.create(definitionModel, { + // ... + toolboxSorter(groups: ToolboxGroup[]) { + // ... + } +}); +``` + +You can also hide certain steps from the toolbox by using the hidden method in the step builder. + +```ts +createStepModel('myStep', 'task', step => { + step.toolbox(false); +}); +``` + +## 0.14.2 + +This version adds the `formatPropertyValue` method to the `PropertyValidatorContext` class. + +## 0.14.1 + +This version adds the `formatPropertyValue` method to: `PropertyContext`, `DefaultValueContext`, `ScopedPropertyContext` and `ValueContext` classes. + +## 0.14.0 + +From now, the nullable any variable editor and the nullable variable editor display the expected variable types to select. This version also allows changes to the labels in the dropdown menu of the dynamic value editor. + ## 0.13.2 This version adds missing translations for the `variableDefinitions` value editor. diff --git a/README.md b/README.md index ffd4f0b..cabc449 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ Powerful workflow editor builder for sequential workflows. Written in TypeScript Pro: * [📖 Pro Editors](https://nocode-js.com/examples/sequential-workflow-editor-pro/webpack-pro-app/public/editors.html) +* [📫 Template System](https://nocode-js.com/examples/sequential-workflow-editor-pro/webpack-pro-app/public/template-system.html) +* [🎱 Dynamic Variables](https://nocode-js.com/examples/sequential-workflow-editor-pro/webpack-pro-app/public/dynamic-variables.html) ## 🚀 Installation diff --git a/demos/webpack-app/package.json b/demos/webpack-app/package.json index e505e36..2b3ba93 100644 --- a/demos/webpack-app/package.json +++ b/demos/webpack-app/package.json @@ -18,8 +18,8 @@ "sequential-workflow-model": "^0.2.0", "sequential-workflow-designer": "^0.21.2", "sequential-workflow-machine": "^0.4.0", - "sequential-workflow-editor-model": "^0.13.2", - "sequential-workflow-editor": "^0.13.2" + "sequential-workflow-editor-model": "^0.14.8", + "sequential-workflow-editor": "^0.14.8" }, "devDependencies": { "ts-loader": "^9.4.2", diff --git a/demos/webpack-app/src/editors/app.ts b/demos/webpack-app/src/editors/app.ts index e361e0f..344b2b5 100644 --- a/demos/webpack-app/src/editors/app.ts +++ b/demos/webpack-app/src/editors/app.ts @@ -47,8 +47,8 @@ export class App { }); if (location.hash) { - const type = location.hash.substring(1); - const step = designer.getDefinition().sequence.find(s => s.type === type); + const type = location.hash.substring(1).toLowerCase(); + const step = designer.getDefinition().sequence.find(s => s.type.toLowerCase() === type); if (step) { designer.selectStepById(step.id); } diff --git a/demos/webpack-app/src/i18n/app.ts b/demos/webpack-app/src/i18n/app.ts index 5da379f..4e5783b 100644 --- a/demos/webpack-app/src/i18n/app.ts +++ b/demos/webpack-app/src/i18n/app.ts @@ -63,7 +63,11 @@ const editorDict: Record> = { 'step.chown.name': 'Uprawnienia', 'step.chown.property:name': 'Nazwa', 'step.chown.property:properties/stringOrNumber': 'Tekst lub liczba', - 'step.chown.property:properties/users': 'Użytkownik' + 'step.chown.property:properties/users': 'Użytkownik', + 'step.chown.property:properties/mode': 'Tryb', + 'step.chown.property:properties/mode:choice:Read': 'Odczyt', + 'step.chown.property:properties/mode:choice:Write': 'Zapis', + 'step.chown.property:properties/mode:choice:Execute': 'Wykonanie' } }; diff --git a/demos/webpack-app/src/i18n/definition-model.ts b/demos/webpack-app/src/i18n/definition-model.ts index 709955d..b0b08f1 100644 --- a/demos/webpack-app/src/i18n/definition-model.ts +++ b/demos/webpack-app/src/i18n/definition-model.ts @@ -2,6 +2,7 @@ import { Dynamic, StringDictionary, createBooleanValueModel, + createChoiceValueModel, createDefinitionModel, createDynamicValueModel, createNumberValueModel, @@ -24,6 +25,7 @@ export interface ChownStep extends Step { properties: { stringOrNumber: Dynamic; users: StringDictionary; + mode: string; }; } @@ -64,6 +66,12 @@ export const definitionModel = createDefinitionModel(model => { uniqueKeys: true }) ); + step.property('mode').value( + createChoiceValueModel({ + choices: ['Read', 'Write', 'Execute'], + defaultValue: 'Read' + }) + ); }) ]); }); diff --git a/docs/I18N-KEYS.md b/docs/I18N-KEYS.md index a95d769..402e56b 100644 --- a/docs/I18N-KEYS.md +++ b/docs/I18N-KEYS.md @@ -21,9 +21,10 @@ This document lists all the I18N keys used in the Sequential Workflow Editor. "generatedString.differentValue": "Generator returns different value than the current value", "nullableAnyVariable.invalidVariableType": "The variable :name has invalid type", "nullableAnyVariable.select": "- Select -", + "nullableAnyVariable.selectTypes": "- Select: :types -", "nullableAnyVariable.variableIsLost": "The variable :name is lost", "nullableAnyVariable.variableIsRequired": "The variable is required", - "nullableVariable.select": "- Select -", + "nullableVariable.selectType": "- Select: :type -", "nullableVariable.variableIsLost": "The variable :name is not found", "nullableVariable.variableIsRequired": "The variable is required", "nullableVariableDefinition.expectedType": "Variable type must be :type", diff --git a/editor/package.json b/editor/package.json index ed19193..f6437ba 100644 --- a/editor/package.json +++ b/editor/package.json @@ -1,6 +1,6 @@ { "name": "sequential-workflow-editor", - "version": "0.13.2", + "version": "0.14.8", "type": "module", "main": "./lib/esm/index.js", "types": "./lib/index.d.ts", @@ -46,11 +46,11 @@ "prettier:fix": "prettier --write ./src ./css" }, "dependencies": { - "sequential-workflow-editor-model": "^0.13.2", + "sequential-workflow-editor-model": "^0.14.8", "sequential-workflow-model": "^0.2.0" }, "peerDependencies": { - "sequential-workflow-editor-model": "^0.13.2", + "sequential-workflow-editor-model": "^0.14.8", "sequential-workflow-model": "^0.2.0" }, "devDependencies": { diff --git a/editor/src/components/dynamic-list-component.spec.ts b/editor/src/components/dynamic-list-component.spec.ts index b7a2105..b97b004 100644 --- a/editor/src/components/dynamic-list-component.spec.ts +++ b/editor/src/components/dynamic-list-component.spec.ts @@ -1,4 +1,4 @@ -import { SimpleEvent, ValueContext } from 'sequential-workflow-editor-model'; +import { I18n, SimpleEvent, ValueContext } from 'sequential-workflow-editor-model'; import { Html } from '../core/html'; import { dynamicListComponent } from './dynamic-list-component'; @@ -6,11 +6,13 @@ interface TestItem { id: number; } -function testItemComponentFactory(item: TestItem) { +function testItemComponentFactory(item: TestItem, _: I18n, index: number) { + const view = Html.element('span', { + class: `test-item-${item.id}` + }); + view.setAttribute('data-index', String(index)); return { - view: Html.element('span', { - class: `test-item-${item.id}` - }), + view, onItemChanged: new SimpleEvent(), onDeleteClicked: new SimpleEvent(), validate: () => { @@ -32,7 +34,9 @@ describe('DynamicListComponent', () => { expect(children.length).toBe(3); expect(children[0].className).toBe('test-item-123'); + expect(children[0].getAttribute('data-index')).toBe('0'); expect(children[1].className).toBe('test-item-456'); + expect(children[1].getAttribute('data-index')).toBe('1'); expect(children[2].className).toBe('swe-validation-error'); }); @@ -47,13 +51,16 @@ describe('DynamicListComponent', () => { expect(children.length).toBe(2); expect(children[0].className).toBe('test-item-135'); + expect(children[0].getAttribute('data-index')).toBe('0'); expect(children[1].className).toBe('swe-validation-error'); component.add({ id: 246 }); expect(children.length).toBe(3); expect(children[0].className).toBe('test-item-135'); + expect(children[0].getAttribute('data-index')).toBe('0'); expect(children[1].className).toBe('test-item-246'); + expect(children[1].getAttribute('data-index')).toBe('1'); expect(children[2].className).toBe('swe-validation-error'); }); diff --git a/editor/src/components/dynamic-list-component.ts b/editor/src/components/dynamic-list-component.ts index 8ad0c10..0b19a78 100644 --- a/editor/src/components/dynamic-list-component.ts +++ b/editor/src/components/dynamic-list-component.ts @@ -23,7 +23,7 @@ export interface DynamicListItemComponent extends Component { export function dynamicListComponent = DynamicListItemComponent>( initialItems: TItem[], - itemComponentFactory: (item: TItem, i18n: I18n) => TItemComponent, + itemComponentFactory: (item: TItem, i18n: I18n, index: number) => TItemComponent, context: ValueContext, configuration?: DynamicListComponentConfiguration ): DynamicListComponent { @@ -74,7 +74,7 @@ export function dynamicListComponent 0) { items.forEach((item, index) => { - const component = itemComponentFactory(item, context.i18n); + const component = itemComponentFactory(item, context.i18n, index); component.onItemChanged.subscribe(item => onItemChanged(item, index)); component.onDeleteClicked.subscribe(() => onItemDeleted(index)); view.insertBefore(component.view, validation.view); diff --git a/editor/src/core/sort-toolbox-groups.spec.ts b/editor/src/core/sort-toolbox-groups.spec.ts new file mode 100644 index 0000000..75b108d --- /dev/null +++ b/editor/src/core/sort-toolbox-groups.spec.ts @@ -0,0 +1,40 @@ +import { Step } from 'sequential-workflow-model'; +import { sortToolboxGroups } from './sort-toolbox-groups'; +import { ToolboxGroup } from '../editor-provider-configuration'; + +function createStep(name: string): Step { + return { + id: name, + type: name, + name, + componentType: 'task', + properties: {} + }; +} + +describe('sortToolboxGroups', () => { + it('sorts correctly', () => { + const groups: ToolboxGroup[] = [ + { + name: 'B', + steps: [createStep('U'), createStep('B'), createStep('A')] + }, + { + name: 'A', + steps: [createStep('G'), createStep('F'), createStep('C')] + } + ]; + + sortToolboxGroups(groups); + + expect(groups[0].name).toBe('A'); + expect(groups[0].steps[0].name).toBe('C'); + expect(groups[0].steps[1].name).toBe('F'); + expect(groups[0].steps[2].name).toBe('G'); + + expect(groups[1].name).toBe('B'); + expect(groups[1].steps[0].name).toBe('A'); + expect(groups[1].steps[1].name).toBe('B'); + expect(groups[1].steps[2].name).toBe('U'); + }); +}); diff --git a/editor/src/core/sort-toolbox-groups.ts b/editor/src/core/sort-toolbox-groups.ts new file mode 100644 index 0000000..7785518 --- /dev/null +++ b/editor/src/core/sort-toolbox-groups.ts @@ -0,0 +1,8 @@ +import { EditorToolboxSorter, ToolboxGroup } from '../editor-provider-configuration'; + +export const sortToolboxGroups: EditorToolboxSorter = (groups: ToolboxGroup[]) => { + groups.forEach(group => { + group.steps.sort((a, b) => a.name.localeCompare(b.name)); + }); + groups.sort((a, b) => a.name.localeCompare(b.name)); +}; diff --git a/editor/src/core/step-i18n-prefix.ts b/editor/src/core/step-i18n-prefix.ts new file mode 100644 index 0000000..e4842ff --- /dev/null +++ b/editor/src/core/step-i18n-prefix.ts @@ -0,0 +1,3 @@ +export function createStepI18nPrefix(stepType: string | null): string { + return stepType ? `step.${stepType}.property:` : 'root.property:'; +} diff --git a/editor/src/editor-provider-configuration.ts b/editor/src/editor-provider-configuration.ts index 7219e5c..512aa15 100644 --- a/editor/src/editor-provider-configuration.ts +++ b/editor/src/editor-provider-configuration.ts @@ -1,11 +1,49 @@ import { I18n, UidGenerator } from 'sequential-workflow-editor-model'; -import { DefinitionWalker } from 'sequential-workflow-model'; +import { DefinitionWalker, Step } from 'sequential-workflow-model'; import { EditorExtension } from './editor-extension'; export interface EditorProviderConfiguration { + /** + * A generator of unique identifiers. + */ uidGenerator: UidGenerator; + + /** + * The definition walker. If it's not set the editor uses the default definition walker from the `sequential-workflow-model` package. + */ definitionWalker?: DefinitionWalker; + + /** + * The translation service for the editor. + */ i18n?: I18n; + + /** + * Determines whether the header of the editor is hidden. + */ isHeaderHidden?: boolean; + + /** + * Sorter for the toolbox groups. By default, the groups are sorted alphabetically. + */ + toolboxSorter?: EditorToolboxSorter; + + /** + * Extensions for the editor. + */ extensions?: EditorExtension[]; } + +export interface ToolboxGroup { + /** + * The name of the group. + */ + name: string; + + /** + * The steps in the group. + */ + steps: Step[]; +} + +export type EditorToolboxSorter = (groups: ToolboxGroup[]) => void; diff --git a/editor/src/editor-provider.ts b/editor/src/editor-provider.ts index e9677a4..002ee5e 100644 --- a/editor/src/editor-provider.ts +++ b/editor/src/editor-provider.ts @@ -18,13 +18,19 @@ import { StepEditorContext, StepEditorProvider, StepLabelProvider, - StepValidator, - ToolboxGroup + StepValidator } from './external-types'; -import { EditorProviderConfiguration } from './editor-provider-configuration'; +import { EditorProviderConfiguration, ToolboxGroup } from './editor-provider-configuration'; import { EditorHeaderData } from './editor-header'; +import { sortToolboxGroups } from './core/sort-toolbox-groups'; export class EditorProvider { + /** + * Creates an editor provider. + * @param definitionModel The definition model. + * @param configuration The configuration. + * @returns The editor provider. + */ public static create( definitionModel: DefinitionModel, configuration: EditorProviderConfiguration @@ -121,29 +127,40 @@ export class EditorProvider { }; } + /** + * Activates the definition (creates a new instance of the definition with the default values). + * @returns The activated definition. + */ public activateDefinition(): TDefinition { return this.activator.activateDefinition(); } + /** + * Activates a step with the default values. + * @param type The type of the step to activate. + * @returns The activated step. + */ public activateStep(type: string): Step { return this.activator.activateStep(type); } public getToolboxGroups(): ToolboxGroup[] { - const stepModels = Object.values(this.definitionModel.steps); + const stepModels = Object.values(this.definitionModel.steps).filter(step => step.toolbox); const groups: ToolboxGroup[] = []; const categories = new Set(stepModels.map(step => step.category)); + categories.forEach((category: string | undefined) => { const name = category ?? this.i18n('toolbox.defaultGroupName', 'Others'); const groupStepModels = stepModels.filter(step => step.category === category); const groupSteps = groupStepModels.map(step => this.activateStep(step.type)); - groupSteps.sort((a, b) => a.name.localeCompare(b.name)); groups.push({ name, steps: groupSteps }); }); - groups.sort((a, b) => a.name.localeCompare(b.name)); + + const sort = this.configuration.toolboxSorter || sortToolboxGroups; + sort(groups); return groups; } diff --git a/editor/src/external-types.ts b/editor/src/external-types.ts index 07619f4..176ab39 100644 --- a/editor/src/external-types.ts +++ b/editor/src/external-types.ts @@ -17,8 +17,3 @@ export type StepLabelProvider = (step: { type: string }) => string; export type StepValidator = (step: Step, _: unknown, definition: Definition) => boolean; export type RootValidator = (definition: Definition) => boolean; - -export interface ToolboxGroup { - name: string; - steps: Step[]; -} diff --git a/editor/src/property-editor/property-editor.ts b/editor/src/property-editor/property-editor.ts index 373af2a..b3468a5 100644 --- a/editor/src/property-editor/property-editor.ts +++ b/editor/src/property-editor/property-editor.ts @@ -13,6 +13,7 @@ import { PropertyValidationErrorComponent, propertyValidationErrorComponent } fr import { Icons } from '../core/icons'; import { PropertyHintComponent, propertyHint } from './property-hint'; import { StackedSimpleEvent } from '../core'; +import { createStepI18nPrefix } from '../core/step-i18n-prefix'; export class PropertyEditor implements Component { public static create( @@ -45,7 +46,7 @@ export class PropertyEditor implements Component { const label = Html.element('h4', { class: 'swe-property-header-label' }); - const i18nPrefix = stepType ? `step.${stepType}.property:` : 'root.property:'; + const i18nPrefix = createStepI18nPrefix(stepType); label.innerText = editorServices.i18n(i18nPrefix + pathStr, propertyModel.label); header.appendChild(label); diff --git a/editor/src/value-editors/choice/choice-value-editor.ts b/editor/src/value-editors/choice/choice-value-editor.ts index f5a5a20..f4a6e3b 100644 --- a/editor/src/value-editors/choice/choice-value-editor.ts +++ b/editor/src/value-editors/choice/choice-value-editor.ts @@ -4,6 +4,7 @@ import { validationErrorComponent } from '../../components/validation-error-comp import { valueEditorContainerComponent } from '../../components/value-editor-container-component'; import { rowComponent } from '../../components/row-component'; import { selectComponent } from '../../components/select-component'; +import { createStepI18nPrefix } from '../../core/step-i18n-prefix'; export const choiceValueEditorId = 'choice'; @@ -13,7 +14,7 @@ export function choiceValueEditor(context: ValueContext): Valu } function onSelected(index: number) { - const value = context.model.configuration.choices[index]; + const value = choices[index]; context.setValue(value); validate(); } @@ -21,8 +22,19 @@ export function choiceValueEditor(context: ValueContext): Valu const select = selectComponent({ stretched: true }); - select.setValues(context.model.configuration.choices); - const startIndex = context.model.configuration.choices.indexOf(context.getValue()); + + const stepType = context.tryGetStepType(); + const i18nPrefix = createStepI18nPrefix(stepType); + + const choices = context.model.configuration.choices; + const translatedChoices = choices.map(choice => { + const pathStr = context.model.path.toString(); + const key = `${i18nPrefix}${pathStr}:choice:${choice}`; + return context.i18n(key, choice); + }); + + select.setValues(translatedChoices); + const startIndex = choices.indexOf(context.getValue()); select.selectIndex(startIndex); select.onSelected.subscribe(onSelected); diff --git a/editor/src/value-editors/nullable-any-variable/nullable-any-variable-editor.ts b/editor/src/value-editors/nullable-any-variable/nullable-any-variable-editor.ts index f70a5b4..0ddbdc3 100644 --- a/editor/src/value-editors/nullable-any-variable/nullable-any-variable-editor.ts +++ b/editor/src/value-editors/nullable-any-variable/nullable-any-variable-editor.ts @@ -35,10 +35,15 @@ export function nullableAnyVariableValueEditor( const select = selectComponent({ stretched: true }); - select.setValues([ - context.i18n('nullableAnyVariable.select', '- Select -'), - ...variables.map(variable => formatVariableNameWithType(variable.name, variable.type)) - ]); + + const expectedTypes = context.model.configuration.valueTypes ? context.model.configuration.valueTypes.join(', ') : null; + const actionText = expectedTypes + ? context.i18n('nullableAnyVariable.selectTypes', '- Select: :types -', { + types: expectedTypes + }) + : context.i18n('nullableAnyVariable.select', '- Select -'); + + select.setValues([actionText, ...variables.map(variable => formatVariableNameWithType(variable.name, variable.type))]); if (startValue) { select.selectIndex(variables.findIndex(variable => variable.name === startValue.name) + 1); } else { diff --git a/editor/src/value-editors/nullable-variable/nullable-variable-value-editor.ts b/editor/src/value-editors/nullable-variable/nullable-variable-value-editor.ts index 8f6817e..7c725d6 100644 --- a/editor/src/value-editors/nullable-variable/nullable-variable-value-editor.ts +++ b/editor/src/value-editors/nullable-variable/nullable-variable-value-editor.ts @@ -32,7 +32,9 @@ export function nullableVariableValueEditor(context: ValueContext formatVariableNameWithType(variable.name, variable.type)) ]); if (startValue) { diff --git a/model/package.json b/model/package.json index ffd743b..36965a7 100644 --- a/model/package.json +++ b/model/package.json @@ -1,6 +1,6 @@ { "name": "sequential-workflow-editor-model", - "version": "0.13.2", + "version": "0.14.8", "homepage": "https://nocode-js.com/", "author": { "name": "NoCode JS", diff --git a/model/src/builders/branched-step-model-builder.ts b/model/src/builders/branched-step-model-builder.ts index 211df5d..b1eddde 100644 --- a/model/src/builders/branched-step-model-builder.ts +++ b/model/src/builders/branched-step-model-builder.ts @@ -9,6 +9,10 @@ const branchesPath = Path.create('branches'); export class BranchedStepModelBuilder extends StepModelBuilder { private readonly branchesBuilder = new PropertyModelBuilder(branchesPath, this.circularDependencyDetector); + /** + * @returns the builder for the branches property. + * @example `builder.branches().value(createBranchesValueModel(...));` + */ public branches(): PropertyModelBuilder { return this.branchesBuilder; } diff --git a/model/src/builders/root-model-builder.ts b/model/src/builders/root-model-builder.ts index 502871c..9ee8c34 100644 --- a/model/src/builders/root-model-builder.ts +++ b/model/src/builders/root-model-builder.ts @@ -12,6 +12,11 @@ export class RootModelBuilder { private readonly propertyBuilders: PropertyModelBuilder[] = []; private readonly sequenceBuilder = new PropertyModelBuilder(sequencePath, this.circularDependencyDetector); + /** + * @param propertyName Name of the property. + * @returns The builder for the property. + * @example `builder.property('foo').value(createStringValueModel({ defaultValue: 'Some value' })).label('Foo');` + */ public property(propertyName: Key): PropertyModelBuilder { const path = Path.create(['properties', String(propertyName)]); const builder = new PropertyModelBuilder(path, this.circularDependencyDetector); @@ -19,6 +24,10 @@ export class RootModelBuilder { return builder; } + /** + * @returns the builder for the sequence property. + * @example `builder.sequence().value(createSequenceValueModel(...));` + */ public sequence(): PropertyModelBuilder { return this.sequenceBuilder; } diff --git a/model/src/builders/sequential-step-model-builder.ts b/model/src/builders/sequential-step-model-builder.ts index 37bd1fe..45a61ba 100644 --- a/model/src/builders/sequential-step-model-builder.ts +++ b/model/src/builders/sequential-step-model-builder.ts @@ -13,6 +13,10 @@ export class SequentialStepModelBuilder extends St this.circularDependencyDetector ); + /** + * @returns the builder for the sequence property. + * @example `builder.sequence().value(createSequenceValueModel(...));` + */ public sequence(): PropertyModelBuilder { return this.sequenceBuilder; } diff --git a/model/src/builders/step-model-builder.ts b/model/src/builders/step-model-builder.ts index 7100536..36fd65c 100644 --- a/model/src/builders/step-model-builder.ts +++ b/model/src/builders/step-model-builder.ts @@ -13,6 +13,7 @@ export class StepModelBuilder { private _label?: string; private _description?: string; private _category?: string; + private _toolbox = true; private _validator?: StepValidator; private readonly nameBuilder = new PropertyModelBuilder(namePath, this.circularDependencyDetector); private readonly propertyBuilder: PropertyModelBuilder[] = []; @@ -57,6 +58,16 @@ export class StepModelBuilder { return this; } + /** + * Sets whether the step should be displayed in the toolbox. Default is `true`. + * @param toolbox Whether the step should be displayed in the toolbox. + * @example `builder.toolbox(false);` + */ + public toolbox(toolbox: boolean): this { + this._toolbox = toolbox; + return this; + } + /** * Sets the validator of the step. * @param validator The validator. @@ -68,7 +79,7 @@ export class StepModelBuilder { /** * @returns The builder for the `name` property. - * @example `builder.name().value(stringValueModel({ defaultValue: 'Some name' })).label('Name');` + * @example `builder.name().value(createStringValueModel({ defaultValue: 'Some name' })).label('Name');` */ public name(): PropertyModelBuilder { return this.nameBuilder; @@ -77,7 +88,7 @@ export class StepModelBuilder { /** * @param propertyName Name of the property in the step. * @returns The builder for the property. - * @example `builder.property('foo').value(stringValueModel({ defaultValue: 'Some value' })).label('Foo');` + * @example `builder.property('foo').value(createStringValueModel({ defaultValue: 'Some value' })).label('Foo');` */ public property( propertyName: Key @@ -103,6 +114,7 @@ export class StepModelBuilder { label: this._label ?? buildLabel(this.type), category: this._category, description: this._description, + toolbox: this._toolbox, validator: this._validator, name: this.nameBuilder.build(), properties: this.propertyBuilder.map(builder => builder.build()) diff --git a/model/src/context/default-value-context.ts b/model/src/context/default-value-context.ts index 8a4eac6..8ae6c5d 100644 --- a/model/src/context/default-value-context.ts +++ b/model/src/context/default-value-context.ts @@ -16,5 +16,6 @@ export class DefaultValueContext { ) {} public readonly getPropertyValue = this.propertyContext.getPropertyValue; + public readonly formatPropertyValue = this.propertyContext.formatPropertyValue; public readonly activateStep = this.activator.activateStep; } diff --git a/model/src/context/property-context.ts b/model/src/context/property-context.ts index a2db199..a6f4427 100644 --- a/model/src/context/property-context.ts +++ b/model/src/context/property-context.ts @@ -1,4 +1,4 @@ -import { Properties } from 'sequential-workflow-model'; +import { Properties, Step } from 'sequential-workflow-model'; import { DefinitionModel, PropertyModel } from '../model'; import { ValueType } from '../types'; import { readPropertyValue } from './read-property-value'; @@ -18,11 +18,45 @@ export class PropertyContext { private readonly definitionModel: DefinitionModel ) {} + /** + * @returns the type of the step, or `null` if the object is root. + */ + public readonly tryGetStepType = (): string | null => { + const type = (this.object as Step).type; + return type ? type : null; + }; + + /** + * Get the value of a property by name. + * @param name The name of the property. + * @returns The value of the property. + */ public readonly getPropertyValue = (name: Key): TProperties[Key] => { return readPropertyValue(name, this.propertyModel, this.object); }; + /** + * @returns The supported value types for variables. + */ public readonly getValueTypes = (): ValueType[] => { return this.definitionModel.valueTypes; }; + + /** + * Format a property value using a formatter function. + * @param name The name of the property. + * @param formatter The formatter function. + * @param undefinedValue The value to return if the property value is `null` or `undefined`. + */ + public readonly formatPropertyValue = ( + name: Key, + formatter: (value: NonNullable) => string, + undefinedValue?: string + ): string => { + const value = this.getPropertyValue(name); + if (value === undefined || value === null) { + return undefinedValue || '?'; + } + return formatter(value); + }; } diff --git a/model/src/context/scoped-property-context.ts b/model/src/context/scoped-property-context.ts index 2ccf67d..5f3c9ee 100644 --- a/model/src/context/scoped-property-context.ts +++ b/model/src/context/scoped-property-context.ts @@ -20,7 +20,9 @@ export class ScopedPropertyContext { private readonly parentsProvider: ParentsProvider ) {} + public readonly tryGetStepType = this.propertyContext.tryGetStepType; public readonly getPropertyValue = this.propertyContext.getPropertyValue; + public readonly formatPropertyValue = this.propertyContext.formatPropertyValue; public readonly getValueTypes = this.propertyContext.getValueTypes; public readonly hasVariable = (variableName: string, valueType: string | null): boolean => { diff --git a/model/src/context/value-context.ts b/model/src/context/value-context.ts index e930337..e386e20 100644 --- a/model/src/context/value-context.ts +++ b/model/src/context/value-context.ts @@ -25,7 +25,9 @@ export class ValueContext ) {} + public readonly tryGetStepType = this.scopedPropertyContext.tryGetStepType; public readonly getPropertyValue = this.scopedPropertyContext.getPropertyValue; + public readonly formatPropertyValue = this.scopedPropertyContext.formatPropertyValue; public readonly getValueTypes = this.scopedPropertyContext.getValueTypes; public readonly hasVariable = this.scopedPropertyContext.hasVariable; public readonly findFirstUndefinedVariable = this.scopedPropertyContext.findFirstUndefinedVariable; diff --git a/model/src/model.ts b/model/src/model.ts index c22de6b..c3c35e2 100644 --- a/model/src/model.ts +++ b/model/src/model.ts @@ -23,6 +23,7 @@ export interface StepModel { type: string; componentType: string; category?: string; + toolbox: boolean; label: string; description?: string; name: PropertyModel; diff --git a/model/src/validator/property-validator-context.ts b/model/src/validator/property-validator-context.ts index 6a8d378..760736f 100644 --- a/model/src/validator/property-validator-context.ts +++ b/model/src/validator/property-validator-context.ts @@ -12,6 +12,7 @@ export class PropertyValidatorContext) {} public readonly getPropertyValue = this.valueContext.getPropertyValue; + public readonly formatPropertyValue = this.valueContext.formatPropertyValue; public readonly getSupportedValueTypes = this.valueContext.getValueTypes; public readonly hasVariable = this.valueContext.hasVariable; public readonly findFirstUndefinedVariable = this.valueContext.findFirstUndefinedVariable; diff --git a/model/src/value-models/any-variables/any-variables-value-model.ts b/model/src/value-models/any-variables/any-variables-value-model.ts index 4f9d3d8..323d2e3 100644 --- a/model/src/value-models/any-variables/any-variables-value-model.ts +++ b/model/src/value-models/any-variables/any-variables-value-model.ts @@ -4,7 +4,9 @@ import { AnyVariables, ValueType } from '../../types'; import { ValueContext } from '../../context'; export interface AnyVariablesValueModelConfiguration { + label?: string; valueTypes?: ValueType[]; + editorId?: string; } export type AnyVariablesValueModel = ValueModel; @@ -16,7 +18,8 @@ export const createAnyVariablesValueModel = ( ): ValueModelFactoryFromModel => ({ create: (path: Path) => ({ id: anyVariablesValueModelId, - label: 'Variables', + label: configuration.label ?? 'Variables', + editorId: configuration.editorId, path, configuration, getDefaultValue() { diff --git a/model/src/value-models/boolean/boolean-value-model-configuration.ts b/model/src/value-models/boolean/boolean-value-model-configuration.ts index 9cc42af..18bbe9e 100644 --- a/model/src/value-models/boolean/boolean-value-model-configuration.ts +++ b/model/src/value-models/boolean/boolean-value-model-configuration.ts @@ -1,4 +1,5 @@ export interface BooleanValueModelConfiguration { + label?: string; defaultValue?: boolean; editorId?: string; } diff --git a/model/src/value-models/boolean/boolean-value-model.ts b/model/src/value-models/boolean/boolean-value-model.ts index 6a9b50b..7b0cd87 100644 --- a/model/src/value-models/boolean/boolean-value-model.ts +++ b/model/src/value-models/boolean/boolean-value-model.ts @@ -11,7 +11,7 @@ export const createBooleanValueModel = (configuration: BooleanValueModelConfigur create: (path: Path) => ({ id: booleanValueModelId, editorId: configuration.editorId, - label: 'Boolean', + label: configuration.label ?? 'Boolean', path, configuration, getDefaultValue() { diff --git a/model/src/value-models/choice/choice-value-model-configuration.ts b/model/src/value-models/choice/choice-value-model-configuration.ts new file mode 100644 index 0000000..34a047a --- /dev/null +++ b/model/src/value-models/choice/choice-value-model-configuration.ts @@ -0,0 +1,18 @@ +export interface ChoiceValueModelConfiguration { + /** + * Label. If not provided, the label is generated from the property name. + */ + label?: string; + /** + * Supported choices. + */ + choices: TValue[]; + /** + * Default value. + */ + defaultValue?: TValue; + /** + * Custom editor ID. + */ + editorId?: string; +} diff --git a/model/src/value-models/choice/choice-value-model-validator.spec.ts b/model/src/value-models/choice/choice-value-model-validator.spec.ts new file mode 100644 index 0000000..28fffb0 --- /dev/null +++ b/model/src/value-models/choice/choice-value-model-validator.spec.ts @@ -0,0 +1,20 @@ +import { createValueContextStub } from '../../test-tools/value-context-stub'; +import { ChoiceValueModel } from './choice-value-model'; +import { ChoiceValueModelConfiguration } from './choice-value-model-configuration'; +import { choiceValueModelValidator } from './choice-value-model-validator'; + +describe('choiceValueModelValidator', () => { + it('returns correct response', () => { + const configuration: ChoiceValueModelConfiguration = { + choices: ['x', 'y'] + }; + + const context1 = createValueContextStub('z', configuration); + const error1 = choiceValueModelValidator(context1); + expect(error1?.$).toBe('Value is not supported'); + + const context2 = createValueContextStub('x', configuration); + const error2 = choiceValueModelValidator(context2); + expect(error2).toBe(null); + }); +}); diff --git a/model/src/value-models/choice/choice-value-model-validator.ts b/model/src/value-models/choice/choice-value-model-validator.ts new file mode 100644 index 0000000..2ffb05c --- /dev/null +++ b/model/src/value-models/choice/choice-value-model-validator.ts @@ -0,0 +1,12 @@ +import { ValueContext } from '../../context'; +import { createValidationSingleError, ValidationResult } from '../../model'; +import { ChoiceValueModel } from './choice-value-model'; + +export function choiceValueModelValidator(context: ValueContext): ValidationResult { + const value = context.getValue(); + const configuration = context.model.configuration; + if (!configuration.choices.includes(value)) { + return createValidationSingleError(context.i18n('choice.notSupportedValue', 'Value is not supported')); + } + return null; +} diff --git a/model/src/value-models/choice/choice-value-model.ts b/model/src/value-models/choice/choice-value-model.ts index 5d41f2b..cecf94a 100644 --- a/model/src/value-models/choice/choice-value-model.ts +++ b/model/src/value-models/choice/choice-value-model.ts @@ -1,11 +1,7 @@ -import { ValueModel, ValidationResult, createValidationSingleError, ValueModelFactory } from '../../model'; +import { ValueModel, ValueModelFactory } from '../../model'; import { Path } from '../../core/path'; -import { ValueContext } from '../../context'; - -export interface ChoiceValueModelConfiguration { - choices: TValue[]; - defaultValue?: TValue; -} +import { ChoiceValueModelConfiguration } from './choice-value-model-configuration'; +import { choiceValueModelValidator } from './choice-value-model-validator'; export type ChoiceValueModel = ValueModel>; @@ -21,7 +17,8 @@ export function createChoiceValueModel( return { create: (path: Path) => ({ id: choiceValueModelId, - label: 'Choice', + label: configuration.label ?? 'Choice', + editorId: configuration.editorId, path, configuration, getDefaultValue() { @@ -34,13 +31,7 @@ export function createChoiceValueModel( return configuration.choices[0]; }, getVariableDefinitions: () => null, - validate(context: ValueContext>): ValidationResult { - const value = context.getValue(); - if (!configuration.choices.includes(value)) { - return createValidationSingleError(context.i18n('choice.notSupportedValue', 'Value is not supported')); - } - return null; - } + validate: choiceValueModelValidator }) }; } diff --git a/model/src/value-models/choice/index.ts b/model/src/value-models/choice/index.ts index 3df4ad8..30b1982 100644 --- a/model/src/value-models/choice/index.ts +++ b/model/src/value-models/choice/index.ts @@ -1 +1,2 @@ +export * from './choice-value-model-configuration'; export * from './choice-value-model'; diff --git a/model/src/value-models/generated-string/generated-string-context.ts b/model/src/value-models/generated-string/generated-string-context.ts index 4279a49..47b3ab6 100644 --- a/model/src/value-models/generated-string/generated-string-context.ts +++ b/model/src/value-models/generated-string/generated-string-context.ts @@ -17,15 +17,5 @@ export class GeneratedStringContext ) {} public readonly getPropertyValue = this.context.getPropertyValue; - - public formatPropertyValue( - name: Key, - formatter: (value: NonNullable) => string - ): string { - const value = this.getPropertyValue(name); - if (value === undefined || value === null) { - return '?'; - } - return formatter(value); - } + public readonly formatPropertyValue = this.context.formatPropertyValue; } diff --git a/model/src/value-models/generated-string/generated-string-value-model.ts b/model/src/value-models/generated-string/generated-string-value-model.ts index 118a04f..0884cbc 100644 --- a/model/src/value-models/generated-string/generated-string-value-model.ts +++ b/model/src/value-models/generated-string/generated-string-value-model.ts @@ -6,7 +6,9 @@ import { GeneratedStringContext } from './generated-string-context'; import { DefaultValueContext } from '../../context/default-value-context'; export interface GeneratedStringValueModelConfiguration { + label?: string; generator(context: GeneratedStringContext): string; + editorId?: string; } export type GeneratedStringVariableValueModel = ValueModel< @@ -22,7 +24,8 @@ export function createGeneratedStringValueModel ({ id: generatedStringValueModelId, - label: 'Generated string', + label: configuration.label ?? 'Generated string', + editorId: configuration.editorId, path, configuration, getDefaultValue(context: DefaultValueContext) { diff --git a/model/src/value-models/nullable-any-variable/nullable-any-variable-value-model.ts b/model/src/value-models/nullable-any-variable/nullable-any-variable-value-model.ts index a9d9166..64b5332 100644 --- a/model/src/value-models/nullable-any-variable/nullable-any-variable-value-model.ts +++ b/model/src/value-models/nullable-any-variable/nullable-any-variable-value-model.ts @@ -4,8 +4,10 @@ import { NullableAnyVariable, ValueType } from '../../types'; import { ValueContext } from '../../context'; export interface NullableAnyVariableValueModelConfiguration { + label?: string; isRequired?: boolean; valueTypes?: ValueType[]; + editorId?: string; } export type NullableAnyVariableValueModel = ValueModel; @@ -17,7 +19,8 @@ export const createNullableAnyVariableValueModel = ( ): ValueModelFactoryFromModel => ({ create: (path: Path) => ({ id: nullableAnyVariableValueModelId, - label: 'Variable', + label: configuration.label ?? 'Variable', + editorId: configuration.editorId, path, configuration, getDefaultValue() { diff --git a/model/src/value-models/nullable-variable/nullable-variable-value-model.ts b/model/src/value-models/nullable-variable/nullable-variable-value-model.ts index c362fa9..b84e997 100644 --- a/model/src/value-models/nullable-variable/nullable-variable-value-model.ts +++ b/model/src/value-models/nullable-variable/nullable-variable-value-model.ts @@ -5,8 +5,10 @@ import { ValueType } from '../../types'; import { ValueContext } from '../../context'; export interface NullableVariableValueModelConfiguration { + label?: string; valueType: ValueType; isRequired?: boolean; + editorId?: string; } export type NullableVariableValueModel = ValueModel; @@ -18,7 +20,8 @@ export const createNullableVariableValueModel = ( ): ValueModelFactoryFromModel => ({ create: (path: Path) => ({ id: nullableVariableValueModelId, - label: 'Variable', + label: configuration.label ?? 'Variable', + editorId: configuration.editorId, path, configuration, getDefaultValue(): NullableVariable { diff --git a/model/src/value-models/number/number-value-model-configuration.ts b/model/src/value-models/number/number-value-model-configuration.ts index 4273e1e..155c687 100644 --- a/model/src/value-models/number/number-value-model-configuration.ts +++ b/model/src/value-models/number/number-value-model-configuration.ts @@ -1,4 +1,5 @@ export interface NumberValueModelConfiguration { + label?: string; defaultValue?: number; min?: number; max?: number; diff --git a/model/src/value-models/number/number-value-model.ts b/model/src/value-models/number/number-value-model.ts index 949e9a2..18ad47e 100644 --- a/model/src/value-models/number/number-value-model.ts +++ b/model/src/value-models/number/number-value-model.ts @@ -10,8 +10,8 @@ export const numberValueModelId = 'number'; export const createNumberValueModel = (configuration: NumberValueModelConfiguration): ValueModelFactoryFromModel => ({ create: (path: Path) => ({ id: numberValueModelId, + label: configuration.label ?? 'Number', editorId: configuration.editorId, - label: 'Number', path, configuration, getDefaultValue() { diff --git a/model/src/value-models/string-dictionary/string-dictionary-value-model-configuration.ts b/model/src/value-models/string-dictionary/string-dictionary-value-model-configuration.ts index 1414752..24e715f 100644 --- a/model/src/value-models/string-dictionary/string-dictionary-value-model-configuration.ts +++ b/model/src/value-models/string-dictionary/string-dictionary-value-model-configuration.ts @@ -1,4 +1,6 @@ export interface StringDictionaryValueModelConfiguration { + label?: string; uniqueKeys?: boolean; valueMinLength?: number; + editorId?: string; } diff --git a/model/src/value-models/string-dictionary/string-dictionary-value-model.ts b/model/src/value-models/string-dictionary/string-dictionary-value-model.ts index 0489436..a0cfc10 100644 --- a/model/src/value-models/string-dictionary/string-dictionary-value-model.ts +++ b/model/src/value-models/string-dictionary/string-dictionary-value-model.ts @@ -13,7 +13,8 @@ export const createStringDictionaryValueModel = ( ): ValueModelFactoryFromModel => ({ create: (path: Path) => ({ id: stringDictionaryValueModelId, - label: 'Dictionary', + label: configuration.label ?? 'Dictionary', + editorId: configuration.editorId, path, configuration, getDefaultValue() { diff --git a/model/src/value-models/string/string-value-model-configuration.ts b/model/src/value-models/string/string-value-model-configuration.ts index 4580def..0cb9290 100644 --- a/model/src/value-models/string/string-value-model-configuration.ts +++ b/model/src/value-models/string/string-value-model-configuration.ts @@ -1,4 +1,5 @@ export interface StringValueModelConfiguration { + label?: string; minLength?: number; defaultValue?: string; pattern?: RegExp; diff --git a/model/src/value-models/string/string-value-model.ts b/model/src/value-models/string/string-value-model.ts index 935f767..37cbd84 100644 --- a/model/src/value-models/string/string-value-model.ts +++ b/model/src/value-models/string/string-value-model.ts @@ -10,8 +10,8 @@ export const stringValueModelId = 'string'; export const createStringValueModel = (configuration: StringValueModelConfiguration): ValueModelFactoryFromModel => ({ create: (path: Path) => ({ id: stringValueModelId, + label: configuration.label ?? 'String', editorId: configuration.editorId, - label: 'String', path, configuration, getDefaultValue() {