Skip to content
4 changes: 2 additions & 2 deletions app/guid-node/metadata/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@
@changed={{action this.schemaChanged}}
>
<div>
{{schema.name}}
{{schema.localizedName}}
<span>
{{fa-icon 'info-circle'}}
<BsTooltip>
{{schema.schema.description}}
{{schema.localizedDescription}}
</BsTooltip>
</span>
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/guid-node/registrations/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,11 @@
@changed={{action this.schemaChanged}}
>
<div>
{{schema.name}}
{{schema.localizedName}}
<span>
{{fa-icon 'info-circle'}}
<BsTooltip>
{{schema.schema.description}}
{{schema.localizedDescription}}
</BsTooltip>
</span>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

import Node from 'ember-osf-web/models/node';

import { WorkflowVariable } from '../../../types';
import { FieldHint } from '../../wizard-form/types';
import { resolveFlowableType } from '../component';
import { FieldValueWithType, WorkflowTaskField } from '../types';

interface ArrayInputRow {
key: number;
values: Record<string, unknown>;
}

interface ArrayInputArgs {
fields: WorkflowTaskField[];
value: FieldValueWithType | undefined;
node?: Node;
fieldHints?: Record<string, FieldHint>;
disabled: boolean;
onChange: (valueWithType: FieldValueWithType) => void;
}

export default class ArrayInput extends Component<ArrayInputArgs> {
@tracked rows: ArrayInputRow[] = [];
@tracked isInitialized = false;

private nextKey = 0;

get rowForms(): Array<{
row: ArrayInputRow;
label: string;
form: { fields: WorkflowTaskField[] };
variables: WorkflowVariable[];
}> {
return this.rows.map((row, index) => ({
row,
label: `#${index + 1}`,
form: { fields: this.args.fields },
variables: this.buildVariablesForRow(row),
}));
}

@action
initialize(): void {
if (this.isInitialized) {
return;
}
this.isInitialized = true;

const existing = this.args.value;
if (existing && Array.isArray(existing.value)) {
const items = existing.value as Array<Record<string, unknown>>;
this.rows = items.map(item => ({
key: this.allocateKey(),
values: { ...item },
}));
}
}

@action
addRow(): void {
this.rows = [
...this.rows,
{ key: this.allocateKey(), values: {} },
];
this.notifyChange();
}

@action
removeRow(key: number): void {
this.rows = this.rows.filter(row => row.key !== key);
this.notifyChange();
}

@action
handleRowChange(key: number, variables: WorkflowVariable[]): void {
const row = this.rows.find(r => r.key === key);
if (!row) {
return;
}
const values: Record<string, unknown> = {};
for (const v of variables) {
values[v.name] = v.value;
}
row.values = values;
this.notifyChange();
}

private allocateKey(): number {
return this.nextKey++;
}

private buildVariablesForRow(row: ArrayInputRow): WorkflowVariable[] {
return this.args.fields
.filter(field => !this.isDisplayField(field))
.map(field => ({
name: field.id,
value: row.values[field.id] !== undefined ? row.values[field.id] : null,
type: resolveFlowableType(field.type),
}));
}

private isDisplayField(field: WorkflowTaskField): boolean {
const type = field.type.toLowerCase();
return ['expression', 'hyperlink', 'link', 'headline', 'headline-with-line', 'spacer', 'horizontal-line']
.includes(type);
}

private notifyChange(): void {
const value = this.rows.map(row => row.values);
this.args.onChange({
value,
type: 'json',
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.ArrayInput {
border: 1px solid #ddd;
border-radius: 4px;
padding: 12px;
background: #fafafa;
}

.ArrayInput__row {
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 12px;
margin-bottom: 12px;
background: #fff;
}

.ArrayInput__rowHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid #eee;
}

.ArrayInput__rowIndex {
font-weight: 600;
color: #666;
font-size: 0.9em;
}

.ArrayInput__addButton {
margin-top: 4px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<div local-class="ArrayInput" {{did-insert this.initialize}}>
{{#each this.rowForms as |entry|}}
<div local-class="ArrayInput__row">
<div local-class="ArrayInput__rowHeader">
<span local-class="ArrayInput__rowIndex">{{entry.label}}</span>
{{#unless @disabled}}
<button
type="button"
class="btn btn-xs btn-danger"
{{on "click" (fn this.removeRow entry.row.key)}}
>
<i class="fa fa-trash"></i>
</button>
{{/unless}}
</div>
<GuidNode::Workflow::-Components::FlowableForm
@form={{entry.form}}
@variables={{entry.variables}}
@node={{@node}}
@fieldHints={{@fieldHints}}
@onChange={{fn this.handleRowChange entry.row.key}}
/>
</div>
{{/each}}

{{#unless @disabled}}
<button
type="button"
class="btn btn-sm btn-default"
local-class="ArrayInput__addButton"
{{on "click" this.addRow}}
>
<i class="fa fa-plus"></i>
{{t 'workflow.console.tasks.dialog.arrayInput.add'}}
</button>
{{/unless}}
</div>
83 changes: 79 additions & 4 deletions app/guid-node/workflow/-components/flowable-form/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,26 @@ import {
WorkflowTaskForm,
WorkflowVariable,
} from '../../types';
import { evaluateExpression } from '../wizard-form/expression-evaluator';
import { FieldHint } from '../wizard-form/types';
import { isValidFieldValue } from './field/component';
import { FieldValueWithType } from './types';

/**
* Interface for Field components to call back into their parent form.
* Provides setValue() for autofill: always goes through the view layer (View → Model).
*/
export interface FlowableFormContext {
setFieldValue(fieldId: string, valueWithType: FieldValueWithType): void;
}

interface FlowableFormArgs {
form: WorkflowTaskForm;
variables?: WorkflowVariable[];
node?: Node;
onChange: (variables: WorkflowVariable[], isValid: boolean) => void;
fieldHints?: Record<string, FieldHint>;
fieldContext?: Record<string, unknown>;
onChange: (variables: WorkflowVariable[], isValid: boolean, isLoading: boolean) => void;
}

export function resolveFlowableType(fieldType: string | undefined): string {
Expand All @@ -38,26 +50,67 @@ export function resolveFlowableType(fieldType: string | undefined): string {
return 'string';
}

interface FieldHandle {
setValue(valueWithType: FieldValueWithType): void;
}

export default class FlowableForm extends Component<FlowableFormArgs> {
@tracked fieldValues: Record<string, FieldValueWithType> = {};
@tracked updatedFieldValues: Record<string, FieldValueWithType> = {};
@tracked loadingFieldIds: Set<string> = new Set();

private fieldRegistry = new Map<string, FieldHandle>();

get formContext(): FlowableFormContext {
return {
setFieldValue: (fieldId: string, valueWithType: FieldValueWithType) => {
this.fieldRegistry.get(fieldId)!.setValue(valueWithType);
},
};
}

get fields(): WorkflowTaskField[] {
return this.args.form.fields || [];
}

get visibleFields(): WorkflowTaskField[] {
return this.fields.filter(field => this.isFieldVisible(field));
}

get hasFields(): boolean {
return this.fields.length > 0;
}

isFieldVisible(field: WorkflowTaskField): boolean {
const hints = this.args.fieldHints;
if (!hints) {
return true;
}
const hint = hints[field.id];
if (!hint || hint.visible === undefined || hint.visible === true) {
return true;
}
if (hint.visible === false) {
return false;
}
const ctx = this.args.fieldContext;
if (!ctx) {
return true;
}
return evaluateExpression(hint.visible, ctx);
}

get isValid(): boolean {
return this.fields
.filter(field => this.isSubmittableField(field))
.filter(field => this.isSubmittableField(field) && this.isFieldVisible(field))
.every(field => {
const fieldValue = this.updatedFieldValues[field.id];
if (fieldValue && fieldValue.valid === false) {
return false;
}
if (!field.required) {
return true;
}
const fieldValue = this.updatedFieldValues[field.id];
const value = fieldValue && fieldValue.value;
return isValidFieldValue(field, value);
});
Expand Down Expand Up @@ -97,6 +150,16 @@ export default class FlowableForm extends Component<FlowableFormArgs> {
this.notifyChange();
}

@action
registerField(fieldId: string, handle: FieldHandle): void {
this.fieldRegistry.set(fieldId, handle);
}

@action
unregisterField(fieldId: string): void {
this.fieldRegistry.delete(fieldId);
}

@action
handleFieldChange(fieldId: string, valueWithType: FieldValueWithType): void {
this.updatedFieldValues = {
Expand All @@ -106,6 +169,18 @@ export default class FlowableForm extends Component<FlowableFormArgs> {
this.notifyChange();
}

@action
handleFieldLoadingChange(fieldId: string, isLoading: boolean): void {
const next = new Set(this.loadingFieldIds);
if (isLoading) {
next.add(fieldId);
} else {
next.delete(fieldId);
}
this.loadingFieldIds = next;
this.notifyChange();
}

private isSubmittableField(field: WorkflowTaskField): boolean {
const type = field.type.toLowerCase();
const displayOnlyTypes = [
Expand All @@ -131,6 +206,6 @@ export default class FlowableForm extends Component<FlowableFormArgs> {
};
});

this.args.onChange(variables, this.isValid);
this.args.onChange(variables, this.isValid, this.loadingFieldIds.size > 0);
}
}
Loading
Loading