#176 - Block to JSON field type

This commit is contained in:
Elio Struyf
2022-02-13 18:11:51 -08:00
parent 1f7519ee60
commit 174c4b7734
12 changed files with 420 additions and 4 deletions
@@ -18,7 +18,15 @@ export interface IDataBlockFormProps {
export const DataBlockForm: React.FunctionComponent<IDataBlockFormProps> = ({ label, schema, model, onUpdate, onClear }: React.PropsWithChildren<IDataBlockFormProps>) => {
const [ bridge, setBridge ] = useState<JSONSchemaBridge | null>(null);
const ajv = new Ajv({ allErrors: true, useDefaults: true });
const ajv = new Ajv({ allErrors: true, useDefaults: true, strict: false });
ajv.addKeyword({
keyword: 'fieldType',
type: "string",
validate: (schema: any, data: any) => {
console.log(`ajv`);
return true;
}
});
const jsonValidator = (crntSchema: object) => {
const validator = ajv.compile(crntSchema);
@@ -0,0 +1,25 @@
import * as React from 'react';
import { useForm } from 'uniforms';
import { SubmitField } from 'uniforms-unstyled';
export interface IJsonFieldControlsProps {
model: any | null;
onClear: () => void;
}
export const JsonFieldControls: React.FunctionComponent<IJsonFieldControlsProps> = ({ model, onClear }: React.PropsWithChildren<IJsonFieldControlsProps>) => {
const { formRef } = useForm();
return (
<div className='data_block__buttons'>
<SubmitField value={model ? `Update` : `Add`} />
<button className='ml-4' onClick={() => {
if (onClear) {
onClear();
}
formRef.reset();
}}>Cancel</button>
</div>
);
};
@@ -0,0 +1,115 @@
import * as React from 'react';
import { useState, useMemo, useCallback, useEffect } from 'react';
import { Field, PanelSettings } from '../../../models';
import { PencilIcon } from '@heroicons/react/outline';
import { VsLabel } from '../VscodeComponents';
import { JsonFieldRecords, JsonFieldForm, JsonFieldSelector } from '.';
import { SortEnd } from 'react-sortable-hoc';
import { arrayMoveImmutable } from 'array-move';
export interface IJsonFieldFieldProps {
label: string;
settings: PanelSettings;
field: Field;
value: any;
onSubmit: (data: any) => void;
}
export const JsonFieldField: React.FunctionComponent<IJsonFieldFieldProps> = ({ label, settings, field, value, onSubmit }: React.PropsWithChildren<IJsonFieldFieldProps>) => {
const [ selectedIndex, setSelectedIndex ] = useState<number | null>(null);
const [ selectedDataType, setSelectedDataType ] = useState<string | null>(null);
const [ selectedSchema, setSelectedSchema ] = useState<any | null>(null);
const onUpdate = useCallback((data: any) => {
const dataClone: any[] = Object.assign([], value);
if (selectedIndex !== null && selectedIndex !== undefined) {
dataClone[selectedIndex] = {
...data,
dataType: selectedSchema?.id
};
} else {
dataClone.push({
...data,
dataType: selectedSchema?.id
});
}
onSubmit(dataClone);
}, [value, selectedIndex, onSubmit, selectedSchema]);
const deleteItem = useCallback((index: number) => {
const dataClone: any[] = Object.assign([], value);
if (!value) {
return;
}
dataClone.splice(index, 1);
onSubmit(dataClone);
}, [value, selectedIndex, onSubmit]);
const onSetDataType = (dataType: string | null) => {
setSelectedDataType(dataType);
if (!dataType) {
setSelectedIndex(null);
setSelectedSchema(null);
}
}
const onSort = useCallback(({ oldIndex, newIndex }: SortEnd) => {
if (!value || value.length === 0) {
return null;
}
if (selectedIndex !== null && selectedIndex !== undefined) {
setSelectedIndex(newIndex);
}
const newEntries = arrayMoveImmutable(value, oldIndex, newIndex);
onSubmit(newEntries);
}, [value, selectedIndex]);
const model = useMemo(() => (value && selectedIndex !== null && selectedIndex !== undefined) ? value[selectedIndex] : null, [value, selectedIndex]);
useEffect(() => {
if (selectedIndex !== null && settings?.dataTypes) {
const dataType = model["dataType"];
const schema = settings?.dataTypes.find(dt => dt.id === dataType);
setSelectedSchema(schema);
setSelectedDataType(dataType);
}
}, [selectedIndex, model, settings?.dataTypes]);
return (
<div className='data_block__field'>
<VsLabel>
<div className={`metadata_field__label`}>
<PencilIcon style={{ width: "16px", height: "16px" }} /> <span style={{ lineHeight: "16px"}}>{label}</span>
</div>
</VsLabel>
<JsonFieldSelector
field={field}
selectedDataType={selectedDataType}
dataTypes={settings.dataTypes}
onSetDataType={onSetDataType}
onSchemaUpdate={setSelectedSchema} />
<JsonFieldForm
label={(model && selectedIndex !== null) ? `Editing: Record ${selectedIndex + 1}` : `Create a new ${selectedSchema?.id}`}
model={model}
schema={selectedSchema?.schema}
onUpdate={onUpdate}
onClear={() => setSelectedIndex(null)} />
<JsonFieldRecords
records={value}
selectedIndex={selectedIndex}
onAdd={() => setSelectedIndex(null)}
onSort={onSort}
onEdit={(index: number) => setSelectedIndex(index)}
onDelete={deleteItem} />
</div>
);
};
@@ -0,0 +1,74 @@
import * as React from 'react';
import { useEffect, useMemo, useState } from 'react';
import JSONSchemaBridge from 'uniforms-bridge-json-schema';
import { AutoFields, AutoForm, ErrorsField } from '../../../components/uniforms-frontmatter';
import { JsonFieldControls } from './JsonFieldControls';
import { JsonFieldRecord } from './JsonFieldRecord';
import { CollectionIcon, PencilIcon, TrashIcon } from '@heroicons/react/outline';
import Ajv from 'ajv';
export interface IJsonFieldFormProps {
label: string;
schema: any | null;
model: any | null;
onUpdate: (data: any) => void;
onClear: () => void;
}
export const JsonFieldForm: React.FunctionComponent<IJsonFieldFormProps> = ({ label, schema, model, onUpdate, onClear }: React.PropsWithChildren<IJsonFieldFormProps>) => {
const [ bridge, setBridge ] = useState<JSONSchemaBridge | null>(null);
const ajv = new Ajv({ allErrors: true, useDefaults: true, strict: false });
ajv.addKeyword({
keyword: 'fieldType',
type: "string",
validate: (schema: any, data: any) => {
console.log(`ajv`);
return true;
}
});
const jsonValidator = (crntSchema: object) => {
const validator = ajv.compile(crntSchema);
return (crntModel: object) => {
validator(crntModel);
return validator.errors?.length ? { details: validator.errors } : null;
};
};
useEffect(() => {
if (schema) {
const schemaValidator = jsonValidator(schema);
const bridge = new JSONSchemaBridge(schema, schemaValidator);
setBridge(bridge);
}
}, [schema]);
if (!bridge || !schema) {
return null;
}
return (
<div className='autoform'>
<AutoForm
schema={bridge}
model={model || {}}
onSubmit={onUpdate}
ref={form => form?.reset()}>
{ label && <h3>{label}</h3> }
<div className={`fields`}>
<AutoFields />
</div>
<div className={`errors`}>
<ErrorsField />
</div>
<JsonFieldControls model={model} onClear={onClear} />
</AutoForm>
</div>
);
};
@@ -0,0 +1,36 @@
import * as React from 'react';
import { PencilIcon, SelectorIcon, TrashIcon } from '@heroicons/react/outline';
import { SortableHandle, SortableElement } from 'react-sortable-hoc';
export interface IJsonFieldRecordProps {
id: number;
index: number;
label: string;
isSelected?: boolean;
onEdit: (id: number) => void;
onDelete: (id: number) => void;
}
const DragHandle = SortableHandle(() => <span className='drag_handler'><SelectorIcon /></span>);
export const JsonFieldRecord = SortableElement(({ label, id, onEdit, onDelete, isSelected }: React.PropsWithChildren<IJsonFieldRecordProps>) => {
return (
<li className={`data_block__record ${isSelected ? `data_block__record_selected` : ``}`}>
<div>
<DragHandle />
<span>{label} - {id+1}</span>
</div>
<div>
<button title='Edit record' className='data_block__list__button data_block__list__button_edit' onClick={() => onEdit(id)}>
<PencilIcon className='data_block__list__button_icon' />
<span className='sr-only'>Edit</span>
</button>
<button title='Delete record' className='data_block__list__button data_block__list__button_delete' onClick={() => onDelete(id)}>
<TrashIcon className='data_block__list__button_icon' />
<span className='sr-only'>Delete</span>
</button>
</div>
</li>
);
});
@@ -0,0 +1,56 @@
import { CollectionIcon, PlusIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { VsLabel } from '../VscodeComponents';
import { JsonFieldRecord } from '.';
import { SortableContainer, SortEnd } from 'react-sortable-hoc';
export interface IJsonFieldRecordsProps {
records: any[];
selectedIndex: number | null;
onAdd: () => void;
onSort: (obj: SortEnd) => void;
onEdit: (id: number) => void;
onDelete: (id: number) => void;
}
const Container = SortableContainer(({children}: React.PropsWithChildren<any>) => {
return <ul>{children}</ul>;
});
export const JsonFieldRecords = ({ records, selectedIndex, onSort, onAdd, onEdit, onDelete }: React.PropsWithChildren<IJsonFieldRecordsProps>) => {
if (!records || !records.length) {
return null;
}
return (
<div className='data_block__list'>
<VsLabel>
<div className={`metadata_field__label`} >
<div>
<CollectionIcon style={{ width: "16px", height: "16px" }} />
<span style={{ lineHeight: "16px"}}>Records</span>
</div>
<button title='Add new record' className='data_block__list__button' onClick={onAdd}>
<PlusIcon style={{ width: "16px", height: "16px" }} />
</button>
</div>
</VsLabel>
<Container onSortEnd={onSort} useDragHandle>
{
records.map((v: any, idx: number) => (
<JsonFieldRecord
key={idx}
id={idx}
index={idx}
label={v?.dataType ?? 'Record'}
isSelected={idx === selectedIndex}
onEdit={onEdit}
onDelete={onDelete} />
))
}
</Container>
</div>
);
};
@@ -0,0 +1,74 @@
import * as React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { Field } from '../../../models';
import { DataType } from '../../../models/DataType';
import { VSCodeDropdown, VSCodeOption } from '@vscode/webview-ui-toolkit/react';
export interface IJsonFieldSelectorProps {
field: Field;
dataTypes: DataType[] | undefined;
selectedDataType: string | null;
onSetDataType: (dataType: string | null) => void;
onSchemaUpdate: (schema: any) => void;
}
const EMPTY_OPTION = "EMPTY_OPTION";
export const JsonFieldSelector: React.FunctionComponent<IJsonFieldSelectorProps> = ({ field, dataTypes, selectedDataType, onSetDataType, onSchemaUpdate }: React.PropsWithChildren<IJsonFieldSelectorProps>) => {
const [ options, setOptions ] = useState<string[]>([]);
const onSchemaSet = useCallback((dataType: string) => {
if (dataTypes && dataType !== EMPTY_OPTION) {
const schema = dataTypes.find(dt => dt.id === dataType);
onSchemaUpdate(schema);
} else {
onSchemaUpdate(null);
}
}, []);
useEffect(() => {
if (field.dataType && dataTypes) {
if (typeof field.dataType === "string") {
onSchemaSet(field.dataType);
} else if (field.dataType instanceof Array && field.dataType.length === 1) {
onSchemaSet(field.dataType[0]);
} else {
setOptions([...field.dataType]);
}
}
}, [field.dataType, dataTypes, onSchemaUpdate]);
useEffect(() => {
if (dataTypes && selectedDataType) {
if (selectedDataType !== EMPTY_OPTION) {
onSchemaSet(selectedDataType);
} else {
onSchemaUpdate(null);
}
}
}, [selectedDataType, dataTypes, onSchemaUpdate])
if (options.length === 0) {
return null;
}
return (
<div className='data_block__selector'>
<h3>Block type</h3>
<VSCodeDropdown
value={selectedDataType ?? EMPTY_OPTION}
onChange={(event: any) => onSetDataType(event.currentTarget.value === EMPTY_OPTION ? null : event.currentTarget.value)}
style={{
width: "100%",
marginBottom: "1rem"
}}>
<VSCodeOption value={EMPTY_OPTION}>&nbsp;</VSCodeOption>
{
options.map((o) => (
<VSCodeOption key={o} value={o}>{o}</VSCodeOption>
))
}
</VSCodeDropdown>
</div>
);
};
@@ -0,0 +1,6 @@
export * from './JsonFieldControls';
export * from './JsonFieldField';
export * from './JsonFieldForm';
export * from './JsonFieldRecord';
export * from './JsonFieldRecords';
export * from './JsonFieldSelector';
+3 -2
View File
@@ -21,6 +21,7 @@ import FieldBoundary from './ErrorBoundary/FieldBoundary';
import { DraftField } from './Fields/DraftField';
import { VsLabel } from './VscodeComponents';
import { DataBlockField } from './DataBlock';
import { JsonFieldField } from './JsonField';
export interface IMetadata {
[prop: string]: string[] | string | null | IMetadata;
@@ -235,12 +236,12 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
}
return null;
} else if (field.type === 'block') {
} else if (field.type === 'json') {
const collectionData = parent[field.name];
return (
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<DataBlockField
<JsonFieldField
label={field.title || field.name}
settings={settings}
field={field}