Activity editor tabs (#745)

* Add activity editor tabs

* WIP: Workflow Context activity tab

* Implement common activity settings
This commit is contained in:
Sipke Schoorstra 2021-03-11 11:28:19 +01:00 committed by GitHub
parent 6f7d512a9f
commit f61df66148
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 237 additions and 136 deletions

View file

@ -1,8 +1,9 @@
import {Component, Host, h, State} from '@stencil/core';
import {Component, Host, h, State, Event} from '@stencil/core';
import {eventBus} from '../../../../services/event-bus';
import state from '../../../../utils/store';
import {ActivityDescriptor, ActivityModel, ActivityPropertyDescriptor, EventTypes} from "../../../../models";
import {propertyDisplayManager} from '../../../../services/property-display-manager';
import {checkBox, FormContext, textArea, textInput} from "../../../../utils/forms";
@Component({
tag: 'elsa-activity-editor-modal',
@ -13,17 +14,21 @@ export class ElsaActivityPickerModal {
@State() activityModel: ActivityModel;
@State() activityDescriptor: ActivityDescriptor;
@State() selectedTab: string = 'Properties';
dialog: HTMLElsaModalDialogElement;
form: HTMLFormElement;
formContext: FormContext;
componentDidLoad() {
eventBus.on(EventTypes.ShowActivityEditor, async (activity: ActivityModel, animate: boolean) => {
this.activityModel = {...activity, properties: activity.properties || []};
this.activityDescriptor = state.activityDescriptors.find(x => x.type == activity.type);
this.formContext = new FormContext(this.activityModel, newValue => this.activityModel = newValue);
await this.dialog.show(animate);
});
}
updateActivity(formData: FormData){
updateActivity(formData: FormData) {
const activity = this.activityModel;
const activityDescriptor = this.activityDescriptor;
const properties: Array<ActivityPropertyDescriptor> = activityDescriptor.properties;
@ -45,17 +50,24 @@ export class ElsaActivityPickerModal {
await this.dialog.hide(true);
}
onTabClick(e: Event, tab: string) {
e.preventDefault();
this.selectedTab = tab;
}
render() {
const tabs = ['Properties', 'Common', 'Behaviors'];
const selectedTab = this.selectedTab;
const inactiveClass = 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300';
const selectedClass = 'border-blue-500 text-blue-600';
const activityModel: ActivityModel = this.activityModel || {type: '', activityId: '', outcomes: [], properties: []};
const activityDescriptor = this.activityDescriptor || {displayName: '', type: '', outcomes: [], category: '', traits: 0, browsable: false, properties: [], description: ''};
const propertyDescriptors: Array<ActivityPropertyDescriptor> = activityDescriptor.properties;
const key = new Date().getTime().toString();
return (
<Host>
<elsa-modal-dialog ref={el => this.dialog = el}>
<div slot="content" class="py-8 pb-0">
<form onSubmit={e => this.onSubmit(e)}>
<form onSubmit={e => this.onSubmit(e)} ref={el => this.form = el}>
<div class="flex px-8">
<div class="space-y-8 divide-y divide-gray-200 w-full">
<div>
@ -68,8 +80,18 @@ export class ElsaActivityPickerModal {
</p>
</div>
<div key={key} class="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
{propertyDescriptors.map(property => this.renderPropertyEditor(activityModel, property))}
<div class="border-b border-gray-200">
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
{tabs.map(tab => {
const isSelected = tab === selectedTab;
const cssClass = isSelected ? selectedClass : inactiveClass;
return <a href="#" onClick={e => this.onTabClick(e, tab)} class={`${cssClass} whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm`}>{tab}</a>;
})}
</nav>
</div>
<div class="mt-8">
{this.renderSelectedTab(activityModel, activityDescriptor)}
</div>
</div>
@ -97,7 +119,59 @@ export class ElsaActivityPickerModal {
);
}
renderSelectedTab(activityModel: ActivityModel, activityDescriptor: ActivityDescriptor) {
return [
this.renderWorkflowContextTab(activityModel),
this.renderCommonTab(activityModel),
this.renderPropertiesTab(activityModel, activityDescriptor)
];
}
renderWorkflowContextTab(activityModel: ActivityModel) {
const formContext = this.formContext;
return (
<div class={`flex ${this.getHiddenClass('Behaviors')}`}>
<div class="space-y-8 w-full">
{checkBox(formContext, 'loadWorkflowContext', 'Load Workflow Context', activityModel.loadWorkflowContext, 'When enabled, this will load the workflow context into memory before executing this activity.', 'loadWorkflowContext')}
{checkBox(formContext, 'saveWorkflowContext', 'Save Workflow Context', activityModel.saveWorkflowContext, 'When enabled, this will save the workflow context back into storage after executing this activity.', 'saveWorkflowContext')}
{checkBox(formContext, 'persistWorkflow', 'Save Workflow Instance', activityModel.persistWorkflow, 'When enabled, this will save the workflow instance back into storage right after executing this activity.', 'persistWorkflow')}
{checkBox(formContext, 'persistOutput', 'Save Activity Output', activityModel.persistOutput, 'When enabled, this will store this activity\'s output as part of the workflow instance. Enable this when you plan to reference this output from other activities', 'persistOutput')}
</div>
</div>
);
}
renderCommonTab(activityModel: ActivityModel) {
const formContext = this.formContext;
return (
<div class={`flex ${this.getHiddenClass('Common')}`}>
<div class="space-y-8 w-full">
{textInput(formContext, 'name', 'Name', activityModel.name, 'The technical name of the activity.', 'activityName')}
{textInput(formContext, 'displayName', 'Display Name', activityModel.displayName, 'A friendly name of the activity.', 'activityDisplayName')}
{textArea(formContext, 'description', 'Description', activityModel.description, 'A custom description for this activity', 'activityDescription')}
</div>
</div>
);
}
renderPropertiesTab(activityModel: ActivityModel, activityDescriptor: ActivityDescriptor) {
const propertyDescriptors: Array<ActivityPropertyDescriptor> = activityDescriptor.properties;
const key = `activity-settings:${activityModel.activityId}`;
return (
<div key={key} class={`grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6 ${this.getHiddenClass('Properties')}`}>
{propertyDescriptors.map(property => this.renderPropertyEditor(activityModel, property))}
</div>
);
}
renderPropertyEditor(activity: ActivityModel, property: ActivityPropertyDescriptor) {
return propertyDisplayManager.display(activity, property);
}
getHiddenClass(tab: string) {
return this.selectedTab == tab ? '' : 'hidden';
}
}

View file

@ -147,10 +147,10 @@ export class ElsaWorkflowDefinitionEditor {
name: x.name,
displayName: x.displayName,
description: x.description,
persistWorkflow: false,
loadWorkflowContext: false,
saveWorkflowContext: false,
persistOutput: false,
persistWorkflow: x.persistWorkflow,
loadWorkflowContext: x.loadWorkflowContext,
saveWorkflowContext: x.saveWorkflowContext,
persistOutput: x.persistOutput,
properties: x.properties
})),
connections: workflowModel.connections.map<ConnectionDefinition>(x => ({
@ -218,7 +218,11 @@ export class ElsaWorkflowDefinitionEditor {
name: source.name,
type: source.type,
properties: source.properties,
outcomes: [...descriptor.outcomes]
outcomes: [...descriptor.outcomes],
persistOutput: source.persistOutput,
persistWorkflow: source.persistWorkflow,
saveWorkflowContext: source.saveWorkflowContext,
loadWorkflowContext: source.loadWorkflowContext
}
}

View file

@ -4,11 +4,7 @@ import {Map} from "../../../../utils/utils";
import {EventTypes, Variables, WorkflowContextFidelity, WorkflowContextOptions, WorkflowDefinition} from "../../../../models";
import {MonacoValueChangedArgs} from "../../monaco/elsa-monaco/elsa-monaco";
import {MarkerSeverity} from "monaco-editor";
interface SelectOption {
value: string;
text: string;
}
import {checkBox, FormContext, selectField, SelectOption, textArea, textInput} from "../../../../utils/forms";
interface VariableDefinition {
name?: string;
@ -28,10 +24,12 @@ export class ElsaWorkflowDefinitionSettingsModal {
@State() newVariable: VariableDefinition = {};
dialog: HTMLElsaModalDialogElement;
monacoEditor: HTMLElsaMonacoElement;
formContext: FormContext;
@Watch('workflowDefinition')
handleWorkflowDefinitionChanged(newValue: WorkflowDefinition) {
this.workflowDefinitionInternal = {...newValue};
this.formContext = new FormContext(this.workflowDefinitionInternal, newValue => this.workflowDefinitionInternal = newValue);
}
componentWillLoad() {
@ -42,24 +40,6 @@ export class ElsaWorkflowDefinitionSettingsModal {
eventBus.on(EventTypes.ShowWorkflowSettings, async () => await this.dialog.show(true));
}
// Supports hierarchical field names, e.g. 'foo.bar.baz`, which will map to e.g. { foo: { bar: { baz: ''} } }.
updateField<T>(fieldName: string, value: T) {
const model = {...this.workflowDefinitionInternal};
const fieldNameHierarchy = fieldName.split('.');
let current = model;
for (const name of fieldNameHierarchy.slice(0, fieldNameHierarchy.length - 1)) {
if (!current[name])
current[name] = {};
current = current[name];
}
const leafFieldName = fieldNameHierarchy.last();
current[leafFieldName] = value;
this.workflowDefinitionInternal = model;
}
onTabClick(e: Event, tab: string) {
e.preventDefault();
this.selectedTab = tab;
@ -75,26 +55,6 @@ export class ElsaWorkflowDefinitionSettingsModal {
setTimeout(() => eventBus.emit(EventTypes.UpdateWorkflowSettings, this, this.workflowDefinitionInternal), 250)
}
onTextInputChange(e: Event) {
const element = e.target as HTMLInputElement;
this.updateField(element.name, element.value.trim());
}
onTextAreaChange(e: Event) {
const element = e.target as HTMLTextAreaElement;
this.updateField(element.name, element.value.trim());
}
onCheckBoxChange(e: Event) {
const element = e.target as HTMLInputElement;
this.updateField(element.name, element.checked);
}
onSelectChange(e: Event) {
const element = e.target as HTMLSelectElement;
this.updateField(element.name, element.value.trim());
}
onMonacoValueChanged(e: MonacoValueChangedArgs) {
// Don't try and parse JSON if it contains errors.
const errorCount = e.markers.filter(x => x.severity == MarkerSeverity.Error).length;
@ -115,7 +75,6 @@ export class ElsaWorkflowDefinitionSettingsModal {
render() {
const workflowDefinition = this.workflowDefinitionInternal;
const tabs = ['Settings', 'Variables', 'Workflow Context'];
const selectedTab = this.selectedTab;
const inactiveClass = 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300';
@ -179,6 +138,7 @@ export class ElsaWorkflowDefinitionSettingsModal {
renderSettingsTab() {
const workflowDefinition = this.workflowDefinitionInternal;
const formContext = this.formContext;
const persistenceBehaviorOptions: Array<SelectOption> = [{
text: 'Suspended',
@ -197,11 +157,11 @@ export class ElsaWorkflowDefinitionSettingsModal {
return (
<div class="flex px-8">
<div class="space-y-8 w-full">
{this.textInput('name', 'Name', workflowDefinition.name, 'The technical name of the workflow.', 'workflowName')}
{this.textInput('displayName', 'Display Name', workflowDefinition.displayName, 'A user-friendly display name of the workflow.', 'workflowDisplayName')}
{this.textArea('description', 'Description', workflowDefinition.description, null, 'workflowDescription')}
{this.selectField('persistenceBehavior', 'Persistence Behavior', workflowDefinition.persistenceBehavior.toString(), persistenceBehaviorOptions, 'The persistence behavior controls how often a workflow instance is persisted during workflow execution.', 'workflowContextFidelity')}
{this.checkBox('isSingleton', 'Singleton', workflowDefinition.isSingleton, 'Singleton workflows will only have one active instance executing at a time.')}
{textInput(formContext, 'name', 'Name', workflowDefinition.name, 'The technical name of the workflow.', 'workflowName')}
{textInput(formContext, 'displayName', 'Display Name', workflowDefinition.displayName, 'A user-friendly display name of the workflow.', 'workflowDisplayName')}
{textArea(formContext, 'description', 'Description', workflowDefinition.description, null, 'workflowDescription')}
{selectField(formContext, 'persistenceBehavior', 'Persistence Behavior', workflowDefinition.persistenceBehavior.toString(), persistenceBehaviorOptions, 'The persistence behavior controls how often a workflow instance is persisted during workflow execution.', 'workflowContextFidelity')}
{checkBox(formContext, 'isSingleton', 'Singleton', workflowDefinition.isSingleton, 'Singleton workflows will only have one active instance executing at a time.')}
</div>
</div>
);
@ -225,6 +185,7 @@ export class ElsaWorkflowDefinitionSettingsModal {
renderWorkflowContextTab() {
const workflowDefinition = this.workflowDefinitionInternal;
const formContext = this.formContext;
const contextOptions: WorkflowContextOptions = workflowDefinition.contextOptions || {
contextType: undefined,
@ -242,71 +203,10 @@ export class ElsaWorkflowDefinitionSettingsModal {
return (
<div class="flex px-8">
<div class="space-y-8 w-full">
{this.textInput('contextOptions.contextType', 'Type', contextOptions.contextType, 'The fully qualified workflow context type name.', 'workflowContextType')}
{this.selectField('contextOptions.contextFidelity', 'Fidelity', contextOptions.contextFidelity.toString(), fidelityOptions, 'The workflow context refresh fidelity controls the behavior of when to load and persist the workflow context.', 'workflowContextFidelity')}
{textInput(formContext, 'contextOptions.contextType', 'Type', contextOptions.contextType, 'The fully qualified workflow context type name.', 'workflowContextType')}
{selectField(formContext, 'contextOptions.contextFidelity', 'Fidelity', contextOptions.contextFidelity.toString(), fidelityOptions, 'The workflow context refresh fidelity controls the behavior of when to load and persist the workflow context.', 'workflowContextFidelity')}
</div>
</div>
);
}
textInput(fieldName: string, label: string, value: string, hint?: string, fieldId?: string) {
fieldId = fieldId || fieldName
return (
<div>
<label htmlFor={fieldName} class="block text-sm font-medium text-gray-700">
{label}
</label>
<div class="mt-1">
<input type="text" id={fieldId} name={fieldName} value={value} onChange={e => this.onTextInputChange(e)} class="focus:ring-blue-500 focus:border-blue-500 block w-full min-w-0 rounded-md sm:text-sm border-gray-300"/>
</div>
{hint && hint.length > 0 ? <p class="mt-2 text-sm text-gray-500">{hint}</p> : undefined}
</div>);
}
checkBox(fieldName: string, label: string, checked: boolean, hint?: string, fieldId?: string) {
fieldId = fieldId || fieldName
return (
<div class="relative flex items-start">
<div class="flex items-center h-5">
<input id={fieldId} name={fieldName} type="checkbox" value="true" checked={checked} onChange={e => this.onCheckBoxChange(e)} class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"/>
</div>
<div class="ml-3 text-sm">
<label htmlFor={fieldId} class="font-medium text-gray-700">{label}</label>
{hint && hint.length > 0 ? <p class="text-gray-500">{hint}</p> : undefined}
</div>
</div>);
}
textArea(fieldName: string, label: string, value: string, hint?: string, fieldId?: string) {
fieldId = fieldId || fieldName
return (
<div>
<label htmlFor={fieldName} class="block text-sm font-medium text-gray-700">
{label}
</label>
<div class="mt-1">
<textarea id={fieldId} name={fieldName} value={value} onChange={e => this.onTextAreaChange(e)} rows={3} class="focus:ring-blue-500 focus:border-blue-500 block w-full min-w-0 rounded-md sm:text-sm border-gray-300"/>
</div>
{hint && hint.length > 0 ? <p class="mt-2 text-sm text-gray-500">{hint}</p> : undefined}
</div>);
}
selectField(fieldName: string, label: string, value: string, options: Array<SelectOption>, hint?: string, fieldId?: string) {
fieldId = fieldId || fieldName
return (
<div>
<label htmlFor={fieldName} class="block text-sm font-medium text-gray-700">
{label}
</label>
<div class="mt-1">
<select id={fieldId} name={fieldName} onChange={e => this.onSelectChange(e)} class="block focus:ring-blue-500 focus:border-blue-500 w-full shadow-sm sm:text-sm border-gray-300 rounded-md">
{options.map(item => {
const selected = item.value === value;
return <option value={item.value} selected={selected}>{item.text}</option>;
})}
</select>
</div>
{hint && hint.length > 0 ? <p class="mt-2 text-sm text-gray-500">{hint}</p> : undefined}
</div>);
}
}

View file

@ -1,23 +1,27 @@
import {ActivityDefinitionProperty, WorkflowPersistenceBehavior} from "./domain";
export interface WorkflowModel {
activities: Array<ActivityModel>
connections: Array<ConnectionModel>
activities: Array<ActivityModel>;
connections: Array<ConnectionModel>;
persistenceBehavior?: WorkflowPersistenceBehavior,
}
export interface ActivityModel {
activityId: string
type: string
name?: string
displayName?: string
description?: string
outcomes: Array<string>
activityId: string;
type: string;
name?: string;
displayName?: string;
description?: string;
outcomes: Array<string>;
properties: Array<ActivityDefinitionProperty>;
persistWorkflow?: boolean;
persistOutput?: boolean;
loadWorkflowContext?: boolean;
saveWorkflowContext?: boolean;
}
export interface ConnectionModel {
sourceId: string,
targetId: string,
outcome: string
sourceId: string;
targetId: string;
outcome: string;
}

View file

@ -17,6 +17,8 @@ export class SendEmailPlugin implements ElsaPlugin {
const props = activityModel.properties || [];
const condition = props.find(x => x.name == 'To') || { expression: '' };
const expression = condition.expression || '';
context.bodyDisplay = <p>{expression}</p>;
const description = activityModel.description;
const bodyText = description && description.length > 0 ? description : expression;
context.bodyDisplay = <p>{bodyText}</p>;
}
}

View file

@ -0,0 +1,117 @@
// Supports hierarchical field names, e.g. 'foo.bar.baz`, which will map to e.g. { foo: { bar: { baz: ''} } }.
import {Event, h} from "@stencil/core";
export interface SelectOption {
value: string;
text: string;
}
export class FormContext {
constructor(model: any, updater: (model: any) => any) {
this.model = model;
this.updater = updater;
}
public model: any;
public updater: (model: any) => any;
}
export function textInput(context: FormContext, fieldName: string, label: string, value: string, hint?: string, fieldId?: string) {
fieldId = fieldId || fieldName
return (
<div>
<label htmlFor={fieldName} class="block text-sm font-medium text-gray-700">
{label}
</label>
<div class="mt-1">
<input type="text" id={fieldId} name={fieldName} value={value} onChange={e => onTextInputChange(e, context)} class="focus:ring-blue-500 focus:border-blue-500 block w-full min-w-0 rounded-md sm:text-sm border-gray-300"/>
</div>
{hint && hint.length > 0 ? <p class="mt-2 text-sm text-gray-500">{hint}</p> : undefined}
</div>);
}
export function checkBox(context: FormContext, fieldName: string, label: string, checked: boolean, hint?: string, fieldId?: string) {
fieldId = fieldId || fieldName
return (
<div class="relative flex items-start">
<div class="flex items-center h-5">
<input id={fieldId} name={fieldName} type="checkbox" value="true" checked={checked} onChange={e => onCheckBoxChange(e, context)} class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"/>
</div>
<div class="ml-3 text-sm">
<label htmlFor={fieldId} class="font-medium text-gray-700">{label}</label>
{hint && hint.length > 0 ? <p class="text-gray-500">{hint}</p> : undefined}
</div>
</div>);
}
export function textArea(context: FormContext, fieldName: string, label: string, value: string, hint?: string, fieldId?: string) {
fieldId = fieldId || fieldName
return (
<div>
<label htmlFor={fieldName} class="block text-sm font-medium text-gray-700">
{label}
</label>
<div class="mt-1">
<textarea id={fieldId} name={fieldName} value={value} onChange={e => onTextAreaChange(e, context)} rows={3} class="focus:ring-blue-500 focus:border-blue-500 block w-full min-w-0 rounded-md sm:text-sm border-gray-300"/>
</div>
{hint && hint.length > 0 ? <p class="mt-2 text-sm text-gray-500">{hint}</p> : undefined}
</div>);
}
export function selectField(context: FormContext, fieldName: string, label: string, value: string, options: Array<SelectOption>, hint?: string, fieldId?: string) {
fieldId = fieldId || fieldName
return (
<div>
<label htmlFor={fieldName} class="block text-sm font-medium text-gray-700">
{label}
</label>
<div class="mt-1">
<select id={fieldId} name={fieldName} onChange={e => onSelectChange(e, context)} class="block focus:ring-blue-500 focus:border-blue-500 w-full shadow-sm sm:text-sm border-gray-300 rounded-md">
{options.map(item => {
const selected = item.value === value;
return <option value={item.value} selected={selected}>{item.text}</option>;
})}
</select>
</div>
{hint && hint.length > 0 ? <p class="mt-2 text-sm text-gray-500">{hint}</p> : undefined}
</div>);
}
export function updateField<T>(context: FormContext, fieldName: string, value: T) {
const fieldNameHierarchy = fieldName.split('.');
let current = {...context.model};
for (const name of fieldNameHierarchy.slice(0, fieldNameHierarchy.length - 1)) {
if (!current[name])
current[name] = {};
current = current[name];
}
const leafFieldName = fieldNameHierarchy.last();
current[leafFieldName] = value;
context.model = current;
context.updater(current);
}
export function onTextInputChange(e: Event, context: FormContext) {
const element = e.target as HTMLInputElement;
updateField(context, element.name, element.value.trim());
}
export function onTextAreaChange(e: Event, context: FormContext) {
const element = e.target as HTMLTextAreaElement;
updateField(context, element.name, element.value.trim());
}
export function onCheckBoxChange(e: Event, context: FormContext) {
const element = e.target as HTMLInputElement;
updateField(context, element.name, element.checked);
}
export function onSelectChange(e: Event, context: FormContext) {
const element = e.target as HTMLSelectElement;
updateField(context, element.name, element.value.trim());
}