Feature/side by side layout (#858)

* WIP: work for side-by-side layout in json forms w/ burnettk

* added support in ObjectFieldTemplate to support side-by-side layout w/ burnettk

* added a restricted grid template for half width forms

* display the error message if field mentioned in ui:layout but the field does not exist

* added docs for side-by-side layout w/ burnettk

* update script

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
Co-authored-by: burnettk <burnettk@users.noreply.github.com>
This commit is contained in:
jasquat 2024-01-05 15:49:40 -05:00 committed by GitHub
parent 4f5a590e1e
commit d5c5bc12ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 198 additions and 17 deletions

View File

@ -170,6 +170,59 @@ This is an example where end_date must be after start_date:
These enhancements provide you with more flexibility and control when building forms in SpiffArena.
By using these features, you can create dynamic, validated forms that enhance the user experience and support your business processes effectively.
### Display Fields Side-By-Side on Same Row
By default, all form fields will be laid out one on top of the other.
In some cases, it might be more user-friendly to put two or more fields next to each other on the same conceptual "row."
Perhaps, you want to let a user fill out a name, and have First Name and Last Name next to each other.
Don't actually do this; use Full name as a single field. :)
But in some other case where you actually want to have fields laid out horizontally instead of vertically, do the following:
Example form schema:
{
"firstName": {
"type": "string",
},
lastName": {
"type": "string",
}
}
Example uiSchema:
{
"ui:layout": [
{
"firstName": {
"sm": 2,
"md": 2,
"lg": 4
},
"lastName": {
"sm": 2,
"md": 2,
"lg": 4
}
}
]
}
In this case, we are saying that we want firstName and lastName in the same row, since there is only one "row" in the UI layout (one element of the array).
We are saying that firstName should take up 4 columns when a large display is used.
The lastName also takes up 4 columns, which fills up the whole row, which has 8 columns available for large displays.
Medium displays have 5 columns available and small displays have 4.
If you just specific a uiSchema like this, it will figure out the column widths for you:
{
"ui:layout": [
{
"firstName": {},
"lastName": {}
}
]
}
### Display UI Help in Web Forms
When designing web forms, it's essential to provide users with contextual help to ensure they understand the purpose and requirements of each field.

View File

@ -7,6 +7,6 @@ function error_handler() {
trap 'error_handler ${LINENO} $?' ERR
set -o errtrace -o errexit -o nounset -o pipefail
gitc Building_Diagrams/sub-processes_and_call_activities.md
python bin/gpt-proofread.py Building_Diagrams/sub-processes_and_call_activities.md
mv Building_Diagrams/sub-processes_and_call_activities.qmd Building_Diagrams/sub-processes_and_call_activities.md
gitc Building_Diagrams/Forms.md
python bin/gpt-proofread.py Building_Diagrams/Forms.md
mv Building_Diagrams/Forms.qmd Building_Diagrams/Forms.md

View File

@ -0,0 +1,2 @@
langchain
openai

View File

@ -7,6 +7,7 @@ import DateRangePickerWidget from '../rjsf/custom_widgets/DateRangePicker/DateRa
import TypeaheadWidget from '../rjsf/custom_widgets/TypeaheadWidget/TypeaheadWidget';
import MarkDownFieldWidget from '../rjsf/custom_widgets/MarkDownFieldWidget/MarkDownFieldWidget';
import NumericRangeField from '../rjsf/custom_widgets/NumericRangeField/NumericRangeField';
import ObjectFieldRestrictedGridTemplate from '../rjsf/custom_templates/ObjectFieldRestrictGridTemplate';
enum DateCheckType {
minimum = 'minimum',
@ -24,6 +25,7 @@ type OwnProps = {
onSubmit?: any;
children?: ReactNode;
noValidate?: boolean;
restrictedWidth?: boolean;
};
export default function CustomForm({
@ -36,6 +38,7 @@ export default function CustomForm({
onSubmit,
children,
noValidate = false,
restrictedWidth = false,
}: OwnProps) {
// set in uiSchema using the "ui:widget" key for a property
const rjsfWidgets = {
@ -45,10 +48,15 @@ export default function CustomForm({
};
// set in uiSchema using the "ui:field" key for a property
const fields: RegistryFieldsType = {
const rjsfFields: RegistryFieldsType = {
'numeric-range': NumericRangeField,
};
const rjsfTemplates: any = {};
if (restrictedWidth) {
rjsfTemplates.ObjectFieldTemplate = ObjectFieldRestrictedGridTemplate;
}
const formatDateString = (dateString?: string) => {
let dateObject = new Date();
if (dateString) {
@ -362,7 +370,8 @@ export default function CustomForm({
validator={validator}
customValidate={customValidate}
noValidate={noValidate}
fields={fields}
fields={rjsfFields}
templates={rjsfTemplates}
omitExtraData
>
{children}

View File

@ -499,6 +499,7 @@ export default function ReactFormBuilder({
onChange={(e: any) => updateData(e.formData)}
schema={postJsonSchema}
uiSchema={postJsonUI}
restrictedWidth
/>
</ErrorBoundary>
</Column>

View File

@ -1,26 +1,67 @@
import React from 'react';
import {
FormContextType,
ObjectFieldTemplatePropertyType,
ObjectFieldTemplateProps,
RJSFSchema,
StrictRJSFSchema,
canExpand,
FormContextType,
getTemplate,
getUiOptions,
ObjectFieldTemplateProps,
ObjectFieldTemplatePropertyType,
RJSFSchema,
StrictRJSFSchema,
} from '@rjsf/utils';
import { Grid, Column } from '@carbon/react';
/** The `ObjectFieldTemplate` is the template to use to render all the inner properties of an object along with the
* title and description if available. If the object is expandable, then an `AddButton` is also rendered after all
* the properties.
/* usage: add ui:layout to an object in the uiSchema
*
* @param props - The `ObjectFieldTemplateProps` for this component
* If using ui:layout then ALL fields must be specified in the desired order.
* The sm, md, and lg options match the Column options for Carbon theme. So they
* specify how many grid columns the field takes up.
*
* Example uiSchema:
*
* {
* "ui:layout": [
* {
* "firstName": {
* "sm": 2,
* "md": 2,
* "lg": 4
* },
* "lastName": {
* "sm": 2,
* "md": 2,
* "lg": 4
* }
* },
* {
* "user": {}
* },
* {
* "details": {}
* }
* ],
* "user": {
* "ui:layout": [
* { "username": {}, "password": {} }
* ]
* }
* }
*/
// these are not used by rjsf but can be passed in if calling this template directly.
type customProps = {
defaultSm: number;
defaultMd: number;
defaultLg: number;
};
export default function ObjectFieldTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: ObjectFieldTemplateProps<T, S, F>) {
>(
props: ObjectFieldTemplateProps<T, S, F>,
{ defaultSm = 4, defaultMd = 8, defaultLg = 16 }: customProps
) {
const {
description,
disabled,
@ -51,6 +92,49 @@ export default function ObjectFieldTemplate<
const {
ButtonTemplates: { AddButton },
} = registry.templates;
const layout = (uiSchema as any)['ui:layout'];
const schemaToUse = schema as any;
let innerElements = null;
if (layout) {
innerElements = layout.map((row: any) => {
const numberOfColumns = Object.keys(row).length;
return (
<Grid condensed fullWidth>
{Object.keys(row).map((name) => {
const element: any = properties.find((property: any) => {
return property.name === name;
});
if (schemaToUse.properties[name]) {
const { sm, md, lg } = row[name];
return (
<Column
className="side-by-side-column"
sm={sm || Math.floor(defaultSm / numberOfColumns)}
md={md || Math.floor(defaultMd / numberOfColumns)}
lg={lg || Math.floor(defaultLg / numberOfColumns)}
>
{element.content}
</Column>
);
}
return (
<div className="error-message">
{`ERROR: '${name}' property was specified in the UI Schema's ui:layout, but property does not exist in the json schema.`}
</div>
);
})}
</Grid>
);
});
} else {
innerElements = properties.map(
(prop: ObjectFieldTemplatePropertyType) => prop.content
);
}
return (
<fieldset id={idSchema.$id}>
{(options.title || title) && (
@ -72,7 +156,9 @@ export default function ObjectFieldTemplate<
registry={registry}
/>
)}
{properties.map((prop: ObjectFieldTemplatePropertyType) => prop.content)}
{innerElements}
{canExpand<T, S, F>(schema, uiSchema, formData) && (
<AddButton
className="object-property-expand"

View File

@ -108,3 +108,7 @@
line-height: var(--cds-helper-text-01-line-height, 1.33333);
letter-spacing: var(--cds-helper-text-01-letter-spacing, 0.32px);
}
.rjsf .side-by-side-column:not(:last-child) {
padding-right: 1rem;
}

View File

@ -0,0 +1,24 @@
import {
FormContextType,
ObjectFieldTemplateProps,
RJSFSchema,
StrictRJSFSchema,
} from '@rjsf/utils';
import ObjectFieldTemplate from '../carbon_theme/ObjectFieldTemplate';
/*
* Returns the ObjectFieldTemplate but with restricted column count for the carbon grid.
* Only affects rjsf object field type with the "ui:layout".
*/
export default function ObjectFieldRestrictedGridTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: ObjectFieldTemplateProps<T, S, F>) {
return ObjectFieldTemplate(props, {
defaultSm: 4,
defaultMd: 5,
defaultLg: 8,
});
}

View File

@ -417,6 +417,7 @@ export default function TaskShow() {
onSubmit={handleFormSubmit}
schema={jsonSchema}
uiSchema={formUiSchema}
restrictedWidth
>
{reactFragmentToHideSubmitButton}
</CustomForm>
@ -427,6 +428,7 @@ export default function TaskShow() {
schema={jsonSchema}
uiSchema={formUiSchema}
noValidate
restrictedWidth
/>
</Column>
</Grid>