Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
const existing = this.args.value;
if (!existing || !Array.isArray(existing.value)) {
return;
}
this.isInitialized = true;
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}} {{did-update this.initialize @value}}>
{{#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