From a949fa2c147ceac9dadd1285fc0429312550dffa Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 4 Jan 2022 18:58:19 +0100 Subject: [PATCH] Implement multi-text input control --- .../src/components.d.ts | 61 +++++++++++- .../src/components/inputs/multi-line.tsx | 1 - .../src/components/inputs/multi-text.tsx | 27 +++++ .../shared/input-tags/input-tags-dropdown.tsx | 99 +++++++++++++++++++ .../shared/input-tags/input-tags.css | 6 ++ .../shared/input-tags/input-tags.tsx | 81 +++++++++++++++ .../src/services/input-control-registry.tsx | 1 + .../Activities/WriteLines.cs | 3 + 8 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 src/designer/elsa-workflows-designer/src/components/inputs/multi-text.tsx create mode 100644 src/designer/elsa-workflows-designer/src/components/shared/input-tags/input-tags-dropdown.tsx create mode 100644 src/designer/elsa-workflows-designer/src/components/shared/input-tags/input-tags.css create mode 100644 src/designer/elsa-workflows-designer/src/components/shared/input-tags/input-tags.tsx diff --git a/src/designer/elsa-workflows-designer/src/components.d.ts b/src/designer/elsa-workflows-designer/src/components.d.ts index 30a7cb82d..f2c5c6114 100644 --- a/src/designer/elsa-workflows-designer/src/components.d.ts +++ b/src/designer/elsa-workflows-designer/src/components.d.ts @@ -5,7 +5,7 @@ * It contains typing information for all components that exist in this project. */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; -import { ActionDefinition, ActionInvokedArgs, Activity, ActivityDescriptor, ActivitySelectedArgs, ContainerSelectedArgs, GraphUpdatedArgs, TabChangedArgs, TabDefinition, Trigger, TriggerDescriptor, Workflow, WorkflowInstance, WorkflowInstanceSummary, WorkflowSummary } from "./models"; +import { ActionDefinition, ActionInvokedArgs, Activity, ActivityDescriptor, ActivitySelectedArgs, ContainerSelectedArgs, GraphUpdatedArgs, SelectListItem, TabChangedArgs, TabDefinition, Trigger, TriggerDescriptor, Workflow, WorkflowInstance, WorkflowInstanceSummary, WorkflowSummary } from "./models"; import { ActivityUpdatedArgs, DeleteActivityRequestedArgs } from "./components/designer/activity-properties-editor/activity-properties-editor"; import { AddActivityArgs } from "./components/designer/canvas/canvas"; import { NodeInputContext } from "./services/node-input-driver"; @@ -65,6 +65,18 @@ export namespace Components { "selectedTabIndex"?: number; "tabs": Array; } + interface ElsaInputTags { + "fieldId"?: string; + "placeHolder"?: string; + "values"?: Array; + } + interface ElsaInputTagsDropdown { + "dropdownValues"?: Array; + "fieldId"?: string; + "fieldName"?: string; + "placeHolder"?: string; + "values"?: Array; + } interface ElsaModalDialog { "actions": Array; "hide": (animate?: boolean) => Promise; @@ -74,6 +86,9 @@ export namespace Components { interface ElsaMultiLineInput { "inputContext": NodeInputContext; } + interface ElsaMultiTextInput { + "inputContext": NodeInputContext; + } interface ElsaPager { "page": number; "pageSize": number; @@ -203,6 +218,18 @@ declare global { prototype: HTMLElsaFormPanelElement; new (): HTMLElsaFormPanelElement; }; + interface HTMLElsaInputTagsElement extends Components.ElsaInputTags, HTMLStencilElement { + } + var HTMLElsaInputTagsElement: { + prototype: HTMLElsaInputTagsElement; + new (): HTMLElsaInputTagsElement; + }; + interface HTMLElsaInputTagsDropdownElement extends Components.ElsaInputTagsDropdown, HTMLStencilElement { + } + var HTMLElsaInputTagsDropdownElement: { + prototype: HTMLElsaInputTagsDropdownElement; + new (): HTMLElsaInputTagsDropdownElement; + }; interface HTMLElsaModalDialogElement extends Components.ElsaModalDialog, HTMLStencilElement { } var HTMLElsaModalDialogElement: { @@ -215,6 +242,12 @@ declare global { prototype: HTMLElsaMultiLineInputElement; new (): HTMLElsaMultiLineInputElement; }; + interface HTMLElsaMultiTextInputElement extends Components.ElsaMultiTextInput, HTMLStencilElement { + } + var HTMLElsaMultiTextInputElement: { + prototype: HTMLElsaMultiTextInputElement; + new (): HTMLElsaMultiTextInputElement; + }; interface HTMLElsaPagerElement extends Components.ElsaPager, HTMLStencilElement { } var HTMLElsaPagerElement: { @@ -332,8 +365,11 @@ declare global { "elsa-dropdown-input": HTMLElsaDropdownInputElement; "elsa-flowchart": HTMLElsaFlowchartElement; "elsa-form-panel": HTMLElsaFormPanelElement; + "elsa-input-tags": HTMLElsaInputTagsElement; + "elsa-input-tags-dropdown": HTMLElsaInputTagsDropdownElement; "elsa-modal-dialog": HTMLElsaModalDialogElement; "elsa-multi-line-input": HTMLElsaMultiLineInputElement; + "elsa-multi-text-input": HTMLElsaMultiTextInputElement; "elsa-pager": HTMLElsaPagerElement; "elsa-panel": HTMLElsaPanelElement; "elsa-radio-list-input": HTMLElsaRadioListInputElement; @@ -397,6 +433,20 @@ declare namespace LocalJSX { "selectedTabIndex"?: number; "tabs"?: Array; } + interface ElsaInputTags { + "fieldId"?: string; + "onValueChanged"?: (event: CustomEvent>) => void; + "placeHolder"?: string; + "values"?: Array; + } + interface ElsaInputTagsDropdown { + "dropdownValues"?: Array; + "fieldId"?: string; + "fieldName"?: string; + "onValueChanged"?: (event: CustomEvent>) => void; + "placeHolder"?: string; + "values"?: Array; + } interface ElsaModalDialog { "actions"?: Array; "onActionInvoked"?: (event: CustomEvent) => void; @@ -407,6 +457,9 @@ declare namespace LocalJSX { interface ElsaMultiLineInput { "inputContext"?: NodeInputContext; } + interface ElsaMultiTextInput { + "inputContext"?: NodeInputContext; + } interface ElsaPager { "onPaginated"?: (event: CustomEvent) => void; "page"?: number; @@ -498,8 +551,11 @@ declare namespace LocalJSX { "elsa-dropdown-input": ElsaDropdownInput; "elsa-flowchart": ElsaFlowchart; "elsa-form-panel": ElsaFormPanel; + "elsa-input-tags": ElsaInputTags; + "elsa-input-tags-dropdown": ElsaInputTagsDropdown; "elsa-modal-dialog": ElsaModalDialog; "elsa-multi-line-input": ElsaMultiLineInput; + "elsa-multi-text-input": ElsaMultiTextInput; "elsa-pager": ElsaPager; "elsa-panel": ElsaPanel; "elsa-radio-list-input": ElsaRadioListInput; @@ -532,8 +588,11 @@ declare module "@stencil/core" { "elsa-dropdown-input": LocalJSX.ElsaDropdownInput & JSXBase.HTMLAttributes; "elsa-flowchart": LocalJSX.ElsaFlowchart & JSXBase.HTMLAttributes; "elsa-form-panel": LocalJSX.ElsaFormPanel & JSXBase.HTMLAttributes; + "elsa-input-tags": LocalJSX.ElsaInputTags & JSXBase.HTMLAttributes; + "elsa-input-tags-dropdown": LocalJSX.ElsaInputTagsDropdown & JSXBase.HTMLAttributes; "elsa-modal-dialog": LocalJSX.ElsaModalDialog & JSXBase.HTMLAttributes; "elsa-multi-line-input": LocalJSX.ElsaMultiLineInput & JSXBase.HTMLAttributes; + "elsa-multi-text-input": LocalJSX.ElsaMultiTextInput & JSXBase.HTMLAttributes; "elsa-pager": LocalJSX.ElsaPager & JSXBase.HTMLAttributes; "elsa-panel": LocalJSX.ElsaPanel & JSXBase.HTMLAttributes; "elsa-radio-list-input": LocalJSX.ElsaRadioListInput & JSXBase.HTMLAttributes; diff --git a/src/designer/elsa-workflows-designer/src/components/inputs/multi-line.tsx b/src/designer/elsa-workflows-designer/src/components/inputs/multi-line.tsx index a85580970..fd1d1773c 100644 --- a/src/designer/elsa-workflows-designer/src/components/inputs/multi-line.tsx +++ b/src/designer/elsa-workflows-designer/src/components/inputs/multi-line.tsx @@ -23,7 +23,6 @@ export class MultiLineInput { } private getEditorHeight = (options: any) => { - debugger; const editorHeightName = options.editorHeight || 'Default'; switch (editorHeightName) { diff --git a/src/designer/elsa-workflows-designer/src/components/inputs/multi-text.tsx b/src/designer/elsa-workflows-designer/src/components/inputs/multi-text.tsx new file mode 100644 index 000000000..be9b2128b --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/inputs/multi-text.tsx @@ -0,0 +1,27 @@ +import {Component, Prop, h} from '@stencil/core'; +import {JsonExpression, LiteralExpression, SyntaxNames} from "../../models"; +import {NodeInputContext} from "../../services/node-input-driver"; +import {getInputPropertyValue, parseJson} from "../../utils"; + +@Component({ + tag: 'elsa-multi-text-input', + shadow: false +}) +export class MultiTextInput { + @Prop() inputContext: NodeInputContext; + + public render() { + const inputContext = this.inputContext; + const inputProperty = inputContext.inputDescriptor; + const fieldId = inputProperty.name; + const input = getInputPropertyValue(inputContext); + const json = (input?.expression as JsonExpression)?.value; + const values = parseJson(json); + return + } + + private onPropertyEditorChanged = (e: CustomEvent>) => { + const json = JSON.stringify(e.detail); + this.inputContext.inputChanged(json, SyntaxNames.Json); + }; +} diff --git a/src/designer/elsa-workflows-designer/src/components/shared/input-tags/input-tags-dropdown.tsx b/src/designer/elsa-workflows-designer/src/components/shared/input-tags/input-tags-dropdown.tsx new file mode 100644 index 000000000..90d5a273a --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/shared/input-tags/input-tags-dropdown.tsx @@ -0,0 +1,99 @@ +import {Component, h, Prop, Event, EventEmitter, State, Watch} from '@stencil/core'; +import {uniq} from 'lodash'; +import {SelectListItem} from '../../../models'; + +@Component({ + tag: 'elsa-input-tags-dropdown', + shadow: false, +}) +export class InputTagsDropdown { + + @Prop() fieldName?: string; + @Prop() fieldId?: string; + @Prop() placeHolder?: string = 'Add tag'; + @Prop() values?: Array = []; + @Prop() dropdownValues?: Array = []; + @Event({bubbles: true}) valueChanged: EventEmitter>; + @State() currentValues?: Array = []; + + @Watch('values') + private valuesChangedHandler(newValue: Array) { + this.updateCurrentValues(newValue); + } + + public componentWillLoad() { + this.updateCurrentValues(this.values); + } + + private updateCurrentValues = (newValue: Array) => { + const dropdownValues = this.dropdownValues || []; + let values: Array = []; + + if (!!newValue) { + newValue.forEach(value => { + const valueKey = typeof (value) == 'string' ? value as string : (value as SelectListItem).value; + const tag = dropdownValues.find(x => x.value == valueKey); + + if (!!tag) + values.push(tag); + }) + } + + this.currentValues = values; + }; + + private async onTagSelected(e: any) { + e.preventDefault(); + + const input = e.target as HTMLSelectElement; + const currentTag: SelectListItem = { + text: input.options[input.selectedIndex].text.trim(), + value: input.value + } + + if (currentTag.value.length == 0) + return; + + const values: Array = uniq([...this.currentValues, currentTag]); + input.value = "Add"; + await this.valueChanged.emit(values); + } + + async onDeleteTagClick(e: any, currentTag: SelectListItem) { + e.preventDefault(); + + this.currentValues = this.currentValues.filter(tag => tag.value !== currentTag.value); + await this.valueChanged.emit(this.currentValues); + } + + render() { + let values: Array = this.currentValues || []; + let dropdownItems = this.dropdownValues.filter(x => values.findIndex(y => y.value === x.value) < 0); + + if (!Array.isArray(values)) + values = []; + + const valuesJson = JSON.stringify(values.map(tag => tag.value)); + + return ( +
+ {values.map(tag => ( + this.onDeleteTagClick(e, tag)} class="inline-block text-xs bg-blue-400 text-white py-2 px-3 mr-1 mb-1 rounded"> + + {tag.text} + × + + ))} + + + +
+ ) + } +} diff --git a/src/designer/elsa-workflows-designer/src/components/shared/input-tags/input-tags.css b/src/designer/elsa-workflows-designer/src/components/shared/input-tags/input-tags.css new file mode 100644 index 000000000..4c9c0b679 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/shared/input-tags/input-tags.css @@ -0,0 +1,6 @@ +.tag-input { + outline: none !important; + box-shadow: none !important; + max-width: max-content !important; + display: inline-block !important; +} diff --git a/src/designer/elsa-workflows-designer/src/components/shared/input-tags/input-tags.tsx b/src/designer/elsa-workflows-designer/src/components/shared/input-tags/input-tags.tsx new file mode 100644 index 000000000..f4f95ced3 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/shared/input-tags/input-tags.tsx @@ -0,0 +1,81 @@ +import {Component, h, Prop, Event, EventEmitter, State, Watch} from '@stencil/core'; +import {uniq} from 'lodash'; + +@Component({ + tag: 'elsa-input-tags', + styleUrl: 'input-tags.css', + shadow: false, +}) +export class InputTags { + + @Prop() fieldId?: string; + @Prop() placeHolder?: string = 'Add tag'; + @Prop({mutable: true}) values?: Array = []; + @Event({bubbles: true}) valueChanged: EventEmitter>; + + private addItem = async (item: string) => { + const values = uniq([...this.values || [], item]); + this.values = values; + await this.valueChanged.emit(values); + }; + + private async onInputKeyDown(e: KeyboardEvent) { + if (e.key != "Enter") + return; + + e.preventDefault(); + + const input = e.target as HTMLInputElement; + const value = input.value.trim(); + + if (value.length == 0) + return; + + await this.addItem(value); + input.value = ''; + } + + async onInputBlur(e: Event) { + const input = e.target as HTMLInputElement; + const value = input.value.trim(); + + if (value.length == 0) + return; + + await this.addItem(value); + input.value = ''; + } + + async onDeleteTagClick(e: Event, tag: string) { + e.preventDefault(); + + this.values = this.values.filter(x => x !== tag); + await this.valueChanged.emit(this.values); + } + + render() { + let values = this.values || []; + + if (!Array.isArray(values)) + values = []; + + // const valuesJson = JSON.stringify(values); + + return ( +
+ {values.map(value => ( + this.onDeleteTagClick(e, value)} class="inline-block text-xs bg-blue-400 text-white py-2 px-3 mr-1 mb-1 rounded"> + {value} + × + + ))} + this.onInputKeyDown(e)} + onBlur={e => this.onInputBlur(e)} + class="tag-input inline-block text-sm outline-none focus:outline-none border-none shadow:none focus:border-none focus:border-transparent focus:shadow-none" + placeholder={this.placeHolder}/> + {/**/} +
+ ) + } +} diff --git a/src/designer/elsa-workflows-designer/src/services/input-control-registry.tsx b/src/designer/elsa-workflows-designer/src/services/input-control-registry.tsx index 4550fffcd..d837cea67 100644 --- a/src/designer/elsa-workflows-designer/src/services/input-control-registry.tsx +++ b/src/designer/elsa-workflows-designer/src/services/input-control-registry.tsx @@ -15,6 +15,7 @@ export class InputControlRegistry { this.add('dropdown', c => ); this.add('check-list', c => ); this.add('radio-list', c => ); + this.add('multi-text', c => ); } public add(uiHint: UIHint, control: RenderActivityPropInputControl) { diff --git a/src/samples/aspnet/Elsa.Samples.Web1/Activities/WriteLines.cs b/src/samples/aspnet/Elsa.Samples.Web1/Activities/WriteLines.cs index 6d91714fb..3afb62309 100644 --- a/src/samples/aspnet/Elsa.Samples.Web1/Activities/WriteLines.cs +++ b/src/samples/aspnet/Elsa.Samples.Web1/Activities/WriteLines.cs @@ -41,6 +41,9 @@ public class WriteLines : Activity, IActivityPropertyOptionsProvider [Input(OptionsProvider = typeof(WriteLines), UIHint = InputUIHints.MultiLine)] public Input Body { get; set; } = default!; + + [Input(UIHint = InputUIHints.MultiText)] + public Input> Tags { get; set; } = default!; protected override void Execute(ActivityExecutionContext context) {