mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-07-06 18:01:24 +02:00
#176 - Block to JSON field type
This commit is contained in:
@@ -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}> </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';
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user