From a60cdc22a951363c590330257438bd5013c9c65b Mon Sep 17 00:00:00 2001 From: b4rtaz Date: Wed, 14 Jun 2023 20:05:36 +0200 Subject: [PATCH] 0.3.2. --- CHANGELOG.md | 5 ++ demos/webpack-app/package.json | 4 +- demos/webpack-app/src/playground/app.ts | 9 +--- .../playground/model/calculate-step-model.ts | 3 ++ .../model/convert-value-step-model.ts | 3 ++ .../src/playground/model/if-step-model.ts | 10 ++-- .../src/playground/model/loop-step-model.ts | 3 ++ .../src/playground/model/root-model.ts | 5 +- .../model/set-string-value-step-model.ts | 2 + editor/css/editor.css | 50 ++++++++++++++++++- editor/package.json | 6 +-- editor/src/components/input-component.spec.ts | 28 +++++++++++ editor/src/components/input-component.ts | 33 ++++++++++++ editor/src/core/append-multiline-text.spec.ts | 21 ++++++++ editor/src/core/append-multiline-text.ts | 10 ++++ editor/src/core/icons.ts | 16 ++++++ editor/src/editor-header.ts | 27 ++++++++++ editor/src/editor-provider-configuration.ts | 1 + editor/src/editor-provider.ts | 43 +++++++++++++--- editor/src/editor.ts | 9 +++- editor/src/external-types.ts | 5 ++ .../{ => property-editor}/property-editor.ts | 25 ++++++++-- editor/src/property-editor/property-hint.ts | 32 ++++++++++++ .../variable-definition-value-editor.ts | 17 +++---- .../number/number-value-editor.ts | 17 +++---- .../string/string-value-editor.ts | 16 +++--- .../variable-definition-item-component.ts | 10 ++-- model/package.json | 2 +- model/src/builders/property-model-builder.ts | 34 +++++++++++++ model/src/builders/step-model-builder.ts | 33 ++++++++++++ model/src/model.ts | 3 ++ .../branches/branches-value-model.ts | 2 +- 32 files changed, 416 insertions(+), 68 deletions(-) create mode 100644 editor/src/components/input-component.spec.ts create mode 100644 editor/src/components/input-component.ts create mode 100644 editor/src/core/append-multiline-text.spec.ts create mode 100644 editor/src/core/append-multiline-text.ts create mode 100644 editor/src/core/icons.ts create mode 100644 editor/src/editor-header.ts rename editor/src/{ => property-editor}/property-editor.ts (72%) create mode 100644 editor/src/property-editor/property-hint.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f71216..0e6ab60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.3.2 + +* The `StepModel` interface has two new properties: `category` and `description`. The category is used to group steps in the toolbox. The description is used to display an additional information about a step in the editor. +* The `PropertyModel` interface has one new property: `hint`. The hint is used to display an additional information about a property in the editor. + ## 0.3.1 Added new value model: boolean (`booleanValueModel({ ... })`). diff --git a/demos/webpack-app/package.json b/demos/webpack-app/package.json index 2063805..e2b2059 100644 --- a/demos/webpack-app/package.json +++ b/demos/webpack-app/package.json @@ -18,8 +18,8 @@ "sequential-workflow-model": "^0.1.3", "sequential-workflow-designer": "^0.13.2", "sequential-workflow-machine": "^0.2.0", - "sequential-workflow-editor-model": "^0.3.1", - "sequential-workflow-editor": "^0.3.1" + "sequential-workflow-editor-model": "^0.3.2", + "sequential-workflow-editor": "^0.3.2" }, "devDependencies": { "ts-loader": "^9.4.2", diff --git a/demos/webpack-app/src/playground/app.ts b/demos/webpack-app/src/playground/app.ts index 5e1ecd0..d14e05d 100644 --- a/demos/webpack-app/src/playground/app.ts +++ b/demos/webpack-app/src/playground/app.ts @@ -3,7 +3,7 @@ import { editorProvider } from './editor-provider'; import { AppState, AppStorage } from './storage'; import { Playground } from './playground'; import { executeMachine } from './machine/machine-executor'; -import { MyDefinition, definitionModel } from './model/definition-model'; +import { MyDefinition } from './model/definition-model'; import { defaultAppState } from './default-state'; import 'sequential-workflow-designer/css/designer.css'; @@ -33,12 +33,7 @@ export class App { } }, toolbox: { - groups: [ - { - name: 'Steps', - steps: Object.keys(definitionModel.steps).map(stepType => editorProvider.activateStep(stepType)) - } - ] + groups: editorProvider.getToolboxGroups() }, undoStackSize: 10, definitionWalker diff --git a/demos/webpack-app/src/playground/model/calculate-step-model.ts b/demos/webpack-app/src/playground/model/calculate-step-model.ts index 3f12961..addde45 100644 --- a/demos/webpack-app/src/playground/model/calculate-step-model.ts +++ b/demos/webpack-app/src/playground/model/calculate-step-model.ts @@ -22,6 +22,9 @@ export interface CalculateStep extends Step { } export const calculateStepModel = createStepModel('calculate', 'task', step => { + step.category('Values'); + step.description('Calculate value from two numbers. Result is stored in variable.'); + const val = dynamicValueModel({ models: [ numberValueModel({}), diff --git a/demos/webpack-app/src/playground/model/convert-value-step-model.ts b/demos/webpack-app/src/playground/model/convert-value-step-model.ts index a956fc3..374b06c 100644 --- a/demos/webpack-app/src/playground/model/convert-value-step-model.ts +++ b/demos/webpack-app/src/playground/model/convert-value-step-model.ts @@ -11,6 +11,9 @@ export interface ConvertValueStep extends Step { } export const convertValueStepModel = createStepModel('convertValue', 'task', step => { + step.category('Values'); + step.description('Convert value from one variable to another.'); + step.property('source') .value( nullableAnyVariableValueModel({ diff --git a/demos/webpack-app/src/playground/model/if-step-model.ts b/demos/webpack-app/src/playground/model/if-step-model.ts index cf13592..efd4d76 100644 --- a/demos/webpack-app/src/playground/model/if-step-model.ts +++ b/demos/webpack-app/src/playground/model/if-step-model.ts @@ -23,6 +23,9 @@ export interface IfStep extends BranchedStep { } export const ifStepModel = createBranchedStepModel('if', 'switch', step => { + step.category('Logic'); + step.description('Check condition and execute different branches.'); + const ab = dynamicValueModel({ models: [ numberValueModel({}), @@ -34,7 +37,7 @@ export const ifStepModel = createBranchedStepModel('if', 'switch', step ] }); - step.property('a').value(ab).label('A'); + step.property('a').value(ab).label('A').hint('Left side of comparison.'); step.property('operator') .label('Operator') @@ -42,9 +45,10 @@ export const ifStepModel = createBranchedStepModel('if', 'switch', step choiceValueModel({ choices: ['==', '===', '!=', '!==', '>', '>=', '<', '<='] }) - ); + ) + .hint('Comparison operator.\nStep supports strict and non-strict operators.'); - step.property('b').value(ab).label('B'); + step.property('b').value(ab).label('B').hint('Right side of comparison.'); step.branches().value( branchesValueModel({ diff --git a/demos/webpack-app/src/playground/model/loop-step-model.ts b/demos/webpack-app/src/playground/model/loop-step-model.ts index 4b7daf4..97b225d 100644 --- a/demos/webpack-app/src/playground/model/loop-step-model.ts +++ b/demos/webpack-app/src/playground/model/loop-step-model.ts @@ -28,6 +28,9 @@ export interface LoopStep extends SequentialStep { } export const loopStepModel = createSequentialStepModel('loop', 'container', step => { + step.category('Logic'); + step.description('Loop over a range of numbers.'); + step.property('from') .label('From') .value( diff --git a/demos/webpack-app/src/playground/model/root-model.ts b/demos/webpack-app/src/playground/model/root-model.ts index 2b447ba..8fae756 100644 --- a/demos/webpack-app/src/playground/model/root-model.ts +++ b/demos/webpack-app/src/playground/model/root-model.ts @@ -3,6 +3,7 @@ import { MyDefinition } from './definition-model'; export const rootModel = createRootModel(root => { root.property('inputs') + .hint('Variables passed to the workflow from the outside.') .value(variableDefinitionsValueModel({})) .dependentProperty('outputs') .customValidator({ @@ -11,7 +12,9 @@ export const rootModel = createRootModel(root => { return inputs.variables.length > 0 ? null : 'At least one input is required'; } }); - root.property('outputs').value(variableDefinitionsValueModel({})).label('Outputs'); + + root.property('outputs').hint('Variables returned from the workflow.').value(variableDefinitionsValueModel({})).label('Outputs'); + root.sequence().value( sequenceValueModel({ sequence: [] diff --git a/demos/webpack-app/src/playground/model/set-string-value-step-model.ts b/demos/webpack-app/src/playground/model/set-string-value-step-model.ts index d70e224..30fe161 100644 --- a/demos/webpack-app/src/playground/model/set-string-value-step-model.ts +++ b/demos/webpack-app/src/playground/model/set-string-value-step-model.ts @@ -19,6 +19,8 @@ export interface SetStringValueStep extends Step { } export const setStringValueStepModel = createStepModel('setStringValue', 'task', step => { + step.category('Values'); + step.property('variable') .value( nullableVariableValueModel({ diff --git a/editor/css/editor.css b/editor/css/editor.css index 50dd6f0..6802f34 100644 --- a/editor/css/editor.css +++ b/editor/css/editor.css @@ -8,7 +8,23 @@ .swe-editor { margin: 10px 0 0; font-size: 13px; - line-height: 1.3em; + line-height: 1.3rem; +} +.swe-editor-header { + padding: 0 10px 5px; +} +.swe-editor-header-title { + margin: 0; + padding: 5px 0 10px; + font-size: 1.4rem; + line-height: 1.3rem; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} +.swe-editor-header-description { + margin: 0 0 10px; + color: #666; } /* properties */ @@ -24,14 +40,44 @@ } .swe-property-header-label { display: block; - flex: 1; padding: 0; margin: 0; font-size: 1.05rem; + line-height: 1.3rem; +} +.swe-property-header-hint-toggle { + display: block; + width: 18px; + height: 18px; + margin: 0 8px; + background: #ddd; + border-radius: 50% 50%; + text-align: center; + font-size: 11px; + cursor: pointer; +} +.swe-property-header-hint-toggle:hover { + background: #eee; +} +.swe-property-header-hint-toggle-icon { + width: 70%; + height: 70%; + margin: 15%; +} +.swe-property-header-hint-toggle-icon path { + fill: #777; } .swe-property-header-control { + flex: 1; text-align: right; } +.swe-property-hint-text { + margin: 0 0 10px; + padding: 6px 10px; + border-radius: 5px; + background: #eee; + border: 1px solid #ddd; +} .swe-validation-error-text { margin: 0 0 10px; padding: 6px 10px; diff --git a/editor/package.json b/editor/package.json index a76df69..e4fbf55 100644 --- a/editor/package.json +++ b/editor/package.json @@ -1,6 +1,6 @@ { "name": "sequential-workflow-editor", - "version": "0.3.1", + "version": "0.3.2", "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.3.1", + "sequential-workflow-editor-model": "^0.3.2", "sequential-workflow-model": "^0.1.3" }, "peerDependencies": { - "sequential-workflow-editor-model": "^0.3.1", + "sequential-workflow-editor-model": "^0.3.2", "sequential-workflow-model": "^0.1.3" }, "devDependencies": { diff --git a/editor/src/components/input-component.spec.ts b/editor/src/components/input-component.spec.ts new file mode 100644 index 0000000..fd33635 --- /dev/null +++ b/editor/src/components/input-component.spec.ts @@ -0,0 +1,28 @@ +import { inputComponent } from './input-component'; + +describe('InputComponent', () => { + it('triggers onChanged event when new character is added to field', () => { + let count = 0; + + const input = inputComponent('Foo'); + input.onChanged.subscribe(value => { + expect(value).toBe('FooB'); + count++; + }); + + (input.view as HTMLInputElement).value = 'FooB'; + input.view.dispatchEvent(new Event('input')); + + expect(count).toBe(1); + }); + + it('renders input[type=text] by default', () => { + const input = inputComponent('x'); + expect(input.view.getAttribute('type')).toBe('text'); + }); + + it('renders input[type=number] when configuration is set', () => { + const input = inputComponent('x', { type: 'number' }); + expect(input.view.getAttribute('type')).toBe('number'); + }); +}); diff --git a/editor/src/components/input-component.ts b/editor/src/components/input-component.ts new file mode 100644 index 0000000..ff9942c --- /dev/null +++ b/editor/src/components/input-component.ts @@ -0,0 +1,33 @@ +import { SimpleEvent } from 'sequential-workflow-editor-model'; +import { Component } from './component'; +import { Html } from '../core'; + +export interface InputComponent extends Component { + onChanged: SimpleEvent; +} + +export interface InputConfiguration { + type?: 'text' | 'number'; + placeholder?: string; +} + +export function inputComponent(value: string, configuration?: InputConfiguration): InputComponent { + const onChanged = new SimpleEvent(); + + const view = Html.element('input', { + class: 'swe-input swe-stretched', + type: configuration?.type ?? 'text' + }); + if (configuration?.placeholder) { + view.setAttribute('placeholder', configuration.placeholder); + } + view.value = value; + view.addEventListener('input', () => { + onChanged.forward(view.value); + }); + + return { + view, + onChanged + }; +} diff --git a/editor/src/core/append-multiline-text.spec.ts b/editor/src/core/append-multiline-text.spec.ts new file mode 100644 index 0000000..4a76d59 --- /dev/null +++ b/editor/src/core/append-multiline-text.spec.ts @@ -0,0 +1,21 @@ +import { appendMultilineText } from './append-multiline-text'; + +describe('appendMultilineText()', () => { + let parent: HTMLElement; + + beforeEach(() => { + parent = document.createElement('div'); + }); + + it('appends correctly if passed text with \\n', () => { + appendMultilineText(parent, 'Hello\nWorld\nNow'); + + expect(parent.innerHTML).toBe('Hello
World
Now'); + }); + + it('appends correctly if passed text with \\r\\n', () => { + appendMultilineText(parent, 'Hello\r\nWorld\r\nToday'); + + expect(parent.innerHTML).toBe('Hello
World
Today'); + }); +}); diff --git a/editor/src/core/append-multiline-text.ts b/editor/src/core/append-multiline-text.ts new file mode 100644 index 0000000..750ce9c --- /dev/null +++ b/editor/src/core/append-multiline-text.ts @@ -0,0 +1,10 @@ +export function appendMultilineText(target: HTMLElement, text: string) { + const lines = text.split(/\r?\n/g); + for (let i = 0; i < lines.length; i++) { + if (i > 0) { + target.appendChild(document.createElement('br')); + } + const line = document.createTextNode(lines[i]); + target.appendChild(line); + } +} diff --git a/editor/src/core/icons.ts b/editor/src/core/icons.ts new file mode 100644 index 0000000..ba6777c --- /dev/null +++ b/editor/src/core/icons.ts @@ -0,0 +1,16 @@ +const ns = 'http://www.w3.org/2000/svg'; + +export class Icons { + public static help = + 'M431-330q1-72 16.5-105t58.5-72q42-38 64.5-70.5T593-647q0-45-30-75t-84-30q-52 0-80 29.5T358-661l-84-37q22-59 74.5-100.5T479-840q100 0 154 55.5T687-651q0 48-20.5 87T601-482q-49 47-59 72t-11 80H431Zm48 250q-29 0-49.5-20.5T409-150q0-29 20.5-49.5T479-220q29 0 49.5 20.5T549-150q0 29-20.5 49.5T479-80Z'; + + public static createSvg(icon: string, cls: string): SVGElement { + const svg = document.createElementNS(ns, 'svg'); + svg.setAttribute('viewBox', '0 -960 960 960'); + svg.classList.add(cls); + const path = document.createElementNS(ns, 'path'); + path.setAttribute('d', icon); + svg.appendChild(path); + return svg; + } +} diff --git a/editor/src/editor-header.ts b/editor/src/editor-header.ts new file mode 100644 index 0000000..1e46915 --- /dev/null +++ b/editor/src/editor-header.ts @@ -0,0 +1,27 @@ +import { Component } from './components/component'; +import { Html } from './core'; +import { appendMultilineText } from './core/append-multiline-text'; + +export interface EditorHeaderData { + title: string; + description?: string; +} + +export class EditorHeader implements Component { + public static create(data: EditorHeaderData): EditorHeader { + const view = Html.element('div', { class: 'swe-editor-header' }); + + const title = Html.element('h3', { class: 'swe-editor-header-title' }); + title.textContent = data.title; + view.appendChild(title); + + if (data.description) { + const description = Html.element('p', { class: 'swe-editor-header-description' }); + appendMultilineText(description, data.description); + view.appendChild(description); + } + return new EditorHeader(view); + } + + private constructor(public readonly view: HTMLElement) {} +} diff --git a/editor/src/editor-provider-configuration.ts b/editor/src/editor-provider-configuration.ts index 634498d..ef48ee5 100644 --- a/editor/src/editor-provider-configuration.ts +++ b/editor/src/editor-provider-configuration.ts @@ -4,4 +4,5 @@ import { DefinitionWalker } from 'sequential-workflow-model'; export interface EditorProviderConfiguration { uidGenerator: UidGenerator; definitionWalker?: DefinitionWalker; + isHeaderHidden?: boolean; } diff --git a/editor/src/editor-provider.ts b/editor/src/editor-provider.ts index a73dfa0..465d918 100644 --- a/editor/src/editor-provider.ts +++ b/editor/src/editor-provider.ts @@ -8,9 +8,11 @@ import { RootValidator, StepEditorContext, StepEditorProvider, - StepValidator + StepValidator, + ToolboxGroup } from './external-types'; import { EditorProviderConfiguration } from './editor-provider-configuration'; +import { EditorHeaderData } from './editor-header'; export class EditorProvider { public static create( @@ -20,7 +22,7 @@ export class EditorProvider { const definitionWalker = configuration.definitionWalker ?? new DefinitionWalker(); const activator = ModelActivator.create(definitionModel, configuration.uidGenerator); const validator = ModelValidator.create(definitionModel, definitionWalker); - return new EditorProvider(activator, validator, definitionModel, definitionWalker); + return new EditorProvider(activator, validator, definitionModel, definitionWalker, configuration); } private readonly services: EditorServices = { @@ -32,16 +34,16 @@ export class EditorProvider { private readonly activator: ModelActivator, private readonly validator: ModelValidator, private readonly definitionModel: DefinitionModel, - private readonly definitionWalker: DefinitionWalker + private readonly definitionWalker: DefinitionWalker, + private readonly configuration: EditorProviderConfiguration ) {} public createRootEditorProvider(): RootEditorProvider { return (definition: Definition, context: GlobalEditorContext): HTMLElement => { const rootContext = DefinitionContext.createForRoot(definition, this.definitionModel, this.definitionWalker); const typeClassName = 'root'; - const editor = Editor.create(this.definitionModel.root.properties, rootContext, this.services, typeClassName); - editor.onValueChanged.subscribe((path: Path) => { - console.log('valueChanged', path.toString()); + const editor = Editor.create(null, this.definitionModel.root.properties, rootContext, this.services, typeClassName); + editor.onValueChanged.subscribe(() => { context.notifyPropertiesChanged(); }); return editor.root; @@ -58,7 +60,16 @@ export class EditorProvider { ); const stepModel = this.definitionModel.steps[step.type]; const typeClassName = stepModel.type; - const editor = Editor.create([stepModel.name, ...stepModel.properties], definitionContext, this.services, typeClassName); + const propertyModels = [stepModel.name, ...stepModel.properties]; + + const headerData: EditorHeaderData | null = this.configuration.isHeaderHidden + ? null + : { + title: stepModel.name.value.getDefaultValue(this.activator), + description: stepModel.description + }; + + const editor = Editor.create(headerData, propertyModels, definitionContext, this.services, typeClassName); editor.onValueChanged.subscribe((path: Path) => { if (path.equals(stepModel.name.value.path)) { @@ -90,4 +101,22 @@ export class EditorProvider { public activateStep(type: string): Step { return this.activator.activateStep(type); } + + public getToolboxGroups(): ToolboxGroup[] { + const stepModels = Object.values(this.definitionModel.steps); + const groups: ToolboxGroup[] = []; + const categories = new Set(stepModels.map(step => step.category)); + categories.forEach((category: string | undefined) => { + const name = category ?? '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)); + return groups; + } } diff --git a/editor/src/editor.ts b/editor/src/editor.ts index 7102ed4..5b843c7 100644 --- a/editor/src/editor.ts +++ b/editor/src/editor.ts @@ -1,9 +1,11 @@ import { DefinitionContext, Path, PropertyModel, PropertyModels, SimpleEvent } from 'sequential-workflow-editor-model'; -import { PropertyEditor } from './property-editor'; +import { PropertyEditor } from './property-editor/property-editor'; import { EditorServices, ValueEditorEditorFactoryResolver } from './value-editors'; +import { EditorHeader, EditorHeaderData } from './editor-header'; export class Editor { public static create( + headerData: EditorHeaderData | null, propertyModels: PropertyModels, definitionContext: DefinitionContext, editorServices: EditorServices, @@ -12,6 +14,11 @@ export class Editor { const root = document.createElement('div'); root.className = `swe-editor swe-type-${typeClassName}`; + if (headerData) { + const header = EditorHeader.create(headerData); + root.appendChild(header.view); + } + const editors = new Map(); for (const propertyModel of propertyModels) { if (ValueEditorEditorFactoryResolver.isHidden(propertyModel.value.id)) { diff --git a/editor/src/external-types.ts b/editor/src/external-types.ts index 3e579ac..1ad7e97 100644 --- a/editor/src/external-types.ts +++ b/editor/src/external-types.ts @@ -14,3 +14,8 @@ export type StepEditorProvider = (step: Step, context: StepEditorContext) => HTM 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.ts b/editor/src/property-editor/property-editor.ts similarity index 72% rename from editor/src/property-editor.ts rename to editor/src/property-editor/property-editor.ts index 6372f91..09c6f3b 100644 --- a/editor/src/property-editor.ts +++ b/editor/src/property-editor/property-editor.ts @@ -6,10 +6,12 @@ import { SimpleEvent, ValueModelContext } from 'sequential-workflow-editor-model'; -import { EditorServices, ValueEditor } from './value-editors'; -import { Html } from './core/html'; -import { Component } from './components/component'; -import { PropertyValidationErrorComponent, propertyValidationErrorComponent } from './components/property-validation-error-component'; +import { EditorServices, ValueEditor } from '../value-editors'; +import { Html } from '../core/html'; +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'; export class PropertyEditor implements Component { public static create( @@ -20,6 +22,7 @@ export class PropertyEditor implements Component { const valueContext = ValueModelContext.create(propertyModel.value, definitionContext); const valueEditorFactory = editorServices.valueEditorFactoryResolver(propertyModel.value.id); const valueEditor = valueEditorFactory(valueContext, editorServices); + let hint: PropertyHintComponent | null = null; const nameClassName = propertyModel.name; const view = Html.element('div', { @@ -35,6 +38,20 @@ export class PropertyEditor implements Component { header.appendChild(label); view.appendChild(header); + + if (propertyModel.hint) { + const toggle = Html.element('span', { + class: 'swe-property-header-hint-toggle' + }); + const toggleIcon = Icons.createSvg(Icons.help, 'swe-property-header-hint-toggle-icon'); + toggle.appendChild(toggleIcon); + toggle.addEventListener('click', () => hint?.toggle(), false); + header.appendChild(toggle); + + hint = propertyHint(propertyModel.hint); + view.appendChild(hint.view); + } + view.appendChild(valueEditor.view); if (valueEditor.controlView) { diff --git a/editor/src/property-editor/property-hint.ts b/editor/src/property-editor/property-hint.ts new file mode 100644 index 0000000..ca3979c --- /dev/null +++ b/editor/src/property-editor/property-hint.ts @@ -0,0 +1,32 @@ +import { Component } from '../components/component'; +import { Html } from '../core'; +import { appendMultilineText } from '../core/append-multiline-text'; + +export interface PropertyHintComponent extends Component { + toggle(): void; +} + +export function propertyHint(text: string): PropertyHintComponent { + let content: HTMLElement | null = null; + const view = Html.element('div', { + class: 'swe-property-hint' + }); + + function toggle() { + if (content) { + view.removeChild(content); + content = null; + } else { + content = Html.element('div', { + class: 'swe-property-hint-text' + }); + appendMultilineText(content, text); + view.appendChild(content); + } + } + + return { + view, + toggle + }; +} diff --git a/editor/src/value-editors/nullable-variable-definition/variable-definition-value-editor.ts b/editor/src/value-editors/nullable-variable-definition/variable-definition-value-editor.ts index 254dbf6..b46d05a 100644 --- a/editor/src/value-editors/nullable-variable-definition/variable-definition-value-editor.ts +++ b/editor/src/value-editors/nullable-variable-definition/variable-definition-value-editor.ts @@ -2,8 +2,8 @@ import { NullableVariableDefinitionValueModel, ValueModelContext } from 'sequent import { valueEditorContainerComponent } from '../../components/value-editor-container-component'; import { ValueEditor } from '../value-editor'; import { validationErrorComponent } from '../../components/validation-error-component'; -import { Html } from '../../core/html'; import { rowComponent } from '../../components/row-component'; +import { inputComponent } from '../../components/input-component'; export const nullableVariableDefinitionValueEditorId = 'nullableVariableDefinition'; @@ -14,22 +14,17 @@ export function nullableVariableDefinitionValueEditor( validation.setDefaultError(context.validate()); } - const input = Html.element('input', { - class: 'swe-input swe-stretched', - type: 'text' - }); - input.value = context.getValue()?.name || ''; - - const row = rowComponent([input]); - - input.addEventListener('input', () => { + const startValue = context.getValue()?.name || ''; + const input = inputComponent(startValue); + input.onChanged.subscribe(value => { context.setValue({ - name: input.value, + name: value, type: context.model.configuration.valueType }); validate(); }); + const row = rowComponent([input.view]); const validation = validationErrorComponent(); const container = valueEditorContainerComponent([row.view, validation.view]); diff --git a/editor/src/value-editors/number/number-value-editor.ts b/editor/src/value-editors/number/number-value-editor.ts index ed35749..2ec2675 100644 --- a/editor/src/value-editors/number/number-value-editor.ts +++ b/editor/src/value-editors/number/number-value-editor.ts @@ -2,8 +2,8 @@ import { NumberValueModel, ValueModelContext } from 'sequential-workflow-editor- import { ValueEditor } from '../value-editor'; import { valueEditorContainerComponent } from '../../components/value-editor-container-component'; import { validationErrorComponent } from '../../components/validation-error-component'; -import { Html } from '../../core/html'; import { rowComponent } from '../../components/row-component'; +import { inputComponent } from '../../components/input-component'; export const numberValueEditorId = 'number'; @@ -12,19 +12,18 @@ export function numberValueEditor(context: ValueModelContext): validation.setDefaultError(context.validate()); } - const input = Html.element('input', { - class: 'swe-input swe-stretched', + const startValue = String(context.getValue()); + const input = inputComponent(startValue, { type: 'number' }); - input.value = String(context.getValue()); - - const row = rowComponent([input]); - - input.addEventListener('input', () => { - context.setValue(parseInt(input.value, 10)); + input.onChanged.subscribe(value => { + const num = value.length > 0 ? Number(value) : NaN; + context.setValue(num); validate(); }); + const row = rowComponent([input.view]); + const validation = validationErrorComponent(); const container = valueEditorContainerComponent([row.view, validation.view]); diff --git a/editor/src/value-editors/string/string-value-editor.ts b/editor/src/value-editors/string/string-value-editor.ts index 209b319..4671d4b 100644 --- a/editor/src/value-editors/string/string-value-editor.ts +++ b/editor/src/value-editors/string/string-value-editor.ts @@ -2,8 +2,8 @@ import { StringValueModel, ValueModelContext } from 'sequential-workflow-editor- import { ValueEditor } from '../value-editor'; import { validationErrorComponent } from '../../components/validation-error-component'; import { valueEditorContainerComponent } from '../../components/value-editor-container-component'; -import { Html } from '../../core/html'; import { rowComponent } from '../../components/row-component'; +import { inputComponent } from '../../components/input-component'; export const stringValueEditorId = 'string'; @@ -12,18 +12,14 @@ export function stringValueEditor(context: ValueModelContext): validation.setDefaultError(context.validate()); } - const input = Html.element('input', { - class: 'swe-input swe-stretched', - type: 'text' - }); - input.value = context.getValue(); - - input.addEventListener('input', () => { - context.setValue(input.value); + const startValue = context.getValue(); + const input = inputComponent(startValue); + input.onChanged.subscribe(value => { + context.setValue(value); validate(); }); - const row = rowComponent([input]); + const row = rowComponent([input.view]); const validation = validationErrorComponent(); const container = valueEditorContainerComponent([row.view, validation.view]); diff --git a/editor/src/value-editors/variable-definitions/variable-definition-item-component.ts b/editor/src/value-editors/variable-definitions/variable-definition-item-component.ts index 2c43e4e..854deba 100644 --- a/editor/src/value-editors/variable-definitions/variable-definition-item-component.ts +++ b/editor/src/value-editors/variable-definitions/variable-definition-item-component.ts @@ -6,6 +6,7 @@ import { rowComponent } from '../../components/row-component'; import { buttonComponent } from '../../components/button-component'; import { selectComponent } from '../../components/select-component'; import { filterValueTypes } from '../../core/filter-value-types'; +import { inputComponent } from '../../components/input-component'; export interface VariableDefinitionItemComponent extends Component { onNameChanged: SimpleEvent; @@ -30,12 +31,9 @@ export function variableDefinitionItemComponent( class: 'swe-variable-definition-item' }); - const input = Html.element('input', { - class: 'swe-input swe-stretched', - type: 'text', + const input = inputComponent(variable.name, { placeholder: 'Variable name' }); - input.value = variable.name; const valueTypes = filterValueTypes(context.getValueTypes(), context.model.configuration.valueTypes); @@ -51,9 +49,9 @@ export function variableDefinitionItemComponent( const validation = validationErrorComponent(); - input.addEventListener('input', () => onNameChanged.forward(input.value), false); + input.onChanged.subscribe(value => onNameChanged.forward(value)); - const row = rowComponent([input, typeSelect.view, deleteButton.view], { + const row = rowComponent([input.view, typeSelect.view, deleteButton.view], { cols: [2, 1, null] }); view.appendChild(row.view); diff --git a/model/package.json b/model/package.json index f7c54e8..16808f0 100644 --- a/model/package.json +++ b/model/package.json @@ -1,6 +1,6 @@ { "name": "sequential-workflow-editor-model", - "version": "0.3.1", + "version": "0.3.2", "homepage": "https://nocode-js.com/", "author": { "name": "NoCode JS", diff --git a/model/src/builders/property-model-builder.ts b/model/src/builders/property-model-builder.ts index ec2f1b5..c9cac2e 100644 --- a/model/src/builders/property-model-builder.ts +++ b/model/src/builders/property-model-builder.ts @@ -8,15 +8,24 @@ import { buildLabel } from '../core/label-builder'; export class PropertyModelBuilder { private _value?: ValueModelFactoryOfValue; private _label?: string; + private _hint?: string; private _dependencies: Path[] = []; private _customValidator?: CustomValidator; public constructor(private readonly path: Path, private readonly circularDependencyDetector: CircularDependencyDetector) {} + /** + * @returns `true` if the model of the value is set, otherwise `false`. + */ public hasValue(): boolean { return !!this._value; } + /** + * Sets the model of the value. + * @param valueModelFactory The factory function that creates the model of the value. + * @example `builder.value(stringValueModel({ defaultValue: 'Some value' }));` + */ public value(valueModelFactory: ValueModelFactoryOfValue): this { if (this._value) { throw new Error(`Model is already set for ${this.path.toString()}`); @@ -25,11 +34,30 @@ export class PropertyModelBuilder): this { if (this._customValidator) { throw new Error('Custom validator is already set'); @@ -57,6 +90,7 @@ export class PropertyModelBuilder { protected readonly circularDependencyDetector = new CircularDependencyDetector(); + private _description?: string; + private _category?: string; private readonly nameBuilder = new PropertyModelBuilder(namePath, this.circularDependencyDetector); private readonly propertyBuilder: PropertyModelBuilder[] = []; @@ -22,10 +24,39 @@ export class StepModelBuilder { } } + /** + * Sets the category of the step. This field is used in the toolbox to group steps. + * @param category The category of the step. + * @example `builder.category('Utilities');` + */ + public category(category: string): this { + this._category = category; + return this; + } + + /** + * Sets the description of the step. + * @param description The description of the step. + * @example `builder.description('This step does something useful.');` + */ + public description(description: string): this { + this._description = description; + return this; + } + + /** + * @returns The builder for the `name` property. + * @example `builder.name().value(stringValueModel({ defaultValue: 'Some name' })).label('Name');` + */ public name(): PropertyModelBuilder { return this.nameBuilder; } + /** + * @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');` + */ public property( propertyName: Key ): PropertyModelBuilder { @@ -47,6 +78,8 @@ export class StepModelBuilder { return { type: this.type, componentType: this.componentType, + category: this._category, + description: this._description, name: this.nameBuilder.build(), properties: this.propertyBuilder.map(builder => builder.build()) }; diff --git a/model/src/model.ts b/model/src/model.ts index 34f53ed..8c8da5e 100644 --- a/model/src/model.ts +++ b/model/src/model.ts @@ -22,6 +22,8 @@ export type StepModels = Record; export interface StepModel { type: string; componentType: string; + category?: string; + description?: string; name: PropertyModel; properties: PropertyModels; } @@ -31,6 +33,7 @@ export type PropertyModels = PropertyModel[]; export interface PropertyModel { name: string; label: string; + hint?: string; dependencies: Path[]; customValidator?: CustomValidator; value: ValueModel; diff --git a/model/src/value-models/branches/branches-value-model.ts b/model/src/value-models/branches/branches-value-model.ts index 23617c5..cf3d7fb 100644 --- a/model/src/value-models/branches/branches-value-model.ts +++ b/model/src/value-models/branches/branches-value-model.ts @@ -14,7 +14,7 @@ export type BranchesValueModel = ValueModel = Record; -export const branchesValueModelId = 'workflow.branches'; +export const branchesValueModelId = 'branches'; export function branchesValueModel( configuration: TConfiguration