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 0ab3748..979cb3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## 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). 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 74057f3..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.14.4", - "sequential-workflow-editor": "^0.14.4" + "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/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/editor/package.json b/editor/package.json index 0c7cbb0..f6437ba 100644 --- a/editor/package.json +++ b/editor/package.json @@ -1,6 +1,6 @@ { "name": "sequential-workflow-editor", - "version": "0.14.4", + "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.14.4", + "sequential-workflow-editor-model": "^0.14.8", "sequential-workflow-model": "^0.2.0" }, "peerDependencies": { - "sequential-workflow-editor-model": "^0.14.4", + "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/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.ts b/editor/src/editor-provider.ts index babe3ef..002ee5e 100644 --- a/editor/src/editor-provider.ts +++ b/editor/src/editor-provider.ts @@ -25,6 +25,12 @@ 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,10 +127,19 @@ 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); } 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/model/package.json b/model/package.json index d6c4312..36965a7 100644 --- a/model/package.json +++ b/model/package.json @@ -1,6 +1,6 @@ { "name": "sequential-workflow-editor-model", - "version": "0.14.4", + "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/context/property-context.ts b/model/src/context/property-context.ts index 1174f1e..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,6 +18,14 @@ 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. diff --git a/model/src/context/scoped-property-context.ts b/model/src/context/scoped-property-context.ts index 66ec37d..5f3c9ee 100644 --- a/model/src/context/scoped-property-context.ts +++ b/model/src/context/scoped-property-context.ts @@ -20,6 +20,7 @@ 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; diff --git a/model/src/context/value-context.ts b/model/src/context/value-context.ts index 04f8a74..e386e20 100644 --- a/model/src/context/value-context.ts +++ b/model/src/context/value-context.ts @@ -25,6 +25,7 @@ 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; 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 69dabc6..cecf94a 100644 --- a/model/src/value-models/choice/choice-value-model.ts +++ b/model/src/value-models/choice/choice-value-model.ts @@ -1,13 +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 { - label?: string; - choices: TValue[]; - defaultValue?: TValue; - editorId?: string; -} +import { ChoiceValueModelConfiguration } from './choice-value-model-configuration'; +import { choiceValueModelValidator } from './choice-value-model-validator'; export type ChoiceValueModel = ValueModel>; @@ -37,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';