From 22ce7e2e816e3ce162d16e11106fe9c207d1a5ef Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 8 Apr 2021 21:19:56 +0200 Subject: [PATCH] Incremental work on workflow instances list screen --- .../elsa-dropdown-button.tsx | 10 +- .../controls/elsa-dropdown-button/models.tsx | 3 +- .../elsa-workflow-instances-list-screen.tsx | 291 ++++++++++++------ .../src/models/domain.ts | 18 ++ .../src/services/elsa-client.ts | 248 ++++++++------- .../elsa-workflows-studio/src/utils/utils.ts | 266 ++++++++-------- 6 files changed, 504 insertions(+), 332 deletions(-) diff --git a/src/designer/elsa-workflows-studio/src/components/controls/elsa-dropdown-button/elsa-dropdown-button.tsx b/src/designer/elsa-workflows-studio/src/components/controls/elsa-dropdown-button/elsa-dropdown-button.tsx index 4f57b876d..b9ae21ee6 100644 --- a/src/designer/elsa-workflows-studio/src/components/controls/elsa-dropdown-button/elsa-dropdown-button.tsx +++ b/src/designer/elsa-workflows-studio/src/components/controls/elsa-dropdown-button/elsa-dropdown-button.tsx @@ -67,7 +67,13 @@ export class ElsaContextMenu { const originClass = this.getOriginClass(); - return
this.contextMenu = el} class={`hidden ${originClass} z-10 absolute mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5`}> + return
this.contextMenu = el} + data-transition-enter="transition ease-out duration-100" + data-transition-enter-start="transform opacity-0 scale-95" + data-transition-leave="transition ease-in duration-75" + data-transition-leave-start="transform opacity-100 scale-100" + data-transition-leave-end="transform opacity-0 scale-95" + class={`hidden ${originClass} z-10 absolute mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5`}> @@ -79,7 +85,7 @@ export class ElsaContextMenu { const selectedCssClass = item.isSelected ? "bg-blue-600 hover:bg-blue-700 text-white" : "hover:bg-gray-100 text-gray-700 hover:text-gray-900"; return !!item.url - ? this.closeContextMenu()} url={item.url} class={`block px-4 py-2 text-sm ${selectedCssClass}`} role="menuitem">{item.text} + ? this.closeContextMenu()} url={item.url} anchorClass={`block px-4 py-2 text-sm ${selectedCssClass} cursor-pointer`} role="menuitem">{item.text} : this.onItemClick(e, item)} class={`block px-4 py-2 text-sm ${selectedCssClass}`} role="menuitem">{item.text}; }) } diff --git a/src/designer/elsa-workflows-studio/src/components/controls/elsa-dropdown-button/models.tsx b/src/designer/elsa-workflows-studio/src/components/controls/elsa-dropdown-button/models.tsx index 496154f2d..a68023b6a 100644 --- a/src/designer/elsa-workflows-studio/src/components/controls/elsa-dropdown-button/models.tsx +++ b/src/designer/elsa-workflows-studio/src/components/controls/elsa-dropdown-button/models.tsx @@ -5,7 +5,8 @@ } export interface DropdownButtonItem { - name: string; + name?: string; + value?: any; text: string; url?: string; isSelected?: boolean; diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-instances-list/elsa-workflow-instances-list-screen/elsa-workflow-instances-list-screen.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-instances-list/elsa-workflow-instances-list-screen/elsa-workflow-instances-list-screen.tsx index c1bfda93f..1e0ae6087 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-instances-list/elsa-workflow-instances-list-screen/elsa-workflow-instances-list-screen.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-instances-list/elsa-workflow-instances-list-screen/elsa-workflow-instances-list-screen.tsx @@ -1,11 +1,12 @@ import {Component, h, Prop, State} from '@stencil/core'; -import {RouterHistory} from "@stencil/router"; +import {LocationSegments, RouterHistory} from "@stencil/router"; import * as collection from 'lodash/collection'; import * as array from 'lodash/array'; import {createElsaClient} from "../../../../services/elsa-client"; -import {OrderBy, PagedList, VersionOptions, WorkflowBlueprintSummary, WorkflowDefinitionSummary, WorkflowStatus} from "../../../../models"; +import {OrderBy, PagedList, VersionOptions, WorkflowBlueprintSummary, WorkflowInstanceSummary, WorkflowStatus} from "../../../../models"; import {DropdownButtonItem, DropdownButtonOrigin} from "../../../controls/elsa-dropdown-button/models"; -import {Map} from '../../../../utils/utils'; +import {Map, parseQuery} from '../../../../utils/utils'; +import {parseQueryString} from "@stencil/router/dist/types/utils/path-utils"; @Component({ tag: 'elsa-workflow-instances-list-screen', @@ -15,27 +16,53 @@ import {Map} from '../../../../utils/utils'; export class ElsaWorkflowInstancesListScreen { @Prop() history?: RouterHistory; @Prop() serverUrl: string; - @State() workflowDefinitions: PagedList = {items: [], page: 1, pageSize: 50, totalCount: 0}; @State() workflowBlueprints: Array = []; + @State() workflowInstances: PagedList = {items: [], page: 1, pageSize: 50, totalCount: 0}; @State() selectedWorkflowId?: string; @State() selectedWorkflowStatus?: WorkflowStatus; @State() selectedOrderBy?: OrderBy; + @State() selectedWorkflowInstanceIds: Array = []; confirmDialog: HTMLElsaConfirmDialogElement; async componentWillLoad() { + this.history.listen(e => this.routeChanged(e)); + this.applyQueryString(this.history.location.search); + await this.loadWorkflowBlueprints(); + await this.loadWorkflowInstances(); + } + + async routeChanged(e: LocationSegments) { + if(!e.pathname.toLowerCase().indexOf('workflow-instances')) + return; + + this.applyQueryString(e.search); + await this.loadWorkflowInstances(); + } + + applyQueryString(queryString?: string){ + const query = parseQuery(queryString); + + this.selectedWorkflowId = !!query.workflow ? query.workflow : null; + this.selectedWorkflowStatus = !!query.status ? this.parseStatusText(query.status) : null; } async loadWorkflowBlueprints() { const elsaClient = this.createClient(); - const page = 0; - const pageSize = 50; const versionOptions: VersionOptions = {isLatest: true}; - const workflowBlueprintPagedList = await elsaClient.workflowRegistryApi.list(page, pageSize, versionOptions); + const workflowBlueprintPagedList = await elsaClient.workflowRegistryApi.list(null, null, versionOptions); this.workflowBlueprints = workflowBlueprintPagedList.items; } + async loadWorkflowInstances() { + const elsaClient = this.createClient(); + const page = 1; + const pageSize = 25; + + this.workflowInstances = await elsaClient.workflowInstancesApi.list(page, pageSize, this.selectedWorkflowId, this.selectedWorkflowStatus, this.selectedOrderBy); + } + createClient() { return createElsaClient(this.serverUrl); } @@ -46,7 +73,7 @@ export class ElsaWorkflowInstancesListScreen { } buildFilterUrl(workflowId?: string, workflowStatus?: WorkflowStatus, orderBy?: OrderBy) { - const workflowStatusText = !!workflowStatus ? workflowStatus.toString() : null; + const workflowStatusText = workflowStatus != null ? this.getStatusText(workflowStatus) : null; const orderByText = !!orderBy ? orderBy.toString() : null; const filters: Map = {}; @@ -64,14 +91,69 @@ export class ElsaWorkflowInstancesListScreen { return `/workflow-instances?${queryString}`; } - render() { - const workflowDefinitions = this.workflowDefinitions.items; - const groupings = collection.groupBy(workflowDefinitions, 'definitionId'); + getStatusColor(status: WorkflowStatus) { + switch (status) { + default: + case WorkflowStatus.Idle: + return "gray"; + case WorkflowStatus.Running: + return "pink"; + case WorkflowStatus.Suspended: + return "blue"; + case WorkflowStatus.Finished: + return "green"; + case WorkflowStatus.Faulted: + return "red"; + case WorkflowStatus.Cancelled: + return "yellow"; + } + } + + getStatusText(status: WorkflowStatus){ + switch (status) { + default: + case WorkflowStatus.Idle: + return "Idle"; + case WorkflowStatus.Running: + return "Running"; + case WorkflowStatus.Suspended: + return "Suspended"; + case WorkflowStatus.Finished: + return "Finished"; + case WorkflowStatus.Faulted: + return "Faulted"; + case WorkflowStatus.Cancelled: + return "Cancelled"; + } + } + parseStatusText(status?: string): WorkflowStatus { + if(!status) + return null; + + switch (status.toLowerCase()) { + default: + case 'idle': + return WorkflowStatus.Idle; + case 'running': + return WorkflowStatus.Running; + case 'suspended': + return WorkflowStatus.Suspended; + case 'finished': + return WorkflowStatus.Finished; + case 'faulted': + return WorkflowStatus.Faulted; + case 'cancelled': + return WorkflowStatus.Cancelled; + } + } + + render() { + const workflowInstances = this.workflowInstances.items; + const workflowBlueprints = this.workflowBlueprints; return (
-
{this.renderBulkActions()} @@ -80,95 +162,105 @@ export class ElsaWorkflowInstancesListScreen {  
{this.renderWorkflowFilter()} - + {this.renderStatusFilter()}
+ +
+
+ + + + + + + + + + + + + + + + + {workflowInstances.map(workflowInstance => { + const workflowBlueprint = workflowBlueprints.find(x => x.id == workflowInstance.definitionId && x.version == workflowInstance.version) ?? {displayName: '(Workflow definition not found)'}; + const displayName = workflowBlueprint.displayName; + const statusColor = this.getStatusColor(workflowInstance.workflowStatus); + const viewUrl = `/workflow-instances/${workflowInstance.id}/viewer`; + const instanceName = !workflowInstance.name ? '' : workflowInstance.name; + const isSelected = this.selectedWorkflowInstanceIds.findIndex(x => x === workflowInstance.id) >= 0; + //var displayContext = WorkflowInstanceDisplayContexts[workflowInstance]; -
-
+ + + ID + + Workflow + + Instance Name + + Status + + Created + + Finished + + Last Executed + + Faulted + +
- - - - - - - - - - {collection.map(groupings, group => { - const versions = collection.orderBy(group, 'version', 'desc'); - const workflowDefinition = versions[0] - const latestVersionNumber = workflowDefinition.version; - const publishedVersion: WorkflowDefinitionSummary = versions.find(x => x.isPublished); - const publishedVersionNumber = !!publishedVersion ? publishedVersion.version : '-'; - let workflowDisplayName = workflowDefinition.displayName; - - if (!workflowDisplayName || workflowDisplayName.trim().length == 0) - workflowDisplayName = workflowDefinition.name; - - if (!workflowDisplayName || workflowDisplayName.trim().length == 0) - workflowDisplayName = 'Untitled'; - - const editUrl = `/workflow-definitions/${workflowDefinition.definitionId}`; - - const editIcon = ( - - - - - ); - - const deleteIcon = ( - - - - - - - - - ); - - return ( - + return + + + + + - - + + + - - - ); - })} - -
Name - Instances - -
+ + + {workflowInstance.id} + + + {displayName} + + + {instanceName} + -
-
-
4 - 999 - 75
-
-
+
- +
+ })} + + + +
+ this.confirmDialog = el}/>
- - this.confirmDialog = el}/>
); } @@ -193,10 +285,23 @@ export class ElsaWorkflowInstancesListScreen { const selectedWorkflowText = !selectedWorkflowId ? "Workflow" : !!selectedWorkflow ? (selectedWorkflow.displayName || selectedWorkflow.name) : "Workflow"; const selectedWorkflowStatus = this.selectedWorkflowStatus; const SelectedOrderBy = this.selectedOrderBy; - let items: Array = latestWorkflowBlueprints.map(x => ({text: x.displayName!, name: x.id, url: this.buildFilterUrl(x.id, selectedWorkflowStatus, SelectedOrderBy), isSelected: x.id == selectedWorkflowId})); + let items: Array = latestWorkflowBlueprints.map(x => ({text: x.displayName!, value: x.id, url: this.buildFilterUrl(x.id, selectedWorkflowStatus, SelectedOrderBy), isSelected: x.id == selectedWorkflowId})); - items = [{text: 'All', name: 'All', url: this.buildFilterUrl(null, selectedWorkflowStatus, SelectedOrderBy), isSelected: !selectedWorkflowId}, ...items]; - - return + items = [{text: 'All', value: null, url: this.buildFilterUrl(null, selectedWorkflowStatus, SelectedOrderBy), isSelected: !selectedWorkflowId}, ...items]; + + return this.selectedWorkflowId = e.detail.value as string}/> + } + + renderStatusFilter(){ + const selectedWorkflowStatus = this.selectedWorkflowStatus; + const selectedWorkflowStatusText = selectedWorkflowStatus != null ? this.getStatusText(selectedWorkflowStatus) : 'Status'; + const statuses: Array = [null, WorkflowStatus.Running, WorkflowStatus.Suspended, WorkflowStatus.Finished, WorkflowStatus.Faulted, WorkflowStatus.Cancelled, WorkflowStatus.Idle]; + const items: Array = statuses.map(x => { + const enumText = x != null ? this.getStatusText(x) : null; + const text = !!enumText ? enumText : 'All'; + return ({text: text, url: this.buildFilterUrl(this.selectedWorkflowId, x, this.selectedOrderBy), isSelected: x == selectedWorkflowStatus}); + }); + + return } } diff --git a/src/designer/elsa-workflows-studio/src/models/domain.ts b/src/designer/elsa-workflows-studio/src/models/domain.ts index 59cf118bc..b1a96379b 100644 --- a/src/designer/elsa-workflows-studio/src/models/domain.ts +++ b/src/designer/elsa-workflows-studio/src/models/domain.ts @@ -46,6 +46,24 @@ export interface WorkflowBlueprintSummary { isLatest: boolean; } +export interface WorkflowInstanceSummary +{ + id: string; + definitionId: string; + tenantId?: string; + version: number; + workflowStatus: WorkflowStatus; + correlationId?: string; + contextType?: string; + contextId?: string; + name?: string; + createdAt?: Date + lastExecutedAt?: Date; + finishedAt?: Date; + cancelledAt?: Date; + faultedAt?: Date; +} + export interface ActivityDefinition { activityId: string; type: string; diff --git a/src/designer/elsa-workflows-studio/src/services/elsa-client.ts b/src/designer/elsa-workflows-studio/src/services/elsa-client.ts index 577a2b063..74281f6a3 100644 --- a/src/designer/elsa-workflows-studio/src/services/elsa-client.ts +++ b/src/designer/elsa-workflows-studio/src/services/elsa-client.ts @@ -1,148 +1,176 @@ import axios, {AxiosRequestConfig} from "axios"; +import * as collection from 'lodash/collection'; import { - ActivityDefinition, - ActivityDescriptor, - ConnectionDefinition, - getVersionOptionsString, - PagedList, - Variables, - VersionOptions, WorkflowBlueprintSummary, - WorkflowContextOptions, - WorkflowDefinition, - WorkflowDefinitionSummary, - WorkflowPersistenceBehavior + ActivityDefinition, + ActivityDescriptor, + ConnectionDefinition, + getVersionOptionsString, OrderBy, + PagedList, + Variables, + VersionOptions, WorkflowBlueprintSummary, + WorkflowContextOptions, + WorkflowDefinition, + WorkflowDefinitionSummary, WorkflowInstanceSummary, + WorkflowPersistenceBehavior, WorkflowStatus } from "../models"; export const createElsaClient = function (serverUrl: string): ElsaClient { - const config: AxiosRequestConfig = { - baseURL: serverUrl - }; + const config: AxiosRequestConfig = { + baseURL: serverUrl + }; - const httpClient = axios.create(config); + const httpClient = axios.create(config); - return { - activitiesApi: { - list: async () => { - const response = await httpClient.get>('v1/activities'); - return response.data; - } - }, - workflowDefinitionsApi: { - list: async (page?: number, pageSize?: number, versionOptions?: VersionOptions) => { - const versionOptionsString = getVersionOptionsString(versionOptions); - const response = await httpClient.get>(`v1/workflow-definitions?version=${versionOptionsString}`); - return response.data; - }, - getByDefinitionAndVersion: async (definitionId: string, versionOptions: VersionOptions) => { - const versionOptionsString = getVersionOptionsString(versionOptions); - const response = await httpClient.get(`v1/workflow-definitions/${definitionId}/${versionOptionsString}`); - return response.data; - }, - save: async request => { - const response = await httpClient.post('v1/workflow-definitions', request); - return response.data; - }, - delete: async definitionId => { - await httpClient.delete(`v1/workflow-definitions/${definitionId}`); - }, - retract: async workflowDefinitionId => { - const response = await httpClient.post(`v1/workflow-definitions/${workflowDefinitionId}/retract`); - return response.data; - }, - export: async (workflowDefinitionId, versionOptions): Promise => { - const versionOptionsString = getVersionOptionsString(versionOptions); - const response = await httpClient.post(`v1/workflow-definitions/${workflowDefinitionId}/${versionOptionsString}/export`, null, { - responseType: 'blob' - }); + return { + activitiesApi: { + list: async () => { + const response = await httpClient.get>('v1/activities'); + return response.data; + } + }, + workflowDefinitionsApi: { + list: async (page?: number, pageSize?: number, versionOptions?: VersionOptions) => { + const versionOptionsString = getVersionOptionsString(versionOptions); + const response = await httpClient.get>(`v1/workflow-definitions?version=${versionOptionsString}`); + return response.data; + }, + getByDefinitionAndVersion: async (definitionId: string, versionOptions: VersionOptions) => { + const versionOptionsString = getVersionOptionsString(versionOptions); + const response = await httpClient.get(`v1/workflow-definitions/${definitionId}/${versionOptionsString}`); + return response.data; + }, + save: async request => { + const response = await httpClient.post('v1/workflow-definitions', request); + return response.data; + }, + delete: async definitionId => { + await httpClient.delete(`v1/workflow-definitions/${definitionId}`); + }, + retract: async workflowDefinitionId => { + const response = await httpClient.post(`v1/workflow-definitions/${workflowDefinitionId}/retract`); + return response.data; + }, + export: async (workflowDefinitionId, versionOptions): Promise => { + const versionOptionsString = getVersionOptionsString(versionOptions); + const response = await httpClient.post(`v1/workflow-definitions/${workflowDefinitionId}/${versionOptionsString}/export`, null, { + responseType: 'blob' + }); - const contentDispositionHeader = response.headers["content-disposition"]; // Only available if the Elsa Server exposes the "Content-Disposition" header. - const fileName = contentDispositionHeader ? contentDispositionHeader.split(";")[1].split("=")[1] : `workflow-definition-${workflowDefinitionId}.json`; - const data = response.data; + const contentDispositionHeader = response.headers["content-disposition"]; // Only available if the Elsa Server exposes the "Content-Disposition" header. + const fileName = contentDispositionHeader ? contentDispositionHeader.split(";")[1].split("=")[1] : `workflow-definition-${workflowDefinitionId}.json`; + const data = response.data; - return { - fileName: fileName, - data: data - }; - }, - import: async (workflowDefinitionId, file: File): Promise => { - const formData = new FormData(); - formData.append("file", file); - const response = await httpClient.post(`v1/workflow-definitions/${workflowDefinitionId}/import`, formData, { - headers: { - 'Content-Type': 'multipart/form-data' - } - }); - return response.data; - } - }, - workflowRegistryApi: { - list: async (page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise> => { - const versionOptionsString = getVersionOptionsString(versionOptions); - const response = await httpClient.get>(`v1/workflow-registry?version=${versionOptionsString}`); - return response.data; - } - }, - scriptingApi: { - getJavaScriptTypeDefinitions: async (workflowDefinitionId: string, context?: string): Promise => { - context = context || ''; - const response = await httpClient.get(`v1/scripting/javascript/type-definitions/${workflowDefinitionId}?t=${new Date().getTime()}&context=${context}`); - return response.data; - } + return { + fileName: fileName, + data: data + }; + }, + import: async (workflowDefinitionId, file: File): Promise => { + const formData = new FormData(); + formData.append("file", file); + const response = await httpClient.post(`v1/workflow-definitions/${workflowDefinitionId}/import`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }); + return response.data; + } + }, + workflowRegistryApi: { + list: async (page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise> => { + const versionOptionsString = getVersionOptionsString(versionOptions); + const response = await httpClient.get>(`v1/workflow-registry?version=${versionOptionsString}`); + return response.data; + } + }, + workflowInstancesApi: { + list: async (page?: number, pageSize?: number, workflowDefinitionId?: string, workflowStatus?: WorkflowStatus, orderBy?: OrderBy, searchTerm?: string): Promise> => { + const queryString = {}; + + if(!!workflowDefinitionId) + queryString['workflow'] = workflowDefinitionId; + + if(workflowStatus != null) + queryString['status'] = workflowStatus; + + if(!!orderBy) + queryString['orderBy'] = orderBy; + + if(!!searchTerm) + queryString['searchTerm'] = searchTerm; + + const queryStringItems = collection.map(queryString, (v, k) => `${k}=${v}`); + const queryStringText = queryStringItems.length > 0 ? `?${queryStringItems.join('&')}` : ''; + const response = await httpClient.get>(`v1/workflow-instances${queryStringText}`); + return response.data; + } + }, + scriptingApi: { + getJavaScriptTypeDefinitions: async (workflowDefinitionId: string, context?: string): Promise => { + context = context || ''; + const response = await httpClient.get(`v1/scripting/javascript/type-definitions/${workflowDefinitionId}?t=${new Date().getTime()}&context=${context}`); + return response.data; + } + } } - } } export interface ElsaClient { - activitiesApi: ActivitiesApi; - workflowDefinitionsApi: WorkflowDefinitionsApi; - workflowRegistryApi: WorkflowRegistryApi; - scriptingApi: ScriptingApi; + activitiesApi: ActivitiesApi; + workflowDefinitionsApi: WorkflowDefinitionsApi; + workflowRegistryApi: WorkflowRegistryApi; + workflowInstancesApi: WorkflowInstancesApi; + scriptingApi: ScriptingApi; } export interface ActivitiesApi { - list(): Promise>; + list(): Promise>; } export interface WorkflowDefinitionsApi { - getByDefinitionAndVersion(definitionId: string, versionOptions: VersionOptions): Promise + getByDefinitionAndVersion(definitionId: string, versionOptions: VersionOptions): Promise - list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise> + list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise> - save(request: SaveWorkflowDefinitionRequest): Promise + save(request: SaveWorkflowDefinitionRequest): Promise - delete(definitionId: string): Promise + delete(definitionId: string): Promise - retract(workflowDefinitionId: string): Promise + retract(workflowDefinitionId: string): Promise - export(workflowDefinitionId: string, versionOptions: VersionOptions): Promise + export(workflowDefinitionId: string, versionOptions: VersionOptions): Promise - import(workflowDefinitionId: string, file: File): Promise + import(workflowDefinitionId: string, file: File): Promise } export interface WorkflowRegistryApi { - list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise> + list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise> +} + +export interface WorkflowInstancesApi { + list(page?: number, pageSize?: number, workflowDefinitionId?: string, workflowStatus?: WorkflowStatus, orderBy?: OrderBy, searchTerm?: string): Promise> } export interface ScriptingApi { - getJavaScriptTypeDefinitions(workflowDefinitionId: string, context?: string): Promise + getJavaScriptTypeDefinitions(workflowDefinitionId: string, context?: string): Promise } export interface SaveWorkflowDefinitionRequest { - workflowDefinitionId?: string - name?: string - displayName?: string - description?: string - variables?: Variables - contextOptions?: WorkflowContextOptions - isSingleton?: boolean - persistenceBehavior?: WorkflowPersistenceBehavior - deleteCompletedInstances?: boolean - publish?: boolean - activities: Array - connections: Array + workflowDefinitionId?: string + name?: string + displayName?: string + description?: string + variables?: Variables + contextOptions?: WorkflowContextOptions + isSingleton?: boolean + persistenceBehavior?: WorkflowPersistenceBehavior + deleteCompletedInstances?: boolean + publish?: boolean + activities: Array + connections: Array } export interface ExportWorkflowResponse { - fileName: string; - data: Blob; + fileName: string; + data: Blob; } diff --git a/src/designer/elsa-workflows-studio/src/utils/utils.ts b/src/designer/elsa-workflows-studio/src/utils/utils.ts index c6e39efcc..d7dc7f93c 100644 --- a/src/designer/elsa-workflows-studio/src/utils/utils.ts +++ b/src/designer/elsa-workflows-studio/src/utils/utils.ts @@ -1,201 +1,215 @@ import {ActivityDefinition, ActivityDefinitionProperty, ActivityModel, ConnectionModel, WorkflowModel} from "../models"; declare global { - interface Array { - distinct(): Array; + interface Array { + distinct(): Array; - last(): T; - } + last(): T; + } } export type Map = { - [key: string]: T + [key: string]: T }; export function format(first: string, middle: string, last: string): string { - return (first || '') + (middle ? ` ${middle}` : '') + (last ? ` ${last}` : ''); + return (first || '') + (middle ? ` ${middle}` : '') + (last ? ` ${last}` : ''); } export interface Array { - distinct(): Array; + distinct(): Array; - last(): T; + last(): T; - find(predicate: (this: void, value: T, index: number, obj: T[]) => value is S, thisArg?: any): S | undefined; + find(predicate: (this: void, value: T, index: number, obj: T[]) => value is S, thisArg?: any): S | undefined; - find(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): T | undefined; + find(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): T | undefined; - push(...items: T[]): number; + push(...items: T[]): number; } Array.prototype.distinct = function () { - return [...new Set(this)]; + return [...new Set(this)]; } if (!Array.prototype.last) { - Array.prototype.last = function () { - return this[this.length - 1]; - }; + Array.prototype.last = function () { + return this[this.length - 1]; + }; } export function isNumeric(str: string): boolean { - return !isNaN(str as any) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)... - !isNaN(parseFloat(str)) // ...and ensure strings of whitespace fail + return !isNaN(str as any) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)... + !isNaN(parseFloat(str)) // ...and ensure strings of whitespace fail } export function getChildActivities(workflowModel: WorkflowModel, parentId?: string) { - if (parentId == null) { - const targetIds = new Set(workflowModel.connections.map(x => x.targetId)); - return workflowModel.activities.filter(x => !targetIds.has(x.activityId)); - } else { - const targetIds = new Set(workflowModel.connections.filter(x => x.sourceId === parentId).map(x => x.targetId)); - return workflowModel.activities.filter(x => targetIds.has(x.activityId)); - } + if (parentId == null) { + const targetIds = new Set(workflowModel.connections.map(x => x.targetId)); + return workflowModel.activities.filter(x => !targetIds.has(x.activityId)); + } else { + const targetIds = new Set(workflowModel.connections.filter(x => x.sourceId === parentId).map(x => x.targetId)); + return workflowModel.activities.filter(x => targetIds.has(x.activityId)); + } } export function getInboundConnections(workflowModel: WorkflowModel, activityId: string) { - return workflowModel.connections.filter(x => x.targetId === activityId); + return workflowModel.connections.filter(x => x.targetId === activityId); } export function getOutboundConnections(workflowModel: WorkflowModel, activityId: string) { - return workflowModel.connections.filter(x => x.sourceId === activityId); + return workflowModel.connections.filter(x => x.sourceId === activityId); } export function removeActivity(workflowModel: WorkflowModel, activityId: string): WorkflowModel { - const inboundConnections = getInboundConnections(workflowModel, activityId); - const outboundConnections = getOutboundConnections(workflowModel, activityId); - const connectionsToRemove = [...inboundConnections, ...outboundConnections]; + const inboundConnections = getInboundConnections(workflowModel, activityId); + const outboundConnections = getOutboundConnections(workflowModel, activityId); + const connectionsToRemove = [...inboundConnections, ...outboundConnections]; - return { - ...workflowModel, - activities: workflowModel.activities.filter(x => x.activityId != activityId), - connections: workflowModel.connections.filter(x => connectionsToRemove.indexOf(x) < 0) - }; + return { + ...workflowModel, + activities: workflowModel.activities.filter(x => x.activityId != activityId), + connections: workflowModel.connections.filter(x => connectionsToRemove.indexOf(x) < 0) + }; } export function findActivity(workflowModel: WorkflowModel, activityId: string) { - return workflowModel.activities.find(x => x.activityId === activityId); + return workflowModel.activities.find(x => x.activityId === activityId); } export function addConnection(workflowModel: WorkflowModel, connection: ConnectionModel); export function addConnection(workflowModel: WorkflowModel, sourceId: string, targetId: string, outcome: string); export function addConnection(workflowModel: WorkflowModel, ...args: any) { - const connection = typeof (args) == 'object' ? args as ConnectionModel : {sourceId: args[0], targetId: args[1], outcome: args[3]}; + const connection = typeof (args) == 'object' ? args as ConnectionModel : {sourceId: args[0], targetId: args[1], outcome: args[3]}; - return { - ...workflowModel, - connections: [...workflowModel.connections, connection] - }; + return { + ...workflowModel, + connections: [...workflowModel.connections, connection] + }; } export function setActivityDefinitionProperty(activityDefinition: ActivityDefinition, name: string, expression: string, syntax: string) { - setProperty(activityDefinition.properties, name, expression, syntax); + setProperty(activityDefinition.properties, name, expression, syntax); } export function setActivityModelProperty(activityModel: ActivityModel, name: string, expression: string, syntax: string) { - setProperty(activityModel.properties, name, expression, syntax); + setProperty(activityModel.properties, name, expression, syntax); } export function setProperty(properties: Array, name: string, expression: string, syntax?: string) { - let property: ActivityDefinitionProperty = properties.find(x => x.name == name); + let property: ActivityDefinitionProperty = properties.find(x => x.name == name); - if(!syntax) - syntax = 'Literal'; + if (!syntax) + syntax = 'Literal'; - if (!property) { - const expressions = {}; - expressions[syntax] = expression; - property = {name: name, expressions: expressions, syntax: syntax}; - properties.push(property); - } else { - property.expressions[syntax] = expression; - property.syntax = syntax; - } + if (!property) { + const expressions = {}; + expressions[syntax] = expression; + property = {name: name, expressions: expressions, syntax: syntax}; + properties.push(property); + } else { + property.expressions[syntax] = expression; + property.syntax = syntax; + } } -export function getOrCreateProperty(activity: ActivityModel , name: string, defaultExpression?: () => string, defaultSyntax?: () => string): ActivityDefinitionProperty { - let property: ActivityDefinitionProperty = activity.properties.find(x => x.name == name); +export function getOrCreateProperty(activity: ActivityModel, name: string, defaultExpression?: () => string, defaultSyntax?: () => string): ActivityDefinitionProperty { + let property: ActivityDefinitionProperty = activity.properties.find(x => x.name == name); - if (!property) { - const expressions = {}; - let syntax = defaultSyntax ? defaultSyntax() : undefined; + if (!property) { + const expressions = {}; + let syntax = defaultSyntax ? defaultSyntax() : undefined; - if(!syntax) - syntax = 'Literal'; + if (!syntax) + syntax = 'Literal'; - expressions[syntax] = defaultExpression ? defaultExpression() : undefined; - property = {name: name, expressions: expressions, syntax: null}; - activity.properties.push(property); - } + expressions[syntax] = defaultExpression ? defaultExpression() : undefined; + property = {name: name, expressions: expressions, syntax: null}; + activity.properties.push(property); + } - return property; + return property; } export function parseJson(json: string): any { - try { - return JSON.parse(json); - } catch (e) { - console.warn(`Error parsing JSON: ${e}`); - } - return undefined; + try { + return JSON.parse(json); + } catch (e) { + console.warn(`Error parsing JSON: ${e}`); + } + return undefined; +} + +export function parseQuery(queryString?: string): any { + + if (!queryString) + return {}; + + const query = {}; + const pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&'); + for (let i = 0; i < pairs.length; i++) { + const pair = pairs[i].split('='); + query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || ''); + } + return query; } export function timeSince(time) { - switch (typeof time) { - case 'number': - break; - case 'string': - time = +new Date(time); - break; - case 'object': - if (time.constructor === Date) time = time.getTime(); - break; - default: - time = +new Date(); - } - const time_formats = [ - [60, 'seconds', 1], // 60 - [120, '1 minute ago', '1 minute from now'], // 60*2 - [3600, 'minutes', 60], // 60*60, 60 - [7200, '1 hour ago', '1 hour from now'], // 60*60*2 - [86400, 'hours', 3600], // 60*60*24, 60*60 - [172800, 'Yesterday', 'Tomorrow'], // 60*60*24*2 - [604800, 'days', 86400], // 60*60*24*7, 60*60*24 - [1209600, 'Last week', 'Next week'], // 60*60*24*7*4*2 - [2419200, 'weeks', 604800], // 60*60*24*7*4, 60*60*24*7 - [4838400, 'Last month', 'Next month'], // 60*60*24*7*4*2 - [29030400, 'months', 2419200], // 60*60*24*7*4*12, 60*60*24*7*4 - [58060800, 'Last year', 'Next year'], // 60*60*24*7*4*12*2 - [2903040000, 'years', 29030400], // 60*60*24*7*4*12*100, 60*60*24*7*4*12 - [5806080000, 'Last century', 'Next century'], // 60*60*24*7*4*12*100*2 - [58060800000, 'centuries', 2903040000] // 60*60*24*7*4*12*100*20, 60*60*24*7*4*12*100 - ]; - let seconds = (+new Date() - time) / 1000, - token = 'ago', - list_choice = 1; - - if (seconds === 0) { - return 'Just now'; - } - if (seconds < 0) { - seconds = Math.abs(seconds); - token = 'from now'; - list_choice = 2; - } - let i = 0, - format; - - // tslint:disable-next-line: no-conditional-assignment - while (format = time_formats[i++]) { - if (seconds < format[0]) { - if (typeof format[2] === 'string') { - return format[list_choice]; - } else { - return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token; - } + switch (typeof time) { + case 'number': + break; + case 'string': + time = +new Date(time); + break; + case 'object': + if (time.constructor === Date) time = time.getTime(); + break; + default: + time = +new Date(); } - } - return time; + const time_formats = [ + [60, 'seconds', 1], // 60 + [120, '1 minute ago', '1 minute from now'], // 60*2 + [3600, 'minutes', 60], // 60*60, 60 + [7200, '1 hour ago', '1 hour from now'], // 60*60*2 + [86400, 'hours', 3600], // 60*60*24, 60*60 + [172800, 'Yesterday', 'Tomorrow'], // 60*60*24*2 + [604800, 'days', 86400], // 60*60*24*7, 60*60*24 + [1209600, 'Last week', 'Next week'], // 60*60*24*7*4*2 + [2419200, 'weeks', 604800], // 60*60*24*7*4, 60*60*24*7 + [4838400, 'Last month', 'Next month'], // 60*60*24*7*4*2 + [29030400, 'months', 2419200], // 60*60*24*7*4*12, 60*60*24*7*4 + [58060800, 'Last year', 'Next year'], // 60*60*24*7*4*12*2 + [2903040000, 'years', 29030400], // 60*60*24*7*4*12*100, 60*60*24*7*4*12 + [5806080000, 'Last century', 'Next century'], // 60*60*24*7*4*12*100*2 + [58060800000, 'centuries', 2903040000] // 60*60*24*7*4*12*100*20, 60*60*24*7*4*12*100 + ]; + let seconds = (+new Date() - time) / 1000, + token = 'ago', + list_choice = 1; + + if (seconds === 0) { + return 'Just now'; + } + if (seconds < 0) { + seconds = Math.abs(seconds); + token = 'from now'; + list_choice = 2; + } + let i = 0, + format; + + // tslint:disable-next-line: no-conditional-assignment + while (format = time_formats[i++]) { + if (seconds < format[0]) { + if (typeof format[2] === 'string') { + return format[list_choice]; + } else { + return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token; + } + } + } + return time; }