diff --git a/CHANGELOG.md b/CHANGELOG.md index 6948ffe..607b576 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.13.0 + +This version introduces several internal changes that allowed for the creation of a new type of editor in the pro version: the hidden dependent value editor. + ## 0.12.1 This version normalizes translations. diff --git a/demos/vanilla-js-app/vanilla-js.html b/demos/vanilla-js-app/vanilla-js.html index 474359e..61883ac 100644 --- a/demos/vanilla-js-app/vanilla-js.html +++ b/demos/vanilla-js-app/vanilla-js.html @@ -23,9 +23,9 @@ } - - - + + + diff --git a/demos/webpack-app/package.json b/demos/webpack-app/package.json index b654890..78aca20 100644 --- a/demos/webpack-app/package.json +++ b/demos/webpack-app/package.json @@ -16,10 +16,10 @@ "dependencies": { "xstate": "^4.38.2", "sequential-workflow-model": "^0.2.0", - "sequential-workflow-designer": "^0.21.1", + "sequential-workflow-designer": "^0.21.2", "sequential-workflow-machine": "^0.4.0", - "sequential-workflow-editor-model": "^0.12.1", - "sequential-workflow-editor": "^0.12.1" + "sequential-workflow-editor-model": "^0.13.0", + "sequential-workflow-editor": "^0.13.0" }, "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 57beb5d..e361e0f 100644 --- a/demos/webpack-app/src/editors/app.ts +++ b/demos/webpack-app/src/editors/app.ts @@ -27,7 +27,8 @@ export class App { rootEditorProvider: () => { const editor = document.createElement('div'); editor.innerHTML = - 'This demo showcases all the supported editors by the Sequential Workflow Editor. GitHub'; + 'This demo showcases all the supported editors by the Sequential Workflow Editor.

' + + 'Start exploring by clicking on each step.'; return editor; }, stepEditorProvider: editorProvider.createStepEditorProvider() diff --git a/demos/webpack-app/src/editors/model/any-variables-step-model.ts b/demos/webpack-app/src/editors/model/any-variables-step-model.ts index f662409..5fcf586 100644 --- a/demos/webpack-app/src/editors/model/any-variables-step-model.ts +++ b/demos/webpack-app/src/editors/model/any-variables-step-model.ts @@ -11,6 +11,8 @@ export interface AnyVariablesStepModel extends Step { } export const anyVariablesStepModel = createStepModel('anyVariables', 'task', step => { + step.description('In this step, you can select a collection of variables of any type.'); + step.property('zeroConfig').value(createAnyVariablesValueModel({})); step.property('onlyBoolean').value( createAnyVariablesValueModel({ diff --git a/demos/webpack-app/src/editors/model/boolean-step-model.ts b/demos/webpack-app/src/editors/model/boolean-step-model.ts index a856bad..45c44a2 100644 --- a/demos/webpack-app/src/editors/model/boolean-step-model.ts +++ b/demos/webpack-app/src/editors/model/boolean-step-model.ts @@ -12,6 +12,8 @@ export interface BooleanStepModel extends Step { } export const booleanStepModel = createStepModel('boolean', 'task', step => { + step.description('This step demonstrates properties with boolean values.'); + step.property('zeroConfig').value(createBooleanValueModel({})); step.property('defaultValueTrue').value( createBooleanValueModel({ diff --git a/demos/webpack-app/src/editors/model/choice-step-model.ts b/demos/webpack-app/src/editors/model/choice-step-model.ts index 6eaeb48..3e766c4 100644 --- a/demos/webpack-app/src/editors/model/choice-step-model.ts +++ b/demos/webpack-app/src/editors/model/choice-step-model.ts @@ -11,6 +11,8 @@ export interface ChoiceStepModel extends Step { } export const choiceStepModel = createStepModel('choice', 'task', step => { + step.description('In this step, you can see properties that allow you to select a value from a predefined list.'); + step.property('minimalConfig').value(createChoiceValueModel({ choices: ['red', 'blue', 'green'] })); step.property('defaultValueAllow').value( diff --git a/demos/webpack-app/src/editors/model/dynamic-step-model.ts b/demos/webpack-app/src/editors/model/dynamic-step-model.ts index 025f2dc..ceefe5d 100644 --- a/demos/webpack-app/src/editors/model/dynamic-step-model.ts +++ b/demos/webpack-app/src/editors/model/dynamic-step-model.ts @@ -19,6 +19,10 @@ export interface DynamicStepModel extends Step { } export const dynamicStepModel = createStepModel('dynamic', 'task', step => { + step.description( + 'This step has properties with dynamic values. For each property, you can change the value type by selecting the desired type.' + ); + step.property('example').value( createDynamicValueModel({ models: [createStringValueModel({}), createBooleanValueModel({})] diff --git a/demos/webpack-app/src/editors/model/generated-string-step-model.ts b/demos/webpack-app/src/editors/model/generated-string-step-model.ts index 6daed2e..77a7e98 100644 --- a/demos/webpack-app/src/editors/model/generated-string-step-model.ts +++ b/demos/webpack-app/src/editors/model/generated-string-step-model.ts @@ -11,6 +11,10 @@ export interface GeneratedStringStepModel extends Step { } export const generatedStringStepModel = createStepModel('generatedString', 'task', step => { + step.description( + 'This step has a property whose value is generated using data from another property. To see how it works, please change the value of the "X" property to 0, 1, 2, etc.' + ); + step.property('x').value(createNumberValueModel({})); step.property('example') diff --git a/editor/css/editor.css b/editor/css/editor.css index 686375d..c02ab8d 100644 --- a/editor/css/editor.css +++ b/editor/css/editor.css @@ -29,6 +29,9 @@ .swe-editor > .swe-validation-error { margin: 0 10px; } +.swe-hidden { + display: none; +} /* properties */ diff --git a/editor/package.json b/editor/package.json index c146616..aba1f15 100644 --- a/editor/package.json +++ b/editor/package.json @@ -1,6 +1,6 @@ { "name": "sequential-workflow-editor", - "version": "0.12.1", + "version": "0.13.0", "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.12.1", + "sequential-workflow-editor-model": "^0.13.0", "sequential-workflow-model": "^0.2.0" }, "peerDependencies": { - "sequential-workflow-editor-model": "^0.12.1", + "sequential-workflow-editor-model": "^0.13.0", "sequential-workflow-model": "^0.2.0" }, "devDependencies": { diff --git a/editor/src/components/property-validation-error-component.ts b/editor/src/components/property-validation-error-component.ts index ba58019..58fb04d 100644 --- a/editor/src/components/property-validation-error-component.ts +++ b/editor/src/components/property-validation-error-component.ts @@ -4,6 +4,7 @@ import { validationErrorComponent } from './validation-error-component'; export interface PropertyValidationErrorComponent extends Component { validate(): void; + isHidden(): boolean; } export function propertyValidationErrorComponent( @@ -21,6 +22,7 @@ export function propertyValidationErrorComponent( return { view: validation.view, - validate + validate, + isHidden: validation.isHidden }; } diff --git a/editor/src/components/validation-error-component.spec.ts b/editor/src/components/validation-error-component.spec.ts new file mode 100644 index 0000000..634b18f --- /dev/null +++ b/editor/src/components/validation-error-component.spec.ts @@ -0,0 +1,38 @@ +import { validationErrorComponent } from './validation-error-component'; + +describe('ValidationErrorComponent', () => { + it('returns correct value for isHidden() and emits changes', () => { + let emitted: boolean | null = null; + const component = validationErrorComponent(); + component.onIsHiddenChanged.subscribe(v => (emitted = v)); + + // test 1 + expect(component.isHidden()).toBe(true); + expect(component.view.children.length).toBe(0); + expect(emitted).toBeNull(); + + // test 2 + emitted = null; + component.setDefaultError({ $: 'Expected 2 characters' }); + + expect(component.isHidden()).toBe(false); + expect(component.view.children.length).toBeGreaterThan(0); + expect(emitted).toBe(false); + + // test 3 + emitted = null; + component.setDefaultError({ $: 'Expected 3 characters' }); + + expect(component.isHidden()).toBe(false); + expect(component.view.children.length).toBeGreaterThan(0); + expect(emitted).toBeNull(); // Visibility did not change + + // test 4 + emitted = null; + component.setDefaultError(null); + + expect(component.isHidden()).toBe(true); + expect(component.view.children.length).toBe(0); + expect(emitted).toBe(true); + }); +}); diff --git a/editor/src/components/validation-error-component.ts b/editor/src/components/validation-error-component.ts index 26eeaeb..d53ff28 100644 --- a/editor/src/components/validation-error-component.ts +++ b/editor/src/components/validation-error-component.ts @@ -1,8 +1,10 @@ -import { ValidationResult } from 'sequential-workflow-editor-model'; +import { SimpleEvent, ValidationResult } from 'sequential-workflow-editor-model'; import { Component } from './component'; import { Html } from '../core/html'; export interface ValidationErrorComponent extends Component { + onIsHiddenChanged: SimpleEvent; + isHidden(): boolean; setError(error: string | null): void; setDefaultError(result: ValidationResult): void; } @@ -11,9 +13,16 @@ export function validationErrorComponent(): ValidationErrorComponent { const view = Html.element('div', { class: 'swe-validation-error' }); + const onIsHiddenChanged = new SimpleEvent(); let child: HTMLElement | null = null; + function isHidden() { + return child === null; + } + function setError(error: string | null) { + const oldState = isHidden(); + if (child) { view.removeChild(child); child = null; @@ -25,6 +34,11 @@ export function validationErrorComponent(): ValidationErrorComponent { child.textContent = error; view.appendChild(child); } + + const newState = isHidden(); + if (oldState !== newState) { + onIsHiddenChanged.forward(newState); + } } function setDefaultError(result: ValidationResult) { @@ -32,7 +46,9 @@ export function validationErrorComponent(): ValidationErrorComponent { } return { + onIsHiddenChanged, view, + isHidden, setError, setDefaultError }; diff --git a/editor/src/core/html.spec.ts b/editor/src/core/html.spec.ts index 18e80f3..d6cd399 100644 --- a/editor/src/core/html.spec.ts +++ b/editor/src/core/html.spec.ts @@ -23,4 +23,16 @@ describe('Html', () => { expect(element.getAttribute('data-test')).toBe('555'); }); + + it('toggles class', () => { + const element = document.createElement('div'); + + Html.toggleClass(element, true, 'foo'); + + expect(element.classList.contains('foo')).toBe(true); + + Html.toggleClass(element, false, 'foo'); + + expect(element.classList.contains('foo')).toBe(false); + }); }); diff --git a/editor/src/core/html.ts b/editor/src/core/html.ts index 8e63cdd..f230ffb 100644 --- a/editor/src/core/html.ts +++ b/editor/src/core/html.ts @@ -17,4 +17,12 @@ export class Html { } return element; } + + public static toggleClass(element: Element, isEnabled: boolean, className: string) { + if (isEnabled) { + element.classList.add(className); + } else { + element.classList.remove(className); + } + } } diff --git a/editor/src/editor.ts b/editor/src/editor.ts index 1f54e33..4a7641c 100644 --- a/editor/src/editor.ts +++ b/editor/src/editor.ts @@ -32,10 +32,6 @@ export class Editor { const editors = new Map(); for (const propertyModel of propertyModels) { - if (editorServices.valueEditorFactoryResolver.isHidden(propertyModel.value.id, propertyModel.value.editorId)) { - continue; - } - const propertyEditor = PropertyEditor.create(propertyModel, stepType, definitionContext, editorServices); root.appendChild(propertyEditor.view); editors.set(propertyModel, propertyEditor); diff --git a/editor/src/property-editor/property-editor.ts b/editor/src/property-editor/property-editor.ts index fa92041..373af2a 100644 --- a/editor/src/property-editor/property-editor.ts +++ b/editor/src/property-editor/property-editor.ts @@ -12,6 +12,7 @@ import { Component } from '../components/component'; import { PropertyValidationErrorComponent, propertyValidationErrorComponent } from '../components/property-validation-error-component'; import { Icons } from '../core/icons'; import { PropertyHintComponent, propertyHint } from './property-hint'; +import { StackedSimpleEvent } from '../core'; export class PropertyEditor implements Component { public static create( @@ -31,9 +32,13 @@ export class PropertyEditor implements Component { let hint: PropertyHintComponent | null = null; const nameClassName = propertyModel.path.last(); + const pathStr = propertyModel.path.toString(); + const view = Html.element('div', { class: `swe-property swe-name-${nameClassName}` }); + view.setAttribute('data-path', pathStr); + const header = Html.element('div', { class: 'swe-property-header' }); @@ -41,7 +46,7 @@ export class PropertyEditor implements Component { class: 'swe-property-header-label' }); const i18nPrefix = stepType ? `step.${stepType}.property:` : 'root.property:'; - label.innerText = editorServices.i18n(i18nPrefix + propertyModel.path.toString(), propertyModel.label); + label.innerText = editorServices.i18n(i18nPrefix + pathStr, propertyModel.label); header.appendChild(label); view.appendChild(header); @@ -61,15 +66,16 @@ export class PropertyEditor implements Component { view.appendChild(valueEditor.view); + let control: HTMLElement | null = null; if (valueEditor.controlView) { - const control = Html.element('div', { + control = Html.element('div', { class: 'swe-property-header-control' }); control.appendChild(valueEditor.controlView); header.appendChild(control); } - let validationError: PropertyValidationErrorComponent | null = null; + let propertyValidationError: PropertyValidationErrorComponent | null = null; if (propertyModel.validator) { const valueContext = ValueContext.createFromDefinitionContext( propertyModel.value, @@ -78,38 +84,65 @@ export class PropertyEditor implements Component { editorServices.i18n ); const validatorContext = PropertyValidatorContext.create(valueContext); - validationError = propertyValidationErrorComponent(propertyModel.validator, validatorContext); - view.appendChild(validationError.view); + propertyValidationError = propertyValidationErrorComponent(propertyModel.validator, validatorContext); + view.appendChild(propertyValidationError.view); } - const editor = new PropertyEditor(view, valueContext.onValueChanged, valueEditor, validationError); - if (propertyModel.validator) { + const editor = new PropertyEditor(view, valueContext.onValueChanged, valueEditor, control, propertyValidationError); + if (propertyValidationError) { valueContext.onValueChanged.subscribe(editor.onValueChangedHandler); } + if (valueEditor.onIsHiddenChanged) { + valueEditor.onIsHiddenChanged.subscribe(editor.onEditorIsHiddenChanged); + } + editor.reloadVisibility(); return editor; } + private readonly onReloadVisibilityRequested = new StackedSimpleEvent(); + public constructor( public readonly view: HTMLElement, public readonly onValueChanged: SimpleEvent, private readonly valueEditor: ValueEditor, - private readonly validationError: PropertyValidationErrorComponent | null - ) {} + private readonly control: HTMLElement | null, + private readonly propertyValidationError: PropertyValidationErrorComponent | null + ) { + this.onReloadVisibilityRequested.subscribe(this.reloadVisibility); + } public reloadDependencies() { if (this.valueEditor.reloadDependencies) { this.valueEditor.reloadDependencies(); } - this.revalidate(); + this.validateProperty(); + this.onReloadVisibilityRequested.push(); } - private revalidate() { - if (this.validationError) { - this.validationError.validate(); + private validateProperty() { + if (this.propertyValidationError) { + this.propertyValidationError.validate(); } } + private readonly reloadVisibility = () => { + const isValueEditorHidden = this.valueEditor.isHidden ? this.valueEditor.isHidden() : false; + const isValidationErrorHidden = this.propertyValidationError ? this.propertyValidationError.isHidden() : true; + + const isPropertyEditorHidden = isValueEditorHidden && isValidationErrorHidden; + + Html.toggleClass(this.view, isPropertyEditorHidden, 'swe-hidden'); + Html.toggleClass(this.valueEditor.view, isValueEditorHidden, 'swe-hidden'); + if (this.control) { + Html.toggleClass(this.control, isValueEditorHidden, 'swe-hidden'); + } + }; + private readonly onValueChangedHandler = () => { - this.revalidate(); + this.validateProperty(); + }; + + private readonly onEditorIsHiddenChanged = () => { + this.onReloadVisibilityRequested.push(); }; } diff --git a/editor/src/value-editors/hidden/hidden-value-editor.spec.ts b/editor/src/value-editors/hidden/hidden-value-editor.spec.ts new file mode 100644 index 0000000..b647041 --- /dev/null +++ b/editor/src/value-editors/hidden/hidden-value-editor.spec.ts @@ -0,0 +1,11 @@ +import { hiddenValueEditor } from './hidden-value-editor'; + +describe('hiddenValueEditor', () => { + it('is hidden', () => { + const editor = hiddenValueEditor(); + + expect(editor.view).toBeDefined(); + expect(editor.isHidden).toBeDefined(); + expect((editor.isHidden as () => boolean)()).toBe(true); + }); +}); diff --git a/editor/src/value-editors/hidden/hidden-value-editor.ts b/editor/src/value-editors/hidden/hidden-value-editor.ts new file mode 100644 index 0000000..ab5bdad --- /dev/null +++ b/editor/src/value-editors/hidden/hidden-value-editor.ts @@ -0,0 +1,11 @@ +import { ValueModel } from 'sequential-workflow-editor-model'; +import { ValueEditor } from '../value-editor'; +import { valueEditorContainerComponent } from '../../components'; + +export function hiddenValueEditor(): ValueEditor { + const container = valueEditorContainerComponent([]); + return { + view: container.view, + isHidden: () => true + }; +} diff --git a/editor/src/value-editors/value-editor-factory-resolver.spec.ts b/editor/src/value-editors/value-editor-factory-resolver.spec.ts index 332af5b..3a6c521 100644 --- a/editor/src/value-editors/value-editor-factory-resolver.spec.ts +++ b/editor/src/value-editors/value-editor-factory-resolver.spec.ts @@ -16,23 +16,5 @@ describe('ValueEditorFactoryResolver', () => { ); }); }); - - describe('isHidden()', () => { - it('string is not hidden', () => { - const is = resolver.isHidden('string', undefined); - expect(is).toBe(false); - }); - - it('"branches" editor is hidden', () => { - const is = resolver.isHidden('branches', undefined); - expect(is).toBe(true); - }); - - it('throws error when editor is not found', () => { - expect(() => resolver.isHidden('some_unknown_mode_id', undefined)).toThrowError( - 'Editor id some_unknown_mode_id is not supported' - ); - }); - }); }); }); diff --git a/editor/src/value-editors/value-editor-factory-resolver.ts b/editor/src/value-editors/value-editor-factory-resolver.ts index 67ca0c0..4e6c8d3 100644 --- a/editor/src/value-editors/value-editor-factory-resolver.ts +++ b/editor/src/value-editors/value-editor-factory-resolver.ts @@ -15,6 +15,7 @@ import { nullableAnyVariableValueEditor, nullableAnyVariableValueEditorId } from import { booleanValueEditor, booleanValueEditorId } from './boolean/boolean-value-editor'; import { generatedStringValueEditor, generatedStringValueEditorId } from './generated-string/generated-string-value-editor'; import { stringDictionaryValueEditor } from './string-dictionary/string-dictionary-value-editor'; +import { hiddenValueEditor } from './hidden/hidden-value-editor'; import { EditorExtension } from '../editor-extension'; const defaultMap: ValueEditorMap = { @@ -30,8 +31,8 @@ const defaultMap: ValueEditorMap = { [stringDictionaryValueModelId]: stringDictionaryValueEditor as ValueEditorFactory, [numberValueEditorId]: numberValueEditor as ValueEditorFactory, [variableDefinitionsValueEditorId]: variableDefinitionsValueEditor as ValueEditorFactory, - [sequenceValueModelId]: null, - [branchesValueModelId]: null + [sequenceValueModelId]: hiddenValueEditor, + [branchesValueModelId]: hiddenValueEditor }; type ValueEditorMap = Record; @@ -62,16 +63,4 @@ export class ValueEditorFactoryResolver { } return editor; } - - public isHidden(valueModelId: string, editorId: string | undefined): boolean { - const id = editorId ?? valueModelId; - const editor = this.map[editorId ?? valueModelId]; - if (editor === null) { - return true; - } - if (editor !== undefined) { - return false; - } - throw new Error(`Editor id ${id} is not supported`); - } } diff --git a/editor/src/value-editors/value-editor.ts b/editor/src/value-editors/value-editor.ts index 43a28bd..83e1ffe 100644 --- a/editor/src/value-editors/value-editor.ts +++ b/editor/src/value-editors/value-editor.ts @@ -1,11 +1,13 @@ -import { ModelActivator, ValueModel, ValueContext, I18n } from 'sequential-workflow-editor-model'; +import { ModelActivator, ValueModel, ValueContext, I18n, SimpleEvent } from 'sequential-workflow-editor-model'; import { ValueEditorFactoryResolver } from './value-editor-factory-resolver'; import { Component } from '../components/component'; // eslint-disable-next-line @typescript-eslint/no-unused-vars export interface ValueEditor extends Component { + onIsHiddenChanged?: SimpleEvent; controlView?: HTMLElement; reloadDependencies?: () => void; + isHidden?: () => boolean; } export type ValueEditorFactory = ( diff --git a/model/package.json b/model/package.json index 4445cd2..fa0fd72 100644 --- a/model/package.json +++ b/model/package.json @@ -1,6 +1,6 @@ { "name": "sequential-workflow-editor-model", - "version": "0.12.1", + "version": "0.13.0", "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 49bf3e8..211df5d 100644 --- a/model/src/builders/branched-step-model-builder.ts +++ b/model/src/builders/branched-step-model-builder.ts @@ -9,7 +9,7 @@ const branchesPath = Path.create('branches'); export class BranchedStepModelBuilder extends StepModelBuilder { private readonly branchesBuilder = new PropertyModelBuilder(branchesPath, this.circularDependencyDetector); - public branches(): PropertyModelBuilder { + public branches(): PropertyModelBuilder { return this.branchesBuilder; } 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 e67a6b7..4279a49 100644 --- a/model/src/value-models/generated-string/generated-string-context.ts +++ b/model/src/value-models/generated-string/generated-string-context.ts @@ -1,6 +1,6 @@ import { Properties } from 'sequential-workflow-model'; import { ValueContext } from '../../context'; -import { GeneratedStringVariableValueModel } from './generated-string-model'; +import { GeneratedStringVariableValueModel } from './generated-string-value-model'; import { DefaultValueContext } from '../../context/default-value-context'; export class GeneratedStringContext { diff --git a/model/src/value-models/generated-string/generated-string-model.ts b/model/src/value-models/generated-string/generated-string-value-model.ts similarity index 100% rename from model/src/value-models/generated-string/generated-string-model.ts rename to model/src/value-models/generated-string/generated-string-value-model.ts diff --git a/model/src/value-models/generated-string/index.ts b/model/src/value-models/generated-string/index.ts index 32de4a0..aae1099 100644 --- a/model/src/value-models/generated-string/index.ts +++ b/model/src/value-models/generated-string/index.ts @@ -1,2 +1,2 @@ export * from './generated-string-context'; -export * from './generated-string-model'; +export * from './generated-string-value-model';