Implement multi-text input control

This commit is contained in:
Sipke Schoorstra 2022-01-04 18:58:19 +01:00
parent d172470c00
commit a949fa2c14
8 changed files with 277 additions and 2 deletions

View file

@ -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<TabDefinition>;
}
interface ElsaInputTags {
"fieldId"?: string;
"placeHolder"?: string;
"values"?: Array<string>;
}
interface ElsaInputTagsDropdown {
"dropdownValues"?: Array<SelectListItem>;
"fieldId"?: string;
"fieldName"?: string;
"placeHolder"?: string;
"values"?: Array<string | SelectListItem>;
}
interface ElsaModalDialog {
"actions": Array<ActionDefinition>;
"hide": (animate?: boolean) => Promise<void>;
@ -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<TabDefinition>;
}
interface ElsaInputTags {
"fieldId"?: string;
"onValueChanged"?: (event: CustomEvent<Array<string>>) => void;
"placeHolder"?: string;
"values"?: Array<string>;
}
interface ElsaInputTagsDropdown {
"dropdownValues"?: Array<SelectListItem>;
"fieldId"?: string;
"fieldName"?: string;
"onValueChanged"?: (event: CustomEvent<Array<string | SelectListItem>>) => void;
"placeHolder"?: string;
"values"?: Array<string | SelectListItem>;
}
interface ElsaModalDialog {
"actions"?: Array<ActionDefinition>;
"onActionInvoked"?: (event: CustomEvent<ActionInvokedArgs>) => void;
@ -407,6 +457,9 @@ declare namespace LocalJSX {
interface ElsaMultiLineInput {
"inputContext"?: NodeInputContext;
}
interface ElsaMultiTextInput {
"inputContext"?: NodeInputContext;
}
interface ElsaPager {
"onPaginated"?: (event: CustomEvent<PagerData>) => 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<HTMLElsaDropdownInputElement>;
"elsa-flowchart": LocalJSX.ElsaFlowchart & JSXBase.HTMLAttributes<HTMLElsaFlowchartElement>;
"elsa-form-panel": LocalJSX.ElsaFormPanel & JSXBase.HTMLAttributes<HTMLElsaFormPanelElement>;
"elsa-input-tags": LocalJSX.ElsaInputTags & JSXBase.HTMLAttributes<HTMLElsaInputTagsElement>;
"elsa-input-tags-dropdown": LocalJSX.ElsaInputTagsDropdown & JSXBase.HTMLAttributes<HTMLElsaInputTagsDropdownElement>;
"elsa-modal-dialog": LocalJSX.ElsaModalDialog & JSXBase.HTMLAttributes<HTMLElsaModalDialogElement>;
"elsa-multi-line-input": LocalJSX.ElsaMultiLineInput & JSXBase.HTMLAttributes<HTMLElsaMultiLineInputElement>;
"elsa-multi-text-input": LocalJSX.ElsaMultiTextInput & JSXBase.HTMLAttributes<HTMLElsaMultiTextInputElement>;
"elsa-pager": LocalJSX.ElsaPager & JSXBase.HTMLAttributes<HTMLElsaPagerElement>;
"elsa-panel": LocalJSX.ElsaPanel & JSXBase.HTMLAttributes<HTMLElsaPanelElement>;
"elsa-radio-list-input": LocalJSX.ElsaRadioListInput & JSXBase.HTMLAttributes<HTMLElsaRadioListInputElement>;

View file

@ -23,7 +23,6 @@ export class MultiLineInput {
}
private getEditorHeight = (options: any) => {
debugger;
const editorHeightName = options.editorHeight || 'Default';
switch (editorHeightName) {

View file

@ -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 <elsa-input-tags fieldId={fieldId} values={values} onValueChanged={this.onPropertyEditorChanged}/>
}
private onPropertyEditorChanged = (e: CustomEvent<Array<string>>) => {
const json = JSON.stringify(e.detail);
this.inputContext.inputChanged(json, SyntaxNames.Json);
};
}

View file

@ -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<string | SelectListItem> = [];
@Prop() dropdownValues?: Array<SelectListItem> = [];
@Event({bubbles: true}) valueChanged: EventEmitter<Array<string | SelectListItem>>;
@State() currentValues?: Array<SelectListItem> = [];
@Watch('values')
private valuesChangedHandler(newValue: Array<string | SelectListItem>) {
this.updateCurrentValues(newValue);
}
public componentWillLoad() {
this.updateCurrentValues(this.values);
}
private updateCurrentValues = (newValue: Array<string | SelectListItem>) => {
const dropdownValues = this.dropdownValues || [];
let values: Array<SelectListItem> = [];
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<SelectListItem> = 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<SelectListItem> = 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 (
<div class="py-2 px-3 bg-white shadow-sm border border-gray-300 rounded-md">
{values.map(tag => (
<a href="#" onClick={e => this.onDeleteTagClick(e, tag)} class="inline-block text-xs bg-blue-400 text-white py-2 px-3 mr-1 mb-1 rounded">
<input type="hidden" value={tag.value}/>
<span>{tag.text}</span>
<span class="text-white hover:text-white ml-1">&times;</span>
</a>
))}
<select
id={this.fieldId}
class="inline-block text-xs py-2 px-3 mr-1 mb-1 pr-8 border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 rounded"
onChange={(e) => this.onTagSelected(e)}>
<option value="Add" disabled selected>{this.placeHolder}</option>
{dropdownItems.map(tag => <option value={tag.value}>{tag.text}</option>)}
</select>
<input type="hidden" name={this.fieldName} value={valuesJson}/>
</div>
)
}
}

View file

@ -0,0 +1,6 @@
.tag-input {
outline: none !important;
box-shadow: none !important;
max-width: max-content !important;
display: inline-block !important;
}

View file

@ -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<string> = [];
@Event({bubbles: true}) valueChanged: EventEmitter<Array<string>>;
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 (
<div class="py-2 px-3 bg-white shadow-sm border border-gray-300 rounded-md">
{values.map(value => (
<a href="#" onClick={e => this.onDeleteTagClick(e, value)} class="inline-block text-xs bg-blue-400 text-white py-2 px-3 mr-1 mb-1 rounded">
<span>{value}</span>
<span class="text-white hover:text-white ml-1">&times;</span>
</a>
))}
<input type="text" id={this.fieldId}
onKeyDown={e => 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}/>
{/*<input type="hidden" name={this.fieldName} value={valuesJson}/>*/}
</div>
)
}
}

View file

@ -15,6 +15,7 @@ export class InputControlRegistry {
this.add('dropdown', c => <elsa-dropdown-input inputContext={c}/>);
this.add('check-list', c => <elsa-check-list-input inputContext={c}/>);
this.add('radio-list', c => <elsa-radio-list-input inputContext={c}/>);
this.add('multi-text', c => <elsa-multi-text-input inputContext={c}/>);
}
public add(uiHint: UIHint, control: RenderActivityPropInputControl) {

View file

@ -41,6 +41,9 @@ public class WriteLines : Activity, IActivityPropertyOptionsProvider
[Input(OptionsProvider = typeof(WriteLines), UIHint = InputUIHints.MultiLine)]
public Input<string> Body { get; set; } = default!;
[Input(UIHint = InputUIHints.MultiText)]
public Input<ICollection<string>> Tags { get; set; } = default!;
protected override void Execute(ActivityExecutionContext context)
{