diff --git a/package-lock.json b/package-lock.json index 399d899e3..d2961fffb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.1", "dependencies": { "@apollo/client": "^3.8.5", - "@edsc/metadata-preview": "^1.5.12", + "@edsc/metadata-preview": "^1.5.13", "@node-saml/node-saml": "^5.1.0", "@rjsf/core": "^5.15.0", "@rjsf/utils": "^5.15.0", @@ -17900,6 +17900,166 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.22.1.tgz", + "integrity": "sha512-5p2rnlVTv6Gpw4PlTLq925nTVh+HFh4MpegX8dPDYJae+NFVjQ67gY7O6iHIzQjLipDiYejFF0yHrhjU3XgLBQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.22.1.tgz", + "integrity": "sha512-1FaBtcFrZqB2hkFbAxY//Pnp8koThvyB6AhjbdVqKD4/pu13Rl91fKt2N9qyeQPUt3xy7ORUvSO+dPk3J6EjXg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.22.1.tgz", + "integrity": "sha512-6rub98tYGfE5I5j0BP8t/2d4BZyu1S7Iz9vUkm0H26snAFHYxLfj3RbQn0xHHIePSetjLnhcg3QlfwUAkD/FYg==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.22.1.tgz", + "integrity": "sha512-nYO5qGtb/1kkTZu3FeTiM+2B2TAb7m2DkLCTgQIs2bk2o9aEs7I96fwySKcoHWQAiQDGR9sMux9vkV4KQXqPaQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.22.1.tgz", + "integrity": "sha512-MCV6RuRpzXbunvzwY644iz8cw4oQxvW7oer9xPkdadYqlEyiJJ6wl7FyJOH7Q6ZYH4yjGAUCvxDBxPbnDu9ZVg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.22.1.tgz", + "integrity": "sha512-RjNgpdM20VUXgV7us/VmlO3Vn2ZRiDnc3/bUxCVvySZWPiVPprpqW/QDWuzkGa+NCUf6saAM5CLsZLSxncXJwg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.22.1.tgz", + "integrity": "sha512-ZgO4C7Rd6Hv/5MnyY2KxOYmIlzk4rplVolDt3NbkNR8DndnyX0Q5IR4acJWNTBICQ21j3zySzKbcJaiJpk/4YA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.22.1.tgz", + "integrity": "sha512-4pozV4eyD0MDET41ZLHAeBo+H04Nm2UEYIk5w/ts40231dRFV7E0cjwbnZvSoc1DXFgecAhiC0L16ruv/ZDCpg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -26892,23 +27052,6 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, - "node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index d958b1bf4..8117a39f7 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ }, "dependencies": { "@apollo/client": "^3.8.5", - "@edsc/metadata-preview": "^1.5.12", + "@edsc/metadata-preview": "^1.5.13", "@node-saml/node-saml": "^5.1.0", "@rjsf/core": "^5.15.0", "@rjsf/utils": "^5.15.0", diff --git a/static/src/js/components/JsonPreview/JsonPreview.jsx b/static/src/js/components/JsonPreview/JsonPreview.jsx index a136288b2..6ca58e242 100644 --- a/static/src/js/components/JsonPreview/JsonPreview.jsx +++ b/static/src/js/components/JsonPreview/JsonPreview.jsx @@ -1,36 +1,242 @@ -import React from 'react' +import React, { useState } from 'react' import Accordion from 'react-bootstrap/Accordion' import JSONPretty from 'react-json-pretty' import { cloneDeep } from 'lodash-es' +import PropTypes from 'prop-types' +import validator from '@rjsf/validator-ajv8' import useAppContext from '../../hooks/useAppContext' import removeEmpty from '../../utils/removeEmpty' +import Button from '../Button/Button' +import CustomModal from '../CustomModal/CustomModal' -const JsonPreview = () => { +const JsonPreview = ({ schema }) => { const { - draft = {} + draft = {}, + setDraft } = useAppContext() - // Remove || {} in MMT-4070 const { ummMetadata = {} } = draft || {} const data = cloneDeep(removeEmpty(ummMetadata)) + const [isEditing, setIsEditing] = useState(false) + const [jsonText, setJsonText] = useState('') + + // Inline, blocking error -- only ever a JSON.parse failure. + const [parseError, setParseError] = useState(null) + + // Schema/structural errors surfaced on Apply. These block saving -- the + // errors modal below is a dead end that only lets the user go back and + // fix the JSON, it never commits the invalid draft. + const [pendingErrors, setPendingErrors] = useState([]) + const [showErrors, setShowErrors] = useState(false) + + const handleEditClick = () => { + setJsonText(JSON.stringify(data, null, 2)) + setParseError(null) + setPendingErrors([]) + setIsEditing(true) + } + + const handleCancel = () => { + setJsonText(JSON.stringify(data, null, 2)) + setParseError(null) + setPendingErrors([]) + setShowErrors(false) + setIsEditing(false) + } + + const handleTextChange = (event) => { + setJsonText(event.target.value) + if (parseError) setParseError(null) + } + + const handleApplyClick = () => { + let parsed + + try { + parsed = JSON.parse(jsonText) + } catch (parseErrorObj) { + setParseError(`Invalid JSON: ${parseErrorObj.message}`) + + return + } + + setParseError(null) + + if (schema) { + const { errors: schemaErrors = [] } = validator.validateFormData(parsed, schema) + + // Only surface structural errors (unknown field, wrong type, oneOf/anyOf + // mismatches). 'required' errors are ignored so the JSON editor stays as + // permissive as the form. + const structuralErrors = schemaErrors.filter(({ name }) => name !== 'required') + + if (structuralErrors.length > 0) { + const messages = structuralErrors.map(({ + name, + property, + message, + params + }) => { + // AJV reports the bad key in params.additionalProperty, not in `message` + if (name === 'additionalProperties' && params?.additionalProperty) { + const location = property ? `${property} ` : '' + + return `${location} must NOT have additional property '${params.additionalProperty}'` + } + + return property ? `${property} ${message}` : message + }) + + setPendingErrors(messages) + setShowErrors(true) + + return + } + } + + setDraft({ + ...draft, + ummMetadata: parsed + }) + + setParseError(null) + setPendingErrors([]) + setShowErrors(false) + setIsEditing(false) + } + + const handleErrorsBack = () => { + setShowErrors(false) + setPendingErrors([]) + } + return ( - - - - JSON - - - - - - + <> + + + + JSON + + +
+ +
+ + +
+
+
+ + { + if (!nextShow) handleCancel() + } + } + size="xl" + header="Editing JSON" + message={ + ( + <> + { + parseError && ( +
+ {parseError} +
+ ) + } + +