diff --git a/package.json b/package.json index e28b4d69..45286a51 100644 --- a/package.json +++ b/package.json @@ -633,6 +633,7 @@ "categories", "draft", "fields", + "json", "block" ], "description": "Define the type of field" @@ -770,6 +771,20 @@ "blockType" ] } + }, + { + "if": { + "properties": { + "type": { + "const": "json" + } + } + }, + "then": { + "required": [ + "dataType" + ] + } } ] } diff --git a/src/components/uniforms-frontmatter/AutoField.tsx b/src/components/uniforms-frontmatter/AutoField.tsx index 2b5f132c..903ea11c 100644 --- a/src/components/uniforms-frontmatter/AutoField.tsx +++ b/src/components/uniforms-frontmatter/AutoField.tsx @@ -1,5 +1,6 @@ import * as invariant from 'invariant'; import { createAutoField } from 'uniforms'; +import { PreviewImageField } from '../../panelWebView/components/Fields/PreviewImageField'; export { AutoFieldProps } from 'uniforms'; import BoolField from './BoolField'; @@ -12,6 +13,7 @@ import SelectField from './SelectField'; import TextField from './TextField'; const AutoField = createAutoField(props => { + if (props.allowedValues) { return props.checkboxes && props.fieldType !== Array ? RadioField @@ -31,6 +33,9 @@ const AutoField = createAutoField(props => { return NestField; case String: return TextField; + case "image": + console.log(`You selected the image type`) + return PreviewImageField; } return invariant(false, 'Unsupported field type: %s', props.fieldType); diff --git a/src/models/PanelSettings.ts b/src/models/PanelSettings.ts index 3da6353c..afad15e2 100644 --- a/src/models/PanelSettings.ts +++ b/src/models/PanelSettings.ts @@ -41,7 +41,7 @@ export interface ContentType { pageBundle?: boolean; } -export type FieldType = "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy" | "fields" | "block"; +export type FieldType = "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy" | "fields" | "json" | "block"; export interface Field { title?: string; @@ -56,6 +56,7 @@ export interface Field { default?: string; fields?: Field[]; blockType?: string | string[]; + dataType?: string | string[]; } export interface DateInfo { diff --git a/src/panelWebView/components/DataBlock/DataBlockForm.tsx b/src/panelWebView/components/DataBlock/DataBlockForm.tsx index fda2408e..12907b89 100644 --- a/src/panelWebView/components/DataBlock/DataBlockForm.tsx +++ b/src/panelWebView/components/DataBlock/DataBlockForm.tsx @@ -18,7 +18,15 @@ export interface IDataBlockFormProps { export const DataBlockForm: React.FunctionComponent = ({ label, schema, model, onUpdate, onClear }: React.PropsWithChildren) => { const [ bridge, setBridge ] = useState(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); diff --git a/src/panelWebView/components/JsonField/JsonFieldControls.tsx b/src/panelWebView/components/JsonField/JsonFieldControls.tsx new file mode 100644 index 00000000..cecd7b56 --- /dev/null +++ b/src/panelWebView/components/JsonField/JsonFieldControls.tsx @@ -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 = ({ model, onClear }: React.PropsWithChildren) => { + const { formRef } = useForm(); + + return ( +
+ + + +
+ ); +}; \ No newline at end of file diff --git a/src/panelWebView/components/JsonField/JsonFieldField.tsx b/src/panelWebView/components/JsonField/JsonFieldField.tsx new file mode 100644 index 00000000..012451b1 --- /dev/null +++ b/src/panelWebView/components/JsonField/JsonFieldField.tsx @@ -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 = ({ label, settings, field, value, onSubmit }: React.PropsWithChildren) => { + const [ selectedIndex, setSelectedIndex ] = useState(null); + const [ selectedDataType, setSelectedDataType ] = useState(null); + const [ selectedSchema, setSelectedSchema ] = useState(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 ( +
+ + +
+ {label} +
+
+ + + + setSelectedIndex(null)} /> + + setSelectedIndex(null)} + onSort={onSort} + onEdit={(index: number) => setSelectedIndex(index)} + onDelete={deleteItem} /> +
+ ); +}; \ No newline at end of file diff --git a/src/panelWebView/components/JsonField/JsonFieldForm.tsx b/src/panelWebView/components/JsonField/JsonFieldForm.tsx new file mode 100644 index 00000000..e3cc1207 --- /dev/null +++ b/src/panelWebView/components/JsonField/JsonFieldForm.tsx @@ -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 = ({ label, schema, model, onUpdate, onClear }: React.PropsWithChildren) => { + const [ bridge, setBridge ] = useState(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 ( +
+ form?.reset()}> + + { label &&

{label}

} + +
+ +
+ +
+ +
+ + +
+
+ ); +}; \ No newline at end of file diff --git a/src/panelWebView/components/JsonField/JsonFieldRecord.tsx b/src/panelWebView/components/JsonField/JsonFieldRecord.tsx new file mode 100644 index 00000000..a1ac9726 --- /dev/null +++ b/src/panelWebView/components/JsonField/JsonFieldRecord.tsx @@ -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(() => ); + +export const JsonFieldRecord = SortableElement(({ label, id, onEdit, onDelete, isSelected }: React.PropsWithChildren) => { + return ( +
  • +
    + + + {label} - {id+1} +
    + +
    + + +
    +
  • + ); +}); \ No newline at end of file diff --git a/src/panelWebView/components/JsonField/JsonFieldRecords.tsx b/src/panelWebView/components/JsonField/JsonFieldRecords.tsx new file mode 100644 index 00000000..0d39d88b --- /dev/null +++ b/src/panelWebView/components/JsonField/JsonFieldRecords.tsx @@ -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) => { + return
      {children}
    ; +}); + +export const JsonFieldRecords = ({ records, selectedIndex, onSort, onAdd, onEdit, onDelete }: React.PropsWithChildren) => { + + if (!records || !records.length) { + return null; + } + + return ( +
    + +
    +
    + + Records +
    + + +
    +
    + + + { + records.map((v: any, idx: number) => ( + + )) + } + +
    + ); +}; \ No newline at end of file diff --git a/src/panelWebView/components/JsonField/JsonFieldSelector.tsx b/src/panelWebView/components/JsonField/JsonFieldSelector.tsx new file mode 100644 index 00000000..81308a45 --- /dev/null +++ b/src/panelWebView/components/JsonField/JsonFieldSelector.tsx @@ -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 = ({ field, dataTypes, selectedDataType, onSetDataType, onSchemaUpdate }: React.PropsWithChildren) => { + const [ options, setOptions ] = useState([]); + + 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 ( +
    +

    Block type

    + onSetDataType(event.currentTarget.value === EMPTY_OPTION ? null : event.currentTarget.value)} + style={{ + width: "100%", + marginBottom: "1rem" + }}> +   + { + options.map((o) => ( + {o} + )) + } + +
    + ); +}; \ No newline at end of file diff --git a/src/panelWebView/components/JsonField/index.ts b/src/panelWebView/components/JsonField/index.ts new file mode 100644 index 00000000..dd3ab69c --- /dev/null +++ b/src/panelWebView/components/JsonField/index.ts @@ -0,0 +1,6 @@ +export * from './JsonFieldControls'; +export * from './JsonFieldField'; +export * from './JsonFieldForm'; +export * from './JsonFieldRecord'; +export * from './JsonFieldRecords'; +export * from './JsonFieldSelector'; diff --git a/src/panelWebView/components/Metadata.tsx b/src/panelWebView/components/Metadata.tsx index fa5a6de1..9b0a15b5 100644 --- a/src/panelWebView/components/Metadata.tsx +++ b/src/panelWebView/components/Metadata.tsx @@ -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 = ({settings, metadata, } return null; - } else if (field.type === 'block') { + } else if (field.type === 'json') { const collectionData = parent[field.name]; return ( -