Skip to content

Commit 2408562

Browse files
Merge pull request #192 from CodeForPhilly/screener-form-testing
FormEditor Updates, Yes/No Question Component
2 parents e3a0a68 + a1be028 commit 2408562

File tree

6 files changed

+226
-31
lines changed

6 files changed

+226
-31
lines changed

builder-frontend/src/components/project/FormEditorView.jsx renamed to builder-frontend/src/components/project/FormEditorView.tsx

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { onMount, onCleanup, createSignal } from "solid-js";
1+
import { onMount, onCleanup, createSignal, Switch, Match } from "solid-js";
22
import { useParams } from "@solidjs/router";
33

44
import { FormEditor } from "@bpmn-io/form-js-editor";
55

6+
import FilterFormComponentsModule from "./formJsExtensions/FilterFormComponentsModule";
7+
import CustomFormFieldsModule from "./formJsExtensions/customFormFields";
8+
69
import { saveFormSchema } from "../../api/screener";
710

811
import "@bpmn-io/form-js/dist/assets/form-js.css";
@@ -16,7 +19,7 @@ function FormEditorView({ formSchema, setFormSchema }) {
1619

1720
let timeoutId;
1821
let container;
19-
let formEditor;
22+
let formEditor: FormEditor;
2023
let emptySchema = {
2124
components: [],
2225
exporter: { name: "form-js (https://demo.bpmn.io)", version: "1.15.0" },
@@ -26,7 +29,13 @@ function FormEditorView({ formSchema, setFormSchema }) {
2629
};
2730

2831
onMount(() => {
29-
formEditor = new FormEditor({ container });
32+
formEditor = new FormEditor({
33+
container,
34+
additionalModules: [
35+
FilterFormComponentsModule,
36+
CustomFormFieldsModule
37+
],
38+
});
3039

3140
if (formSchema()) {
3241
formEditor.importSchema(formSchema()).catch((err) => {
@@ -63,30 +72,41 @@ function FormEditorView({ formSchema, setFormSchema }) {
6372
};
6473

6574
return (
66-
<>
67-
<div className="overflow-auto">
68-
<div className="h-full" ref={(el) => (container = el)} />
75+
<div class="flex flex-row">
76+
<div class="flex-8 overflow-auto">
77+
<div class="h-full" ref={(el) => (container = el)} />
6978
</div>
70-
71-
<div className="fixed z-50 top-19 right-4 flex ml-auto mr-8 gap-2 justify-center">
72-
{isUnsaved() && (
73-
<span className="underline text-sm flex items-center text-gray-500">
74-
unsaved changes
75-
</span>
76-
)}
77-
{isSaving() && (
78-
<span className="text-sm flex items-center text-gray-500">
79-
saving ...
80-
</span>
81-
)}
82-
<button
83-
onClick={handleSave}
84-
className="px-2 text-emerald-500 h-8 border-2 rounded hover:bg-emerald-100"
85-
>
86-
Save
87-
</button>
79+
<div class="flex-1 border-l-4 border-l-gray-200">
80+
<div class="flex flex-col p-10 gap-4">
81+
<Switch>
82+
<Match when={isUnsaved()}>
83+
<button
84+
onClick={handleSave}
85+
class="px-2 text-yellow-500 h-8 border-2 rounded hover:bg-yellow-100"
86+
>
87+
Save changes
88+
</button>
89+
</Match>
90+
<Match when={isSaving()}>
91+
<button
92+
onClick={handleSave}
93+
class="px-2 text-gray-300 h-8 border-2 rounded"
94+
>
95+
Saving...
96+
</button>
97+
</Match>
98+
<Match when={!isUnsaved() && !isSaving()}>
99+
<button
100+
onClick={handleSave}
101+
class="px-2 text-emerald-500 h-8 border-2 rounded hover:bg-emerald-100"
102+
>
103+
Save changes
104+
</button>
105+
</Match>
106+
</Switch>
107+
</div>
88108
</div>
89-
</>
109+
</div>
90110
);
91111
}
92112

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Form-js module that overwrites the "formFields" service.
3+
* The Module extends FormFields, but has different registration logic
4+
* to skip certain field types.
5+
*
6+
* Based on FormFields from the following location:
7+
* https://github.com/bpmn-io/form-js/blob/develop/packages/form-js-viewer/src/render/FormFields.js
8+
*/
9+
import { FormFields } from "@bpmn-io/form-js-viewer";
10+
11+
const FIELD_TYPES_TO_SKIP = [
12+
"documentPreview",
13+
"expression",
14+
"file",
15+
"filepicker",
16+
"html",
17+
"iframe",
18+
"image",
19+
]
20+
21+
class FilterFormComponentsModule extends FormFields {
22+
register(type: string, formField: any) {
23+
if (FIELD_TYPES_TO_SKIP.includes(type)) {
24+
// Skip registering this form field type
25+
return;
26+
}
27+
this._formFields[type] = formField;
28+
}
29+
}
30+
export default {
31+
__init__: ['formFields'],
32+
formFields: ['type', FilterFormComponentsModule]
33+
};
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* File's contents are an augmented version of Radio.js from the following location:
3+
* https://github.com/bpmn-io/form-js/blob/develop/packages/form-js-viewer/src/render/components/form-fields/Radio.js
4+
* The Preact component has been modified to create a Yes/No Question form field.
5+
*
6+
* Helper functions [formFieldClasses(...)] and Preact "HTM" syntax taken from the following location:
7+
* https://github.com/bpmn-io/form-js-examples/blob/master/custom-components/app/extension/render/Range.js
8+
*/
9+
import { html } from 'htm/preact';
10+
import { useRef } from 'preact/hooks';
11+
12+
import classNames from 'classnames';
13+
import isEqual from 'lodash/isEqual';
14+
15+
import { Radio, Description, Errors, Label } from '@bpmn-io/form-js';
16+
import { iconsByType } from '@bpmn-io/form-js-viewer';
17+
18+
19+
const YES_NO_TYPE = "yes_no";
20+
21+
export function YesNoQuestion(props: any) {
22+
const { disabled, errors = [], domId, onBlur, onFocus, field, readonly, value } = props;
23+
const { description, label, validate = {} } = field;
24+
const { required } = validate;
25+
26+
const descriptionId = `${domId}-description`;
27+
const errorMessageId = `${domId}-error-message`;
28+
29+
/* Handle focus/blur */
30+
const outerDivRef = useRef<HTMLDivElement>();
31+
const onRadioBlur = (e: FocusEvent) => {
32+
if (outerDivRef.current.contains(e.relatedTarget as Node)) {
33+
return;
34+
}
35+
onBlur && onBlur();
36+
};
37+
const onRadioFocus = (e: FocusEvent) => {
38+
if (outerDivRef.current.contains(e.relatedTarget as Node)) {
39+
return;
40+
}
41+
onFocus && onFocus();
42+
};
43+
44+
/* Handle options */
45+
const onChange = (newValue: boolean) => {
46+
props.onChange({value: newValue});
47+
};
48+
const yesNoOptions = [
49+
{ label: 'Yes', value: true },
50+
{ label: 'No', value: false }
51+
];
52+
53+
return (html`
54+
<div class=${formFieldClasses(YES_NO_TYPE, { errors, disabled, readonly })} ref=${outerDivRef}>
55+
<${Label} label=${label} required=${required} />
56+
${yesNoOptions.map((option, index) => {
57+
const itemDomId = `${domId}-${index}`;
58+
const isChecked = isEqual(option.value, value);
59+
return (html`
60+
<div
61+
className=${classNames('fjs-inline-label', {'fjs-checked': isChecked})}
62+
key=${option.value}>
63+
<input
64+
checked=${isChecked}
65+
class="fjs-input"
66+
disabled=${disabled}
67+
readOnly=${readonly}
68+
name=${domId}
69+
id=${itemDomId}
70+
type="radio"
71+
onClick=${() => onChange(option.value)}
72+
onBlur=${onRadioBlur}
73+
onFocus=${onRadioFocus}
74+
aria-describedby=${[descriptionId, errorMessageId].join(' ')}
75+
required=${required}
76+
aria-invalid=${errors.length > 0}
77+
/>
78+
<${Label}
79+
htmlFor=${itemDomId}
80+
label=${option.label}
81+
class=${classNames({ 'fjs-checked': isChecked })}
82+
required=${false}
83+
/>
84+
</div>`
85+
);
86+
})}
87+
<${Description} description=${description} />
88+
<${Errors} errors=${errors} />
89+
</div>`
90+
);
91+
}
92+
93+
YesNoQuestion.config = {
94+
/* Extend the default configuration of Radio Groups */
95+
...Radio.config,
96+
type: YES_NO_TYPE,
97+
name: 'Yes/No',
98+
label: 'Yes/No question',
99+
icon: iconsByType("radio"),
100+
group: 'selection',
101+
propertiesPanelEntries: [
102+
'key',
103+
'label',
104+
'description',
105+
'disabled',
106+
'readonly'
107+
]
108+
};
109+
110+
function formFieldClasses(type, { errors = [], disabled = false, readonly = false } = {}) {
111+
if (!type) {
112+
throw new Error('type required');
113+
}
114+
115+
return classNames(
116+
'fjs-form-field',
117+
`fjs-form-field-${type}`,
118+
{
119+
'fjs-has-errors': errors.length > 0,
120+
'fjs-disabled': disabled,
121+
'fjs-readonly': readonly
122+
}
123+
);
124+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { YesNoQuestion } from './YesNoQuestion';
2+
3+
/*
4+
* This is a module definition to register custom
5+
* form fields with the form-js FormEditor.
6+
*/
7+
class CustomFormFieldsModule {
8+
constructor(formFields) {
9+
formFields.register(YesNoQuestion.config.type, YesNoQuestion);
10+
}
11+
}
12+
13+
export default {
14+
__init__: [ 'customFields' ],
15+
customFields: [ 'type', CustomFormFieldsModule ]
16+
};

builder-frontend/src/components/project/preview/FormRenderer.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import debounce from "lodash.debounce";
44
import { Form } from "@bpmn-io/form-js-viewer";
55

66
import { PreviewFormData } from "./types";
7+
import CustomFormFieldsModule from "../formJsExtensions/customFormFields";
78

89
import "@bpmn-io/form-js/dist/assets/form-js.css";
910

1011
function FormRenderer({ schema, submitForm }: { schema: Accessor<any>; submitForm: (data: PreviewFormData) => void }) {
1112
let container: HTMLDivElement | undefined;
1213

1314
onMount(() => {
14-
const form = new Form({ container });
15+
const form = new Form({ container, additionalModules: [ CustomFormFieldsModule ] });
1516
const debouncedSubmit = debounce(
1617
(data: PreviewFormData) => submitForm(data),
1718
500

builder-frontend/src/components/project/preview/Results.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,16 @@ export default function Results(
2222
<>
2323
<div class="text-md font-semibold text-gray-600">Inputs</div>
2424
<div class="p-2">
25-
<div class="flex flex-col">
25+
<table class="bg-gray-200 p-3 rounded-md">
2626
<For each={Object.entries(inputData())}>
2727
{([key, value]) => (
28-
<div class="flex text-md text-gray-700">
29-
<span class="font-medium capitalize">{key}: {value?.toString() || "--"}</span>
30-
</div>
28+
<tr class="text-md text-gray-700">
29+
<td class="px-3 py-2 font-mono font-bold">{key}:</td>
30+
<td class="px-3 py-2 font-mono">{value?.toString() || "--"}</td>
31+
</tr>
3132
)}
3233
</For>
33-
</div>
34+
</table>
3435
</div>
3536
</>
3637
)}

0 commit comments

Comments
 (0)