From bacc242940cf90560525012f22453098cfacd775 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Wed, 5 May 2021 07:31:52 -0700 Subject: [PATCH] Fixes, added basic editor --- .../app/form/component/CustomFieldTemplate.js | 52 ++++ ui/src/app/form/component/index.js | 3 + ui/src/app/i18n/hooks.js | 20 +- .../MetadataFilterConfigurationListItem.js | 4 +- .../MetadataFilterVersionListItem.js | 4 +- .../domain/source/SourceDefinition.js | 5 + ui/src/app/metadata/editor/MetadataEditor.js | 6 +- .../app/metadata/editor/MetadataEditorForm.js | 18 +- .../app/metadata/editor/MetadataEditorNav.js | 4 +- ui/src/app/metadata/hoc/MetadataSchema.js | 4 +- ui/src/app/metadata/hooks/api.js | 4 - ui/src/app/metadata/hooks/schema.js | 236 +++--------------- ui/src/app/metadata/hooks/utility.js | 216 ++++++++++++++++ 13 files changed, 345 insertions(+), 231 deletions(-) create mode 100644 ui/src/app/form/component/CustomFieldTemplate.js create mode 100644 ui/src/app/form/component/index.js create mode 100644 ui/src/app/metadata/hooks/utility.js diff --git a/ui/src/app/form/component/CustomFieldTemplate.js b/ui/src/app/form/component/CustomFieldTemplate.js new file mode 100644 index 000000000..39a1a0202 --- /dev/null +++ b/ui/src/app/form/component/CustomFieldTemplate.js @@ -0,0 +1,52 @@ +import React from "react"; + +import Form from "react-bootstrap/Form"; +import ListGroup from "react-bootstrap/ListGroup"; + +import {Translate} from '../../i18n/components/translate'; + +export function CustomFieldTemplate ({ + id, + children, + displayLabel, + rawErrors = [], + rawHelp, + rawDescription, + ...props +}) { + + console.log(props); + return ( + <>{!props.hidden ? + {children} + {displayLabel && rawDescription ? ( + 0 ? "text-danger" : "text-muted"}> + + + ) : null} + {rawErrors.length > 0 && ( + + {rawErrors.map((error) => { + return ( + + + {error} + + + ); + })} + + )} + {rawHelp && ( + 0 ? "text-danger" : "text-muted"} + id={id}> + + + )} + + : <>} + ); +}; + +export default CustomFieldTemplate; \ No newline at end of file diff --git a/ui/src/app/form/component/index.js b/ui/src/app/form/component/index.js new file mode 100644 index 000000000..a8888fab1 --- /dev/null +++ b/ui/src/app/form/component/index.js @@ -0,0 +1,3 @@ +export const fields = { + // SchemaField: CustomSchemaField +}; \ No newline at end of file diff --git a/ui/src/app/i18n/hooks.js b/ui/src/app/i18n/hooks.js index 24555514d..4638d5e8d 100644 --- a/ui/src/app/i18n/hooks.js +++ b/ui/src/app/i18n/hooks.js @@ -1,6 +1,17 @@ import { useContext } from 'react'; import { I18nContext } from './context/I18n.provider'; +export function translate(value, interpolated) { + return Object.entries(interpolated).reduce((current, interpolate) => { + let reg = new RegExp(`{\\s*${interpolate[0]}\\s*}`, 'gm'); + return current.replace(reg, interpolate[1]); + }, value); +} + +export function getMessage(value, messages) { + return messages.hasOwnProperty(value) ? messages[value] : (Object.keys(messages).length ? value : ''); +} + export function useCurrentLanguage () { } @@ -11,13 +22,10 @@ export function useCurrentLocale () { export function useTranslation (value, interpolated = {}) { const messages = useContext(I18nContext); - let val = messages.hasOwnProperty(value) ? messages[value] : (Object.keys(messages).length ? value : ''); + const val = getMessage(value, messages); return useInterpolatedTranslation(val, interpolated); } export function useInterpolatedTranslation(value, interpolated) { - return Object.entries(interpolated).reduce((current, interpolate) => { - let reg = new RegExp(`{\\s*${interpolate[0]}\\s*}`, 'gm'); - return current.replace(reg, interpolate[1]); - }, value); -} + return translate(value, interpolated); +} \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js index 6b2fa4d09..d99d5767b 100644 --- a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js @@ -6,16 +6,16 @@ import { faArrowCircleDown, faArrowCircleUp, faChevronUp, faEdit, faTrash } from import { Translate } from '../../../../i18n/components/translate'; import { Link } from 'react-router-dom'; import { getDefinition } from '../../../domain/index'; -import { useMetadataSchema } from '../../../hooks/api'; import { MetadataConfiguration } from '../../../component/MetadataConfiguration'; import { useMetadataConfiguration } from '../../../hooks/configuration'; +import useFetch from 'use-http'; export function MetadataFilterConfigurationListItem ({ filter, isLast, isFirst, onOrderUp, onOrderDown, editable, onRemove, index }) { const [open, setOpen] = React.useState(false); const definition = React.useMemo(() => getDefinition(filter['@type'], ), [filter]); - const { get, response } = useMetadataSchema(); + const { get, response } = useFetch(''); const [schema, setSchema] = React.useState(); diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilterVersionListItem.js b/ui/src/app/metadata/domain/filter/component/MetadataFilterVersionListItem.js index 8a765daf5..3a69b5717 100644 --- a/ui/src/app/metadata/domain/filter/component/MetadataFilterVersionListItem.js +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilterVersionListItem.js @@ -3,8 +3,8 @@ import { faCheckSquare, faSquare } from '@fortawesome/free-regular-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { MetadataConfiguration } from '../../../component/MetadataConfiguration'; import { getDefinition } from '../../../domain/index'; -import { useMetadataSchema } from '../../../hooks/api'; import { useMetadataConfiguration } from '../../../hooks/configuration'; +import useFetch from 'use-http'; export function MetadataFilterVersionListItem ({ filters, width, selected, index, comparing, limited, onSelect }) { @@ -13,7 +13,7 @@ export function MetadataFilterVersionListItem ({ filters, width, selected, index const definition = React.useMemo(() => getDefinition(type), [type]); - const { get, response } = useMetadataSchema(); + const { get, response } = useFetch(``); const [schema, setSchema] = React.useState(); diff --git a/ui/src/app/metadata/domain/source/SourceDefinition.js b/ui/src/app/metadata/domain/source/SourceDefinition.js index 39d5ed28d..f838317ac 100644 --- a/ui/src/app/metadata/domain/source/SourceDefinition.js +++ b/ui/src/app/metadata/domain/source/SourceDefinition.js @@ -115,6 +115,11 @@ export const SourceBase = { } }; return validators; + }, + uiSchema: { + mdui: { + 'ui:widget': 'hidden' + } } } diff --git a/ui/src/app/metadata/editor/MetadataEditor.js b/ui/src/app/metadata/editor/MetadataEditor.js index 87abaf913..e1998e820 100644 --- a/ui/src/app/metadata/editor/MetadataEditor.js +++ b/ui/src/app/metadata/editor/MetadataEditor.js @@ -34,10 +34,6 @@ export function MetadataEditor () { console.log('cancel!'); }; - React.useEffect(() => { - console.log(current) - }, [current]); - return (
@@ -96,7 +92,7 @@ export function MetadataEditor () {
- +
diff --git a/ui/src/app/metadata/editor/MetadataEditorForm.js b/ui/src/app/metadata/editor/MetadataEditorForm.js index 3c8b325b4..12e6e5e64 100644 --- a/ui/src/app/metadata/editor/MetadataEditorForm.js +++ b/ui/src/app/metadata/editor/MetadataEditorForm.js @@ -2,13 +2,27 @@ import React from 'react'; import Form from '@rjsf/bootstrap-4'; -export function MetadataEditorForm ({ metadata, definition, schema }) { +import { fields } from '../../form/component'; +import { CustomFieldTemplate } from '../../form/component/CustomFieldTemplate'; +import { useUiSchema } from '../hooks/schema'; +export function MetadataEditorForm ({ metadata, definition, schema, current }) { + const uiSchema = useUiSchema(definition, schema, current); + + const [data, setData] = React.useState(metadata); + + React.useEffect(() => setData(metadata), [metadata]); return ( <> -
+
setData(formData) } + schema={schema} + uiSchema={uiSchema} + FieldTemplate={CustomFieldTemplate} + fields={ fields }>
+
{JSON.stringify(data, null, 4)}
); } \ No newline at end of file diff --git a/ui/src/app/metadata/editor/MetadataEditorNav.js b/ui/src/app/metadata/editor/MetadataEditorNav.js index 5fcb52851..6304c5be8 100644 --- a/ui/src/app/metadata/editor/MetadataEditorNav.js +++ b/ui/src/app/metadata/editor/MetadataEditorNav.js @@ -17,15 +17,13 @@ export function MetadataEditorNav ({ definition, current, base, children, format React.useEffect(() => { setActive(definition ? definition.steps.find(s => s.id === current)?.label : null); - - console.log(definition.steps, current); }, [current, definition]); return ( {format === 'dropdown' ? - +   diff --git a/ui/src/app/metadata/hoc/MetadataSchema.js b/ui/src/app/metadata/hoc/MetadataSchema.js index 2dcbb76ae..2a8f4e5f8 100644 --- a/ui/src/app/metadata/hoc/MetadataSchema.js +++ b/ui/src/app/metadata/hoc/MetadataSchema.js @@ -1,7 +1,7 @@ import React from 'react'; import { useParams } from 'react-router'; -import { useMetadataSchema } from '../hooks/api'; import { getDefinition } from '../domain/index'; +import useFetch from 'use-http'; export const MetadataSchemaContext = React.createContext(); export const MetadataDefinitionContext = React.createContext(); @@ -14,7 +14,7 @@ export function MetadataSchema({ entity, children }) { type === 'source' ? type : entity['@type'] ), [type, entity]); - const { get, response } = useMetadataSchema(); + const { get, response } = useFetch(``, {}, []); const [schema, setSchema] = React.useState(); diff --git a/ui/src/app/metadata/hooks/api.js b/ui/src/app/metadata/hooks/api.js index ac2f66355..897ed4cbf 100644 --- a/ui/src/app/metadata/hooks/api.js +++ b/ui/src/app/metadata/hooks/api.js @@ -53,10 +53,6 @@ export function useMetadataProviderOrder() { return useFetch(`${API_BASE_PATH}/MetadataResolversPositionOrder`); } -export function useMetadataSchema() { - return useFetch(``); -} - export function useMetadataHistory(type, id, opts = {}, i) { return useFetch(`${API_BASE_PATH}${getMetadataPath(type)}/${id}/Versions`, opts, i); diff --git a/ui/src/app/metadata/hooks/schema.js b/ui/src/app/metadata/hooks/schema.js index ad5acdb65..f7b3ee2c3 100644 --- a/ui/src/app/metadata/hooks/schema.js +++ b/ui/src/app/metadata/hooks/schema.js @@ -1,216 +1,42 @@ -function getDefinition(path, definitions) { - let def = path.split('/').pop(); - return definitions[def]; -} - -export function getPropertyItemSchema(items, definitions) { - if (!items) { return null; } - return items.$ref ? getDefinition(items.$ref, definitions) : items; -} - -export function getStepProperty(property, model, definitions) { - if (!property) { return null; } - property = property.$ref ? { ...property, ...getDefinition(property.$ref, definitions) } : property; - return { - name: property.title, - value: model, - type: property.type, - items: getPropertyItemSchema(property.items, definitions), - properties: getStepProperties( - property, - model, - definitions - ), - widget: property.widget instanceof String ? { id: property.widget } : { ...property.widget } - }; -} - - -export function getStepProperties(schema, model, definitions = {}) { - if (!schema || !schema.properties) { return []; } - return Object - .keys(schema.properties) - .map(property => { - return { - ...getStepProperty( - schema.properties[property], - model && model.hasOwnProperty(property) ? model[property] : null, - definitions - ), - id: property - }; - }); -} - - -function omit(key, obj) { - if (!obj) { - return obj; - } - const { [key]: omitted, ...rest } = obj; - return rest; -} - -export const rollupDifferences = (prop) => { - let updates = { - ...prop - }; +import React from 'react'; - if (prop.properties) { - updates = { - ...updates, - properties: [ - ...prop.properties.map(p => rollupDifferences(p)) - ] - }; - } +import assignInWith from 'lodash/assignInWith'; +import { I18nContext } from '../../i18n/context/I18n.provider'; - prop.differences = prop.properties.some(p => p.differences); - - return updates; -}; - -export const getConfigurationSections = (models, definition, schema) => { - return !definition || !schema || !models ? null : - ({ - dates: models.map(m => m ? m.modifiedDate : null), - sections: definition.steps - .filter(step => step.id !== 'summary') - .map( - (step, num) => { - return ({ - id: step.id, - pageNumber: num + 1, - index: step.index, - label: step.label, - properties: getStepProperties( - getSplitSchema(schema, step), - definition.formatter({}), - schema.definitions || {} - ) - }); - } - ) - .map((section) => { - return { - ...section, - properties: assignValueToProperties(models, section.properties, definition) - }; - }) - .map((section) => ({ - ...section, - differences: section.properties.some(prop => prop.differences) - })) - }); -}; - -const getDifferences = (models, prop) => { - return models.some((model, index, array) => { - if (!array) { - return false; +const fillInRootProperties = (keys, ui) => { + return keys.reduce((sch, key, idx) => { + if (!sch.hasOwnProperty(key)) { + sch[key] = {}; } - const prop1 = omit('modifiedDate', model[prop.id]); - const prop2 = omit('modifiedDate', array[0][prop.id]); - return JSON.stringify(prop1) !== JSON.stringify(prop2); - }); -}; - -export const assignValueToProperties = (models, properties, definition) => { - return properties.map(prop => { - const differences = getDifferences(models, prop); + return sch; + }, ui); +} - const widget = prop.type === 'array' && prop.widget && prop.widget.data ? ({ - ...prop.widget, - data: prop.widget.data.map(item => ({ - ...item, - differences: models - .map((model) => { - const value = model[prop.id]; - return value ? value.indexOf(item.key) > -1 : false; - }) - .reduce((current, val) => current !== val ? true : false, false) - })) - }) : null; +export function useUiSchema(definition, schema, current) { - switch (prop.type) { - case 'object': - return { - ...prop, - properties: assignValueToProperties( - models.map(model => definition.formatter(model)[prop.id] || {}), - prop.properties, - definition - ), - differences: getDifferences(models, prop) - }; - default: - return { - ...prop, - differences, - value: models.map(model => { - return model[prop.id]; - }), - widget - }; - } - }); -}; + const ui = React.useMemo(() => definition ? { ...definition.uiSchema } : {}, [definition]); + const schemaKeys = React.useMemo(() => schema ? Object.keys(schema.properties) : [], [schema]); + const step = React.useMemo(() => definition ? definition.steps.find(step => step.id === current) : {fields: []}, [definition, current]); -export const getLimitedPropertiesFn = (properties) => { - return ([ - ...properties - .filter(p => p.differences) - .map(p => { - const parsed = { ...p }; - if (p.widget && p.widget.data) { - parsed.widget = { - ...p.widget, - data: p.widget.data.filter(item => item.differences) - }; - } - if (p.properties) { - parsed.properties = getLimitedPropertiesFn(p.properties); - } - return parsed; - }) - ]); -}; + const filled = React.useMemo(() => fillInRootProperties(schemaKeys, ui), [schemaKeys, ui]); -export const getSplitSchema = (schema, step) => { - if (!schema || !step || !step.fields || !step.fields.length || !schema.properties) { - return schema; - } - const keys = Object.keys(schema.properties).filter(key => step.fields.indexOf(key) > -1); - const required = (schema.required || []).filter(val => keys.indexOf(val) > -1); - let s = { - type: schema.type, - properties: { - ...keys.reduce((properties, key) => ({ ...properties, [key]: schema.properties[key] }), {}) - } - }; - - if (step.override) { - Object.keys(step.override).forEach(key => { - let override = step.override[key]; - if (s.properties.hasOwnProperty(key)) { - s.properties[key] = { ...s.properties[key], ...override }; + const mapped = React.useMemo(() => { + return Object.keys(filled).reduce((sch, key) => { + const obj = { ...filled[key] }; + if (step.fields.indexOf(key) === -1) { + obj["ui:widget"] = 'hidden'; } - }); - } + sch[key] = obj; + return sch; + }, {}) + }, [filled, step]); - if (step.order) { - s.order = step.order; - } + return mapped; +} - if (schema.definitions) { - s.definitions = schema.definitions; - } - if (required && required.length) { - s.required = required; - } - if (step.fieldsets) { - s.fieldsets = step.fieldsets; - } - return s; -}; +export function useMetadataSchema(schema) { + return schema; +} + +export * from './utility'; \ No newline at end of file diff --git a/ui/src/app/metadata/hooks/utility.js b/ui/src/app/metadata/hooks/utility.js new file mode 100644 index 000000000..ad5acdb65 --- /dev/null +++ b/ui/src/app/metadata/hooks/utility.js @@ -0,0 +1,216 @@ +function getDefinition(path, definitions) { + let def = path.split('/').pop(); + return definitions[def]; +} + +export function getPropertyItemSchema(items, definitions) { + if (!items) { return null; } + return items.$ref ? getDefinition(items.$ref, definitions) : items; +} + +export function getStepProperty(property, model, definitions) { + if (!property) { return null; } + property = property.$ref ? { ...property, ...getDefinition(property.$ref, definitions) } : property; + return { + name: property.title, + value: model, + type: property.type, + items: getPropertyItemSchema(property.items, definitions), + properties: getStepProperties( + property, + model, + definitions + ), + widget: property.widget instanceof String ? { id: property.widget } : { ...property.widget } + }; +} + + +export function getStepProperties(schema, model, definitions = {}) { + if (!schema || !schema.properties) { return []; } + return Object + .keys(schema.properties) + .map(property => { + return { + ...getStepProperty( + schema.properties[property], + model && model.hasOwnProperty(property) ? model[property] : null, + definitions + ), + id: property + }; + }); +} + + +function omit(key, obj) { + if (!obj) { + return obj; + } + const { [key]: omitted, ...rest } = obj; + return rest; +} + +export const rollupDifferences = (prop) => { + let updates = { + ...prop + }; + + if (prop.properties) { + updates = { + ...updates, + properties: [ + ...prop.properties.map(p => rollupDifferences(p)) + ] + }; + } + + prop.differences = prop.properties.some(p => p.differences); + + return updates; +}; + +export const getConfigurationSections = (models, definition, schema) => { + return !definition || !schema || !models ? null : + ({ + dates: models.map(m => m ? m.modifiedDate : null), + sections: definition.steps + .filter(step => step.id !== 'summary') + .map( + (step, num) => { + return ({ + id: step.id, + pageNumber: num + 1, + index: step.index, + label: step.label, + properties: getStepProperties( + getSplitSchema(schema, step), + definition.formatter({}), + schema.definitions || {} + ) + }); + } + ) + .map((section) => { + return { + ...section, + properties: assignValueToProperties(models, section.properties, definition) + }; + }) + .map((section) => ({ + ...section, + differences: section.properties.some(prop => prop.differences) + })) + }); +}; + +const getDifferences = (models, prop) => { + return models.some((model, index, array) => { + if (!array) { + return false; + } + const prop1 = omit('modifiedDate', model[prop.id]); + const prop2 = omit('modifiedDate', array[0][prop.id]); + return JSON.stringify(prop1) !== JSON.stringify(prop2); + }); +}; + +export const assignValueToProperties = (models, properties, definition) => { + return properties.map(prop => { + const differences = getDifferences(models, prop); + + const widget = prop.type === 'array' && prop.widget && prop.widget.data ? ({ + ...prop.widget, + data: prop.widget.data.map(item => ({ + ...item, + differences: models + .map((model) => { + const value = model[prop.id]; + return value ? value.indexOf(item.key) > -1 : false; + }) + .reduce((current, val) => current !== val ? true : false, false) + })) + }) : null; + + switch (prop.type) { + case 'object': + return { + ...prop, + properties: assignValueToProperties( + models.map(model => definition.formatter(model)[prop.id] || {}), + prop.properties, + definition + ), + differences: getDifferences(models, prop) + }; + default: + return { + ...prop, + differences, + value: models.map(model => { + return model[prop.id]; + }), + widget + }; + } + }); +}; + +export const getLimitedPropertiesFn = (properties) => { + return ([ + ...properties + .filter(p => p.differences) + .map(p => { + const parsed = { ...p }; + if (p.widget && p.widget.data) { + parsed.widget = { + ...p.widget, + data: p.widget.data.filter(item => item.differences) + }; + } + if (p.properties) { + parsed.properties = getLimitedPropertiesFn(p.properties); + } + return parsed; + }) + ]); +}; + +export const getSplitSchema = (schema, step) => { + if (!schema || !step || !step.fields || !step.fields.length || !schema.properties) { + return schema; + } + const keys = Object.keys(schema.properties).filter(key => step.fields.indexOf(key) > -1); + const required = (schema.required || []).filter(val => keys.indexOf(val) > -1); + let s = { + type: schema.type, + properties: { + ...keys.reduce((properties, key) => ({ ...properties, [key]: schema.properties[key] }), {}) + } + }; + + if (step.override) { + Object.keys(step.override).forEach(key => { + let override = step.override[key]; + if (s.properties.hasOwnProperty(key)) { + s.properties[key] = { ...s.properties[key], ...override }; + } + }); + } + + if (step.order) { + s.order = step.order; + } + + if (schema.definitions) { + s.definitions = schema.definitions; + } + if (required && required.length) { + s.required = required; + } + if (step.fieldsets) { + s.fieldsets = step.fieldsets; + } + + return s; +};