Skip to content

Commit

Permalink
Replace @jupyterlab/rjsf with FormComponent from @jupyterlab/ui-compo…
Browse files Browse the repository at this point in the history
…nents (#625)

* added form component

* custom array field

* custom form field validation

* css color picker

* removed lumino

* array field error validation

* handle different types of input in custom array fields.

* eslint

* ui test update

* input step option added

* input step option added

* updated schema

* rebase on main

* removed unused properties.

* hide additional properties fields.

* css edit

* css for required span.

* submit form with enter.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update Playwright Snapshots

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 9, 2024
1 parent 872bc06 commit 3b38e52
Show file tree
Hide file tree
Showing 11 changed files with 434 additions and 801 deletions.
3 changes: 1 addition & 2 deletions packages/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
"watch": "tsc -w"
},
"dependencies": {
"@deathbeds/jupyterlab-rjsf": "^1.1.0",
"@jupyter/collaborative-drive": "^3.1.0-alpha.0",
"@jupyter/ydoc": "^3.0.0",
"@jupytercad/occ-worker": "^3.0.0",
Expand All @@ -51,7 +50,7 @@
"@jupyterlab/observables": "^5.0.0",
"@jupyterlab/services": "^7.0.0",
"@jupyterlab/translation": "^4.0.0",
"@jupyterlab/ui-components": "^4.0.0",
"@jupyterlab/ui-components": "^4.3.1",
"@lumino/commands": "^2.0.0",
"@lumino/coreutils": "^2.0.0",
"@lumino/messaging": "^2.0.0",
Expand Down
112 changes: 112 additions & 0 deletions packages/base/src/panelview/customarrayfield.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React from 'react';

interface IProps {
formData?: any[];
name: string;
required: boolean;
schema: any;
errorSchema?: { [key: string]: any };
onChange: (updatedValue: any[]) => void;
onBlur: (name: string, value: any) => void;
}

const CustomArrayField: React.FC<IProps> = props => {
const {
formData = [],
name,
required,
schema,
errorSchema = {},
onChange,
onBlur
} = props;

const handleInputChange = (index: number, value: any) => {
const updatedValue = [...formData];
updatedValue[index] = value;
onChange(updatedValue);
};

const renderInputField = (value: any, index: number) => {
const { enum: enumOptions, type: itemType } = schema.items || {};
if (enumOptions) {
return (
<select
value={value || ''}
required={required}
onChange={e => handleInputChange(index, e.target.value)}
onBlur={() => onBlur(name, value)}
>
{enumOptions.map((option: string, i: number) => (
<option key={i} value={option}>
{option}
</option>
))}
</select>
);
} else if (itemType === 'number') {
return (
<input
type="number"
value={value}
step="any"
required={required}
onChange={e =>
handleInputChange(
index,
e.target.value === '' ? null : parseFloat(e.target.value)
)
}
onBlur={() => onBlur(name, value)}
/>
);
} else if (itemType === 'boolean') {
return (
<input
type="checkbox"
checked={!!value}
onChange={e => handleInputChange(index, e.target.checked)}
onBlur={() => onBlur(name, value)}
/>
);
} else {
return (
<input
type="text"
value={value}
required={required}
onChange={e => handleInputChange(index, e.target.value)}
onBlur={() => onBlur(name, value)}
/>
);
}
};

return (
<fieldset>
<legend>{name}</legend>
<p className="field-description">{schema.description}</p>
<div className="custom-array-wrapper">
{formData.map((value: any, index: number) => (
<div key={index} className="array-item">
{renderInputField(value, index)}

{errorSchema?.[index]?.__errors?.length > 0 && (
<div className="validationErrors">
{errorSchema?.[index]?.__errors.map(
(error: string, errorIndex: number) => (
<div key={`${index}-${errorIndex}`} className="error">
{error}
</div>
)
)}
</div>
)}
</div>
))}
</div>
</fieldset>
);
};

export default CustomArrayField;
120 changes: 66 additions & 54 deletions packages/base/src/panelview/formbuilder.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { SchemaForm } from '@deathbeds/jupyterlab-rjsf';
import { MessageLoop } from '@lumino/messaging';
import { Widget } from '@lumino/widgets';
import { ISubmitEvent } from '@rjsf/core';
import * as React from 'react';

import { FormComponent } from '@jupyterlab/ui-components';
import validatorAjv8 from '@rjsf/validator-ajv8';
import { IDict } from '../types';
import CustomArrayField from './customarrayfield';

interface IStates {
internalData?: IDict;
Expand All @@ -24,33 +23,18 @@ interface IProps {
cancel?: () => void;
}

// Reusing the datalayer/jupyter-react component:
// https://github.com/datalayer/jupyter-react/blob/main/packages/react/src/jupyter/lumino/Lumino.tsx
export const LuminoSchemaForm = (
props: React.PropsWithChildren<any>
): JSX.Element => {
const ref = React.useRef<HTMLDivElement>(null);
const { children } = props;
React.useEffect(() => {
const widget = children as SchemaForm;
try {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
ref.current!.insertBefore(widget.node, null);
MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
} catch (e) {
console.warn('Exception while attaching Lumino widget.', e);
}
return () => {
try {
if (widget.isAttached || widget.node.isConnected) {
Widget.detach(widget);
}
} catch (e) {
// The widget is destroyed already by React.
}
};
}, [children]);
return <div ref={ref} />;
const WrappedFormComponent = (props: any): JSX.Element => {
const { fields, ...rest } = props;
return (
<FormComponent
{...rest}
validator={validatorAjv8}
fields={{
...fields,
ArrayField: CustomArrayField
}}
/>
);
};

export class ObjectPropertiesForm extends React.Component<IProps, IStates> {
Expand All @@ -76,10 +60,14 @@ export class ObjectPropertiesForm extends React.Component<IProps, IStates> {
);
};

componentDidUpdate(prevProps: IProps, prevState: IStates): void {
componentDidUpdate(prevProps: IProps): void {
if (prevProps.sourceData !== this.props.sourceData) {
this.setState(old => ({ ...old, internalData: this.props.sourceData }));
}

if (prevProps.schema !== this.props.schema) {
this.setState(old => ({ ...old, schema: this.props.schema }));
}
}

buildForm(): JSX.Element[] {
Expand Down Expand Up @@ -159,33 +147,54 @@ export class ObjectPropertiesForm extends React.Component<IProps, IStates> {

const submitRef = React.createRef<HTMLButtonElement>();

const formSchema = new SchemaForm(schema ?? {}, {
liveValidate: true,
formData: this.state.internalData,
onSubmit: this.onFormSubmit,
onFocus: (id, value) => {
this.props.syncSelectedField
? this.props.syncSelectedField(id, value, this.props.parentType)
: null;
},
onBlur: (id, value) => {
this.props.syncSelectedField
? this.props.syncSelectedField(null, value, this.props.parentType)
: null;
},
uiSchema: this.generateUiSchema(this.props.schema),
children: (
<button ref={submitRef} type="submit" style={{ display: 'none' }} />
)
});
return (
<div
className="jpcad-property-panel"
data-path={this.props.filePath ?? ''}
>
<div className="jpcad-property-outer jp-scrollbar-tiny">
<LuminoSchemaForm>{formSchema}</LuminoSchemaForm>
<div
className="jpcad-property-outer jp-scrollbar-tiny"
onKeyUp={(e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
submitRef.current?.click();
}
}}
>
<WrappedFormComponent
schema={schema}
uiSchema={this.generateUiSchema(this.props.schema)}
formData={this.state.internalData}
onSubmit={this.onFormSubmit}
liveValidate
onFocus={(id, value) => {
this.props.syncSelectedField
? this.props.syncSelectedField(
id,
value,
this.props.parentType
)
: null;
}}
onBlur={(id, value) => {
this.props.syncSelectedField
? this.props.syncSelectedField(
null,
value,
this.props.parentType
)
: null;
}}
children={
<button
ref={submitRef}
type="submit"
style={{ display: 'none' }}
/>
}
/>
</div>

<div className="jpcad-property-buttons">
{this.props.cancel ? (
<button
Expand All @@ -198,7 +207,10 @@ export class ObjectPropertiesForm extends React.Component<IProps, IStates> {

<button
className="jp-Dialog-button jp-mod-accept jp-mod-styled"
onClick={() => submitRef.current?.click()}
type="button"
onClick={() => {
submitRef.current?.click();
}}
>
<div className="jp-Dialog-buttonLabel">Submit</div>
</button>
Expand Down
Loading

0 comments on commit 3b38e52

Please sign in to comment.