From 360ef4787dd97462196ff0cee2777c02b0f1b80c Mon Sep 17 00:00:00 2001 From: Janet Cobb Date: Thu, 11 Jul 2024 03:03:09 -0400 Subject: [PATCH] Add learning resources creator (#54) * Temporarily replace app with Creator component I know this will have to be fixed at some point, but this seems like the easiest way to get started. * Add basic type selection * Add named types for item kinds * Use Patternfly FormGroup for radio buttons * Add form with bundle input * Add title input * Add description input * Unify input prop types * Use a loop for common item inputs * Add placeholders * Add URL input when type is documentation * Add separate ItemFormContainer * Add duration input when type is quickstart * Add required indicators to field groups * Move type field to own component * Add labeled FormGroup around type input * Add labels to DurationInput * Use InputProps for all applicable components * Use grid to divide screen into columns * Make step headers fancy * Remove incorrect section around title * Clamp duration input at 0 * Show tile preview with QuickStartCatalog * Move QuickStartTile usage to separate component * Move lr-c-quickstart_tile into WrappedQuickStartTile * Use WrappedQuickStartTile in Creator * Move label colors to itemKindMeta * Make creator look minimally usable * Make page header full row * Add "Live card preview" header * Use margin-inline-end for rc-step-index * Define ItemKind using itemKindMeta * Add new item types * Define fields in item metadata * Use useMemo for QuickStart * Half-functioning quickstart tester * Use QuickStartDrawer directly * Add skeleton of wizard * Move duration field into wizard * Properly set input IDs for labels * Remove now-unused components * Allow selecting multiple bundles * Prevent attempt to read quickstart tasks when they don't exist * Require progressing through wizard in order * Use cast instead of lambda for inputItemDesc * Remove uses of useId * Add task overview page * Separate Creator code into several files * Use hidden steps instead of setting key on Wizard * Allow adding more tasks * Add dependency on Patternfly code-editor * Add basic panel editing * Allow removing tasks * Cleanup task array modification * Remove ItemFormElement * Add label to remove task buttons * Move string array input to component * Allow modifying quickstart prerequisites * Allow editing task work-check values * Fix setting value of CodeEditor * Add yaml dependency * Add rendering of files and basic display * Improve quickstart name generation * Remove log statement * Don't include icon in quickstart file * Add ability to download files * Restore original app homepage * Allow switching between viewer and creator * Use a flag to enable creator * Accept YAML for step config rather than form fields * Include per-type metadata in export * Use footer config from chrome in creator * Use react-router for creator/viewer parts * Update quickstarts extension * Ensure quickstart is not focused on * Fix task editor description * Don't use useMemo for selectedType * Remove unnecessary string template * Remove unnecessary children node * Remove memo of getAvailableBundles() * Normalize SelectMultiTypeahead hooks * Remove useMemo import from CreatorInputs * Import icons from dynamic rather than esm * Use undefined to indicate no results in SelectMultiTypeahead * Remove extraneous lambda * Replace only whitespace in quickstart name * Don't useMemo another selectedType * Use whitespace regex in SelectMultiTypeahead * Use a single Form element * Prevent form submission * Fix nested FormGroups * Show errors when parsing task YAML * Fix handling of no search results in SelectMultiTypeahead * Use quickstart state directly * Fix quickstart metadata * Ensure Wizard always take up at least all height * Wrap task error display * Use useMemo in SelectMultiTypeahead * Use unleash directly in App * Make Creator its own independent app * Move creator preview to separate file * Always show quickstart drawer * Cleanup leftover context parts in Creator * Sync preview task to wizard step * Always use a string key in SelectMultiTypeahead * Remove unused option id in SelectMultiTypeahead * Use an actually-unique ID in SelectMultiTypeahead * Add Data Driven Forms dependencies * Remove commented-out code * Add isItemKind type guard * Use basic data-driven form with overview page * Add ALL_ITEM_KINDS * Match type and details steps of original form * Get tile preview working * Add basic panel editor * Revert to uncontrolled panel preview * Add field to show task YAML errors * Ensure that task errors wrap * Add step to download files * Revert "Revert to uncontrolled panel preview" This reverts commit 7c6fa87c9b8d90bd23074da151cb1044048d0b8b. * Update task preview as moving through wizard * Remove missing content placeholder * Remove unused code * Fix "explicit use of any" error on FormValue * Document the PropUpdater hack * Replace ugly hack with much cleaner hack * Remove logging * Don't use FormTemplate * Fix extra array around tags * Use WizardProps type for wizard schema * Update Patternfly and tsc-transform-inputs in order to get automatic module finding * Use custom buttons for wizard footer * Remove SelectMultiTypeahead * Update schema comments * Remove unused CSS * Remove unnecessary Viewer * Rename App to Viewer * Encapsulate uses of itemKindMeta * Standardize on "kind" rather than "type" * Remove broken import * Further kind fixes * Fix button handling * Move CreatorPreview CSS to proper component * Remove CatalogSection CSS import * Sync react-code-editor --- fec.config.js | 1 + package-lock.json | 468 ++++++++++++++++++--- package.json | 12 +- src/AppEntry.tsx | 4 +- src/Creator.tsx | 240 +++++++++++ src/{App.scss => Viewer.scss} | 0 src/{App.tsx => Viewer.tsx} | 4 +- src/components/CatalogSection.scss | 15 - src/components/CatalogSection.tsx | 68 +-- src/components/WrappedQuickStartTile.scss | 16 + src/components/WrappedQuickStartTile.tsx | 75 ++++ src/components/creator/CreatorPreview.scss | 4 + src/components/creator/CreatorPreview.tsx | 90 ++++ src/components/creator/CreatorWizard.tsx | 313 ++++++++++++++ src/components/creator/meta.ts | 80 ++++ src/components/creator/schema.tsx | 334 +++++++++++++++ 16 files changed, 1593 insertions(+), 131 deletions(-) create mode 100644 src/Creator.tsx rename src/{App.scss => Viewer.scss} (100%) rename src/{App.tsx => Viewer.tsx} (99%) create mode 100644 src/components/WrappedQuickStartTile.scss create mode 100644 src/components/WrappedQuickStartTile.tsx create mode 100644 src/components/creator/CreatorPreview.scss create mode 100644 src/components/creator/CreatorPreview.tsx create mode 100644 src/components/creator/CreatorWizard.tsx create mode 100644 src/components/creator/meta.ts create mode 100644 src/components/creator/schema.tsx diff --git a/fec.config.js b/fec.config.js index 10734899..9be6a090 100644 --- a/fec.config.js +++ b/fec.config.js @@ -26,6 +26,7 @@ module.exports = { __dirname, './src/components/GlobalLearningResourcesPage/GlobalLearningResourcesPage' ), + './Creator': path.resolve(__dirname, './src/Creator.tsx'), }, exclude: ['react-router-dom'], shared: [ diff --git a/package-lock.json b/package-lock.json index f11e48dc..27967b3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,11 @@ "version": "1.1.0", "hasInstallScript": true, "dependencies": { - "@patternfly/quickstarts": "5.2.0-prerelease.3", - "@patternfly/react-core": "^5.1.2", + "@data-driven-forms/pf4-component-mapper": "^3.23.0", + "@data-driven-forms/react-form-renderer": "^3.23.0", + "@patternfly/quickstarts": "^5.4.0-prerelease.1", + "@patternfly/react-code-editor": "^5.3.4", + "@patternfly/react-core": "^5.3.4", "@patternfly/react-table": "^5.1.1", "@redhat-cloud-services/frontend-components": "^4.2.9", "@redhat-cloud-services/frontend-components-notifications": "^4.0.4", @@ -20,12 +23,13 @@ "axios": "^1.6.7", "react": "18.2.0", "react-dom": "18.2.0", - "react-router-dom": "^6.22.3" + "react-router-dom": "^6.22.3", + "yaml": "^2.4.5" }, "devDependencies": { "@redhat-cloud-services/eslint-config-redhat-cloud-services": "^2.0.3", "@redhat-cloud-services/frontend-components-config": "^6.0.5", - "@redhat-cloud-services/tsc-transform-imports": "^1.0.4", + "@redhat-cloud-services/tsc-transform-imports": "^1.0.15", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^14.1.2", "@types/react": "18.2.46", @@ -657,6 +661,64 @@ "ms": "^2.1.1" } }, + "node_modules/@data-driven-forms/common": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@data-driven-forms/common/-/common-3.23.0.tgz", + "integrity": "sha512-R0kXzrDj+oqYqFt315qW255QXO5KtpV6lnW72My3nfbWSdx8Y0afKMvxbO+qKTWJJ/xwxYsNg4/XR0oWgt7LSQ==", + "dependencies": { + "clsx": "^1.0.4", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.2 || ^18.0.0", + "react-dom": "^16.13.1 || ^17.0.2 || ^18.0.0" + } + }, + "node_modules/@data-driven-forms/common/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@data-driven-forms/pf4-component-mapper": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@data-driven-forms/pf4-component-mapper/-/pf4-component-mapper-3.23.0.tgz", + "integrity": "sha512-mLyDhbDU3qqXT5RP8lnSAOiXZo5JBVzCe9qTLQCwq4QGvL7A13hlFzbHBvuytg4RO6XIYxTBpnFacEmQqTl/hQ==", + "dependencies": { + "@data-driven-forms/common": "^3.23.0", + "downshift": "^5.4.3", + "lodash": "^4.17.21", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@data-driven-forms/react-form-renderer": "^3.23.0", + "@patternfly/react-core": "^5.0.0", + "@patternfly/react-icons": "^5.0.0", + "react": "^17.0.2 || ^18.0.0", + "react-dom": "^17.0.2 || ^18.0.0" + } + }, + "node_modules/@data-driven-forms/react-form-renderer": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@data-driven-forms/react-form-renderer/-/react-form-renderer-3.23.0.tgz", + "integrity": "sha512-IcbJ/s4vR0blPtaofCMZ7oIIooITWBM6yQjEHDyANVDLOdxeJUYkmD0U0Qyy7cZyJj/zBjOfCueR65ZDchqcfQ==", + "dependencies": { + "final-form": "^4.20.4", + "final-form-arrays": "^3.0.2", + "final-form-focus": "^1.1.2", + "lodash": "^4.17.15", + "prop-types": "^15.7.2", + "react-final-form": "^6.5.0", + "react-final-form-arrays": "^3.1.1" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.2 || ^18.0.0", + "react-dom": "^16.13.1 || ^17.0.2 || ^18.0.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -1792,6 +1854,30 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", "dev": true }, + "node_modules/@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "dependencies": { + "@monaco-editor/loader": "^1.4.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -1897,9 +1983,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/@patternfly/quickstarts": { - "version": "5.2.0-prerelease.3", - "resolved": "https://registry.npmjs.org/@patternfly/quickstarts/-/quickstarts-5.2.0-prerelease.3.tgz", - "integrity": "sha512-WNBKV841bNAr4oty31w2vrTvh+PdNvMnT7RbdpYGwW3ubgr9Za2r/ZH2qO4cJPdnMfjTySzhXTVKxFtMlIsoDg==", + "version": "5.4.0-prerelease.1", + "resolved": "https://registry.npmjs.org/@patternfly/quickstarts/-/quickstarts-5.4.0-prerelease.1.tgz", + "integrity": "sha512-Sl9LdZh2mbk1q2NaEG6Tmopl9KbUoKKl9jgQU9zwBaI+u5x7ZiVbD2Q0uAyliraKQNZPUs86TA0roAeYh3iFeg==", "dependencies": { "@patternfly/react-catalog-view-extension": "^5.0.0", "dompurify": "^2.2.6", @@ -1925,6 +2011,23 @@ "react-dom": "^17 || ^18" } }, + "node_modules/@patternfly/react-code-editor": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-5.3.4.tgz", + "integrity": "sha512-pwL7NkRsQps5D6xOe2nepS4yRTbU/Im4Y09Yg2teB5G/f5oGI5OW9pUAagjcK0TAV/Gb6YILCwixoaXtuw0n+g==", + "dependencies": { + "@monaco-editor/react": "^4.6.0", + "@patternfly/react-core": "^5.3.4", + "@patternfly/react-icons": "^5.3.2", + "@patternfly/react-styles": "^5.3.1", + "react-dropzone": "14.2.3", + "tslib": "^2.5.0" + }, + "peerDependencies": { + "react": "^17 || ^18", + "react-dom": "^17 || ^18" + } + }, "node_modules/@patternfly/react-component-groups": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@patternfly/react-component-groups/-/react-component-groups-5.0.0.tgz", @@ -1942,13 +2045,13 @@ } }, "node_modules/@patternfly/react-core": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-5.1.2.tgz", - "integrity": "sha512-MeSasp7PgkqlirlbbGuEj6j3KqXVoNkE3c3N6rfxTZOF025ullDJjtzf/L/Fiyht4tH1uNCtkdlpnea6jqTMPg==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-5.3.4.tgz", + "integrity": "sha512-zr2yeilIoFp8MFOo0vNgI8XuM+P2466zHvy4smyRNRH2/but2WObqx7Wu4ftd/eBMYdNqmTeuXe6JeqqRqnPMQ==", "dependencies": { - "@patternfly/react-icons": "^5.1.2", - "@patternfly/react-styles": "^5.1.2", - "@patternfly/react-tokens": "^5.1.2", + "@patternfly/react-icons": "^5.3.2", + "@patternfly/react-styles": "^5.3.1", + "@patternfly/react-tokens": "^5.3.1", "focus-trap": "7.5.2", "react-dropzone": "^14.2.3", "tslib": "^2.5.0" @@ -1959,18 +2062,18 @@ } }, "node_modules/@patternfly/react-icons": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-5.1.2.tgz", - "integrity": "sha512-hgf3OchvNyCcxqDrRJCkxauFdxENtVX2d6uTkMfOQWP3hs8hqYGHR5S0pe2teJ1SwAs2Rgtf7ezzmzKAouAjkw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-5.3.2.tgz", + "integrity": "sha512-GEygYbl0H4zD8nZuTQy2dayKIrV2bMMeWKSOEZ16Y3EYNgYVUOUnN+J0naAEuEGH39Xb1DE9n+XUbE1PC4CxPA==", "peerDependencies": { "react": "^17 || ^18", "react-dom": "^17 || ^18" } }, "node_modules/@patternfly/react-styles": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-5.1.2.tgz", - "integrity": "sha512-rGNo8MstZG2r3yDS1tWwYDctK1qWW5RT1UwKF1DrQfhZ8ruEEL6m2ZXXM0u62hmM3qq4Q8h5lgn/bVHBnOHSLA==" + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-5.3.1.tgz", + "integrity": "sha512-H6uBoFH3bJjD6PP75qZ4k+2TtF59vxf9sIVerPpwrGJcRgBZbvbMZCniSC3+S2LQ8DgXLnDvieq78jJzHz0hiA==" }, "node_modules/@patternfly/react-table": { "version": "5.1.2", @@ -1990,9 +2093,9 @@ } }, "node_modules/@patternfly/react-tokens": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-5.1.2.tgz", - "integrity": "sha512-hu/6kEEMnyDc4GiMiaEau3kYq0BZoB3X1tZLcNfg9zQZnOydUgaLcUgR8+IlMF/nVVIqNjZF2RA/5lmKAVz2cQ==" + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-5.3.1.tgz", + "integrity": "sha512-VYK0uVP2/2RJ7ZshJCCLeq0Boih5I1bv+9Z/Bg6h12dCkLs85XsxAX9Ve+BGIo5DF54/mzcRHE1RKYap4ISXuw==" }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", @@ -2404,9 +2507,9 @@ } }, "node_modules/@redhat-cloud-services/tsc-transform-imports": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@redhat-cloud-services/tsc-transform-imports/-/tsc-transform-imports-1.0.4.tgz", - "integrity": "sha512-5qI4QYSZqH4dSTfMqxLaVHGTRqocHVaXGGUK6JXei4/sP6ZMq3liJmLmnvXMe9gSziMVz0I8ij4wuKpB0FuC8A==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@redhat-cloud-services/tsc-transform-imports/-/tsc-transform-imports-1.0.15.tgz", + "integrity": "sha512-VafxHzUizeYFBGLSvspF/t7IO4kB1W73cAF6XPz6603iI4tLvy1UWFobhsaqFRCYfUz165IzHoE6usc0sy1D2A==", "dev": true, "dependencies": { "glob": "10.3.3" @@ -5519,6 +5622,11 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, + "node_modules/compute-scroll-into-view": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", + "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5728,6 +5836,15 @@ "node": ">=10" } }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -6744,6 +6861,25 @@ "tslib": "^2.0.3" } }, + "node_modules/downshift": { + "version": "5.4.7", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-5.4.7.tgz", + "integrity": "sha512-xaH0RNqwJ5pAsyk9qBmR9XJWmg1OOWMfrhzYv0NH2NjJxn77S3zBcfClw341UfhGyKg5v+qVqg/CQzvAgBNCXQ==", + "dependencies": { + "@babel/runtime": "^7.10.2", + "compute-scroll-into-view": "^1.0.14", + "prop-types": "^15.7.2", + "react-is": "^16.13.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/downshift/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -7907,6 +8043,37 @@ "node": ">=8" } }, + "node_modules/final-form": { + "version": "4.20.10", + "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.10.tgz", + "integrity": "sha512-TL48Pi1oNHeMOHrKv1bCJUrWZDcD3DIG6AGYVNOnyZPr7Bd/pStN0pL+lfzF5BNoj/FclaoiaLenk4XUIFVYng==", + "dependencies": { + "@babel/runtime": "^7.10.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/final-form" + } + }, + "node_modules/final-form-arrays": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/final-form-arrays/-/final-form-arrays-3.1.0.tgz", + "integrity": "sha512-TWBvun+AopgBLw9zfTFHBllnKMVNEwCEyDawphPuBGGqNsuhGzhT7yewHys64KFFwzIs6KEteGLpKOwvTQEscQ==", + "peerDependencies": { + "final-form": "^4.20.8" + } + }, + "node_modules/final-form-focus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/final-form-focus/-/final-form-focus-1.1.2.tgz", + "integrity": "sha512-Gd+Bd2Ll7ijo3/sd6kJ/bwLkhc2bUJPxTON6fIqee/008EJpACWhT+zoWCm9q6NcfMcWRS+Sp5ikRX8iqdXeGQ==", + "peerDependencies": { + "final-form": ">=1.3.0" + } + }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -13291,6 +13458,12 @@ "node": ">=10" } }, + "node_modules/monaco-editor": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.49.0.tgz", + "integrity": "sha512-2I8/T3X/hLxB2oPHgqcNYUVdA/ZEFShT7IAujifIPMfKkNbLOqY8XCoyHCXrsdjb36dW9MwoTwBCFpXKMwNwaQ==", + "peer": true + }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -14787,6 +14960,36 @@ "react": ">= 16.8 || 18.0.0" } }, + "node_modules/react-final-form": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.9.tgz", + "integrity": "sha512-x3XYvozolECp3nIjly+4QqxdjSSWfcnpGEL5K8OBT6xmGrq5kBqbA6+/tOqoom9NwqIPPbxPNsOViFlbKgowbA==", + "dependencies": { + "@babel/runtime": "^7.15.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/final-form" + }, + "peerDependencies": { + "final-form": "^4.20.4", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-final-form-arrays": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-final-form-arrays/-/react-final-form-arrays-3.1.4.tgz", + "integrity": "sha512-siVFAolUAe29rMR6u8VwepoysUcUdh6MLV2OWnCtKpsPRUdT9VUgECjAPaVMAH2GROZNiVB9On1H9MMrm9gdpg==", + "dependencies": { + "@babel/runtime": "^7.19.4" + }, + "peerDependencies": { + "final-form": "^4.15.0", + "final-form-arrays": ">=1.0.4", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-final-form": "^6.2.1" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -16056,6 +16259,11 @@ "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", "dev": true }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -18216,12 +18424,14 @@ "dev": true }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/yargs": { @@ -18753,6 +18963,48 @@ } } }, + "@data-driven-forms/common": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@data-driven-forms/common/-/common-3.23.0.tgz", + "integrity": "sha512-R0kXzrDj+oqYqFt315qW255QXO5KtpV6lnW72My3nfbWSdx8Y0afKMvxbO+qKTWJJ/xwxYsNg4/XR0oWgt7LSQ==", + "requires": { + "clsx": "^1.0.4", + "lodash": "^4.17.15", + "prop-types": "^15.7.2" + }, + "dependencies": { + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + } + } + }, + "@data-driven-forms/pf4-component-mapper": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@data-driven-forms/pf4-component-mapper/-/pf4-component-mapper-3.23.0.tgz", + "integrity": "sha512-mLyDhbDU3qqXT5RP8lnSAOiXZo5JBVzCe9qTLQCwq4QGvL7A13hlFzbHBvuytg4RO6XIYxTBpnFacEmQqTl/hQ==", + "requires": { + "@data-driven-forms/common": "^3.23.0", + "downshift": "^5.4.3", + "lodash": "^4.17.21", + "prop-types": "^15.7.2" + } + }, + "@data-driven-forms/react-form-renderer": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@data-driven-forms/react-form-renderer/-/react-form-renderer-3.23.0.tgz", + "integrity": "sha512-IcbJ/s4vR0blPtaofCMZ7oIIooITWBM6yQjEHDyANVDLOdxeJUYkmD0U0Qyy7cZyJj/zBjOfCueR65ZDchqcfQ==", + "requires": { + "final-form": "^4.20.4", + "final-form-arrays": "^3.0.2", + "final-form-focus": "^1.1.2", + "lodash": "^4.17.15", + "prop-types": "^15.7.2", + "react-final-form": "^6.5.0", + "react-final-form-arrays": "^3.1.1" + } + }, "@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -19611,6 +19863,22 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", "dev": true }, + "@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "requires": { + "state-local": "^1.0.6" + } + }, + "@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "requires": { + "@monaco-editor/loader": "^1.4.0" + } + }, "@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -19691,9 +19959,9 @@ } }, "@patternfly/quickstarts": { - "version": "5.2.0-prerelease.3", - "resolved": "https://registry.npmjs.org/@patternfly/quickstarts/-/quickstarts-5.2.0-prerelease.3.tgz", - "integrity": "sha512-WNBKV841bNAr4oty31w2vrTvh+PdNvMnT7RbdpYGwW3ubgr9Za2r/ZH2qO4cJPdnMfjTySzhXTVKxFtMlIsoDg==", + "version": "5.4.0-prerelease.1", + "resolved": "https://registry.npmjs.org/@patternfly/quickstarts/-/quickstarts-5.4.0-prerelease.1.tgz", + "integrity": "sha512-Sl9LdZh2mbk1q2NaEG6Tmopl9KbUoKKl9jgQU9zwBaI+u5x7ZiVbD2Q0uAyliraKQNZPUs86TA0roAeYh3iFeg==", "requires": { "@patternfly/react-catalog-view-extension": "^5.0.0", "dompurify": "^2.2.6", @@ -19709,6 +19977,19 @@ "@patternfly/react-styles": "^5.0.0" } }, + "@patternfly/react-code-editor": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-5.3.4.tgz", + "integrity": "sha512-pwL7NkRsQps5D6xOe2nepS4yRTbU/Im4Y09Yg2teB5G/f5oGI5OW9pUAagjcK0TAV/Gb6YILCwixoaXtuw0n+g==", + "requires": { + "@monaco-editor/react": "^4.6.0", + "@patternfly/react-core": "^5.3.4", + "@patternfly/react-icons": "^5.3.2", + "@patternfly/react-styles": "^5.3.1", + "react-dropzone": "14.2.3", + "tslib": "^2.5.0" + } + }, "@patternfly/react-component-groups": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@patternfly/react-component-groups/-/react-component-groups-5.0.0.tgz", @@ -19722,28 +20003,28 @@ } }, "@patternfly/react-core": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-5.1.2.tgz", - "integrity": "sha512-MeSasp7PgkqlirlbbGuEj6j3KqXVoNkE3c3N6rfxTZOF025ullDJjtzf/L/Fiyht4tH1uNCtkdlpnea6jqTMPg==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-5.3.4.tgz", + "integrity": "sha512-zr2yeilIoFp8MFOo0vNgI8XuM+P2466zHvy4smyRNRH2/but2WObqx7Wu4ftd/eBMYdNqmTeuXe6JeqqRqnPMQ==", "requires": { - "@patternfly/react-icons": "^5.1.2", - "@patternfly/react-styles": "^5.1.2", - "@patternfly/react-tokens": "^5.1.2", + "@patternfly/react-icons": "^5.3.2", + "@patternfly/react-styles": "^5.3.1", + "@patternfly/react-tokens": "^5.3.1", "focus-trap": "7.5.2", "react-dropzone": "^14.2.3", "tslib": "^2.5.0" } }, "@patternfly/react-icons": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-5.1.2.tgz", - "integrity": "sha512-hgf3OchvNyCcxqDrRJCkxauFdxENtVX2d6uTkMfOQWP3hs8hqYGHR5S0pe2teJ1SwAs2Rgtf7ezzmzKAouAjkw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-5.3.2.tgz", + "integrity": "sha512-GEygYbl0H4zD8nZuTQy2dayKIrV2bMMeWKSOEZ16Y3EYNgYVUOUnN+J0naAEuEGH39Xb1DE9n+XUbE1PC4CxPA==", "requires": {} }, "@patternfly/react-styles": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-5.1.2.tgz", - "integrity": "sha512-rGNo8MstZG2r3yDS1tWwYDctK1qWW5RT1UwKF1DrQfhZ8ruEEL6m2ZXXM0u62hmM3qq4Q8h5lgn/bVHBnOHSLA==" + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-5.3.1.tgz", + "integrity": "sha512-H6uBoFH3bJjD6PP75qZ4k+2TtF59vxf9sIVerPpwrGJcRgBZbvbMZCniSC3+S2LQ8DgXLnDvieq78jJzHz0hiA==" }, "@patternfly/react-table": { "version": "5.1.2", @@ -19759,9 +20040,9 @@ } }, "@patternfly/react-tokens": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-5.1.2.tgz", - "integrity": "sha512-hu/6kEEMnyDc4GiMiaEau3kYq0BZoB3X1tZLcNfg9zQZnOydUgaLcUgR8+IlMF/nVVIqNjZF2RA/5lmKAVz2cQ==" + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-5.3.1.tgz", + "integrity": "sha512-VYK0uVP2/2RJ7ZshJCCLeq0Boih5I1bv+9Z/Bg6h12dCkLs85XsxAX9Ve+BGIo5DF54/mzcRHE1RKYap4ISXuw==" }, "@pkgjs/parseargs": { "version": "0.11.0", @@ -20062,9 +20343,9 @@ } }, "@redhat-cloud-services/tsc-transform-imports": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@redhat-cloud-services/tsc-transform-imports/-/tsc-transform-imports-1.0.4.tgz", - "integrity": "sha512-5qI4QYSZqH4dSTfMqxLaVHGTRqocHVaXGGUK6JXei4/sP6ZMq3liJmLmnvXMe9gSziMVz0I8ij4wuKpB0FuC8A==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@redhat-cloud-services/tsc-transform-imports/-/tsc-transform-imports-1.0.15.tgz", + "integrity": "sha512-VafxHzUizeYFBGLSvspF/t7IO4kB1W73cAF6XPz6603iI4tLvy1UWFobhsaqFRCYfUz165IzHoE6usc0sy1D2A==", "dev": true, "requires": { "glob": "10.3.3" @@ -22506,6 +22787,11 @@ } } }, + "compute-scroll-into-view": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", + "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -22657,6 +22943,14 @@ "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" + }, + "dependencies": { + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + } } }, "create-jest": { @@ -23424,6 +23718,24 @@ "tslib": "^2.0.3" } }, + "downshift": { + "version": "5.4.7", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-5.4.7.tgz", + "integrity": "sha512-xaH0RNqwJ5pAsyk9qBmR9XJWmg1OOWMfrhzYv0NH2NjJxn77S3zBcfClw341UfhGyKg5v+qVqg/CQzvAgBNCXQ==", + "requires": { + "@babel/runtime": "^7.10.2", + "compute-scroll-into-view": "^1.0.14", + "prop-types": "^15.7.2", + "react-is": "^16.13.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -24330,6 +24642,26 @@ "to-regex-range": "^5.0.1" } }, + "final-form": { + "version": "4.20.10", + "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.10.tgz", + "integrity": "sha512-TL48Pi1oNHeMOHrKv1bCJUrWZDcD3DIG6AGYVNOnyZPr7Bd/pStN0pL+lfzF5BNoj/FclaoiaLenk4XUIFVYng==", + "requires": { + "@babel/runtime": "^7.10.0" + } + }, + "final-form-arrays": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/final-form-arrays/-/final-form-arrays-3.1.0.tgz", + "integrity": "sha512-TWBvun+AopgBLw9zfTFHBllnKMVNEwCEyDawphPuBGGqNsuhGzhT7yewHys64KFFwzIs6KEteGLpKOwvTQEscQ==", + "requires": {} + }, + "final-form-focus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/final-form-focus/-/final-form-focus-1.1.2.tgz", + "integrity": "sha512-Gd+Bd2Ll7ijo3/sd6kJ/bwLkhc2bUJPxTON6fIqee/008EJpACWhT+zoWCm9q6NcfMcWRS+Sp5ikRX8iqdXeGQ==", + "requires": {} + }, "finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -28318,6 +28650,12 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, + "monaco-editor": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.49.0.tgz", + "integrity": "sha512-2I8/T3X/hLxB2oPHgqcNYUVdA/ZEFShT7IAujifIPMfKkNbLOqY8XCoyHCXrsdjb36dW9MwoTwBCFpXKMwNwaQ==", + "peer": true + }, "mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -29408,6 +29746,22 @@ "prop-types": "^15.8.1" } }, + "react-final-form": { + "version": "6.5.9", + "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.9.tgz", + "integrity": "sha512-x3XYvozolECp3nIjly+4QqxdjSSWfcnpGEL5K8OBT6xmGrq5kBqbA6+/tOqoom9NwqIPPbxPNsOViFlbKgowbA==", + "requires": { + "@babel/runtime": "^7.15.4" + } + }, + "react-final-form-arrays": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-final-form-arrays/-/react-final-form-arrays-3.1.4.tgz", + "integrity": "sha512-siVFAolUAe29rMR6u8VwepoysUcUdh6MLV2OWnCtKpsPRUdT9VUgECjAPaVMAH2GROZNiVB9On1H9MMrm9gdpg==", + "requires": { + "@babel/runtime": "^7.19.4" + } + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -30360,6 +30714,11 @@ "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", "dev": true }, + "state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -31920,10 +32279,9 @@ "dev": true }, "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==" }, "yargs": { "version": "17.7.2", diff --git a/package.json b/package.json index e4784b47..419d28be 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,11 @@ "postinstall": "ts-patch install" }, "dependencies": { - "@patternfly/quickstarts": "5.2.0-prerelease.3", - "@patternfly/react-core": "^5.1.2", + "@data-driven-forms/pf4-component-mapper": "^3.23.0", + "@data-driven-forms/react-form-renderer": "^3.23.0", + "@patternfly/quickstarts": "^5.4.0-prerelease.1", + "@patternfly/react-code-editor": "^5.3.4", + "@patternfly/react-core": "^5.3.4", "@patternfly/react-table": "^5.1.1", "@redhat-cloud-services/frontend-components": "^4.2.9", "@redhat-cloud-services/frontend-components-notifications": "^4.0.4", @@ -31,12 +34,13 @@ "axios": "^1.6.7", "react": "18.2.0", "react-dom": "18.2.0", - "react-router-dom": "^6.22.3" + "react-router-dom": "^6.22.3", + "yaml": "^2.4.5" }, "devDependencies": { "@redhat-cloud-services/eslint-config-redhat-cloud-services": "^2.0.3", "@redhat-cloud-services/frontend-components-config": "^6.0.5", - "@redhat-cloud-services/tsc-transform-imports": "^1.0.4", + "@redhat-cloud-services/tsc-transform-imports": "^1.0.15", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^14.1.2", "@types/react": "18.2.46", diff --git a/src/AppEntry.tsx b/src/AppEntry.tsx index 65ca672d..7b317c39 100644 --- a/src/AppEntry.tsx +++ b/src/AppEntry.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { App } from './App'; +import { Viewer } from './Viewer'; -const AppEntry = (props: { bundle: string }) => ; +const AppEntry = (props: { bundle: string }) => ; export default AppEntry; diff --git a/src/Creator.tsx b/src/Creator.tsx new file mode 100644 index 00000000..cc0f4369 --- /dev/null +++ b/src/Creator.tsx @@ -0,0 +1,240 @@ +import React, { useMemo, useState } from 'react'; +import YAML, { YAMLError } from 'yaml'; +import { + Grid, + GridItem, + PageGroup, + PageSection, + Title, +} from '@patternfly/react-core'; +import { + QuickStart, + QuickStartSpec, + QuickStartTask, +} from '@patternfly/quickstarts'; +import CreatorWizard, { EMPTY_TASK } from './components/creator/CreatorWizard'; +import { ItemKind, metaForKind } from './components/creator/meta'; +import CreatorPreview from './components/creator/CreatorPreview'; + +export type CreatorErrors = { + taskErrors: Map; +}; + +const BASE_METADATA = { + name: 'test-quickstart', +}; + +function makeDemoQuickStart( + kind: ItemKind | null, + baseQuickStart: QuickStart, + taskContents: string[] +): [QuickStart, CreatorErrors] { + const kindMeta = kind !== null ? metaForKind(kind) : null; + + const [tasks, taskErrors] = (() => { + if (kindMeta?.hasTasks !== true) return [undefined, new Map()]; + + const out: QuickStartTask[] = []; + const errors: CreatorErrors['taskErrors'] = new Map(); + + if (baseQuickStart.spec.tasks !== undefined) { + for (let index = 0; index < baseQuickStart.spec.tasks.length; ++index) { + const task = baseQuickStart.spec.tasks[index]; + + try { + out.push({ + ...YAML.parse(taskContents[index]), + title: task.title, + }); + } catch (e) { + if (!(e instanceof YAMLError)) throw e; + + out.push({ ...EMPTY_TASK, title: task.title }); + errors.set(index, e.message); + } + } + } + + return [out, errors]; + })(); + + return [ + { + ...baseQuickStart, + metadata: { + ...baseQuickStart.metadata, + name: 'test-quickstart', + ...(kindMeta?.extraMetadata ?? {}), + }, + spec: { + ...baseQuickStart.spec, + tasks: tasks, + }, + }, + { taskErrors }, + ]; +} + +const Creator = () => { + const [rawKind, setRawKind] = useState(null); + + const [rawQuickStart, setRawQuickStart] = useState({ + metadata: { + name: 'test-quickstart', + }, + spec: { + displayName: '', + icon: null, + description: '', + }, + }); + + const selectedKind = + rawKind !== null ? { id: rawKind, meta: metaForKind(rawKind) } : null; + + const [bundles, setBundles] = useState([]); + const [taskContents, setTaskContents] = useState([]); + + const [currentTask, setCurrentTask] = useState(null); + + const updateSpec = ( + updater: (old: QuickStartSpec) => Partial + ) => { + setRawQuickStart((old) => ({ + ...old, + spec: { + ...old.spec, + ...updater(old.spec), + }, + })); + }; + + const setKind = (newKind: ItemKind | null) => { + if (newKind !== null) { + const meta = metaForKind(newKind); + + setRawQuickStart((old) => { + const updates: Partial = {}; + + updates.spec = { ...old.spec }; + + updates.spec.type = { + text: meta.displayName, + color: meta.tagColor, + }; + + if ( + meta.hasTasks && + (old.spec.tasks === undefined || old.spec.tasks.length === 0) + ) { + updates.spec.tasks = [EMPTY_TASK]; + } + + if (!meta.hasTasks) { + updates.spec.tasks = undefined; + updates.spec.introduction = undefined; + updates.spec.prerequisites = []; + } + + if (!meta.fields.url) updates.spec.link = undefined; + if (!meta.fields.duration) updates.spec.durationMinutes = undefined; + + updates.metadata = { ...BASE_METADATA, ...meta.extraMetadata }; + + return { ...old, ...updates }; + }); + + if (meta.hasTasks) { + setTaskContents((old) => (old.length === 0 ? [''] : old)); + } else if (!meta.hasTasks) { + setTaskContents([]); + } + } + + setRawKind(newKind); + }; + + const [quickStart, errors] = useMemo( + () => makeDemoQuickStart(rawKind, rawQuickStart, taskContents), + [rawKind, rawQuickStart, taskContents] + ); + + const files = useMemo(() => { + const effectiveName = quickStart.spec.displayName + .toLowerCase() + .replaceAll(/\s/g, '-') + .replaceAll(/(^-+)|(-+$)/g, ''); + + const adjustedQuickstart = { ...quickStart }; + adjustedQuickstart.spec = { ...adjustedQuickstart.spec }; + adjustedQuickstart.metadata = { + ...adjustedQuickstart.metadata, + name: effectiveName, + }; + + delete adjustedQuickstart.spec['icon']; + + return [ + { + name: 'metadata.yaml', + content: YAML.stringify({ + kind: 'QuickStarts', + name: effectiveName, + tags: bundles + .toSorted() + .map((bundle) => ({ kind: 'bundle', value: bundle })), + }), + }, + { + name: `${effectiveName}.yaml`, + content: YAML.stringify(adjustedQuickstart), + }, + ]; + }, [quickStart, bundles]); + + if ((quickStart.spec.tasks?.length ?? 0) != taskContents.length) { + throw new Error( + `Mismatch between quickstart tasks and task contents: ${quickStart.spec.tasks?.length} vs ${taskContents.length}` + ); + } + + return ( + + + + Add new learning resources + + +

Description

+
+ + + + + { + updateSpec(() => spec); + }} + onChangeBundles={setBundles} + onChangeTaskContents={setTaskContents} + onChangeCurrentTask={setCurrentTask} + errors={errors} + files={files} + /> + + + + + + + +
+ ); +}; + +export default Creator; diff --git a/src/App.scss b/src/Viewer.scss similarity index 100% rename from src/App.scss rename to src/Viewer.scss diff --git a/src/App.tsx b/src/Viewer.tsx similarity index 99% rename from src/App.tsx rename to src/Viewer.tsx index ade1237f..cb2c94f6 100644 --- a/src/App.tsx +++ b/src/Viewer.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import './App.scss'; +import './Viewer.scss'; import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome'; import { LoadingBox, @@ -26,7 +26,7 @@ import { BookmarkIcon, OutlinedBookmarkIcon } from '@patternfly/react-icons'; import { useFlag } from '@unleash/proxy-client-react'; import useQuickStarts from './hooks/useQuickStarts'; -export const App = ({ bundle }: { bundle: string }) => { +export const Viewer = ({ bundle }: { bundle: string }) => { const chrome = useChrome(); const { activeQuickStartID, allQuickStartStates, setFilter, loading } = React.useContext(QuickStartContext); diff --git a/src/components/CatalogSection.scss b/src/components/CatalogSection.scss index ab4bcf32..ede3a7d9 100644 --- a/src/components/CatalogSection.scss +++ b/src/components/CatalogSection.scss @@ -22,18 +22,3 @@ margin-bottom: var(--pf-global--spacer--md); } } - -.lr-c-quickstart_tile { - .pf-v5-c-card__header { - height: 0; - } - .pf-v5-c-title { - flex: 1; - } - >div { - height: 100%; - >a { - color: var(--pf-v5-global--palette--black-1000); - } - } -} \ No newline at end of file diff --git a/src/components/CatalogSection.tsx b/src/components/CatalogSection.tsx index b7964767..47113bdd 100644 --- a/src/components/CatalogSection.tsx +++ b/src/components/CatalogSection.tsx @@ -1,7 +1,6 @@ import { AllQuickStartStates, QuickStart, - QuickStartTile, getQuickStartStatus, } from '@patternfly/quickstarts'; import { @@ -12,34 +11,18 @@ import { FlexItem, Gallery, GalleryItem, - Icon, Split, SplitItem, Text, TextContent, Title, } from '@patternfly/react-core'; -import React, { PropsWithChildren, SyntheticEvent, useState } from 'react'; -import { - AngleRightIcon, - BookmarkIcon, - OutlinedBookmarkIcon, -} from '@patternfly/react-icons'; +import React, { PropsWithChildren, useState } from 'react'; +import { AngleRightIcon } from '@patternfly/react-icons'; import { useFlag } from '@unleash/proxy-client-react'; import './CatalogSection.scss'; - -const OutlinedBookmarkedIcon = () => ( - - - -); - -const BookmarkedIcon = () => ( - - - -); +import WrappedQuickStartTile from './WrappedQuickStartTile'; const CatalogWrapper: React.FC< PropsWithChildren<{ @@ -178,39 +161,18 @@ const CatalogSection = ({ {sectionCount ? ( {sectionQuickStarts.map((quickStart) => ( - - ): void => { - if (showBookmarks) { - e.preventDefault(); - e.stopPropagation(); - toggleFavorite( - quickStart.metadata.name, - !quickStart.metadata.favorite - ); - } - }, - }} - quickStart={{ - ...quickStart, - spec: { - ...quickStart.spec, - // remove any lingering icons - icon: null, - }, - }} + + + toggleFavorite(quickStart.metadata.name, newState), + } + : null + } isActive={quickStart.metadata.name === activeQuickStartID} status={getQuickStartStatus( allQuickStartStates || {}, diff --git a/src/components/WrappedQuickStartTile.scss b/src/components/WrappedQuickStartTile.scss new file mode 100644 index 00000000..f537f51f --- /dev/null +++ b/src/components/WrappedQuickStartTile.scss @@ -0,0 +1,16 @@ +.lr-c-quickstart_tile { + height: 100%; + + .pf-v5-c-card__header { + height: 0; + } + .pf-v5-c-title { + flex: 1; + } + >div { + height: 100%; + >a { + color: var(--pf-v5-global--palette--black-1000); + } + } +} diff --git a/src/components/WrappedQuickStartTile.tsx b/src/components/WrappedQuickStartTile.tsx new file mode 100644 index 00000000..54c510c0 --- /dev/null +++ b/src/components/WrappedQuickStartTile.tsx @@ -0,0 +1,75 @@ +import React, { SyntheticEvent } from 'react'; +import { + QuickStart, + QuickStartStatus, + QuickStartTile, +} from '@patternfly/quickstarts'; + +import { BookmarkIcon, OutlinedBookmarkIcon } from '@patternfly/react-icons'; +import { Icon } from '@patternfly/react-core'; +import './WrappedQuickStartTile.scss'; + +const OutlinedBookmarkedIcon = () => ( + + + +); + +const BookmarkedIcon = () => ( + + + +); + +type BookmarksConfig = { + isFavorite: boolean; + setFavorite: (newState: boolean) => Promise; +} | null; + +const WrappedQuickStartTile = ({ + quickStart, + bookmarks, + isActive, + status, +}: { + quickStart: QuickStart; + bookmarks: BookmarksConfig; + isActive: boolean; + status: QuickStartStatus; +}) => { + return ( +
+ ): void => { + e.preventDefault(); + e.stopPropagation(); + bookmarks.setFavorite(!bookmarks.isFavorite); + }, + } + : undefined + } + quickStart={{ + ...quickStart, + spec: { + ...quickStart.spec, + // remove any lingering icons + icon: null, + }, + }} + isActive={isActive} + status={status} + /> +
+ ); +}; + +export default WrappedQuickStartTile; diff --git a/src/components/creator/CreatorPreview.scss b/src/components/creator/CreatorPreview.scss new file mode 100644 index 00000000..3db4e210 --- /dev/null +++ b/src/components/creator/CreatorPreview.scss @@ -0,0 +1,4 @@ +.rc-tile-preview-wrapper { + width: 300px; + height: fit-content; +} diff --git a/src/components/creator/CreatorPreview.tsx b/src/components/creator/CreatorPreview.tsx new file mode 100644 index 00000000..ddd6c5ff --- /dev/null +++ b/src/components/creator/CreatorPreview.tsx @@ -0,0 +1,90 @@ +import { + AllQuickStartStates, + QuickStart, + QuickStartContext, + QuickStartDrawer, + QuickStartStatus, + useValuesForQuickStartContext, +} from '@patternfly/quickstarts'; +import { Title } from '@patternfly/react-core'; +import WrappedQuickStartTile from '../WrappedQuickStartTile'; +import React, { useContext, useMemo, useState } from 'react'; +import { ItemMeta } from './meta'; +import './CreatorPreview.scss'; + +const CreatorPreview = ({ + kindMeta, + quickStart, + currentTask, +}: { + kindMeta: ItemMeta | null; + quickStart: QuickStart; + currentTask: number | null; +}) => { + const allQuickStarts = useMemo(() => [quickStart], [quickStart]); + const [quickStartStates, setQuickStartStates] = useState( + {} + ); + + const [prevTask, setPrevTask] = useState(currentTask); + + const parentContext = useContext(QuickStartContext); + + const quickstartValues = useValuesForQuickStartContext({ + allQuickStarts: [quickStart], + activeQuickStartID: + kindMeta?.hasTasks === true ? quickStart.metadata.name : '', + setActiveQuickStartID: () => {}, + allQuickStartStates: quickStartStates, + setAllQuickStartStates: (states) => setQuickStartStates(states), + useQueryParams: false, + footer: parentContext.footer, + focusOnQuickStart: false, + }); + + if (quickstartValues.allQuickStarts?.[0] !== quickStart) { + quickstartValues.setAllQuickStarts?.([quickStart]); + } + + if ( + prevTask !== currentTask || + quickstartValues?.activeQuickStartState === undefined + ) { + setPrevTask(currentTask); + + if (currentTask !== null) { + quickstartValues.setQuickStartTaskNumber?.( + quickStart.metadata.name, + currentTask + ); + } else { + quickstartValues.restartQuickStart?.( + quickStart.metadata.name, + quickStart.spec.tasks?.length ?? 0 + ); + } + } + + return ( + + +
+ + Live card preview + + +
+ +
+
+
+
+ ); +}; + +export default CreatorPreview; diff --git a/src/components/creator/CreatorWizard.tsx b/src/components/creator/CreatorWizard.tsx new file mode 100644 index 00000000..58214e2a --- /dev/null +++ b/src/components/creator/CreatorWizard.tsx @@ -0,0 +1,313 @@ +import { + Button, + ClipboardCopy, + ClipboardCopyVariant, +} from '@patternfly/react-core'; +import DownloadIcon from '@patternfly/react-icons/dist/dynamic/icons/download-icon'; +import React, { useContext, useEffect, useMemo } from 'react'; +import { ItemKind, isItemKind, metaForKind } from './meta'; +import { CreatorErrors } from '../../Creator'; +import { QuickStartSpec } from '@patternfly/quickstarts'; +import { + AnyObject, + FormRenderer, + FormSpy, +} from '@data-driven-forms/react-form-renderer'; +import DdfWizardContext from '@data-driven-forms/react-form-renderer/wizard-context'; +import pf4ComponentMapper from '@data-driven-forms/pf4-component-mapper/component-mapper'; +import { + NAME_BUNDLES, + NAME_DESCRIPTION, + NAME_DURATION, + NAME_KIND, + NAME_PANEL_INTRODUCTION, + NAME_PREREQUISITES, + NAME_TASKS_ARRAY, + NAME_TASK_TITLES, + NAME_TITLE, + NAME_URL, + makeSchema, + taskFromStepName, +} from './schema'; +import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome'; +import { downloadFile } from '@redhat-cloud-services/frontend-components-utilities/helpers'; + +export type TaskState = { + title: string; + yamlContent: string; +}; + +export const EMPTY_TASK: TaskState = { + title: '', + yamlContent: '', +}; + +type CreatorFiles = { + name: string; + content: string; +}[]; + +type CreatorWizardProps = { + onChangeKind: (newKind: ItemKind | null) => void; + onChangeQuickStartSpec: (newValue: QuickStartSpec) => void; + onChangeBundles: (newValue: string[]) => void; + onChangeTaskContents: (contents: string[]) => void; + onChangeCurrentTask: (index: number | null) => void; + files: CreatorFiles; + errors: CreatorErrors; +}; + +type FormValue = AnyObject; + +type UpdaterProps = { + values: FormValue; + onChangeKind: (newKind: ItemKind | null) => void; + onChangeBundles: (bundles: string[]) => void; + onChangeQuickStartSpec: (newValue: QuickStartSpec) => void; + onChangeTaskContents: (contents: string[]) => void; +}; + +const DEFAULT_TASK_TITLES: string[] = ['']; + +const PropUpdater = ({ + values, + onChangeKind, + onChangeBundles, + onChangeQuickStartSpec, + onChangeTaskContents, +}: UpdaterProps) => { + const bundles = values[NAME_BUNDLES]; + + useEffect(() => { + onChangeBundles(bundles ?? []); + }, [bundles]); + + const rawKind: string | undefined = values[NAME_KIND]; + const title: string | undefined = values[NAME_TITLE]; + const description: string | undefined = values[NAME_DESCRIPTION]; + const url: string | undefined = values[NAME_URL]; + const duration: number | string | undefined = values[NAME_DURATION]; + const prerequisites: string[] | undefined = values[NAME_PREREQUISITES]; + const introduction: string | undefined = values[NAME_PANEL_INTRODUCTION]; + + const taskTitles: string[] = values[NAME_TASK_TITLES] ?? DEFAULT_TASK_TITLES; + const taskValues: { content: string | undefined }[] | undefined = + values[NAME_TASKS_ARRAY]; + + const kind = + typeof rawKind === 'string' && isItemKind(rawKind) ? rawKind : null; + + const meta = kind !== null ? metaForKind(kind) : null; + + useEffect(() => { + onChangeKind(kind); + }, [kind]); + + const taskContents = useMemo(() => { + if (meta?.hasTasks !== true) { + return []; + } + + const effective = []; + + for (let i = 0; i < (taskTitles?.length ?? 0); ++i) { + effective.push(taskValues?.[i]?.content ?? ''); + } + + return effective; + }, [meta, taskTitles, taskValues]); + + useEffect(() => { + onChangeQuickStartSpec({ + type: + meta !== null + ? { + text: meta.displayName, + color: meta.tagColor, + } + : undefined, + displayName: title ?? '', + description: description ?? '', + icon: null, + link: + meta?.fields?.url && url !== undefined + ? { + text: 'View documentation', + href: url, + } + : undefined, + durationMinutes: + meta?.fields?.duration && typeof duration === 'number' + ? duration + : undefined, + prerequisites: meta?.hasTasks === true ? prerequisites : undefined, + introduction: meta?.hasTasks === true ? introduction : undefined, + tasks: + meta?.hasTasks === true + ? (taskTitles ?? []).map((t) => ({ title: t })) + : undefined, + }); + }, [ + meta, + rawKind, + title, + description, + url, + duration, + prerequisites, + introduction, + taskTitles, + ]); + + useEffect(() => { + onChangeTaskContents(taskContents); + }, [taskContents]); + + // Allow use as JSX component + return undefined; +}; + +const CreatorWizardContext = React.createContext<{ + errors: CreatorErrors; + files: CreatorFiles; + onChangeCurrentTask: (index: number | null) => void; +}>({ + errors: { + taskErrors: new Map(), + }, + files: [], + onChangeCurrentTask: () => {}, +}); + +const TaskErrorPreview = ({ index }: { index: number }) => { + const context = useContext(CreatorWizardContext); + const error = context.errors.taskErrors.get(index); + + return error !== undefined ? ( +
{error}
+ ) : undefined; +}; + +const FileDownload = () => { + const { files } = useContext(CreatorWizardContext); + + return ( +
+ Download these files. + {files.map((file) => ( +
+ + + + {file.content} + +
+ ))} +
+ ); +}; + +// Watches for changes in the current step, then calls onChangeCurrentTask so +// that Creator can update the live preview. +const WizardSpy = () => { + const wizardContext = useContext(DdfWizardContext); + const creatorContext = useContext(CreatorWizardContext); + + useEffect(() => { + creatorContext.onChangeCurrentTask( + taskFromStepName(wizardContext.currentStep.name) + ); + }, [wizardContext.currentStep.name]); + + return undefined; +}; + +const CreatorWizard = ({ + onChangeKind, + onChangeQuickStartSpec, + onChangeBundles, + onChangeTaskContents, + onChangeCurrentTask, + files, + errors, +}: CreatorWizardProps) => { + const chrome = useChrome(); + const schema = useMemo(() => makeSchema(chrome), []); + + const context = useMemo( + () => ({ + errors, + files, + onChangeCurrentTask, + }), + [errors, files] + ); + + const componentMapper = { + ...pf4ComponentMapper, + 'lr-task-error': TaskErrorPreview, + 'lr-download-files': FileDownload, + 'lr-wizard-spy': WizardSpy, + }; + + return ( + + {}} + schema={schema} + componentMapper={componentMapper} + > + {({ formFields }) => ( +
e.preventDefault()} className="pf-v5-c-form"> + + {/* + In order to display the live preview, we need to update the parent + whenever the form state changes. Unfortunately, as best as I can + tell, there is no way to pass FormRenderer a callback that's called + whenever a value changes. + + The example at [0] shows using a custom component in the schema to + watch the values, but it seems clearer to just add it once here + (and it avoids introducing another custom component name). + + [0]: https://github.com/data-driven-forms/react-forms/blob/master/packages/react-renderer-demo/src/examples/components/examples/value-listener.js + */} + {(props) => ( + + )} + + <>{formFields} +
+ )} +
+
+ ); +}; + +export default CreatorWizard; diff --git a/src/components/creator/meta.ts b/src/components/creator/meta.ts new file mode 100644 index 00000000..daafa0c5 --- /dev/null +++ b/src/components/creator/meta.ts @@ -0,0 +1,80 @@ +import { QuickStartType } from '@patternfly/quickstarts'; + +const rawItemKindMeta = Object.freeze({ + documentation: { + displayName: 'Documentation', + tagColor: 'orange', + fields: { + url: true, + }, + extraMetadata: { + externalDocumentation: true, + }, + }, + quickstart: { + displayName: 'Quickstart', + tagColor: 'green', + hasDuration: true, + fields: { + duration: true, + }, + hasTasks: true, + extraMetadata: {}, + }, + learningPath: { + displayName: 'Learning path', + tagColor: 'cyan', + fields: { + url: true, + }, + extraMetadata: { + learningPath: true, + }, + }, + other: { + displayName: 'Other', + tagColor: 'purple', + fields: { + url: true, + }, + extraMetadata: { + otherResource: true, + }, + }, +} as const); + +export type ItemMeta = { + displayName: string; + tagColor: QuickStartType['color']; + fields: { + url?: boolean; + duration?: boolean; + }; + hasTasks?: boolean; + extraMetadata: object; +}; + +const itemKindMeta: { + [k in keyof typeof rawItemKindMeta]: ItemMeta; +} = rawItemKindMeta; + +export type ItemKind = keyof typeof itemKindMeta; + +export function isItemKind(kind: string): kind is ItemKind { + return Object.hasOwn(itemKindMeta, kind); +} + +export function metaForKind(kind: ItemKind): ItemMeta { + return itemKindMeta[kind]; +} + +export const ALL_ITEM_KINDS = Object.freeze( + Object.keys(itemKindMeta) +) as readonly ItemKind[]; + +export const ALL_KIND_ENTRIES: readonly [ItemKind, ItemMeta][] = Object.entries( + itemKindMeta +).map(([k, v]) => { + if (!isItemKind(k)) throw new Error('unexpected item kind'); + return [k, v]; +}); diff --git a/src/components/creator/schema.tsx b/src/components/creator/schema.tsx new file mode 100644 index 00000000..65562bb0 --- /dev/null +++ b/src/components/creator/schema.tsx @@ -0,0 +1,334 @@ +import { + ConditionProp, + Field, + FormSpy, + Schema, + componentTypes, + dataTypes, + validatorTypes, +} from '@data-driven-forms/react-form-renderer'; +import { + ALL_ITEM_KINDS, + ALL_KIND_ENTRIES, + ItemKind, + ItemMeta, + isItemKind, + metaForKind, +} from './meta'; +import { ChromeAPI } from '@redhat-cloud-services/types'; +import { + WizardButtonsProps, + WizardProps, +} from '@data-driven-forms/pf4-component-mapper'; +import { WizardNextStepFunctionArgument } from '@data-driven-forms/pf4-component-mapper/wizard/wizard'; +import React from 'react'; +import { Button } from '@patternfly/react-core'; + +const CustomButtons = (props: WizardButtonsProps) => { + return ( + + {(state) => { + // Ensure that the form never says "Submit", and hide the button on the + // last step. + + const computedNext = props.nextStep + ? props.selectNext(props.nextStep, () => state) + : undefined; + + return ( + <> + {computedNext !== undefined + ? props.renderNextButton({ submitLabel: 'Next' }) + : null} + + + ); + }} + + ); +}; + +const REQUIRED = { + type: validatorTypes.REQUIRED, +} as const; + +function kindMetaCondition(test: (meta: ItemMeta) => boolean): ConditionProp { + return { + when: NAME_KIND, + is: (kind: string | undefined) => { + return ( + typeof kind === 'string' && isItemKind(kind) && test(metaForKind(kind)) + ); + }, + }; +} + +type Bundles = ReturnType; + +function detailsStepName(kind: ItemKind): string { + return `step-details-${kind}`; +} + +export const NAME_KIND = 'kind'; +export const NAME_TITLE = 'title'; +export const NAME_BUNDLES = 'bundles'; +export const NAME_DESCRIPTION = 'description'; +export const NAME_DURATION = 'duration'; +export const NAME_URL = 'url'; + +export const NAME_PANEL_INTRODUCTION = 'panel-overview'; +export const NAME_PREREQUISITES = 'prerequisites'; +export const NAME_TASK_TITLES = 'task-titles'; + +const STEP_PANEL_OVERVIEW = 'step-panel-overview'; +const STEP_DOWNLOAD = 'step-download'; + +function makeDetailsStep(kind: ItemKind, bundles: Bundles) { + const meta = metaForKind(kind); + + const fields: Field[] = []; + + fields.push( + { + component: componentTypes.TEXT_FIELD, + name: NAME_TITLE, + label: 'Title', + isRequired: true, + validate: [REQUIRED], + }, + { + component: componentTypes.SELECT, + name: NAME_BUNDLES, + label: 'Bundles', + simpleValue: true, + isMulti: true, + options: bundles.map((b) => ({ + value: b.id, + label: `${b.title} (${b.id})`, + })), + }, + { + component: componentTypes.TEXT_FIELD, + name: NAME_DESCRIPTION, + label: 'Description', + isRequired: true, + validate: [REQUIRED], + } + ); + + if (meta.fields.duration) { + fields.push({ + component: componentTypes.TEXT_FIELD, + name: NAME_DURATION, + label: 'Duration', + dataType: dataTypes.NUMBER, + isRequired: true, + validate: [REQUIRED], + }); + } + + if (meta.fields.url) { + fields.push({ + component: componentTypes.TEXT_FIELD, + name: NAME_URL, + label: 'URL', + isRequired: true, + validate: [ + REQUIRED, + { + type: validatorTypes.URL, + }, + ], + condition: kindMetaCondition((meta) => meta.fields.url === true), + }); + } + + return { + name: detailsStepName(kind), + title: `${meta.displayName} details`, + fields: fields, + nextStep: meta.hasTasks ? STEP_PANEL_OVERVIEW : STEP_DOWNLOAD, + }; +} + +const MAX_TASKS = 10; + +export const NAME_TASKS_ARRAY = 'tasks'; +export const NAME_TASK_CONTENT = 'content'; + +const TASK_STEP_PREFIX = 'step-task-detail-'; + +function taskStepName(index: number): string { + return `${TASK_STEP_PREFIX}${index}`; +} + +export function taskFromStepName(name: string): number | null { + if (name.startsWith(TASK_STEP_PREFIX)) { + return parseInt(name.substring(TASK_STEP_PREFIX.length)); + } + + return null; +} + +function makeTaskStep(index: number) { + return { + name: taskStepName(index), + title: `Task ${index + 1}`, + fields: [ + { + component: componentTypes.TEXTAREA, + name: `${NAME_TASKS_ARRAY}[${index}].${NAME_TASK_CONTENT}`, + label: 'Task data (YAML)', + resizeOrientation: 'vertical', + }, + { + component: 'lr-task-error', + name: `internal-task-errors[${index}]`, + index: index, + }, + ], + nextStep: ({ values }: WizardNextStepFunctionArgument) => { + if (index + 1 < (values?.[NAME_TASK_TITLES]?.length ?? 0)) { + return taskStepName(index + 1); + } + + return STEP_DOWNLOAD; + }, + }; +} + +export function makeSchema(chrome: ChromeAPI): Schema { + const bundles = chrome.getAvailableBundles(); + + const taskSteps = []; + + for (let i = 0; i < MAX_TASKS; ++i) { + taskSteps.push(makeTaskStep(i)); + } + + const wizardProps: WizardProps & { + component: string; + name: string; + } = { + component: componentTypes.WIZARD, + name: 'wizard-learning-resource', + isDynamic: true, + crossroads: [NAME_KIND, NAME_TASK_TITLES], + fields: [ + { + name: 'step-kind', + title: 'Select content type', + fields: [ + { + component: componentTypes.SELECT, + name: NAME_KIND, + label: 'Type', + simpleValue: true, + options: ALL_KIND_ENTRIES.map(([name, value]) => ({ + value: name, + label: value.displayName, + })), + isRequired: true, + validate: [REQUIRED], + }, + ], + nextStep: { + when: NAME_KIND, + stepMapper: Object.fromEntries( + ALL_ITEM_KINDS.map((kind) => [kind, detailsStepName(kind)]) + ), + }, + }, + ...ALL_ITEM_KINDS.map((kind) => makeDetailsStep(kind, bundles)), + { + name: STEP_PANEL_OVERVIEW, + title: 'Panel overview', + fields: [ + { + component: componentTypes.TEXTAREA, + name: NAME_PANEL_INTRODUCTION, + label: 'Introduction (Markdown)', + resizeOrientation: 'vertical', + }, + { + component: componentTypes.FIELD_ARRAY, + name: NAME_PREREQUISITES, + label: 'Prerequisites', + noItemsMessage: 'No prerequisites have been added.', + fields: [ + { + component: componentTypes.TEXT_FIELD, + label: 'Prerequisite', + }, + ], + }, + { + component: componentTypes.FIELD_ARRAY, + name: NAME_TASK_TITLES, + label: 'Tasks', + minItems: 1, + maxItems: MAX_TASKS, + noItemsMessage: 'No tasks have been added.', + initialValue: [''], + fields: [ + { + component: componentTypes.TEXT_FIELD, + label: 'Title', + }, + ], + }, + ], + nextStep: taskStepName(0), + }, + ...taskSteps, + { + name: STEP_DOWNLOAD, + title: 'Download files', + fields: [ + { + component: 'lr-download-files', + name: 'internal-download', + }, + ], + }, + ], + }; + + const schema = { + fields: [wizardProps], + }; + + for (const step of schema.fields) { + if (step.component === componentTypes.WIZARD) { + for (const page of step.fields) { + // Add an lr-wizard-spy component to all wizard steps. It must be here (rather + // than at the top level of the schema) so that it is inside the WizardContext. + page.fields.push({ + component: 'lr-wizard-spy', + name: `internal-wizard-spies.${page.name}`, + }); + + // Use custom buttons for each step. + if (page.buttons === undefined) { + page.buttons = CustomButtons; + } + } + } + } + + return schema; +}