diff --git a/.env b/.env index ea2bb90a66..1cd857776d 100644 --- a/.env +++ b/.env @@ -46,3 +46,5 @@ ENABLE_CHECKLIST_QUALITY='' ENABLE_GRADING_METHOD_IN_PROBLEMS=false # "Multi-level" blocks are unsupported in libraries LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder" +# Fallback in local style files +PARAGON_THEME_URLS={} diff --git a/.env.development b/.env.development index 883f0f6bf6..a98555c811 100644 --- a/.env.development +++ b/.env.development @@ -49,3 +49,5 @@ ENABLE_CHECKLIST_QUALITY=true ENABLE_GRADING_METHOD_IN_PROBLEMS=false # "Multi-level" blocks are unsupported in libraries LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder" +# Fallback in local style files +PARAGON_THEME_URLS={} diff --git a/.env.test b/.env.test index 3d2f1c08c0..2f66ab211f 100644 --- a/.env.test +++ b/.env.test @@ -41,3 +41,4 @@ ENABLE_CHECKLIST_QUALITY=true ENABLE_GRADING_METHOD_IN_PROBLEMS=false # "Multi-level" blocks are unsupported in libraries LIBRARY_UNSUPPORTED_BLOCKS="conditional,step-builder,problem-builder" +PARAGON_THEME_URLS= diff --git a/README.rst b/README.rst index d2ee44049e..96fade1c60 100644 --- a/README.rst +++ b/README.rst @@ -165,21 +165,7 @@ Feature: New React XBlock Editors .. image:: ./docs/readme-images/feature-problem-editor.png -This allows an operator to enable the use of new React editors for the HTML, Video, and Problem XBlocks, all of which are provided here. - -Requirements ------------- - -* ``edx-platform`` Waffle flags: - - * ``new_core_editors.use_new_text_editor``: must be enabled for the new HTML Xblock editor to be used in Studio - * ``new_core_editors.use_new_video_editor``: must be enabled for the new Video Xblock editor to be used in Studio - * ``new_core_editors.use_new_problem_editor``: must be enabled for the new Problem Xblock editor to be used in Studio - -Feature Description -------------------- - -When a corresponding waffle flag is set, upon editing a block in Studio, the view is rendered by this MFE instead of by the XBlock's authoring view. The user remains in Studio. +New React editors for the HTML, Video, and Problem XBlocks are provided here and are rendered by this MFE instead of by the XBlock's authoring view. Feature: New Proctoring Exams View ================================== @@ -193,10 +179,6 @@ Requirements * ``ZENDESK_*``: necessary if automatic ZenDesk ticket creation is desired -* ``edx-platform`` Feature flags: - - * ``ENABLE_EXAM_SETTINGS_HTML_VIEW``: this feature flag must be enabled for the link to the settings view to be shown - * `edx-exams `_: for this feature to work, the ``edx-exams`` IDA must be deployed and its API accessible by the browser Configuration @@ -221,16 +203,6 @@ Feature: Advanced Settings .. image:: ./docs/readme-images/feature-advanced-settings.png -Requirements ------------- - -* ``edx-platform`` Waffle flags: - - * ``contentstore.new_studio_mfe.use_new_advanced_settings_page``: this feature flag must be enabled for the link to the settings view to be shown. It can be enabled on a per-course basis. - -Feature Description -------------------- - In Studio, the "Advanced Settings" page for each enabled course will now be served by this frontend, instead of the UI built into edx-platform. The advanced settings page holds many different settings for the course, such as what features or XBlocks are enabled. Feature: Files & Uploads @@ -238,16 +210,6 @@ Feature: Files & Uploads .. image:: ./docs/readme-images/feature-files-uploads.png -Requirements ------------- - -* ``edx-platform`` Waffle flags: - - * ``contentstore.new_studio_mfe.use_new_files_uploads_page``: this feature flag must be enabled for the link to the Files & Uploads page to go to the MFE. It can be enabled on a per-course basis. - -Feature Description -------------------- - In Studio, the "Files & Uploads" page for each enabled course will now be served by this frontend, instead of the UI built into edx-platform. This page allows managing static asset files like PDFs, images, etc. used for the course. Feature: Course Updates @@ -255,26 +217,11 @@ Feature: Course Updates .. image:: ./docs/readme-images/feature-course-updates.png -Requirements ------------- - -* ``edx-platform`` Waffle flags: - - * ``contentstore.new_studio_mfe.use_new_updates_page``: this feature flag must be enabled. - Feature: Import/Export Pages ============================ .. image:: ./docs/readme-images/feature-export.png -Requirements ------------- - -* ``edx-platform`` Waffle flags: - - * ``contentstore.new_studio_mfe.use_new_export_page``: this feature flag will change the CMS to link to the new export page. - * ``contentstore.new_studio_mfe.use_new_import_page``: this feature flag will change the CMS to link to the new import page. - Feature: Tagging/Taxonomy Pages ================================ @@ -380,6 +327,20 @@ For more information about these options, see the `Getting Help`_ page. .. _Getting Help: https://openedx.org/community/connect +Legacy Studio +************* + +If you would like to use legacy studio for certain features, you can set the following waffle flags in ``edx-platform``: + * ``legacy_studio.text_editor``: loads the legacy HTML Xblock editor when editing a text block + * ``legacy_studio.video_editor``: loads the legacy Video editor when editing a video block + * ``legacy_studio.problem_editor``: loads the legacy Problem editor when editing a problem block + * ``legacy_studio.advanced_settings``: Advanced Settings page + * ``legacy_studio.updates``: Updates page + * ``legacy_studio.export``: Export page + * ``legacy_studio.import``: Import page + * ``legacy_studio.files_uploads``: Files page + * ``legacy_studio.exam_settings``: loads the legacy Exam Settings + License ******* diff --git a/catalog-info.yaml b/catalog-info.yaml index 9fa7a240ea..7a5bd5eaae 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -14,6 +14,6 @@ metadata: openedx.org/arch-interest-groups: "" openedx.org/release: "master" spec: - owner: group:2u-tnl + owner: user:bradenmacdonald type: 'website' lifecycle: 'production' diff --git a/codecov.yml b/codecov.yml index 64b8f80010..3202455e6d 100644 --- a/codecov.yml +++ b/codecov.yml @@ -10,4 +10,5 @@ coverage: threshold: 0% ignore: - "src/grading-settings/grading-scale/react-ranger.js" + - "src/generic/DraggableList/verticalSortableList.ts" - "src/index.js" diff --git a/package-lock.json b/package-lock.json index f413039839..aac5b18f44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,8 +24,8 @@ "@edx/frontend-component-footer": "^14.6.0", "@edx/frontend-component-header": "^6.2.0", "@edx/frontend-enterprise-hotjar": "^7.2.0", - "@edx/frontend-platform": "^8.3.1", - "@edx/openedx-atlas": "^0.6.0", + "@edx/frontend-platform": "^8.3.7", + "@edx/openedx-atlas": "^0.7.0", "@openedx-plugins/course-app-calculator": "file:plugins/course-apps/calculator", "@openedx-plugins/course-app-edxnotes": "file:plugins/course-apps/edxnotes", "@openedx-plugins/course-app-learning_assistant": "file:plugins/course-apps/learning_assistant", @@ -36,9 +36,9 @@ "@openedx-plugins/course-app-teams": "file:plugins/course-apps/teams", "@openedx-plugins/course-app-wiki": "file:plugins/course-apps/wiki", "@openedx-plugins/course-app-xpert_unit_summary": "file:plugins/course-apps/xpert_unit_summary", - "@openedx/frontend-build": "^14.3.3", + "@openedx/frontend-build": "^14.5.0", "@openedx/frontend-plugin-framework": "^1.7.0", - "@openedx/paragon": "^22.16.0", + "@openedx/paragon": "^23.5.0", "@redux-devtools/extension": "^3.3.0", "@reduxjs/toolkit": "1.9.7", "@tanstack/react-query": "4.36.1", @@ -55,7 +55,6 @@ "meilisearch": "^0.41.0", "moment": "2.30.1", "moment-shortformat": "^2.1.0", - "npm": "^10.8.1", "prop-types": "^15.8.1", "react": "^18.3.1", "react-datepicker": "^4.13.0", @@ -67,14 +66,13 @@ "react-responsive": "9.0.2", "react-router": "6.27.0", "react-router-dom": "6.27.0", - "react-select": "5.8.0", + "react-select": "5.10.1", "react-textarea-autosize": "^8.5.3", "react-transition-group": "4.4.5", "redux": "4.0.5", "redux-logger": "^3.0.6", "redux-thunk": "^2.4.1", "reselect": "^4.1.5", - "start": "^5.1.0", "tinymce": "^5.10.4", "universal-cookie": "^4.0.4", "uuid": "^3.4.0", @@ -1964,6 +1962,215 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "license": "MIT" }, + "node_modules/@bundled-es-modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-Rk453EklPUPC3NRWc3VUNI/SSUjdBaFoaQvFRmNBNtMHVtOFD5AntiWg5kEE1hqcPqedYFDzxE3ZcMYPcA195w==", + "license": "ISC", + "dependencies": { + "deepmerge": "^4.3.1" + } + }, + "node_modules/@bundled-es-modules/glob": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/glob/-/glob-10.4.2.tgz", + "integrity": "sha512-740y5ofkzydsFao5EXJrGilcIL6EFEw/cmPf2uhTw9J6G1YOhiIFjNFCHdpgEiiH5VlU3G0SARSjlFlimRRSMA==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "buffer": "^6.0.3", + "events": "^3.3.0", + "glob": "^10.4.2", + "patch-package": "^8.0.0", + "path": "^0.12.7", + "stream": "^0.0.3", + "string_decoder": "^1.3.0", + "url": "^0.11.3" + } + }, + "node_modules/@bundled-es-modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@bundled-es-modules/glob/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/@bundled-es-modules/glob/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@bundled-es-modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@bundled-es-modules/glob/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/@bundled-es-modules/memfs": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/memfs/-/memfs-4.17.0.tgz", + "integrity": "sha512-ykdrkEmQr9BV804yd37ikXfNnvxrwYfY9Z2/EtMHFEFadEjsQXJ1zL9bVZrKNLDtm91UdUOEHso6Aweg93K6xQ==", + "license": "Apache-2.0", + "dependencies": { + "assert": "^2.1.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "memfs": "^4.17.0", + "path": "^0.12.7", + "stream": "^0.0.3", + "util": "^0.12.5" + } + }, + "node_modules/@bundled-es-modules/memfs/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/@bundled-es-modules/memfs/node_modules/memfs": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.0.tgz", + "integrity": "sha512-4eirfZ7thblFmqFjywlTmuWVSvccHAJbn1r8qQLzmTO11qcqpohOjmY2mFce6x7x7WtskzRqApPD0hv+Oa74jg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.3.0", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/@bundled-es-modules/postcss-calc-ast-parser": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/postcss-calc-ast-parser/-/postcss-calc-ast-parser-0.1.6.tgz", + "integrity": "sha512-y65TM5zF+uaxo9OeekJ3rxwTINlQvrkbZLogYvQYVoLtxm4xEiHfZ7e/MyiWbStYyWZVZkVqsaVU6F4SUK5XUA==", + "license": "ISC", + "dependencies": { + "postcss-calc-ast-parser": "^0.1.4" + } + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "license": "Apache-2.0" + }, "node_modules/@codemirror/autocomplete": { "version": "6.18.6", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", @@ -1977,9 +2184,9 @@ } }, "node_modules/@codemirror/commands": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.0.tgz", - "integrity": "sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz", + "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -2108,9 +2315,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.36.4", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.4.tgz", - "integrity": "sha512-ZQ0V5ovw/miKEXTvjgzRyjnrk9TwriUB1k4R5p7uNnHR9Hus+D1SXHGdJshijEzPFjU25xea/7nhIeSqYFKdbA==", + "version": "6.36.6", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.6.tgz", + "integrity": "sha512-uxugGLet+Nzp0Jcit8Hn3LypM8ioMLKTsdf8FRoT3HWvZtb9GhaWMe0Cc15rz90Ljab4YFJiAulmIVB74OY0IQ==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.5.0", @@ -2344,9 +2551,9 @@ } }, "node_modules/@edx/frontend-component-footer": { - "version": "14.6.0", - "resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-14.6.0.tgz", - "integrity": "sha512-cgRhom6W/WErQ9yvLmfgB6ANBs+rBDLOH73NcvJIhfwWgAg67q+MLUscIbcX9N/9Yykk+kb7Ytr3CDefiKS7HA==", + "version": "14.7.0", + "resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-14.7.0.tgz", + "integrity": "sha512-cnJT8MlxfT3QRgh3Jfn28dSBMvDpuARK4Z0sD45tBI9LMgtuaP0kDq2Etn/ueRQGgV1yB1r/EPb/m5R7EUvPLQ==", "license": "AGPL-3.0", "dependencies": { "@fortawesome/fontawesome-svg-core": "6.7.2", @@ -2483,16 +2690,16 @@ } }, "node_modules/@edx/frontend-platform": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.3.3.tgz", - "integrity": "sha512-xj8uKY4k9DgScYWsBFx8B1cngZ6HTPHvmd7W+NpBB4Kqw9yCT1OUii4p8/8khF68vb7hcTQuu13A9hM0lkE5bw==", + "version": "8.3.7", + "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.3.7.tgz", + "integrity": "sha512-ya5ObMvtJlfQmoeL36OtzjFBh0hzJgXN/R2ppyIJ+IbCtY2BCfv5NqvmKD7CplwnSGJTBugpv5hQHeGmi+v97w==", "license": "AGPL-3.0", "dependencies": { "@cospired/i18n-iso-languages": "4.2.0", "@formatjs/intl-pluralrules": "4.3.3", "@formatjs/intl-relativetimeformat": "10.0.1", - "axios": "1.8.4", - "axios-cache-interceptor": "1.6.2", + "axios": "1.9.0", + "axios-cache-interceptor": "1.8.0", "form-urlencoded": "4.1.4", "glob": "7.2.3", "history": "4.10.1", @@ -2533,9 +2740,9 @@ } }, "node_modules/@edx/openedx-atlas": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@edx/openedx-atlas/-/openedx-atlas-0.6.2.tgz", - "integrity": "sha512-28Q8vzJDMS4wUxdkbIUBQpzWJ3HTdMaGlaEhFjrVGfuZkh++1AG6Tn/7FMD88cegalYAkphu530VQCHEkMZQhw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@edx/openedx-atlas/-/openedx-atlas-0.7.0.tgz", + "integrity": "sha512-jqv0IV1pHsSn9+RO8Rdsr8jm3SOd84CCzzmo2QC9yvh1MK1+p4YDURQLpmmgKJ0JzE5Cb6ImhnNL/ogpJ2wetQ==", "license": "AGPL-3.0", "bin": { "atlas": "atlas" @@ -2589,20 +2796,20 @@ } }, "node_modules/@emnapi/core": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz", - "integrity": "sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", + "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.0.1", + "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", - "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", "license": "MIT", "optional": true, "dependencies": { @@ -2610,9 +2817,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz", - "integrity": "sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", + "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", "license": "MIT", "optional": true, "dependencies": { @@ -2749,9 +2956,9 @@ "license": "MIT" }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", - "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", + "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -2890,9 +3097,9 @@ "license": "MIT" }, "node_modules/@formatjs/cli": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/@formatjs/cli/-/cli-6.6.3.tgz", - "integrity": "sha512-vW9EQdHmxQg/+s9K39ZwKcIyyhmEMHOtsv1KyQFtjv+pbE3XmiB5ohoo4wAx3HDsrufrTsplGnQdQ+KB2wY/bA==", + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@formatjs/cli/-/cli-6.6.4.tgz", + "integrity": "sha512-VSDPsT7AO/mtth1rEBwl97Us5dMgqZpI8v7QJXakB4f90pDJsqHBdBeTbjHYrlFr2WvBLVo3/6mGPw9DeX7PUg==", "license": "MIT", "bin": { "formatjs": "bin/formatjs" @@ -3218,9 +3425,9 @@ } }, "node_modules/@formatjs/ts-transformer/node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -3347,6 +3554,102 @@ "deprecated": "Use @eslint/object-schema instead", "license": "BSD-3-Clause" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -3822,6 +4125,60 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.2.0.tgz", + "integrity": "sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.1", + "@jsonjoy.com/util": "^1.1.2", + "hyperdyperid": "^1.2.0", + "thingies": "^1.20.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", + "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -3866,9 +4223,9 @@ } }, "node_modules/@lezer/javascript": { - "version": "1.4.21", - "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.21.tgz", - "integrity": "sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.1.tgz", + "integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -3886,9 +4243,9 @@ } }, "node_modules/@lezer/markdown": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.4.2.tgz", - "integrity": "sha512-iYewCigG/517D0xJPQd7RGaCjZAFwROiH8T9h7OTtz0bRVtkxzFhGBFJ9JGKgBBs4uuo1cvxzyQ5iKhDLMcLUQ==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.4.3.tgz", + "integrity": "sha512-kfw+2uMrQ/wy/+ONfrH83OkdFNM0ye5Xq96cLlaCy7h5UT9FO54DU4oRoIc0CSBh5NWmWuiIJA7NGLMJbQ+Oxg==", "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0", @@ -3913,21 +4270,21 @@ "license": "MIT" }, "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.7.tgz", - "integrity": "sha512-5yximcFK5FNompXfJFoWanu5l8v1hNGqNHh9du1xETp9HWk/B/PzvchX55WYOPaIeNglG8++68AAiauBAtbnzw==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz", + "integrity": "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.3.1", - "@emnapi/runtime": "^1.3.1", + "@emnapi/core": "^1.4.0", + "@emnapi/runtime": "^1.4.0", "@tybys/wasm-util": "^0.9.0" } }, "node_modules/@newrelic/publish-sourcemap": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@newrelic/publish-sourcemap/-/publish-sourcemap-5.1.3.tgz", - "integrity": "sha512-CuHiYXRVU4kDJ4D0nZYVRlRKb8V+s8MFpIyA2D5UBNCOntf/8jv+rxJR1wJ8WYkTio7f+uBKXn/K4GzrhWvKUw==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@newrelic/publish-sourcemap/-/publish-sourcemap-5.1.4.tgz", + "integrity": "sha512-35Nm26FxnVi7Nrfrl7nMHObIrHlkCStIPul/fQnru7RBlZIZiYKeGA9eEKBuho1ccoFK783w12nuntSV6wohlg==", "license": "New Relic proprietary", "dependencies": { "superagent": "^10.1.0", @@ -3955,8 +4312,20 @@ "eslint-scope": "5.1.1" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "license": "MIT", @@ -4031,9 +4400,9 @@ "link": true }, "node_modules/@openedx/frontend-build": { - "version": "14.4.0", - "resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.4.0.tgz", - "integrity": "sha512-9TrJ2x9n7VkMymah2e1+6cRhC0kpNdFDv63s2RbPdCHjfoFBvaelqfrwMw1KXfudnc6EO3M7scQTw2z3vfNrpA==", + "version": "14.5.0", + "resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.5.0.tgz", + "integrity": "sha512-HY0PdXvXBxrvJHj8HsRA+VNCHDePENFhqOIvbSz9Ke7HDwHpfDjg2CeKk41aU/8iTyj3eESfPwKQr5fTE0A3Ww==", "license": "AGPL-3.0", "dependencies": { "@babel/cli": "7.24.8", @@ -4269,9 +4638,9 @@ } }, "node_modules/@openedx/paragon": { - "version": "22.17.0", - "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.17.0.tgz", - "integrity": "sha512-MzOLQ0myaOErwumPJwxVZXTw7zJKrARtu4YMSaISF5Sz6pE1/dYz9qfRcqaraYRcJGNdbPRzOG0v3iqbZo1uHQ==", + "version": "23.5.0", + "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.5.0.tgz", + "integrity": "sha512-Pb6JvRON/8wfdALy2z3PXyFADYsKbv+NXqL5pkVXr9wlSf2jSe/dcKXy4EgN/wKpUCphShZRQWakbLPWCC78hw==", "license": "Apache-2.0", "workspaces": [ "example", @@ -4281,20 +4650,32 @@ "dependent-usage-analyzer" ], "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.1.1", - "@fortawesome/react-fontawesome": "^0.1.18", "@popperjs/core": "^2.11.4", + "@tokens-studio/sd-transforms": "^1.2.4", + "axios": "^0.27.2", "bootstrap": "^4.6.2", "chalk": "^4.1.2", "child_process": "^1.0.2", + "chroma-js": "^2.4.2", "classnames": "^2.3.1", + "cli-progress": "^3.12.0", + "commander": "^9.4.1", "email-prop-type": "^3.0.0", "file-selector": "^0.6.0", - "font-awesome": "^4.7.0", "glob": "^8.0.3", "inquirer": "^8.2.5", + "js-toml": "^1.0.0", "lodash.uniqby": "^4.7.0", + "log-update": "^4.0.0", "mailto-link": "^2.0.0", + "minimist": "^1.2.8", + "ora": "^5.4.1", + "postcss": "^8.4.21", + "postcss-combine-duplicated-selectors": "^10.0.3", + "postcss-custom-media": "^9.1.2", + "postcss-import": "^15.1.0", + "postcss-map": "^0.11.0", + "postcss-minify": "^1.1.0", "prop-types": "^15.8.1", "react-bootstrap": "^1.6.5", "react-colorful": "^5.6.1", @@ -4307,6 +4688,8 @@ "react-responsive": "^8.2.0", "react-table": "^7.7.0", "react-transition-group": "^4.4.2", + "sass": "^1.58.3", + "style-dictionary": "^4.3.2", "tabbable": "^5.3.3", "uncontrollable": "^7.2.1", "uuid": "^9.0.0" @@ -4320,19 +4703,6 @@ "react-intl": "^5.25.1 || ^6.4.0" } }, - "node_modules/@openedx/paragon/node_modules/@fortawesome/react-fontawesome": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.19.tgz", - "integrity": "sha512-Hyb+lB8T18cvLNX0S3llz7PcSOAJMLwiVKBuuzwM/nI5uoBw+gQjnf9il0fR1C3DKOI5Kc79pkJ4/xB0Uw9aFQ==", - "license": "MIT", - "dependencies": { - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "@fortawesome/fontawesome-svg-core": "~1 || ~6", - "react": ">=16.x" - } - }, "node_modules/@openedx/paragon/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -4342,6 +4712,15 @@ "balanced-match": "^1.0.0" } }, + "node_modules/@openedx/paragon/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/@openedx/paragon/node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -4374,6 +4753,34 @@ "node": ">=10" } }, + "node_modules/@openedx/paragon/node_modules/postcss-custom-media": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-9.1.5.tgz", + "integrity": "sha512-GStyWMz7Qbo/Gtw1xVspzVSX8eipgNg4lpsO3CAeY4/A1mzok+RV6MCv3fg62trWijh/lYEj6vps4o8JcBBpDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^1.0.2", + "@csstools/css-parser-algorithms": "^2.2.0", + "@csstools/css-tokenizer": "^2.1.1", + "@csstools/media-query-list-parser": "^2.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "node_modules/@openedx/paragon/node_modules/react-responsive": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-8.2.0.tgz", @@ -4405,6 +4812,15 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@parcel/watcher": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", @@ -4701,6 +5117,16 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz", @@ -4750,9 +5176,9 @@ } }, "node_modules/@polka/url": { - "version": "1.0.0-next.28", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", - "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "license": "MIT" }, "node_modules/@popperjs/core": { @@ -5228,9 +5654,9 @@ "license": "MIT" }, "node_modules/@testing-library/react": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.2.0.tgz", - "integrity": "sha512-2cSskAvA1QNtKc8Y9VJQRv0tm3hLVgxRGDB+KYhIaPQJ1I+RHbhIXcM+zClKXzMes/wshsMVzf4B9vS4IZpqDQ==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", "dev": true, "license": "MIT", "dependencies": { @@ -5286,6 +5712,32 @@ "react-dom": "^18.0.0 || ^17.0.1 || ^16.7.0" } }, + "node_modules/@tokens-studio/sd-transforms": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@tokens-studio/sd-transforms/-/sd-transforms-1.3.0.tgz", + "integrity": "sha512-zVbiYjTGWpSuwzZwiuvcWf79CQEcTMKSxrOaQJ0zHXFxEmrpETWeIRxv2IO8rtMos/cS8mvnDwPngoHQOMs1SA==", + "license": "MIT", + "dependencies": { + "@bundled-es-modules/deepmerge": "^4.3.1", + "@bundled-es-modules/postcss-calc-ast-parser": "^0.1.6", + "@tokens-studio/types": "^0.5.1", + "colorjs.io": "^0.5.2", + "expr-eval-fork": "^2.0.2", + "is-mergeable-object": "^1.1.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "style-dictionary": "^4.3.0 || ^5.0.0-rc.0" + } + }, + "node_modules/@tokens-studio/types": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@tokens-studio/types/-/types-0.5.2.tgz", + "integrity": "sha512-rzMcZP0bj2E5jaa7Fj0LGgYHysoCrbrxILVbT0ohsCUH5uCHY/u6J7Qw/TE0n6gR9Js/c9ZO9T8mOoz0HdLMbA==", + "license": "MIT" + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -5335,9 +5787,9 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -5655,12 +6107,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", - "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", + "version": "22.15.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.2.tgz", + "integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==", "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/node-forge": { @@ -5816,9 +6268,9 @@ "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -6076,9 +6528,9 @@ } }, "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.3.2.tgz", - "integrity": "sha512-ddnlXgRi0Fog5+7U5Q1qY62wl95Q1lB4tXQX1UIA9YHmRCHN2twaQW0/4tDVGCvTVEU3xEayU7VemEr7GcBYUw==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.2.tgz", + "integrity": "sha512-vxtBno4xvowwNmO/ASL0Y45TpHqmNkAaDtz4Jqb+clmcVSSl8XCG/PNFFkGsXXXS6AMjP+ja/TtNCFFa1QwLRg==", "cpu": [ "arm64" ], @@ -6089,9 +6541,9 @@ ] }, "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.3.2.tgz", - "integrity": "sha512-tnl9xoEeg503jis+LW5cuq4hyLGQyqaoBL8VdPSqcewo/FL1C8POHbzl+AL25TidWYJD+R6bGUTE381kA1sT9w==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.7.2.tgz", + "integrity": "sha512-qhVa8ozu92C23Hsmv0BF4+5Dyyd5STT1FolV4whNgbY6mj3kA0qsrGPe35zNR3wAN7eFict3s4Rc2dDTPBTuFQ==", "cpu": [ "x64" ], @@ -6102,9 +6554,9 @@ ] }, "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.3.2.tgz", - "integrity": "sha512-zyPn9LFCCjhKPeCtECZaiMUgkYN/VpLb4a9Xv7QriJmTaQxsuDtXqOHifrzUXIhorJTyS+5MOKDuNL0X9I4EHA==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.7.2.tgz", + "integrity": "sha512-zKKdm2uMXqLFX6Ac7K5ElnnG5VIXbDlFWzg4WJ8CGUedJryM5A3cTgHuGMw1+P5ziV8CRhnSEgOnurTI4vpHpg==", "cpu": [ "x64" ], @@ -6115,9 +6567,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.3.2.tgz", - "integrity": "sha512-UWx56Wh59Ro69fe+Wfvld4E1n9KG0e3zeouWLn8eSasyi/yVH/7ZW3CLTVFQ81oMKSpXwr5u6RpzttDXZKiO4g==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.7.2.tgz", + "integrity": "sha512-8N1z1TbPnHH+iDS/42GJ0bMPLiGK+cUqOhNbMKtWJ4oFGzqSJk/zoXFzcQkgtI63qMcUI7wW1tq2usZQSb2jxw==", "cpu": [ "arm" ], @@ -6128,9 +6580,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.3.2.tgz", - "integrity": "sha512-VYGQXsOEJtfaoY2fOm8Z9ii5idFaHFYlrq3yMFZPaFKo8ufOXYm8hnfru7qetbM9MX116iWaPC0ZX5sK+1Dr+g==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.7.2.tgz", + "integrity": "sha512-tjYzI9LcAXR9MYd9rO45m1s0B/6bJNuZ6jeOxo1pq1K6OBuRMMmfyvJYval3s9FPPGmrldYA3mi4gWDlWuTFGA==", "cpu": [ "arm" ], @@ -6141,9 +6593,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.3.2.tgz", - "integrity": "sha512-3zP420zxJfYPD1rGp2/OTIBxF8E3+/6VqCG+DEO6kkDgBiloa7Y8pw1o7N9BfgAC+VC8FPZsFXhV2lpx+lLRMQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.7.2.tgz", + "integrity": "sha512-jon9M7DKRLGZ9VYSkFMflvNqu9hDtOCEnO2QAryFWgT6o6AXU8du56V7YqnaLKr6rAbZBWYsYpikF226v423QA==", "cpu": [ "arm64" ], @@ -6154,9 +6606,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.3.2.tgz", - "integrity": "sha512-ZWjSleUgr88H4Kei7yT4PlPqySTuWN1OYDDcdbmMCtLWFly3ed+rkrcCb3gvqXdDbYrGOtzv3g2qPEN+WWNv5Q==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.7.2.tgz", + "integrity": "sha512-c8Cg4/h+kQ63pL43wBNaVMmOjXI/X62wQmru51qjfTvI7kmCy5uHTJvK/9LrF0G8Jdx8r34d019P1DVJmhXQpA==", "cpu": [ "arm64" ], @@ -6167,9 +6619,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.3.2.tgz", - "integrity": "sha512-p+5OvYJ2UOlpjes3WfBlxyvQok2u26hLyPxLFHkYlfzhZW0juhvBf/tvewz1LDFe30M7zL9cF4OOO5dcvtk+cw==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.7.2.tgz", + "integrity": "sha512-A+lcwRFyrjeJmv3JJvhz5NbcCkLQL6Mk16kHTNm6/aGNc4FwPHPE4DR9DwuCvCnVHvF5IAd9U4VIs/VvVir5lg==", "cpu": [ "ppc64" ], @@ -6179,10 +6631,36 @@ "linux" ] }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.7.2.tgz", + "integrity": "sha512-hQQ4TJQrSQW8JlPm7tRpXN8OCNP9ez7PajJNjRD1ZTHQAy685OYqPrKjfaMw/8LiHCt8AZ74rfUVHP9vn0N69Q==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.7.2.tgz", + "integrity": "sha512-NoAGbiqrxtY8kVooZ24i70CjLDlUFI7nDj3I9y54U94p+3kPxwd2L692YsdLa+cqQ0VoqMWoehDFp21PKRUoIQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.3.2.tgz", - "integrity": "sha512-yweY7I6SqNn3kvj6vE4PQRo7j8Oz6+NiUhmgciBNAUOuI3Jq0bnW29hbHJdxZRSN1kYkQnSkbbA1tT8VnK816w==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.7.2.tgz", + "integrity": "sha512-KaZByo8xuQZbUhhreBTW+yUnOIHUsv04P8lKjQ5otiGoSJ17ISGYArc+4vKdLEpGaLbemGzr4ZeUbYQQsLWFjA==", "cpu": [ "s390x" ], @@ -6193,9 +6671,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.3.2.tgz", - "integrity": "sha512-fNIvtzJcGN9hzWTIayrTSk2+KHQrqKbbY+I88xMVMOFV9t4AXha4veJdKaIuuks+2JNr6GuuNdsL7+exywZ32w==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.7.2.tgz", + "integrity": "sha512-dEidzJDubxxhUCBJ/SHSMJD/9q7JkyfBMT77Px1npl4xpg9t0POLvnWywSk66BgZS/b2Hy9Y1yFaoMTFJUe9yg==", "cpu": [ "x64" ], @@ -6206,9 +6684,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.3.2.tgz", - "integrity": "sha512-OaFEw8WAjiwBGxutQgkWhoAGB5BQqZJ8Gjt/mW+m6DWNjimcxU22uWCuEtfw1CIwLlKPOzsgH0429fWmZcTGkg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.7.2.tgz", + "integrity": "sha512-RvP+Ux3wDjmnZDT4XWFfNBRVG0fMsc+yVzNFUqOflnDfZ9OYujv6nkh+GOr+watwrW4wdp6ASfG/e7bkDradsw==", "cpu": [ "x64" ], @@ -6219,25 +6697,25 @@ ] }, "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.3.2.tgz", - "integrity": "sha512-u+sumtO7M0AGQ9bNQrF4BHNpUyxo23FM/yXZfmVAicTQ+mXtG06O7pm5zQUw3Mr4jRs2I84uh4O0hd8bdouuvQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.7.2.tgz", + "integrity": "sha512-y797JBmO9IsvXVRCKDXOxjyAE4+CcZpla2GSoBQ33TVb3ILXuFnMrbR/QQZoauBYeOFuu4w3ifWLw52sdHGz6g==", "cpu": [ "wasm32" ], "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.7" + "@napi-rs/wasm-runtime": "^0.2.9" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.3.2.tgz", - "integrity": "sha512-ZAJKy95vmDIHsRFuPNqPQRON8r2mSMf3p9DoX+OMOhvu2c8OXGg8MvhGRf3PNg45ozRrPdXDnngURKgaFfpGoQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.7.2.tgz", + "integrity": "sha512-gtYTh4/VREVSLA+gHrfbWxaMO/00y+34htY7XpioBTy56YN2eBjkPrY1ML1Zys89X3RJDKVaogzwxlM1qU7egg==", "cpu": [ "arm64" ], @@ -6248,9 +6726,9 @@ ] }, "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.3.2.tgz", - "integrity": "sha512-nQG4YFAS2BLoKVQFK/FrWJvFATI5DQUWQrcPcsWG9Ve5BLLHZuPOrJ2SpAJwLXQrRv6XHSFAYGI8wQpBg/CiFA==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.7.2.tgz", + "integrity": "sha512-Ywv20XHvHTDRQs12jd3MY8X5C8KLjDbg/jyaal/QLKx3fAShhJyD4blEANInsjxW3P7isHx1Blt56iUDDJO3jg==", "cpu": [ "ia32" ], @@ -6261,9 +6739,9 @@ ] }, "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.3.2.tgz", - "integrity": "sha512-XBWpUP0mHya6yGBwNefhyEa6V7HgYKCxEAY4qhTm/PcAQyBPNmjj97VZJOJkVdUsyuuii7xmq0pXWX/c2aToHQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.7.2.tgz", + "integrity": "sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA==", "cpu": [ "x64" ], @@ -6475,6 +6953,23 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "license": "Apache-2.0" }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@zip.js/zip.js": { + "version": "2.7.60", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.60.tgz", + "integrity": "sha512-vA3rLyqdxBrVo1FWSsbyoecaqWTV+vgPRf0QKeM7kVDG0r+lHUqd7zQDv1TO9k4BcAoNzNDSNrrel24Mk6addA==", + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=16.5.0" + } + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -6925,6 +7420,19 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, "node_modules/assert-ok": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-ok/-/assert-ok-1.0.0.tgz", @@ -6941,7 +7449,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7048,20 +7555,21 @@ } }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, "node_modules/axios-cache-interceptor": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-1.6.2.tgz", - "integrity": "sha512-YLbAODIHZZIcD4b3WYFVQOa5W2TY/WnJ6sBHqAg6Z+hx+RVj8/OcjQyRopO6awn7/kOkGL5X9TP16AucnlJ/lw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-1.8.0.tgz", + "integrity": "sha512-cTNnPGJyQkxnWp0EWvE3NRvgURU5cWw/Qx3dIhXyHSM4Ip0c7EEe0I3an0Jwa549m1CAOg57ibj27YRNLmQCcg==", "license": "MIT", "dependencies": { "cache-parser": "1.2.5", @@ -7264,6 +7772,15 @@ "node": ">=10" } }, + "node_modules/babel-plugin-macros/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.13", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz", @@ -7418,9 +7935,9 @@ "optional": true }, "node_modules/bare-fs": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.2.tgz", - "integrity": "sha512-S5mmkMesiduMqnz51Bfh0Et9EX0aTCJxhsI4bvzFFLs8Z1AV8RDHadfY5CyLwdoLHgXbNBEN1gQcbEtGwuvixw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.3.tgz", + "integrity": "sha512-OeEZYIg+2qepaWLyphaOXHAHKo3xkM8y3BeGAvHdMN8GNWvEAU1Yw6rYpGzu/wDDbKxgEjVeVDpgGhDzaeMpjg==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -7872,9 +8389,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001715", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz", - "integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==", + "version": "1.0.30001717", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz", + "integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==", "funding": [ { "type": "opencollective", @@ -7922,6 +8439,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "license": "MIT" + }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -7937,6 +8460,20 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "license": "MIT" }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, "node_modules/child_process": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz", @@ -7973,6 +8510,12 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/chroma-js": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.6.0.tgz", + "integrity": "sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A==", + "license": "(BSD-3-Clause AND Apache-2.0)" + }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", @@ -8057,6 +8600,18 @@ "node": ">=8" } }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/cli-spinners": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", @@ -8225,6 +8780,12 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "license": "MIT" }, + "node_modules/colorjs.io": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -8253,10 +8814,13 @@ "license": "ISC" }, "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-2.0.0.tgz", + "integrity": "sha512-4m5s3Me2xxlVKG9PkZpQqHQR7bgpnN7joDMJ4yvVkVXngjoITG76IaZmzmywSeRTeTpc6N6r3H3+KyUurV8OYw==", "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -9524,6 +10088,12 @@ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "license": "MIT" }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -9546,9 +10116,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.126", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.126.tgz", - "integrity": "sha512-AtH1uLcTC72LA4vfYcEJJkrMk/MY/X0ub8Hv7QGAePW2JkeUFHEL/QfS4J77R6M87Sss8O0OcqReSaN1bpyA+Q==", + "version": "1.5.143", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.143.tgz", + "integrity": "sha512-QqklJMOFBMqe46k8iIOwA9l2hz57V2OKMmP5eSWcUvwx+mASAsbU+wkF1pHjn9ZVSBPrsYWr4/W/95y5SwYg2g==", "license": "ISC" }, "node_modules/email-prop-type": { @@ -9586,6 +10156,15 @@ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "license": "MIT" }, + "node_modules/emoji-regex-xs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz", + "integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", @@ -9780,9 +10359,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "license": "MIT" }, "node_modules/es-object-atoms": { @@ -10032,23 +10611,23 @@ } }, "node_modules/eslint-import-resolver-typescript": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.2.5.tgz", - "integrity": "sha512-VtSNsVbyDlubDcx5Lb1K1Y8G4MxUuC9XVALX1z2EIXaLobCedvFPQ2XRemobQStn04G9MRi3iu1JFLKI4/8fig==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.3.4.tgz", + "integrity": "sha512-buzw5z5VtiQMysYLH9iW9BV04YyZebsw+gPi+c4FCjfS9i6COYOrEWw9t3m3wA9PFBfqcBCqWf32qrXLbwafDw==", "license": "ISC", "dependencies": { "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", - "tinyglobby": "^0.2.12", - "unrs-resolver": "^1.3.2" + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.3" }, "engines": { "node": "^16.17.0 || >=18.6.0" }, "funding": { - "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + "url": "https://opencollective.com/eslint-import-resolver-typescript" }, "peerDependencies": { "eslint": "*", @@ -10400,9 +10979,9 @@ "license": "0BSD" }, "node_modules/eslint-plugin-formatjs/node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -10899,6 +11478,12 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/expr-eval-fork": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expr-eval-fork/-/expr-eval-fork-2.0.2.tgz", + "integrity": "sha512-NaAnObPVwHEYrODd7Jzp3zzT9pgTAlUUL4MZiZu9XAYPDpx89cPsfyEImFb2XY0vQNbrqg2CG7CLiI+Rs3seaQ==", + "license": "MIT" + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -11386,6 +11971,15 @@ "node": ">=8" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -11444,9 +12038,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -11463,15 +12057,6 @@ } } }, - "node_modules/font-awesome": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", - "integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg==", - "license": "(OFL-1.1 AND MIT)", - "engines": { - "node": ">=0.10.3" - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -11487,6 +12072,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", @@ -11581,16 +12194,26 @@ "node": ">=6" } }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -11603,15 +12226,18 @@ "license": "MIT" }, "node_modules/formidable": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", - "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", "license": "MIT", "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", - "hexoid": "^2.0.0", "once": "^1.4.0" }, + "engines": { + "node": ">=14.0.0" + }, "funding": { "url": "https://ko-fi.com/tunnckoCore/commissions" } @@ -12185,9 +12811,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -12205,15 +12831,6 @@ "he": "bin/he" } }, - "node_modules/hexoid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", - "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/history": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", @@ -12295,9 +12912,9 @@ } }, "node_modules/html-entities": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.3.tgz", - "integrity": "sha512-D3AfvN7SjhTgBSA8L1BN4FpPzuEd06uy4lHwSoRWr0lndi9BKaNzPLKGOWZ2ocSGguozr08TTb2jhCLHaemruw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", "funding": [ { "type": "github", @@ -12451,9 +13068,9 @@ } }, "node_modules/http-parser-js": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", - "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", "license": "MIT" }, "node_modules/http-proxy": { @@ -12485,9 +13102,9 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", - "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.8", @@ -12542,6 +13159,15 @@ "node": ">=10.17.0" } }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, "node_modules/hyphenate-style-name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", @@ -12922,6 +13548,22 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -13252,12 +13894,34 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/is-mergeable-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-mergeable-object/-/is-mergeable-object-1.1.1.tgz", + "integrity": "sha512-CPduJfuGg8h8vW74WOxHtHmtQutyQBzR+3MjQ6iDHIYdbOnm1YC7jv43SqCoU8OPGTJD4nibmiryA4kmogbGrA==", + "license": "MIT" + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", "license": "MIT", - "engines": { + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { "node": ">=0.12.0" } }, @@ -13320,13 +13984,15 @@ } }, "node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true, + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-plain-object": { @@ -13685,6 +14351,21 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jake": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", @@ -14716,6 +15397,16 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/js-toml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/js-toml/-/js-toml-1.0.1.tgz", + "integrity": "sha512-rHd/IolpFm2V5BmHCEY8CckHs8NDsYZZ64H5RNgA6Opsr9vX4QyTiQPplgtqg7b3ztqYShZC38nl6CUg7QuhXg==", + "license": "MIT", + "dependencies": { + "chevrotain": "^11.0.3", + "xregexp": "^5.1.1" + } + }, "node_modules/js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -14830,13 +15521,13 @@ "license": "MIT" }, "node_modules/json-stable-stringify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.2.1.tgz", - "integrity": "sha512-Lp6HbbBgosLmJbjx0pBLbgvx68FaFU1sdkmBuckmhhJ88kL13OA51CDtR2yJB50eCNMH9wRqtQNNiAqQH4YXnA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", "license": "MIT", "dependencies": { "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "isarray": "^2.0.5", "jsonify": "^0.0.1", "object-keys": "^1.1.1" @@ -14938,6 +15629,15 @@ "node": ">=0.10.0" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -15188,6 +15888,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -15657,6 +16375,25 @@ "node": ">= 6" } }, + "node_modules/minimist-options/node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -15753,6 +16490,21 @@ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "license": "MIT" }, + "node_modules/napi-postinstall": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.2.tgz", + "integrity": "sha512-Wy1VI/hpKHwy1MsnFxHCJxqFwmmxD0RA/EKPL7e6mfbsY01phM2SZyJnRdU0bLvhu0Quby1DCcAZti3ghdl4/A==", + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -15931,166 +16683,6 @@ "node": ">=0.10.0" } }, - "node_modules/npm": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/npm/-/npm-10.9.2.tgz", - "integrity": "sha512-iriPEPIkoMYUy3F6f3wwSZAU93E0Eg6cHwIR6jzzOXWSy+SD/rOODEs74cVONHKSx2obXtuUoyidVEhISrisgQ==", - "bundleDependencies": [ - "@isaacs/string-locale-compare", - "@npmcli/arborist", - "@npmcli/config", - "@npmcli/fs", - "@npmcli/map-workspaces", - "@npmcli/package-json", - "@npmcli/promise-spawn", - "@npmcli/redact", - "@npmcli/run-script", - "@sigstore/tuf", - "abbrev", - "archy", - "cacache", - "chalk", - "ci-info", - "cli-columns", - "fastest-levenshtein", - "fs-minipass", - "glob", - "graceful-fs", - "hosted-git-info", - "ini", - "init-package-json", - "is-cidr", - "json-parse-even-better-errors", - "libnpmaccess", - "libnpmdiff", - "libnpmexec", - "libnpmfund", - "libnpmhook", - "libnpmorg", - "libnpmpack", - "libnpmpublish", - "libnpmsearch", - "libnpmteam", - "libnpmversion", - "make-fetch-happen", - "minimatch", - "minipass", - "minipass-pipeline", - "ms", - "node-gyp", - "nopt", - "normalize-package-data", - "npm-audit-report", - "npm-install-checks", - "npm-package-arg", - "npm-pick-manifest", - "npm-profile", - "npm-registry-fetch", - "npm-user-validate", - "p-map", - "pacote", - "parse-conflict-json", - "proc-log", - "qrcode-terminal", - "read", - "semver", - "spdx-expression-parse", - "ssri", - "supports-color", - "tar", - "text-table", - "tiny-relative-date", - "treeverse", - "validate-npm-package-name", - "which", - "write-file-atomic" - ], - "license": "Artistic-2.0", - "workspaces": [ - "docs", - "smoke-tests", - "mock-globals", - "mock-registry", - "workspaces/*" - ], - "dependencies": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^8.0.0", - "@npmcli/config": "^9.0.0", - "@npmcli/fs": "^4.0.0", - "@npmcli/map-workspaces": "^4.0.2", - "@npmcli/package-json": "^6.1.0", - "@npmcli/promise-spawn": "^8.0.2", - "@npmcli/redact": "^3.0.0", - "@npmcli/run-script": "^9.0.1", - "@sigstore/tuf": "^3.0.0", - "abbrev": "^3.0.0", - "archy": "~1.0.0", - "cacache": "^19.0.1", - "chalk": "^5.3.0", - "ci-info": "^4.1.0", - "cli-columns": "^4.0.0", - "fastest-levenshtein": "^1.0.16", - "fs-minipass": "^3.0.3", - "glob": "^10.4.5", - "graceful-fs": "^4.2.11", - "hosted-git-info": "^8.0.2", - "ini": "^5.0.0", - "init-package-json": "^7.0.2", - "is-cidr": "^5.1.0", - "json-parse-even-better-errors": "^4.0.0", - "libnpmaccess": "^9.0.0", - "libnpmdiff": "^7.0.0", - "libnpmexec": "^9.0.0", - "libnpmfund": "^6.0.0", - "libnpmhook": "^11.0.0", - "libnpmorg": "^7.0.0", - "libnpmpack": "^8.0.0", - "libnpmpublish": "^10.0.1", - "libnpmsearch": "^8.0.0", - "libnpmteam": "^7.0.0", - "libnpmversion": "^7.0.0", - "make-fetch-happen": "^14.0.3", - "minimatch": "^9.0.5", - "minipass": "^7.1.1", - "minipass-pipeline": "^1.2.4", - "ms": "^2.1.2", - "node-gyp": "^11.0.0", - "nopt": "^8.0.0", - "normalize-package-data": "^7.0.0", - "npm-audit-report": "^6.0.0", - "npm-install-checks": "^7.1.1", - "npm-package-arg": "^12.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-profile": "^11.0.1", - "npm-registry-fetch": "^18.0.2", - "npm-user-validate": "^3.0.0", - "p-map": "^4.0.0", - "pacote": "^19.0.1", - "parse-conflict-json": "^4.0.0", - "proc-log": "^5.0.0", - "qrcode-terminal": "^0.12.0", - "read": "^4.0.0", - "semver": "^7.6.3", - "spdx-expression-parse": "^4.0.0", - "ssri": "^12.0.0", - "supports-color": "^9.4.0", - "tar": "^6.2.1", - "text-table": "~0.2.0", - "tiny-relative-date": "^1.3.0", - "treeverse": "^3.0.0", - "validate-npm-package-name": "^6.0.0", - "which": "^5.0.0", - "write-file-atomic": "^6.0.0" - }, - "bin": { - "npm": "bin/npm-cli.js", - "npx": "bin/npx-cli.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -16103,2533 +16695,164 @@ "node": ">=8" } }, - "node_modules/npm/node_modules/@isaacs/cliui": { - "version": "8.0.2", - "inBundle": true, - "license": "ISC", + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "boolbase": "^1.0.0" }, - "engines": { - "node": ">=12" + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "inBundle": true, + "node_modules/nwsapi": { + "version": "2.2.20", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz", + "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=0.10.0" } }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "inBundle": true, + "node_modules/object-code": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/object-code/-/object-code-1.3.3.tgz", + "integrity": "sha512-/Ds4Xd5xzrtUOJ+xJQ57iAy0BZsZltOHssnDgcZ8DOhgh41q1YJCnTPnWdWSLkNGNnxYzhYChjc5dgC9mEERCA==", "license": "MIT" }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "inBundle": true, + "node_modules/object-filter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/object-filter/-/object-filter-1.0.2.tgz", + "integrity": "sha512-NahvP2vZcy1ZiiYah30CEPw0FpDcSkSePJBMpzl5EQgCmISijiGuJm3SPYp7U+Lf2TljyaIw3E5EgkEx/TNEVA==", + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, "engines": { - "node": ">=12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "inBundle": true, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" }, "engines": { - "node": ">=12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/npm/node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">= 0.4" } }, - "node_modules/npm/node_modules/@isaacs/string-locale-compare": { - "version": "1.1.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/@npmcli/agent": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "license": "MIT", "dependencies": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/fs": "^4.0.0", - "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/map-workspaces": "^4.0.1", - "@npmcli/metavuln-calculator": "^8.0.0", - "@npmcli/name-from-folder": "^3.0.0", - "@npmcli/node-gyp": "^4.0.0", - "@npmcli/package-json": "^6.0.1", - "@npmcli/query": "^4.0.0", - "@npmcli/redact": "^3.0.0", - "@npmcli/run-script": "^9.0.1", - "bin-links": "^5.0.0", - "cacache": "^19.0.1", - "common-ancestor-path": "^1.0.1", - "hosted-git-info": "^8.0.0", - "json-parse-even-better-errors": "^4.0.0", - "json-stringify-nice": "^1.1.4", - "lru-cache": "^10.2.2", - "minimatch": "^9.0.4", - "nopt": "^8.0.0", - "npm-install-checks": "^7.1.0", - "npm-package-arg": "^12.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-registry-fetch": "^18.0.1", - "pacote": "^19.0.0", - "parse-conflict-json": "^4.0.0", - "proc-log": "^5.0.0", - "proggy": "^3.0.0", - "promise-all-reject-late": "^1.0.0", - "promise-call-limit": "^3.0.1", - "read-package-json-fast": "^4.0.0", - "semver": "^7.3.7", - "ssri": "^12.0.0", - "treeverse": "^3.0.0", - "walk-up-path": "^3.0.1" - }, - "bin": { - "arborist": "bin/index.js" + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">= 0.4" } }, - "node_modules/npm/node_modules/@npmcli/config": { - "version": "9.0.0", - "inBundle": true, - "license": "ISC", + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "license": "MIT", "dependencies": { - "@npmcli/map-workspaces": "^4.0.1", - "@npmcli/package-json": "^6.0.1", - "ci-info": "^4.0.0", - "ini": "^5.0.0", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "walk-up-path": "^3.0.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/npm/node_modules/@npmcli/fs": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "license": "MIT", "dependencies": { - "semver": "^7.3.5" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">= 0.4" } }, - "node_modules/npm/node_modules/@npmcli/git": { - "version": "6.0.1", - "inBundle": true, - "license": "ISC", + "node_modules/object.hasown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", + "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", + "license": "MIT", "dependencies": { - "@npmcli/promise-spawn": "^8.0.0", - "ini": "^5.0.0", - "lru-cache": "^10.0.1", - "npm-pick-manifest": "^10.0.0", - "proc-log": "^5.0.0", - "promise-inflight": "^1.0.1", - "promise-retry": "^2.0.1", - "semver": "^7.3.5", - "which": "^5.0.0" + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/installed-package-contents": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-bundled": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" - }, - "bin": { - "installed-package-contents": "bin/index.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/map-workspaces": { - "version": "4.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/name-from-folder": "^3.0.0", - "@npmcli/package-json": "^6.0.0", - "glob": "^10.2.2", - "minimatch": "^9.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { - "version": "8.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "cacache": "^19.0.0", - "json-parse-even-better-errors": "^4.0.0", - "pacote": "^20.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/metavuln-calculator/node_modules/pacote": { - "version": "20.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^6.0.0", - "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/package-json": "^6.0.0", - "@npmcli/promise-spawn": "^8.0.0", - "@npmcli/run-script": "^9.0.0", - "cacache": "^19.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^7.0.2", - "npm-package-arg": "^12.0.0", - "npm-packlist": "^9.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-registry-fetch": "^18.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "sigstore": "^3.0.0", - "ssri": "^12.0.0", - "tar": "^6.1.11" - }, - "bin": { - "pacote": "bin/index.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/name-from-folder": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/node-gyp": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/package-json": { - "version": "6.1.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^6.0.0", - "glob": "^10.2.2", - "hosted-git-info": "^8.0.0", - "json-parse-even-better-errors": "^4.0.0", - "normalize-package-data": "^7.0.0", - "proc-log": "^5.0.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/promise-spawn": { - "version": "8.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "which": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/query": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^6.1.2" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/redact": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/run-script": { - "version": "9.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/node-gyp": "^4.0.0", - "@npmcli/package-json": "^6.0.0", - "@npmcli/promise-spawn": "^8.0.0", - "node-gyp": "^11.0.0", - "proc-log": "^5.0.0", - "which": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "inBundle": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/npm/node_modules/@sigstore/protobuf-specs": { - "version": "0.3.2", - "inBundle": true, - "license": "Apache-2.0", - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@sigstore/tuf": { - "version": "3.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2", - "tuf-js": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@tufjs/canonical-json": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/abbrev": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/agent-base": { - "version": "7.1.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/aggregate-error": { - "version": "3.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/ansi-regex": { - "version": "5.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/ansi-styles": { - "version": "6.2.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/npm/node_modules/aproba": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/archy": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/balanced-match": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/bin-links": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "cmd-shim": "^7.0.0", - "npm-normalize-package-bin": "^4.0.0", - "proc-log": "^5.0.0", - "read-cmd-shim": "^5.0.0", - "write-file-atomic": "^6.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/binary-extensions": { - "version": "2.3.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/brace-expansion": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/npm/node_modules/cacache": { - "version": "19.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^4.0.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/chownr": { - "version": "3.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/minizlib": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/mkdirp": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/p-map": { - "version": "7.0.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/tar": { - "version": "7.4.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/yallist": { - "version": "5.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/chalk": { - "version": "5.3.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/npm/node_modules/chownr": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/ci-info": { - "version": "4.1.0", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/cidr-regex": { - "version": "4.1.1", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "ip-regex": "^5.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/npm/node_modules/clean-stack": { - "version": "2.2.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/npm/node_modules/cli-columns": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/npm/node_modules/cmd-shim": { - "version": "7.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/color-convert": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/npm/node_modules/color-name": { - "version": "1.1.4", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/common-ancestor-path": { - "version": "1.0.1", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/cross-spawn": { - "version": "7.0.6", - "inBundle": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/cssesc": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm/node_modules/debug": { - "version": "4.3.7", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/npm/node_modules/diff": { - "version": "5.2.0", - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/npm/node_modules/eastasianwidth": { - "version": "0.2.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/emoji-regex": { - "version": "8.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/encoding": { - "version": "0.1.13", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/npm/node_modules/env-paths": { - "version": "2.2.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/npm/node_modules/err-code": { - "version": "2.0.3", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/exponential-backoff": { - "version": "3.1.1", - "inBundle": true, - "license": "Apache-2.0" - }, - "node_modules/npm/node_modules/fastest-levenshtein": { - "version": "1.0.16", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/npm/node_modules/foreground-child": { - "version": "3.3.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/fs-minipass": { - "version": "3.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/glob": { - "version": "10.4.5", - "inBundle": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/graceful-fs": { - "version": "4.2.11", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/hosted-git-info": { - "version": "8.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/http-cache-semantics": { - "version": "4.1.1", - "inBundle": true, - "license": "BSD-2-Clause" - }, - "node_modules/npm/node_modules/http-proxy-agent": { - "version": "7.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/https-proxy-agent": { - "version": "7.0.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/iconv-lite": { - "version": "0.6.3", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm/node_modules/ignore-walk": { - "version": "7.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minimatch": "^9.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/imurmurhash": { - "version": "0.1.4", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/npm/node_modules/indent-string": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/ini": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/init-package-json": { - "version": "7.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/package-json": "^6.0.0", - "npm-package-arg": "^12.0.0", - "promzard": "^2.0.0", - "read": "^4.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4", - "validate-npm-package-name": "^6.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/ip-address": { - "version": "9.0.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/npm/node_modules/ip-regex": { - "version": "5.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/is-cidr": { - "version": "5.1.0", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "cidr-regex": "^4.1.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/npm/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/isexe": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/jackspeak": { - "version": "3.4.3", - "inBundle": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/npm/node_modules/jsbn": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/json-parse-even-better-errors": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/json-stringify-nice": { - "version": "1.1.4", - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/jsonparse": { - "version": "1.3.1", - "engines": [ - "node >= 0.2.0" - ], - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/just-diff": { - "version": "6.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/just-diff-apply": { - "version": "5.5.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/libnpmaccess": { - "version": "9.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-package-arg": "^12.0.0", - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmdiff": { - "version": "7.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^8.0.0", - "@npmcli/installed-package-contents": "^3.0.0", - "binary-extensions": "^2.3.0", - "diff": "^5.1.0", - "minimatch": "^9.0.4", - "npm-package-arg": "^12.0.0", - "pacote": "^19.0.0", - "tar": "^6.2.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmexec": { - "version": "9.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^8.0.0", - "@npmcli/run-script": "^9.0.1", - "ci-info": "^4.0.0", - "npm-package-arg": "^12.0.0", - "pacote": "^19.0.0", - "proc-log": "^5.0.0", - "read": "^4.0.0", - "read-package-json-fast": "^4.0.0", - "semver": "^7.3.7", - "walk-up-path": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmfund": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^8.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmhook": { - "version": "11.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmorg": { - "version": "7.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmpack": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^8.0.0", - "@npmcli/run-script": "^9.0.1", - "npm-package-arg": "^12.0.0", - "pacote": "^19.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmpublish": { - "version": "10.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "ci-info": "^4.0.0", - "normalize-package-data": "^7.0.0", - "npm-package-arg": "^12.0.0", - "npm-registry-fetch": "^18.0.1", - "proc-log": "^5.0.0", - "semver": "^7.3.7", - "sigstore": "^3.0.0", - "ssri": "^12.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmsearch": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmteam": { - "version": "7.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^18.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmversion": { - "version": "7.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^6.0.1", - "@npmcli/run-script": "^9.0.1", - "json-parse-even-better-errors": "^4.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.7" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/lru-cache": { - "version": "10.4.3", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/make-fetch-happen": { - "version": "14.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "ssri": "^12.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/make-fetch-happen/node_modules/negotiator": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/npm/node_modules/minimatch": { - "version": "9.0.5", - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/minipass": { - "version": "7.1.2", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/npm/node_modules/minipass-collect": { - "version": "2.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/npm/node_modules/minipass-fetch": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/npm/node_modules/minipass-fetch/node_modules/minizlib": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/npm/node_modules/minipass-flush": { - "version": "1.0.5", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-pipeline": { - "version": "1.2.4", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-sized": { - "version": "1.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minizlib": { - "version": "2.1.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/mkdirp": { - "version": "1.0.4", - "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/ms": { - "version": "2.1.3", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/mute-stream": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/node-gyp": { - "version": "11.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "glob": "^10.3.10", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "tar": "^7.4.3", - "which": "^5.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/chownr": { - "version": "3.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/minizlib": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/mkdirp": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/tar": { - "version": "7.4.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/yallist": { - "version": "5.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/nopt": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "abbrev": "^2.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/nopt/node_modules/abbrev": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/normalize-package-data": { - "version": "7.0.0", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^8.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-audit-report": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-bundled": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-normalize-package-bin": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-install-checks": { - "version": "7.1.1", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "semver": "^7.1.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-normalize-package-bin": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-package-arg": { - "version": "12.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "hosted-git-info": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^6.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-packlist": { - "version": "9.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "ignore-walk": "^7.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-pick-manifest": { - "version": "10.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-install-checks": "^7.1.0", - "npm-normalize-package-bin": "^4.0.0", - "npm-package-arg": "^12.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-profile": { - "version": "11.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-registry-fetch": "^18.0.0", - "proc-log": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-registry-fetch": { - "version": "18.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/redact": "^3.0.0", - "jsonparse": "^1.3.1", - "make-fetch-happen": "^14.0.0", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minizlib": "^3.0.1", - "npm-package-arg": "^12.0.0", - "proc-log": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-registry-fetch/node_modules/minizlib": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/npm/node_modules/npm-user-validate": { - "version": "3.0.0", - "inBundle": true, - "license": "BSD-2-Clause", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/p-map": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/package-json-from-dist": { - "version": "1.0.1", - "inBundle": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/npm/node_modules/pacote": { - "version": "19.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^6.0.0", - "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/package-json": "^6.0.0", - "@npmcli/promise-spawn": "^8.0.0", - "@npmcli/run-script": "^9.0.0", - "cacache": "^19.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^7.0.2", - "npm-package-arg": "^12.0.0", - "npm-packlist": "^9.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-registry-fetch": "^18.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "sigstore": "^3.0.0", - "ssri": "^12.0.0", - "tar": "^6.1.11" - }, - "bin": { - "pacote": "bin/index.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/parse-conflict-json": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^4.0.0", - "just-diff": "^6.0.0", - "just-diff-apply": "^5.2.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/path-key": { - "version": "3.1.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/path-scurry": { - "version": "1.11.1", - "inBundle": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "6.1.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm/node_modules/proc-log": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/proggy": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/promise-all-reject-late": { - "version": "1.0.1", - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/promise-call-limit": { - "version": "3.0.2", - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/promise-inflight": { - "version": "1.0.1", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/promise-retry": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/promzard": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "read": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/qrcode-terminal": { - "version": "0.12.0", - "inBundle": true, - "bin": { - "qrcode-terminal": "bin/qrcode-terminal.js" - } - }, - "node_modules/npm/node_modules/read": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "mute-stream": "^2.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/read-cmd-shim": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/read-package-json-fast": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/retry": { - "version": "0.12.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/npm/node_modules/rimraf": { - "version": "5.0.10", - "inBundle": true, - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/safer-buffer": { - "version": "2.1.2", - "inBundle": true, - "license": "MIT", - "optional": true - }, - "node_modules/npm/node_modules/semver": { - "version": "7.6.3", - "inBundle": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/shebang-command": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/shebang-regex": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/signal-exit": { - "version": "4.1.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/sigstore": { - "version": "3.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^3.0.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.3.2", - "@sigstore/sign": "^3.0.0", - "@sigstore/tuf": "^3.0.0", - "@sigstore/verify": "^2.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/bundle": { - "version": "3.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/core": { - "version": "2.0.0", - "inBundle": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/sign": { - "version": "3.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^3.0.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.3.2", - "make-fetch-happen": "^14.0.1", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/verify": { - "version": "2.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^3.0.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.3.2" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/smart-buffer": { - "version": "4.2.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/npm/node_modules/socks": { - "version": "2.8.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/npm/node_modules/socks-proxy-agent": { - "version": "8.0.4", - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.1", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/spdx-correct": { - "version": "3.2.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-exceptions": { - "version": "2.5.0", - "inBundle": true, - "license": "CC-BY-3.0" - }, - "node_modules/npm/node_modules/spdx-expression-parse": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-license-ids": { - "version": "3.0.20", - "inBundle": true, - "license": "CC0-1.0" - }, - "node_modules/npm/node_modules/sprintf-js": { - "version": "1.1.3", - "inBundle": true, - "license": "BSD-3-Clause" - }, - "node_modules/npm/node_modules/ssri": { - "version": "12.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/string-width": { - "version": "4.2.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/strip-ansi": { - "version": "6.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/supports-color": { - "version": "9.4.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/npm/node_modules/tar": { - "version": "6.2.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { - "version": "2.1.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/text-table": { - "version": "0.2.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/tiny-relative-date": { - "version": "1.3.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/treeverse": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@tufjs/models": "3.0.1", - "debug": "^4.3.6", - "make-fetch-happen": "^14.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/tuf-js/node_modules/@tufjs/models": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/unique-filename": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/unique-slug": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/util-deprecate": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/validate-npm-package-license": { - "version": "3.0.4", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/validate-npm-package-name": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/walk-up-path": { - "version": "3.0.1", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/which": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/which/node_modules/isexe": { - "version": "3.1.1", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/npm/node_modules/wrap-ansi": { - "version": "8.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "9.2.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { - "version": "5.1.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/write-file-atomic": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/yallist": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/nwsapi": { - "version": "2.2.19", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.19.tgz", - "integrity": "sha512-94bcyI3RsqiZufXjkr3ltkI86iEl+I7uiHVDtcq9wJUTwYQJ5odHDeSzkkrRzi80jJ8MaeZgqKjH1bAWAFw9bA==", - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-code": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/object-code/-/object-code-1.3.3.tgz", - "integrity": "sha512-/Ds4Xd5xzrtUOJ+xJQ57iAy0BZsZltOHssnDgcZ8DOhgh41q1YJCnTPnWdWSLkNGNnxYzhYChjc5dgC9mEERCA==", - "license": "MIT" - }, - "node_modules/object-filter": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/object-filter/-/object-filter-1.0.2.tgz", - "integrity": "sha512-NahvP2vZcy1ZiiYah30CEPw0FpDcSkSePJBMpzl5EQgCmISijiGuJm3SPYp7U+Lf2TljyaIw3E5EgkEx/TNEVA==", - "license": "MIT" - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.hasown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", - "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object.values": { @@ -18866,6 +17089,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -18943,6 +17172,74 @@ "tslib": "^2.0.3" } }, + "node_modules/patch-package": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", + "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^9.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "rimraf": "^2.6.3", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.0.33", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-package/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "license": "MIT", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -18982,6 +17279,28 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -18997,6 +17316,27 @@ "node": ">=8" } }, + "node_modules/path-unified": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/path-unified/-/path-unified-0.2.0.tgz", + "integrity": "sha512-MNKqvrKbbbb5p7XHXV6ZAsf/1f/yJQa13S/fcX0uua8ew58Tgc6jXV+16JyAbnR/clgCH+euKDxrF2STxMHdrg==", + "license": "MIT" + }, + "node_modules/path/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/path/node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -19289,6 +17629,24 @@ "postcss": "^8.2.2" } }, + "node_modules/postcss-calc-ast-parser": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/postcss-calc-ast-parser/-/postcss-calc-ast-parser-0.1.4.tgz", + "integrity": "sha512-CebpbHc96zgFjGgdQ6BqBy6XIUgRx1xXWCAAk6oke02RZ5nxwo9KQejTg8y7uYEeI9kv8jKQPYjoe6REsY23vw==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^3.3.1" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/postcss-calc-ast-parser/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "license": "MIT" + }, "node_modules/postcss-colormin": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", @@ -19301,10 +17659,25 @@ "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-combine-duplicated-selectors": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/postcss-combine-duplicated-selectors/-/postcss-combine-duplicated-selectors-10.0.3.tgz", + "integrity": "sha512-IP0BmwFloCskv7DV7xqvzDXqMHpwdczJa6ZvIW8abgHdcIHs9mCJX2ltFhu3EwA51ozp13DByng30+Ke+eIExA==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >=14.0.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.1.0" } }, "node_modules/postcss-convert-values": { @@ -19399,6 +17772,23 @@ "postcss": "^8.4.31" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, "node_modules/postcss-loader": { "version": "7.3.4", "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", @@ -19433,6 +17823,52 @@ "node": ">=10" } }, + "node_modules/postcss-map": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/postcss-map/-/postcss-map-0.11.0.tgz", + "integrity": "sha512-cgHYZrH9aAMds90upYUPhYz8xnAcRD45SwuNns/nQHONIrPQDhpwk3JLsAQGOndQxnRVXfB6nB+3WqSMy8fqlA==", + "license": "Unlicense", + "dependencies": { + "js-yaml": "^3.12.0", + "postcss": "^7.0.2", + "reduce-function-call": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-map/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "license": "ISC" + }, + "node_modules/postcss-map/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "license": "MIT", + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-map/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/postcss-media-query-parser": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", @@ -19474,6 +17910,19 @@ "postcss": "^8.4.31" } }, + "node_modules/postcss-minify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify/-/postcss-minify-1.1.0.tgz", + "integrity": "sha512-9D64ueIW0DL2FdLajQTlXrnTN8Ox9NjuXqigKMmB819RhdClNPYx5Zp3i5x0ghjjy3vGrLBBYEYvJjY/1eMNbw==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0", + "postcss-value-parser": "^4.1" + }, + "peerDependencies": { + "postcss": "^8.0" + } + }, "node_modules/postcss-minify-font-values": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", @@ -19946,9 +18395,9 @@ } }, "node_modules/prebuild-install/node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -20005,6 +18454,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-error": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", @@ -20050,6 +18514,15 @@ "dev": true, "license": "MIT" }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -20113,10 +18586,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/psl": { "version": "1.15.0", @@ -20789,9 +19265,9 @@ } }, "node_modules/react-onclickoutside": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.1.tgz", - "integrity": "sha512-LdrrxK/Yh9zbBQdFbMTXPp3dTSN9B+9YJQucdDu3JNKRrbdU+H+/TVONJoWtOwy4II8Sqf1y/DTI6w/vGPYW0w==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.2.tgz", + "integrity": "sha512-h6Hbf1c8b7tIYY4u90mDdBLY4+AGQVMFtIE89HgC0DtVCh/JfKl477gYqUtGLmjZBKK3MJxomP/lFiLbz4sq9A==", "license": "MIT", "funding": { "type": "individual", @@ -20987,9 +19463,9 @@ } }, "node_modules/react-select": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz", - "integrity": "sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==", + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.1.tgz", + "integrity": "sha512-roPEZUL4aRZDx6DcsD+ZNreVl+fM8VsKn0Wtex1v4IazH60ILp5xhdlp464IsEAlJdXeD+BhDAFsBVMfvLQueA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.0", @@ -21000,11 +19476,11 @@ "memoize-one": "^6.0.0", "prop-types": "^15.6.0", "react-transition-group": "^4.3.0", - "use-isomorphic-layout-effect": "^1.1.2" + "use-isomorphic-layout-effect": "^1.2.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/react-shallow-renderer": { @@ -21088,9 +19564,9 @@ "license": "MIT" }, "node_modules/react-textarea-autosize": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.8.tgz", - "integrity": "sha512-iUiIj70JefrTuSJ4LbVFiSqWiHHss5L63L717bqaWHMgkm9sz6eEvro4vZ3uQfGJbevzwT6rHOszHKA8RkhRMg==", + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz", + "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.13", @@ -21120,6 +19596,24 @@ "react-dom": ">=16.6.0" } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/read-pkg": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-6.0.0.tgz", @@ -21309,6 +19803,15 @@ "node": ">=8" } }, + "node_modules/reduce-function-call": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz", + "integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/redux": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", @@ -21921,9 +20424,9 @@ } }, "node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", @@ -22251,9 +20754,9 @@ } }, "node_modules/sharp/node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -22481,7 +20984,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -22728,16 +21230,6 @@ "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", "license": "MIT" }, - "node_modules/start": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/start/-/start-5.1.0.tgz", - "integrity": "sha512-lirwWQmvBC65bnxU3HzKx5m7vfZJZTx/FrKyPWbtobcvujGbinQQRrNodtcgkp4mTZ00umzDeg7lraN351l0aA==", - "deprecated": "Deprecated in favor of https://github.com/deepsweet/start", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -22747,6 +21239,15 @@ "node": ">= 0.8" } }, + "node_modules/stream": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.3.tgz", + "integrity": "sha512-aMsbn7VKrl4A2T7QAQQbzgN7NVc70vgF5INQrBXqn4dCXN1zy3L9HGgLO5s7PExmdrzTJ8uR/27aviW8or8/+A==", + "license": "MIT", + "dependencies": { + "component-emitter": "^2.0.0" + } + }, "node_modules/streamx": { "version": "2.22.0", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", @@ -22811,6 +21312,27 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/string-width/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -22912,6 +21434,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -22967,6 +21502,55 @@ ], "license": "MIT" }, + "node_modules/style-dictionary": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/style-dictionary/-/style-dictionary-4.4.0.tgz", + "integrity": "sha512-+xU0IA1StzqAqFs/QtXkK+XJa7wpS4X5H+JQccRKsRCElgeLGocFU1U/UMvMUylKFw6vwGV+Y/a2wb2pm5rFFQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@bundled-es-modules/deepmerge": "^4.3.1", + "@bundled-es-modules/glob": "^10.4.2", + "@bundled-es-modules/memfs": "^4.9.4", + "@zip.js/zip.js": "^2.7.44", + "chalk": "^5.3.0", + "change-case": "^5.3.0", + "commander": "^12.1.0", + "is-plain-obj": "^4.1.0", + "json5": "^2.2.2", + "patch-package": "^8.0.0", + "path-unified": "^0.2.0", + "prettier": "^3.3.3", + "tinycolor2": "^1.6.0" + }, + "bin": { + "style-dictionary": "bin/style-dictionary.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/style-dictionary/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/style-dictionary/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/style-loader": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", @@ -23204,6 +21788,15 @@ "node": ">=14.18.0" } }, + "node_modules/superagent/node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/superagent/node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -23594,6 +22187,18 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "license": "MIT" }, + "node_modules/thingies": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", + "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", + "license": "Unlicense", + "engines": { + "node": ">=10.18" + }, + "peerDependencies": { + "tslib": "^2" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -23618,13 +22223,19 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, "node_modules/tinyglobby": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", - "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", "license": "MIT", "dependencies": { - "fdir": "^6.4.3", + "fdir": "^6.4.4", "picomatch": "^4.0.2" }, "engines": { @@ -23635,9 +22246,9 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", - "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" @@ -23754,6 +22365,22 @@ "punycode": "^2.1.0" } }, + "node_modules/tree-dump": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", + "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/trim-newlines": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", @@ -23780,9 +22407,9 @@ } }, "node_modules/ts-jest": { - "version": "29.3.0", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.0.tgz", - "integrity": "sha512-4bfGBX7Gd1Aqz3SyeDS9O276wEU/BInZxskPrbhZLyv+c1wskDCqDFMJQJLWrIr/fKoAH4GE5dKUlrdyvo+39A==", + "version": "29.3.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.2.tgz", + "integrity": "sha512-bJJkrWc6PjFVz5g2DGCNUo8z7oFEYaz1xP1NpeDU7KNLMWPpEyV8Chbpkn8xjzgRDpQhnGMyvyldoL7h8JXyug==", "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", @@ -23793,7 +22420,7 @@ "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.1", - "type-fest": "^4.37.0", + "type-fest": "^4.39.1", "yargs-parser": "^21.1.1" }, "bin": { @@ -23841,9 +22468,9 @@ } }, "node_modules/ts-jest/node_modules/type-fest": { - "version": "4.38.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.38.0.tgz", - "integrity": "sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==", + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.1.tgz", + "integrity": "sha512-9YvLNnORDpI+vghLU/Nf+zSv0kL47KbVJ1o3sKgoTefl6i+zebxbiDQWoe/oWWqPhIgQdRZRT1KA9sCPL810SA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -24160,9 +22787,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -24175,20 +22802,14 @@ } }, "node_modules/unicode-emoji-utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/unicode-emoji-utils/-/unicode-emoji-utils-1.2.0.tgz", - "integrity": "sha512-djUB91p/6oYpgps4W5K/MAvM+UspoAANHSUW495BrxeLRoned3iNPEDQgrKx9LbLq93VhNz0NWvI61vcfrwYoA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/unicode-emoji-utils/-/unicode-emoji-utils-1.3.1.tgz", + "integrity": "sha512-6PiQxmnlsOsqzZCZz0sykSyMy/r1HiJiOWWXV98+BDva583DU4CtBeyDNsi4wMYUIbjUtMs4RgAuyft0EKLoVw==", "license": "MIT", "dependencies": { - "emoji-regex": "10.3.0" + "emoji-regex-xs": "^2.0.0" } }, - "node_modules/unicode-emoji-utils/node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", - "license": "MIT" - }, "node_modules/unicode-match-property-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", @@ -24258,29 +22879,35 @@ } }, "node_modules/unrs-resolver": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.3.2.tgz", - "integrity": "sha512-ZKQBC351Ubw0PY8xWhneIfb6dygTQeUHtCcNGd0QB618zabD/WbFMYdRyJ7xeVT+6G82K5v/oyZO0QSHFtbIuw==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.7.2.tgz", + "integrity": "sha512-BBKpaylOW8KbHsu378Zky/dGh4ckT/4NW/0SHRABdqRLcQJ2dAOjDo9g97p04sWflm0kqPqpUatxReNV/dqI5A==", + "hasInstallScript": true, "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.2.2" + }, "funding": { "url": "https://github.com/sponsors/JounQin" }, "optionalDependencies": { - "@unrs/resolver-binding-darwin-arm64": "1.3.2", - "@unrs/resolver-binding-darwin-x64": "1.3.2", - "@unrs/resolver-binding-freebsd-x64": "1.3.2", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.3.2", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.3.2", - "@unrs/resolver-binding-linux-arm64-gnu": "1.3.2", - "@unrs/resolver-binding-linux-arm64-musl": "1.3.2", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.3.2", - "@unrs/resolver-binding-linux-s390x-gnu": "1.3.2", - "@unrs/resolver-binding-linux-x64-gnu": "1.3.2", - "@unrs/resolver-binding-linux-x64-musl": "1.3.2", - "@unrs/resolver-binding-wasm32-wasi": "1.3.2", - "@unrs/resolver-binding-win32-arm64-msvc": "1.3.2", - "@unrs/resolver-binding-win32-ia32-msvc": "1.3.2", - "@unrs/resolver-binding-win32-x64-msvc": "1.3.2" + "@unrs/resolver-binding-darwin-arm64": "1.7.2", + "@unrs/resolver-binding-darwin-x64": "1.7.2", + "@unrs/resolver-binding-freebsd-x64": "1.7.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.7.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.7.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.7.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.7.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.7.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.7.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.7.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.7.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.7.2", + "@unrs/resolver-binding-linux-x64-musl": "1.7.2", + "@unrs/resolver-binding-wasm32-wasi": "1.7.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.7.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.7.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.7.2" } }, "node_modules/update-browserslist-db": { @@ -24322,6 +22949,19 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/url-loader": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", @@ -24377,6 +23017,12 @@ "requires-port": "^1.0.0" } }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" + }, "node_modules/use-callback-ref": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", @@ -24466,14 +23112,27 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", - "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -24622,13 +23281,14 @@ } }, "node_modules/webpack": { - "version": "5.98.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", - "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", + "version": "5.99.7", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.7.tgz", + "integrity": "sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==", "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", @@ -24645,7 +23305,7 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.0", + "schema-utils": "^4.3.2", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", @@ -25180,6 +23840,24 @@ "node": ">=8" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -25244,6 +23922,15 @@ "node": ">=0.10.0" } }, + "node_modules/xregexp": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-5.1.2.tgz", + "integrity": "sha512-6hGgEMCGhqCTFEJbqmWrNIPqfpdirdGWkqshu7fFZddmTSfgv5Sn9D2SaKloR79s5VUiUlpwzg3CM3G6D3VIlw==", + "license": "MIT", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.9" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -25260,12 +23947,15 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/yargs": { diff --git a/package.json b/package.json index f0fe601b20..f1f27d045e 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ ], "scripts": { "build": "fedx-scripts webpack", - "i18n_extract": "fedx-scripts formatjs extract", + "i18n_extract": "fedx-scripts formatjs extract --include=plugins", "stylelint": "stylelint \"plugins/**/*.scss\" \"src/**/*.scss\" \"scss/**/*.scss\" --config .stylelintrc.json", "lint": "npm run stylelint && fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .", "lint:fix": "npm run stylelint -- --fix && fedx-scripts eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx .", @@ -21,7 +21,8 @@ "dev": "PUBLIC_PATH=/authoring/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io", "test": "TZ=UTC fedx-scripts jest --coverage --passWithNoTests", "test:ci": "TZ=UTC fedx-scripts jest --silent --coverage --passWithNoTests", - "types": "tsc --noEmit" + "types": "tsc --noEmit", + "replace-variables": "paragon replace-variables -p src -t usage" }, "author": "edX", "license": "AGPL-3.0", @@ -34,8 +35,8 @@ }, "dependencies": { "@codemirror/lang-html": "^6.0.0", - "@codemirror/lang-xml": "^6.0.0", "@codemirror/lang-markdown": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", "@codemirror/lint": "^6.2.1", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", @@ -48,8 +49,8 @@ "@edx/frontend-component-footer": "^14.6.0", "@edx/frontend-component-header": "^6.2.0", "@edx/frontend-enterprise-hotjar": "^7.2.0", - "@edx/frontend-platform": "^8.3.1", - "@edx/openedx-atlas": "^0.6.0", + "@edx/frontend-platform": "^8.3.7", + "@edx/openedx-atlas": "^0.7.0", "@openedx-plugins/course-app-calculator": "file:plugins/course-apps/calculator", "@openedx-plugins/course-app-edxnotes": "file:plugins/course-apps/edxnotes", "@openedx-plugins/course-app-learning_assistant": "file:plugins/course-apps/learning_assistant", @@ -60,9 +61,9 @@ "@openedx-plugins/course-app-teams": "file:plugins/course-apps/teams", "@openedx-plugins/course-app-wiki": "file:plugins/course-apps/wiki", "@openedx-plugins/course-app-xpert_unit_summary": "file:plugins/course-apps/xpert_unit_summary", - "@openedx/frontend-build": "^14.3.3", + "@openedx/frontend-build": "^14.5.0", "@openedx/frontend-plugin-framework": "^1.7.0", - "@openedx/paragon": "^22.16.0", + "@openedx/paragon": "^23.5.0", "@redux-devtools/extension": "^3.3.0", "@reduxjs/toolkit": "1.9.7", "@tanstack/react-query": "4.36.1", @@ -79,7 +80,6 @@ "meilisearch": "^0.41.0", "moment": "2.30.1", "moment-shortformat": "^2.1.0", - "npm": "^10.8.1", "prop-types": "^15.8.1", "react": "^18.3.1", "react-datepicker": "^4.13.0", @@ -91,14 +91,13 @@ "react-responsive": "9.0.2", "react-router": "6.27.0", "react-router-dom": "6.27.0", - "react-select": "5.8.0", + "react-select": "5.10.1", "react-textarea-autosize": "^8.5.3", "react-transition-group": "4.4.5", "redux": "4.0.5", "redux-logger": "^3.0.6", "redux-thunk": "^2.4.1", "reselect": "^4.1.5", - "start": "^5.1.0", "tinymce": "^5.10.4", "universal-cookie": "^4.0.4", "uuid": "^3.4.0", @@ -120,5 +119,8 @@ "jest-expect-message": "^1.1.3", "react-test-renderer": "^18.3.1", "redux-mock-store": "^1.5.4" + }, + "overrides": { + "axios": "^1.15.0" } } diff --git a/plugins/course-apps/xpert_unit_summary/settings-modal/SettingsModal.scss b/plugins/course-apps/xpert_unit_summary/settings-modal/SettingsModal.scss index 264e0c7b1a..bfbb607435 100644 --- a/plugins/course-apps/xpert_unit_summary/settings-modal/SettingsModal.scss +++ b/plugins/course-apps/xpert_unit_summary/settings-modal/SettingsModal.scss @@ -1,5 +1,4 @@ -@import "~@edx/brand/paragon/variables"; -@import "~@openedx/paragon/scss/core/utilities-only"; +@import "~@openedx/paragon/styles/scss/core/utilities-only"; .summary-radio { display: flex; diff --git a/src/accessibility-page/AccessibilityPage.jsx b/src/accessibility-page/AccessibilityPage.jsx index c37cdf6abc..e7a0a2770c 100644 --- a/src/accessibility-page/AccessibilityPage.jsx +++ b/src/accessibility-page/AccessibilityPage.jsx @@ -9,30 +9,30 @@ import messages from './messages'; import AccessibilityBody from './AccessibilityBody'; import AccessibilityForm from './AccessibilityForm'; +import { COMMUNITY_ACCESSIBILITY_LINK, ACCESSIBILITY_EMAIL } from './constants'; + const AccessibilityPage = ({ // injected intl, -}) => { - const communityAccessibilityLink = 'https://www.edx.org/accessibility'; - const email = 'accessibility@edx.org'; - return ( - <> - - - {intl.formatMessage(messages.pageTitle, { - siteName: process.env.SITE_NAME, - })} - - -
- - - - - - - ); -}; +}) => ( + <> + + + {intl.formatMessage(messages.pageTitle, { + siteName: process.env.SITE_NAME, + })} + + +
+ + + + + + +); AccessibilityPage.propTypes = { // injected diff --git a/src/accessibility-page/constants.ts b/src/accessibility-page/constants.ts new file mode 100644 index 0000000000..84d1a444c7 --- /dev/null +++ b/src/accessibility-page/constants.ts @@ -0,0 +1,2 @@ +export const COMMUNITY_ACCESSIBILITY_LINK = 'https://www.edx.org/accessibility'; +export const ACCESSIBILITY_EMAIL = 'accessibility@edx.org'; diff --git a/src/advanced-settings/data/api.js b/src/advanced-settings/data/api.js index f240357c9f..92a8a97272 100644 --- a/src/advanced-settings/data/api.js +++ b/src/advanced-settings/data/api.js @@ -1,5 +1,10 @@ -import { camelCaseObject, getConfig } from '@edx/frontend-platform'; +/* eslint-disable import/prefer-default-export */ +import { + camelCaseObject, + getConfig, +} from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { camelCase } from 'lodash'; import { convertObjectToSnakeCase } from '../../utils'; const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; @@ -14,7 +19,19 @@ const getProctoringErrorsApiUrl = () => `${getApiBaseUrl()}/api/contentstore/v1/ export async function getCourseAdvancedSettings(courseId) { const { data } = await getAuthenticatedHttpClient() .get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`); - return camelCaseObject(data); + const keepValues = {}; + Object.keys(data).forEach((key) => { + keepValues[camelCase(key)] = { value: data[key].value }; + }); + const formattedData = {}; + const formattedCamelCaseData = camelCaseObject(data); + Object.keys(formattedCamelCaseData).forEach((key) => { + formattedData[key] = { + ...formattedCamelCaseData[key], + value: keepValues[key]?.value, + }; + }); + return formattedData; } /** @@ -26,7 +43,19 @@ export async function getCourseAdvancedSettings(courseId) { export async function updateCourseAdvancedSettings(courseId, settings) { const { data } = await getAuthenticatedHttpClient() .patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings)); - return camelCaseObject(data); + const keepValues = {}; + Object.keys(data).forEach((key) => { + keepValues[camelCase(key)] = { value: data[key].value }; + }); + const formattedData = {}; + const formattedCamelCaseData = camelCaseObject(data); + Object.keys(formattedCamelCaseData).forEach((key) => { + formattedData[key] = { + ...formattedCamelCaseData[key], + value: keepValues[key]?.value, + }; + }); + return formattedData; } /** @@ -36,5 +65,17 @@ export async function updateCourseAdvancedSettings(courseId, settings) { */ export async function getProctoringExamErrors(courseId) { const { data } = await getAuthenticatedHttpClient().get(`${getProctoringErrorsApiUrl()}${courseId}`); - return camelCaseObject(data); + const keepValues = {}; + Object.keys(data).forEach((key) => { + keepValues[camelCase(key)] = { value: data[key].value }; + }); + const formattedData = {}; + const formattedCamelCaseData = camelCaseObject(data); + Object.keys(formattedCamelCaseData).forEach((key) => { + formattedData[key] = { + ...formattedCamelCaseData[key], + value: keepValues[key]?.value, + }; + }); + return formattedData; } diff --git a/src/advanced-settings/data/api.test.js b/src/advanced-settings/data/api.test.js new file mode 100644 index 0000000000..5679d3ebfa --- /dev/null +++ b/src/advanced-settings/data/api.test.js @@ -0,0 +1,236 @@ +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { + getCourseAdvancedSettings, + updateCourseAdvancedSettings, + getProctoringExamErrors, +} from './api'; + +jest.mock('@edx/frontend-platform/auth', () => ({ + getAuthenticatedHttpClient: jest.fn(), +})); + +describe('courseSettings API', () => { + const mockHttpClient = { + get: jest.fn(), + patch: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + getAuthenticatedHttpClient.mockReturnValue(mockHttpClient); + }); + + describe('getCourseAdvancedSettings', () => { + it('should fetch and unformat course advanced settings', async () => { + const fakeData = { + key_snake_case: { + display_name: 'To come camelCase', + testCamelCase: 'This key must not be formatted', + PascalCase: 'To come camelCase', + 'kebab-case': 'To come camelCase', + UPPER_CASE: 'To come camelCase', + lowercase: 'This key must not be formatted', + UPPERCASE: 'To come lowercase', + 'Title Case': 'To come camelCase', + 'dot.case': 'To come camelCase', + SCREAMING_SNAKE_CASE: 'To come camelCase', + MixedCase: 'To come camelCase', + 'Train-Case': 'To come camelCase', + nestedOption: { + anotherOption: 'To come camelCase', + }, + // value is an object with various cases + // this contain must not be formatted to camelCase + value: { + snake_case: 'snake_case', + camelCase: 'camelCase', + PascalCase: 'PascalCase', + 'kebab-case': 'kebab-case', + UPPER_CASE: 'UPPER_CASE', + lowercase: 'lowercase', + UPPERCASE: 'UPPERCASE', + 'Title Case': 'Title Case', + 'dot.case': 'dot.case', + SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE', + MixedCase: 'MixedCase', + 'Train-Case': 'Train-Case', + nestedOption: { + anotherOption: 'nestedContent', + }, + }, + }, + }; + const expected = { + keySnakeCase: { + displayName: 'To come camelCase', + testCamelCase: 'This key must not be formatted', + pascalCase: 'To come camelCase', + kebabCase: 'To come camelCase', + upperCase: 'To come camelCase', + lowercase: 'This key must not be formatted', + uppercase: 'To come lowercase', + titleCase: 'To come camelCase', + dotCase: 'To come camelCase', + screamingSnakeCase: 'To come camelCase', + mixedCase: 'To come camelCase', + trainCase: 'To come camelCase', + nestedOption: { + anotherOption: 'To come camelCase', + }, + value: fakeData.key_snake_case.value, + }, + }; + + mockHttpClient.get.mockResolvedValue({ data: fakeData }); + + const result = await getCourseAdvancedSettings('course-v1:Test+T101+2024'); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024?fetch_all=0`, + ); + expect(result).toEqual(expected); + }); + }); + + describe('updateCourseAdvancedSettings', () => { + it('should update and unformat course advanced settings', async () => { + const fakeData = { + key_snake_case: { + display_name: 'To come camelCase', + testCamelCase: 'This key must not be formatted', // because already be camelCase + PascalCase: 'To come camelCase', + 'kebab-case': 'To come camelCase', + UPPER_CASE: 'To come camelCase', + lowercase: 'This key must not be formatted', // because camelCase in lowercase not formatted + UPPERCASE: 'To come lowercase', // because camelCase in UPPERCASE format to lowercase + 'Title Case': 'To come camelCase', + 'dot.case': 'To come camelCase', + SCREAMING_SNAKE_CASE: 'To come camelCase', + MixedCase: 'To come camelCase', + 'Train-Case': 'To come camelCase', + nestedOption: { + anotherOption: 'To come camelCase', + }, + // value is an object with various cases + // this contain must not be formatted to camelCase + value: { + snake_case: 'snake_case', + camelCase: 'camelCase', + PascalCase: 'PascalCase', + 'kebab-case': 'kebab-case', + UPPER_CASE: 'UPPER_CASE', + lowercase: 'lowercase', + UPPERCASE: 'UPPERCASE', + 'Title Case': 'Title Case', + 'dot.case': 'dot.case', + SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE', + MixedCase: 'MixedCase', + 'Train-Case': 'Train-Case', + nestedOption: { + anotherOption: 'nestedContent', + }, + }, + }, + }; + const expected = { + keySnakeCase: { + displayName: 'To come camelCase', + testCamelCase: 'This key must not be formatted', + pascalCase: 'To come camelCase', + kebabCase: 'To come camelCase', + upperCase: 'To come camelCase', + lowercase: 'This key must not be formatted', + uppercase: 'To come lowercase', + titleCase: 'To come camelCase', + dotCase: 'To come camelCase', + screamingSnakeCase: 'To come camelCase', + mixedCase: 'To come camelCase', + trainCase: 'To come camelCase', + nestedOption: { + anotherOption: 'To come camelCase', + }, + value: fakeData.key_snake_case.value, + }, + }; + + mockHttpClient.patch.mockResolvedValue({ data: fakeData }); + + const result = await updateCourseAdvancedSettings('course-v1:Test+T101+2024', {}); + expect(mockHttpClient.patch).toHaveBeenCalledWith( + `${process.env.STUDIO_BASE_URL}/api/contentstore/v0/advanced_settings/course-v1:Test+T101+2024`, + {}, + ); + expect(result).toEqual(expected); + }); + }); + + describe('getProctoringExamErrors', () => { + it('should fetch proctoring errors and return unformat object', async () => { + const fakeData = { + key_snake_case: { + display_name: 'To come camelCase', + testCamelCase: 'This key must not be formatted', + PascalCase: 'To come camelCase', + 'kebab-case': 'To come camelCase', + UPPER_CASE: 'To come camelCase', + lowercase: 'This key must not be formatted', + UPPERCASE: 'To come lowercase', + 'Title Case': 'To come camelCase', + 'dot.case': 'To come camelCase', + SCREAMING_SNAKE_CASE: 'To come camelCase', + MixedCase: 'To come camelCase', + 'Train-Case': 'To come camelCase', + nestedOption: { + anotherOption: 'To come camelCase', + }, + // value is an object with various cases + // this contain must not be formatted to camelCase + value: { + snake_case: 'snake_case', + camelCase: 'camelCase', + PascalCase: 'PascalCase', + 'kebab-case': 'kebab-case', + UPPER_CASE: 'UPPER_CASE', + lowercase: 'lowercase', + UPPERCASE: 'UPPERCASE', + 'Title Case': 'Title Case', + 'dot.case': 'dot.case', + SCREAMING_SNAKE_CASE: 'SCREAMING_SNAKE_CASE', + MixedCase: 'MixedCase', + 'Train-Case': 'Train-Case', + nestedOption: { + anotherOption: 'nestedContent', + }, + }, + }, + }; + const expected = { + keySnakeCase: { + displayName: 'To come camelCase', + testCamelCase: 'This key must not be formatted', + pascalCase: 'To come camelCase', + kebabCase: 'To come camelCase', + upperCase: 'To come camelCase', + lowercase: 'This key must not be formatted', + uppercase: 'To come lowercase', + titleCase: 'To come camelCase', + dotCase: 'To come camelCase', + screamingSnakeCase: 'To come camelCase', + mixedCase: 'To come camelCase', + trainCase: 'To come camelCase', + nestedOption: { + anotherOption: 'To come camelCase', + }, + value: fakeData.key_snake_case.value, + }, + }; + + mockHttpClient.get.mockResolvedValue({ data: fakeData }); + + const result = await getProctoringExamErrors('course-v1:Test+T101+2024'); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `${process.env.STUDIO_BASE_URL}/api/contentstore/v1/proctoring_errors/course-v1:Test+T101+2024`, + ); + expect(result).toEqual(expected); + }); + }); +}); diff --git a/src/advanced-settings/scss/AdvancedSettings.scss b/src/advanced-settings/scss/AdvancedSettings.scss index c4252d6147..e973e11cee 100644 --- a/src/advanced-settings/scss/AdvancedSettings.scss +++ b/src/advanced-settings/scss/AdvancedSettings.scss @@ -32,7 +32,7 @@ bottom: 0; width: 100%; padding: 0 .625rem; - z-index: $zindex-modal; + z-index: var(--pgn-elevation-modal-zindex); } .alert-proctoring-error { @@ -66,13 +66,13 @@ .setting-sidebar-supplementary { .setting-sidebar-supplementary-about { .setting-sidebar-supplementary-about-title { - font: normal $font-weight-bold 1.125rem/1.5rem $font-family-base; - color: $headings-color; + font: normal var(--pgn-typography-font-weight-bold) 1.125rem/1.5rem var(--pgn-typography-font-family-base); + color: var(--pgn-color-headings-base); margin-bottom: 1.25rem; } .setting-sidebar-supplementary-about-descriptions { - font: normal $font-weight-normal .875rem/1.5rem $font-family-base; + font: normal var(--pgn-typography-font-weight-normal) .875rem/1.5rem var(--pgn-typography-font-family-base); color: $text-color-base; } } @@ -81,16 +81,16 @@ list-style: none; .setting-sidebar-supplementary-other-link { - font: normal $font-weight-normal .875rem/1.5rem $font-family-base; + font: normal var(--pgn-typography-font-weight-normal) .875rem/1.5rem var(--pgn-typography-font-family-base); line-height: 1.5rem; - color: $info-500; + color: var(--pgn-color-info-500); margin-bottom: .5rem; } } .setting-sidebar-supplementary-other-title { - font: normal $font-weight-bold 1.125rem/1.5rem $font-family-base; - color: $headings-color; + font: normal var(--pgn-typography-font-weight-bold) 1.125rem/1.5rem var(--pgn-typography-font-family-base); + color: var(--pgn-color-headings-base); margin-bottom: 1.25rem; } } @@ -102,7 +102,7 @@ display: inline-block; margin-right: 5px; margin-bottom: 5px; - color: $danger; + color: var(--pgn-color-danger-base); } .modal-error-item-title { @@ -113,12 +113,12 @@ .modal-popup-content { max-width: 200px; - color: $white; - background-color: $black; + color: var(--pgn-color-white); + background-color: var(--pgn-color-black); filter: drop-shadow(0 2px 4px rgba(0 0 0 / .15)); font-weight: 400; } .pgn__modal-popup__arrow::after { - border-top-color: $black; + border-top-color: var(--pgn-color-black); } diff --git a/src/advanced-settings/scss/_variables.scss b/src/advanced-settings/scss/_variables.scss index 22c26a836f..3a4d279bcb 100644 --- a/src/advanced-settings/scss/_variables.scss +++ b/src/advanced-settings/scss/_variables.scss @@ -1 +1 @@ -$text-color-base: $gray-700; +$text-color-base: var(--pgn-color-gray-700); diff --git a/src/assets/scss/_form.scss b/src/assets/scss/_form.scss index 9ca6aa170b..1fbe63e5ee 100644 --- a/src/assets/scss/_form.scss +++ b/src/assets/scss/_form.scss @@ -1,14 +1,14 @@ .form-group-custom { .pgn__form-label { - font: normal $font-weight-bold .75rem/1.25rem $font-family-base; - color: $gray-500; + font: normal var(--pgn-typography-font-weight-bold) .75rem/1.25rem var(--pgn-typography-font-family-base); + color: var(--pgn-color-gray-500); margin-bottom: .5rem; } .pgn__form-control-description, .pgn__form-text { - font: normal $font-weight-normal .75rem/1.25rem $font-family-base; - color: $gray-500; + font: normal var(--pgn-typography-font-weight-normal) .75rem/1.25rem var(--pgn-typography-font-family-base); + color: var(--pgn-color-gray-500); margin-top: .5rem; } @@ -19,12 +19,12 @@ .form-group-custom_isInvalid { input { - border-color: $form-feedback-invalid-color; + border-color: var(--pgn-color-form-feedback-invalid); } } .feedback-error { - color: $form-feedback-invalid-color; + color: var(--pgn-color-form-feedback-invalid); } } @@ -34,40 +34,40 @@ .datepicker-custom-control { display: block; width: 100%; - font-size: $input-font-size; - font-weight: $input-font-weight; - line-height: $input-line-height; - background: $input-bg; - border-color: $input-border-color; - border-width: $input-border-width; - box-shadow: $input-box-shadow; - border-radius: $input-border-radius; - color: $input-color; - padding: $input-padding-y $input-padding-x; - height: $input-height; + font-size: var(--pgn-typography-form-input-font-size-base); + font-weight: var(--pgn-typography-form-input-font-weight); + line-height: var(--pgn-typography-form-input-line-height-base); + background: var(--pgn-color-form-input-bg-base); + border-color: var(--pgn-color-form-input-border); + border-width: var(--pgn-size-form-input-width-border); + box-shadow: var(--pgn-elevation-form-input-base); + border-radius: var(--pgn-size-form-input-radius-border-base); + color: var(--pgn-color-form-input-base); + padding: var(--pgn-spacing-form-input-padding-y-base) var(--pgn-spacing-form-input-padding-x-base); + height: var(--pgn-size-form-input-height-base); resize: none; &:focus, :focus-visible { - color: $input-focus-color; - background-color: $input-bg; - border-color: $input-focus-border-color; - box-shadow: $input-focus-box-shadow; + color: var(--pgn-color-form-input-focus-base); + background-color: var(--pgn-color-form-input-bg-base); + border-color: var(--pgn-color-form-input-focus-border); + box-shadow: var(--pgn-elevation-form-input-focus); outline: 0; } &::placeholder { - color: $input-placeholder-color; + color: var(--pgn-color-form-input-placeholder); } } .datepicker-custom-control_readonly { border-color: transparent; - background: $input-disabled-bg; + background: var(--pgn-color-form-input-bg-disabled); } .datepicker-custom-control_isInvalid { - border-color: $form-feedback-invalid-color; + border-color: var(--pgn-color-form-feedback-invalid); } .datepicker-custom-control-icon { @@ -76,7 +76,7 @@ right: 1.188rem; top: 50%; transform: translateY(-50%); - color: $black; + color: var(--pgn-color-black); } } diff --git a/src/assets/scss/_utilities.scss b/src/assets/scss/_utilities.scss index 2caa9558e5..5d256e5e0a 100644 --- a/src/assets/scss/_utilities.scss +++ b/src/assets/scss/_utilities.scss @@ -1,5 +1,5 @@ .text-black { - color: $black; + color: var(--pgn-color-black); } .h-200px { diff --git a/src/assets/scss/_variables.scss b/src/assets/scss/_variables.scss index a3f23431f6..87d094f24e 100644 --- a/src/assets/scss/_variables.scss +++ b/src/assets/scss/_variables.scss @@ -1,2 +1,2 @@ -$text-color-base: $gray-700; +$text-color-base: var(--pgn-color-gray-700); $text-color-weak: #3E3E3C; diff --git a/src/certificates/scss/Certificates.scss b/src/certificates/scss/Certificates.scss index 8ec8d23bda..76cdad4ed9 100644 --- a/src/certificates/scss/Certificates.scss +++ b/src/certificates/scss/Certificates.scss @@ -2,7 +2,7 @@ .certificates { .section-title { - color: $black; + color: var(--pgn-color-black); } .sub-header-actions { @@ -11,7 +11,7 @@ .certificate-details { .certificate-details__info { - color: $black; + color: var(--pgn-color-black); justify-content: space-between; align-items: baseline; } @@ -22,7 +22,7 @@ .certificate-details__info-paragraph-course-number { flex: 1; - color: $gray-700; + color: var(--pgn-color-gray-700); text-align: right; } } @@ -74,7 +74,7 @@ } } - @media (max-width: map-get($grid-breakpoints, "xl")) { + @media (--pgn-size-breakpoint-max-width-xl) { .signatory { display: flex; flex-direction: column; diff --git a/src/content-tags-drawer/ContentTagsCollapsible.scss b/src/content-tags-drawer/ContentTagsCollapsible.scss index 0da1ea82f1..c9e3c54ae4 100644 --- a/src/content-tags-drawer/ContentTagsCollapsible.scss +++ b/src/content-tags-drawer/ContentTagsCollapsible.scss @@ -38,7 +38,7 @@ .add-tags-button:not([disabled]):hover { background-color: transparent; - color: $info-900 !important; + color: var(--pgn-color-info-900) !important; } .react-select-add-tags__control { diff --git a/src/content-tags-drawer/ContentTagsDrawer.scss b/src/content-tags-drawer/ContentTagsDrawer.scss index 76889a4401..30a3730cac 100644 --- a/src/content-tags-drawer/ContentTagsDrawer.scss +++ b/src/content-tags-drawer/ContentTagsDrawer.scss @@ -16,7 +16,7 @@ .tags-drawer-cancel-button:hover { background-color: transparent; - color: $gray-300 !important; + color: var(--pgn-color-gray-300) !important; } .other-description { @@ -25,7 +25,7 @@ .enable-taxonomies-button:not([disabled]):hover { background-color: transparent; - color: $info-900 !important; + color: var(--pgn-color-info-900) !important; } } diff --git a/src/content-tags-drawer/ContentTagsDrawer.test.jsx b/src/content-tags-drawer/ContentTagsDrawer.test.jsx index 000badea95..5035b2fe43 100644 --- a/src/content-tags-drawer/ContentTagsDrawer.test.jsx +++ b/src/content-tags-drawer/ContentTagsDrawer.test.jsx @@ -21,6 +21,7 @@ const path = '/content/:contentId?/*'; const mockOnClose = jest.fn(); const mockSetBlockingSheet = jest.fn(); const mockNavigate = jest.fn(); +const mockSidebarAction = jest.fn(); mockContentTaxonomyTagsData.applyMock(); mockTaxonomyListData.applyMock(); mockTaxonomyTagsData.applyMock(); @@ -40,6 +41,11 @@ jest.mock('react-router-dom', () => ({ useNavigate: () => mockNavigate, })); +jest.mock('../library-authoring/common/context/SidebarContext', () => ({ + ...jest.requireActual('../library-authoring/common/context/SidebarContext'), + useSidebarContext: () => ({ sidebarAction: mockSidebarAction() }), +})); + const renderDrawer = (contentId, drawerParams = {}) => ( render( @@ -184,6 +190,26 @@ describe('', () => { expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument(); }); + it('should change to edit mode sidebar action is set to JumpToManageTags', async () => { + mockSidebarAction.mockReturnValueOnce('jump-to-manage-tags'); + renderDrawer(stagedTagsId, { variant: 'component' }); + expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument(); + + // Show delete tag buttons + expect(screen.getAllByRole('button', { + name: /delete/i, + }).length).toBe(2); + + // Show add a tag select + expect(screen.getByText(/add a tag/i)).toBeInTheDocument(); + + // Show cancel button + expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument(); + + // Show save button + expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument(); + }); + it('should change to read mode when click on `Cancel` on drawer variant', async () => { renderDrawer(stagedTagsId); expect(await screen.findByText('Taxonomy 1')).toBeInTheDocument(); diff --git a/src/content-tags-drawer/ContentTagsDrawer.tsx b/src/content-tags-drawer/ContentTagsDrawer.tsx index 9253205b36..7b278dcce0 100644 --- a/src/content-tags-drawer/ContentTagsDrawer.tsx +++ b/src/content-tags-drawer/ContentTagsDrawer.tsx @@ -14,6 +14,7 @@ import ContentTagsCollapsible from './ContentTagsCollapsible'; import Loading from '../generic/Loading'; import { useCreateContentTagsDrawerContext } from './ContentTagsDrawerHelper'; import { ContentTagsDrawerContext, ContentTagsDrawerSheetContext } from './common/context'; +import { SidebarActions, useSidebarContext } from '../library-authoring/common/context/SidebarContext'; interface TaxonomyListProps { contentId: string; @@ -244,6 +245,7 @@ const ContentTagsDrawer = ({ if (contentId === undefined) { throw new Error('Error: contentId cannot be null.'); } + const { sidebarAction } = useSidebarContext(); const context = useCreateContentTagsDrawerContext(contentId, !readOnly, variant === 'drawer'); const { blockingSheet } = useContext(ContentTagsDrawerSheetContext); @@ -260,6 +262,7 @@ const ContentTagsDrawer = ({ closeToast, setCollapsibleToInitalState, otherTaxonomies, + toEditMode, } = context; let onCloseDrawer: () => void; @@ -302,8 +305,13 @@ const ContentTagsDrawer = ({ // First call of the initial collapsible states React.useEffect(() => { - setCollapsibleToInitalState(); - }, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded]); + // Open tag edit mode when sidebarAction is JumpToManageTags + if (sidebarAction === SidebarActions.JumpToManageTags) { + toEditMode(); + } else { + setCollapsibleToInitalState(); + } + }, [isTaxonomyListLoaded, isContentTaxonomyTagsLoaded, sidebarAction, toEditMode]); const renderFooter = () => { if (isTaxonomyListLoaded && isContentTaxonomyTagsLoaded) { diff --git a/src/content-tags-drawer/ContentTagsDropDownSelector.scss b/src/content-tags-drawer/ContentTagsDropDownSelector.scss index 963e8072b0..e8ddf21bf3 100644 --- a/src/content-tags-drawer/ContentTagsDropDownSelector.scss +++ b/src/content-tags-drawer/ContentTagsDropDownSelector.scss @@ -7,7 +7,7 @@ &:hover { background-color: transparent; - color: $info-900 !important; + color: var(--pgn-color-info-900) !important; } } @@ -19,7 +19,8 @@ // In the future, this customizability should be implemented in paragon instead input.pgn__form-checkbox-input { &:indeterminate { - @extend :checked; /* stylelint-disable-line scss/at-extend-no-missing-placeholder */ + border-color: var(--pgn-color-form-control-indicator-checked-border-base); + background-image: var(--pgn-other-content-form-control-checkbox-indicator-icon-checked-base); } } } @@ -34,6 +35,6 @@ } .dropdown-selector-tag-actions:focus-visible { - outline: solid 2px $info-900; + outline: solid 2px var(--pgn-color-info-900); border-radius: 4px; } diff --git a/src/content-tags-drawer/TagsTree.scss b/src/content-tags-drawer/TagsTree.scss index ebb5920ca0..0864f8de44 100644 --- a/src/content-tags-drawer/TagsTree.scss +++ b/src/content-tags-drawer/TagsTree.scss @@ -9,13 +9,13 @@ &:hover { svg { - color: $gray-900; + color: var(--pgn-color-gray-900); } } &:focus-visible { border: 2px solid; - border-color: $gray-900; + border-color: var(--pgn-color-gray-900); } } } diff --git a/src/content-tags-drawer/data/apiHooks.jsx b/src/content-tags-drawer/data/apiHooks.jsx index c32be8e3f7..01ef3a3a7b 100644 --- a/src/content-tags-drawer/data/apiHooks.jsx +++ b/src/content-tags-drawer/data/apiHooks.jsx @@ -7,6 +7,7 @@ import { useMutation, useQueryClient, } from '@tanstack/react-query'; +import { useParams } from 'react-router'; import { getTaxonomyTagsData, getContentTaxonomyTagsData, @@ -14,7 +15,7 @@ import { updateContentTaxonomyTags, getContentTaxonomyTagsCount, } from './api'; -import { libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks'; +import { libraryAuthoringQueryKeys, libraryQueryPredicate, xblockQueryKeys } from '../../library-authoring/data/apiHooks'; import { getLibraryId } from '../../generic/key-utils'; /** @typedef {import("../../taxonomy/data/types.js").TagListData} TagListData */ @@ -129,6 +130,7 @@ export const useContentData = (contentId, enabled) => ( export const useContentTaxonomyTagsUpdater = (contentId) => { const queryClient = useQueryClient(); const unitIframe = window.frames['xblock-iframe']; + const { unitId } = useParams(); return useMutation({ /** @@ -158,6 +160,10 @@ export const useContentTaxonomyTagsUpdater = (contentId) => { queryClient.invalidateQueries(xblockQueryKeys.componentMetadata(contentId)); // Invalidate content search to update tags count queryClient.invalidateQueries(['content_search'], { predicate: (query) => libraryQueryPredicate(query, libraryId) }); + // If the tags for a compoent were edited from Unit page, invalidate children query to fetch count again. + if (unitId) { + queryClient.invalidateQueries(libraryAuthoringQueryKeys.containerChildren(unitId)); + } } }, onSuccess: /* istanbul ignore next */ () => { diff --git a/src/content-tags-drawer/data/apiHooks.test.jsx b/src/content-tags-drawer/data/apiHooks.test.jsx index 2ce95d8465..ebfaf9e779 100644 --- a/src/content-tags-drawer/data/apiHooks.test.jsx +++ b/src/content-tags-drawer/data/apiHooks.test.jsx @@ -157,7 +157,7 @@ describe('useContentTaxonomyTagsUpdater', () => { const contentId = 'testerContent'; const taxonomyId = 123; - const mutation = useContentTaxonomyTagsUpdater(contentId); + const mutation = renderHook(() => useContentTaxonomyTagsUpdater(contentId)).result.current; const tagsData = [{ taxonomy: taxonomyId, tags: ['tag1', 'tag2'], diff --git a/src/course-checklist/ChecklistSection/ChecklistSection.scss b/src/course-checklist/ChecklistSection/ChecklistSection.scss index f067970138..89405540de 100644 --- a/src/course-checklist/ChecklistSection/ChecklistSection.scss +++ b/src/course-checklist/ChecklistSection/ChecklistSection.scss @@ -13,10 +13,10 @@ .assignment-list { display: inline; - padding-inline-start: map-get($spacers, 1); + padding-inline-start: var(--pgn-spacing-spacer-1); } //complete checklist item style .checklist-item-complete { - box-shadow: -5px 0 0 0 $success-500; + box-shadow: -5px 0 0 0 var(--pgn-color-success-500); } diff --git a/src/course-libraries/CourseLibraries.test.tsx b/src/course-libraries/CourseLibraries.test.tsx index 5df2fe3134..4c44144246 100644 --- a/src/course-libraries/CourseLibraries.test.tsx +++ b/src/course-libraries/CourseLibraries.test.tsx @@ -118,6 +118,46 @@ describe('', () => { userEvent.click(reviewActionBtn); expect(await screen.findByRole('tab', { name: 'Review Content Updates 5' })).toHaveAttribute('aria-selected', 'true'); }); + + it('show alert if max lastPublishedDate is greated than the local storage value', async () => { + const lastPublishedDate = new Date('2025-05-01T22:20:44.989042Z'); + localStorage.setItem( + `outOfSyncCountAlert-${mockGetEntityLinks.courseKey}`, + String(lastPublishedDate.getTime() - 1000), + ); + + await renderCourseLibrariesPage(mockGetEntityLinks.courseKey); + const allTab = await screen.findByRole('tab', { name: 'Libraries' }); + const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 5' }); + // review tab should be open by default as outOfSyncCount is greater than 0 + expect(reviewTab).toHaveAttribute('aria-selected', 'true'); + + userEvent.click(allTab); + const alert = await screen.findByRole('alert'); + expect(await within(alert).findByText( + '5 library components are out of sync. Review updates to accept or ignore changes', + )).toBeInTheDocument(); + }); + + it('doesnt show alert if max lastPublishedDate is less than the local storage value', async () => { + const lastPublishedDate = new Date('2025-05-01T22:20:44.989042Z'); + localStorage.setItem( + `outOfSyncCountAlert-${mockGetEntityLinks.courseKey}`, + String(lastPublishedDate.getTime() + 1000), + ); + + await renderCourseLibrariesPage(mockGetEntityLinks.courseKey); + const allTab = await screen.findByRole('tab', { name: 'Libraries' }); + const reviewTab = await screen.findByRole('tab', { name: 'Review Content Updates 5' }); + // review tab should be open by default as outOfSyncCount is greater than 0 + expect(reviewTab).toHaveAttribute('aria-selected', 'true'); + userEvent.click(allTab); + expect(allTab).toHaveAttribute('aria-selected', 'true'); + + screen.logTestingPlaygroundURL(); + + expect(screen.queryByRole('alert')).not.toBeInTheDocument(); + }); }); describe('', () => { @@ -160,7 +200,7 @@ describe('', () => { it('update changes works', async () => { const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries'); - const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey; + const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey; axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {}); await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey); const updateBtns = await screen.findAllByRole('button', { name: 'Update' }); @@ -176,7 +216,7 @@ describe('', () => { it('update changes works in preview modal', async () => { const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries'); - const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey; + const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey; axiosMock.onPost(libraryBlockChangesUrl(usageKey)).reply(200, {}); await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey); const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' }); @@ -195,7 +235,7 @@ describe('', () => { it('ignore change works', async () => { const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries'); - const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey; + const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey; axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {}); await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey); const ignoreBtns = await screen.findAllByRole('button', { name: 'Ignore' }); @@ -218,7 +258,7 @@ describe('', () => { it('ignore change works in preview', async () => { const mockInvalidateQueries = jest.spyOn(queryClient, 'invalidateQueries'); - const usageKey = mockGetEntityLinks.response.results[0].downstreamUsageKey; + const usageKey = mockGetEntityLinks.response[0].downstreamUsageKey; axiosMock.onDelete(libraryBlockChangesUrl(usageKey)).reply(204, {}); await renderCourseLibrariesReviewPage(mockGetEntityLinksSummaryByDownstreamContext.courseKey); const previewBtns = await screen.findAllByRole('button', { name: 'Review Updates' }); diff --git a/src/course-libraries/CourseLibraries.tsx b/src/course-libraries/CourseLibraries.tsx index 3c1a0730fe..2d0cfadf0f 100644 --- a/src/course-libraries/CourseLibraries.tsx +++ b/src/course-libraries/CourseLibraries.tsx @@ -164,7 +164,7 @@ export const CourseLibraries: React.FC = ({ courseId }) => { if (tabKey !== CourseLibraryTabs.review) { return null; } - if (!outOfSyncCount || outOfSyncCount === 0) { + if (!outOfSyncCount) { return ( diff --git a/src/course-libraries/OutOfSyncAlert.tsx b/src/course-libraries/OutOfSyncAlert.tsx index 27b88f2c83..da36c40869 100644 --- a/src/course-libraries/OutOfSyncAlert.tsx +++ b/src/course-libraries/OutOfSyncAlert.tsx @@ -18,12 +18,11 @@ interface OutOfSyncAlertProps { * in course can be updated. Following are the conditions for displaying the alert. * * * The alert is displayed if components are out of sync. -* * If the user clicks on dismiss button, the state is stored in localstorage of user -* in this format: outOfSyncCountAlert-${courseId} = . -* * If the number of sync components don't change for the course and the user opens outline +* * If the user clicks on dismiss button, the state of dismiss is stored in localstorage of user +* in this format: outOfSyncCountAlert-${courseId} = . +* * If there are not new published components for the course and the user opens outline * in the same browser, they don't see the alert again. -* * If the number changes, i.e., if a new component is out of sync or the user updates or ignores -* a component, the alert is displayed again. +* * If there is a new published component upstream, the alert is displayed again. */ export const OutOfSyncAlert: React.FC = ({ showAlert, @@ -35,6 +34,8 @@ export const OutOfSyncAlert: React.FC = ({ const intl = useIntl(); const { data, isLoading } = useEntityLinksSummaryByDownstreamContext(courseId); const outOfSyncCount = data?.reduce((count, lib) => count + (lib.readyToSyncCount || 0), 0); + const lastPublishedDate = data?.map(lib => new Date(lib.lastPublishedAt || 0).getTime()) + .reduce((acc, lastPublished) => Math.max(lastPublished, acc), 0); const alertKey = `outOfSyncCountAlert-${courseId}`; useEffect(() => { @@ -46,13 +47,14 @@ export const OutOfSyncAlert: React.FC = ({ setShowAlert(false); return; } - const dismissedAlert = localStorage.getItem(alertKey); - setShowAlert(parseInt(dismissedAlert || '', 10) !== outOfSyncCount); - }, [outOfSyncCount, isLoading, data]); + const dismissedAlertDate = parseInt(localStorage.getItem(alertKey) ?? '0', 10); + + setShowAlert((lastPublishedDate ?? 0) > dismissedAlertDate); + }, [outOfSyncCount, lastPublishedDate, isLoading, data]); const dismissAlert = () => { setShowAlert(false); - localStorage.setItem(alertKey, String(outOfSyncCount)); + localStorage.setItem(alertKey, Date.now().toString()); onDismiss?.(); }; diff --git a/src/course-libraries/ReviewTabContent.tsx b/src/course-libraries/ReviewTabContent.tsx index 1c756c5ca9..36634335bd 100644 --- a/src/course-libraries/ReviewTabContent.tsx +++ b/src/course-libraries/ReviewTabContent.tsx @@ -1,5 +1,5 @@ import React, { - useCallback, useContext, useEffect, useMemo, useState, + useCallback, useContext, useMemo, useState, } from 'react'; import { getConfig } from '@edx/frontend-platform'; import { useIntl } from '@edx/frontend-platform/i18n'; @@ -14,11 +14,9 @@ import { useToggle, } from '@openedx/paragon'; -import { - tail, keyBy, orderBy, merge, omitBy, -} from 'lodash'; +import { tail, keyBy } from 'lodash'; import { useQueryClient } from '@tanstack/react-query'; -import { Loop, Warning } from '@openedx/paragon/icons'; +import { Loop } from '@openedx/paragon/icons'; import messages from './messages'; import previewChangesMessages from '../course-unit/preview-changes/messages'; import { courseLibrariesQueryKeys, useEntityLinks } from './data/apiHooks'; @@ -37,7 +35,6 @@ import { useLoadOnScroll } from '../hooks'; import DeleteModal from '../generic/delete-modal/DeleteModal'; import { PublishableEntityLink } from './data/api'; import AlertError from '../generic/alert-error'; -import AlertMessage from '../generic/alert-message'; interface Props { courseId: string; @@ -102,10 +99,8 @@ const BlockCard: React.FC = ({ info, actions }) => { const ComponentReviewList = ({ outOfSyncComponents, - onSearchUpdate, }: { outOfSyncComponents: PublishableEntityLink[]; - onSearchUpdate: () => void; }) => { const intl = useIntl(); const { showToast } = useContext(ToastContext); @@ -113,24 +108,15 @@ const ComponentReviewList = ({ // ignore changes confirmation modal toggle. const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useToggle(false); const { - hits: downstreamInfo, + hits, isLoading: isIndexDataLoading, - searchKeywords, - searchSortOrder, hasError, hasNextPage, isFetchingNextPage, fetchNextPage, - } = useSearchContext() as { - hits: ContentHit[]; - isLoading: boolean; - searchKeywords: string; - searchSortOrder: SearchSortOption; - hasError: boolean; - hasNextPage: boolean | undefined, - isFetchingNextPage: boolean; - fetchNextPage: () => void; - }; + } = useSearchContext(); + + const downstreamInfo = hits as ContentHit[]; useLoadOnScroll( hasNextPage, @@ -143,24 +129,14 @@ const ComponentReviewList = ({ () => keyBy(outOfSyncComponents, 'downstreamUsageKey'), [outOfSyncComponents], ); - const downstreamInfoByKey = useMemo( - () => keyBy(downstreamInfo, 'usageKey'), - [downstreamInfo], - ); const queryClient = useQueryClient(); - useEffect(() => { - if (searchKeywords) { - onSearchUpdate(); - } - }, [searchKeywords]); - // Toggle preview changes modal const [isModalOpen, openModal, closeModal] = useToggle(false); const acceptChangesMutation = useAcceptLibraryBlockChanges(); const ignoreChangesMutation = useIgnoreLibraryBlockChanges(); - const setSeletecdBlockData = (info: ContentHit) => { + const setSelectedBlockData = useCallback((info: ContentHit) => { setBlockData({ displayName: info.displayName, downstreamBlockId: info.usageKey, @@ -168,17 +144,18 @@ const ComponentReviewList = ({ upstreamBlockVersionSynced: outOfSyncComponentsByKey[info.usageKey].versionSynced, isVertical: info.blockType === 'vertical', }); - }; + }, [outOfSyncComponentsByKey]); + // Show preview changes on review const onReview = useCallback((info: ContentHit) => { - setSeletecdBlockData(info); + setSelectedBlockData(info); openModal(); - }, [setSeletecdBlockData, openModal]); + }, [setSelectedBlockData, openModal]); const onIgnoreClick = useCallback((info: ContentHit) => { - setSeletecdBlockData(info); + setSelectedBlockData(info); openConfirmModal(); - }, [setSeletecdBlockData, openConfirmModal]); + }, [setSelectedBlockData, openConfirmModal]); const reloadLinks = useCallback((usageKey: string) => { const courseKey = outOfSyncComponentsByKey[usageKey].downstreamContextKey; @@ -236,19 +213,6 @@ const ComponentReviewList = ({ } }, [blockData]); - const orderInfo = useMemo(() => { - if (searchSortOrder !== SearchSortOption.RECENTLY_MODIFIED) { - return downstreamInfo; - } - if (isIndexDataLoading) { - return []; - } - let merged = merge(downstreamInfoByKey, outOfSyncComponentsByKey); - merged = omitBy(merged, (o) => !o.displayName); - const ordered = orderBy(Object.values(merged), 'updated', 'desc'); - return ordered; - }, [downstreamInfoByKey, outOfSyncComponentsByKey]); - if (isIndexDataLoading) { return ; } @@ -259,7 +223,7 @@ const ComponentReviewList = ({ return ( <> - {orderInfo?.map((info) => ( + {downstreamInfo?.map((info) => ( ))} - - )} - /> + {blockData && ( + + )} { const intl = useIntl(); const { - data: linkPages, + data: outOfSyncComponents, isLoading: isSyncComponentsLoading, - hasNextPage, - isFetchingNextPage, - fetchNextPage, isError, error, } = useEntityLinks({ courseId, readyToSync: true }); - const outOfSyncComponents = useMemo( - () => linkPages?.pages?.reduce((links, page) => [...links, ...page.results], []) ?? [], - [linkPages], - ); const downstreamKeys = useMemo( () => outOfSyncComponents?.map(link => link.downstreamUsageKey), [outOfSyncComponents], ); - useLoadOnScroll( - hasNextPage, - isFetchingNextPage, - fetchNextPage, - true, - ); - - const onSearchUpdate = () => { - if (hasNextPage && !isFetchingNextPage) { - fetchNextPage(); - } - }; - const disableSortOptions = [ SearchSortOption.RELEVANCE, SearchSortOption.OLDEST, @@ -384,7 +322,6 @@ const ReviewTabContent = ({ courseId }: Props) => { ); diff --git a/src/course-libraries/__mocks__/linkCourseSummary.json b/src/course-libraries/__mocks__/linkCourseSummary.json index 32e98e8987..05039086d2 100644 --- a/src/course-libraries/__mocks__/linkCourseSummary.json +++ b/src/course-libraries/__mocks__/linkCourseSummary.json @@ -3,17 +3,20 @@ "upstreamContextTitle": "CS problems 3", "upstreamContextKey": "lib:OpenedX:CSPROB3", "readyToSyncCount": 5, - "totalCount": 14 + "totalCount": 14, + "lastPublishedAt": "2025-05-01T20:20:44.989042Z" }, { "upstreamContextTitle": "CS problems 2", "upstreamContextKey": "lib:OpenedX:CSPROB2", "readyToSyncCount": 0, - "totalCount": 21 + "totalCount": 21, + "lastPublishedAt": "2025-05-01T21:20:44.989042Z" }, { "upstreamContextTitle": "CS problems", "upstreamContextKey": "lib:OpenedX:CSPROB", - "totalCount": 3 + "totalCount": 3, + "lastPublishedAt": "2025-05-01T22:20:44.989042Z" } ] diff --git a/src/course-libraries/__mocks__/publishableEntityLinks.json b/src/course-libraries/__mocks__/publishableEntityLinks.json index 9988dee71a..1dac4b2dbd 100644 --- a/src/course-libraries/__mocks__/publishableEntityLinks.json +++ b/src/course-libraries/__mocks__/publishableEntityLinks.json @@ -1,79 +1,72 @@ -{ - "count": 7, - "next": null, - "previous": null, - "num_pages": 1, - "current_page": 1, - "results": [ - { - "id": 875, - "upstreamContextTitle": "CS problems 3", - "upstreamVersion": 10, - "readyToSync": true, - "upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3", - "upstreamContextKey": "lib:OpenedX:CSPROB3", - "downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem3", - "downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", - "versionSynced": 2, - "versionDeclined": null, - "created": "2025-02-08T14:07:05.588484Z", - "updated": "2025-02-08T14:07:05.588484Z" - }, - { - "id": 876, - "upstreamContextTitle": "CS problems 3", - "upstreamVersion": 10, - "readyToSync": true, - "upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3", - "upstreamContextKey": "lib:OpenedX:CSPROB3", - "downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem6", - "downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", - "versionSynced": 2, - "versionDeclined": null, - "created": "2025-02-08T14:07:05.588484Z", - "updated": "2025-02-08T14:07:05.588484Z" - }, - { - "id": 884, - "upstreamContextTitle": "CS problems 3", - "upstreamVersion": 26, - "readyToSync": true, - "upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477", - "upstreamContextKey": "lib:OpenedX:CSPROB3", - "downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@html+block@257e68e3386d4a8f8739d45b67e76a9b", - "downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", - "versionSynced": 16, - "versionDeclined": null, - "created": "2025-02-08T14:07:05.588484Z", - "updated": "2025-02-08T14:07:05.588484Z" - }, - { - "id": 889, - "upstreamContextTitle": "CS problems 3", - "upstreamVersion": 10, - "readyToSync": true, - "upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3", - "upstreamContextKey": "lib:OpenedX:CSPROB3", - "downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@a4455860b03647219ff8b01cde49cf37", - "downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", - "versionSynced": 2, - "versionDeclined": null, - "created": "2025-02-08T14:07:05.588484Z", - "updated": "2025-02-08T14:07:05.588484Z" - }, - { - "id": 890, - "upstreamContextTitle": "CS problems 3", - "upstreamVersion": 10, - "readyToSync": true, - "upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3", - "upstreamContextKey": "lib:OpenedX:CSPROB3", - "downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@210e356cfa304b0aac591af53f6a6ae0", - "downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", - "versionSynced": 2, - "versionDeclined": null, - "created": "2025-02-08T14:07:05.588484Z", - "updated": "2025-02-08T14:07:05.588484Z" - } - ] -} +[ + { + "id": 875, + "upstreamContextTitle": "CS problems 3", + "upstreamVersion": 10, + "readyToSync": true, + "upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3", + "upstreamContextKey": "lib:OpenedX:CSPROB3", + "downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem3", + "downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", + "versionSynced": 2, + "versionDeclined": null, + "created": "2025-02-08T14:07:05.588484Z", + "updated": "2025-02-08T14:07:05.588484Z" + }, + { + "id": 876, + "upstreamContextTitle": "CS problems 3", + "upstreamVersion": 10, + "readyToSync": true, + "upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3", + "upstreamContextKey": "lib:OpenedX:CSPROB3", + "downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@problem6", + "downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", + "versionSynced": 2, + "versionDeclined": null, + "created": "2025-02-08T14:07:05.588484Z", + "updated": "2025-02-08T14:07:05.588484Z" + }, + { + "id": 884, + "upstreamContextTitle": "CS problems 3", + "upstreamVersion": 26, + "readyToSync": true, + "upstreamUsageKey": "lb:OpenedX:CSPROB3:html:ca4d2b1f-0b64-4a2d-88fa-592f7e398477", + "upstreamContextKey": "lib:OpenedX:CSPROB3", + "downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@html+block@257e68e3386d4a8f8739d45b67e76a9b", + "downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", + "versionSynced": 16, + "versionDeclined": null, + "created": "2025-02-08T14:07:05.588484Z", + "updated": "2025-02-08T14:07:05.588484Z" + }, + { + "id": 889, + "upstreamContextTitle": "CS problems 3", + "upstreamVersion": 10, + "readyToSync": true, + "upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3", + "upstreamContextKey": "lib:OpenedX:CSPROB3", + "downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@a4455860b03647219ff8b01cde49cf37", + "downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", + "versionSynced": 2, + "versionDeclined": null, + "created": "2025-02-08T14:07:05.588484Z", + "updated": "2025-02-08T14:07:05.588484Z" + }, + { + "id": 890, + "upstreamContextTitle": "CS problems 3", + "upstreamVersion": 10, + "readyToSync": true, + "upstreamUsageKey": "lb:OpenedX:CSPROB3:problem:d40264d5-80c4-4be8-bfb6-086391de1cd3", + "upstreamContextKey": "lib:OpenedX:CSPROB3", + "downstreamUsageKey": "block-v1:OpenEdx+DemoX+CourseX+type@problem+block@210e356cfa304b0aac591af53f6a6ae0", + "downstreamContextKey": "course-v1:OpenEdx+DemoX+CourseX", + "versionSynced": 2, + "versionDeclined": null, + "created": "2025-02-08T14:07:05.588484Z", + "updated": "2025-02-08T14:07:05.588484Z" + } +] diff --git a/src/course-libraries/data/api.mocks.ts b/src/course-libraries/data/api.mocks.ts index 1f4d0e5bac..3614a51517 100644 --- a/src/course-libraries/data/api.mocks.ts +++ b/src/course-libraries/data/api.mocks.ts @@ -28,27 +28,17 @@ export async function mockGetEntityLinks( case mockGetEntityLinks.courseKeyLoading: return new Promise(() => {}); case mockGetEntityLinks.courseKeyEmpty: - return Promise.resolve({ - next: null, - previous: null, - nextPageNum: null, - previousPageNum: null, - count: 0, - numPages: 0, - currentPage: 0, - results: [], - }); + return Promise.resolve([]); default: { - const { response } = mockGetEntityLinks; + let { response } = mockGetEntityLinks; if (readyToSync !== undefined) { - response.results = response.results.filter((o) => o.readyToSync === readyToSync); - response.count = response.results.length; + response = response.filter((o) => o.readyToSync === readyToSync); } return Promise.resolve(response); } } } -mockGetEntityLinks.courseKey = mockLinksResult.results[0].downstreamContextKey; +mockGetEntityLinks.courseKey = mockLinksResult[0].downstreamContextKey; mockGetEntityLinks.invalidCourseKey = 'course_key_error'; mockGetEntityLinks.courseKeyLoading = 'courseKeyLoading'; mockGetEntityLinks.courseKeyEmpty = 'courseKeyEmpty'; @@ -85,7 +75,7 @@ export async function mockGetEntityLinksSummaryByDownstreamContext( return Promise.resolve(mockGetEntityLinksSummaryByDownstreamContext.response); } } -mockGetEntityLinksSummaryByDownstreamContext.courseKey = mockLinksResult.results[0].downstreamContextKey; +mockGetEntityLinksSummaryByDownstreamContext.courseKey = mockLinksResult[0].downstreamContextKey; mockGetEntityLinksSummaryByDownstreamContext.invalidCourseKey = 'course_key_error'; mockGetEntityLinksSummaryByDownstreamContext.courseKeyLoading = 'courseKeySummaryLoading'; mockGetEntityLinksSummaryByDownstreamContext.courseKeyEmpty = 'courseKeyEmpty'; diff --git a/src/course-libraries/data/api.ts b/src/course-libraries/data/api.ts index 4dd04c9bd5..af8108c534 100644 --- a/src/course-libraries/data/api.ts +++ b/src/course-libraries/data/api.ts @@ -38,32 +38,13 @@ export interface PublishableEntityLinkSummary { upstreamContextTitle: string; readyToSyncCount: number; totalCount: number; + lastPublishedAt: string; } export const getEntityLinks = async ( downstreamContextKey?: string, readyToSync?: boolean, upstreamUsageKey?: string, - pageParam?: number, - pageSize?: number, -): Promise> => { - const { data } = await getAuthenticatedHttpClient() - .get(getEntityLinksByDownstreamContextUrl(), { - params: { - course_id: downstreamContextKey, - ready_to_sync: readyToSync, - upstream_usage_key: upstreamUsageKey, - page_size: pageSize, - page: pageParam, - }, - }); - return camelCaseObject(data); -}; - -export const getUnpaginatedEntityLinks = async ( - downstreamContextKey?: string, - readyToSync?: boolean, - upstreamUsageKey?: string, ): Promise => { const { data } = await getAuthenticatedHttpClient() .get(getEntityLinksByDownstreamContextUrl(), { diff --git a/src/course-libraries/data/apiHooks.test.tsx b/src/course-libraries/data/apiHooks.test.tsx index b46f87c3fa..f1063ce803 100644 --- a/src/course-libraries/data/apiHooks.test.tsx +++ b/src/course-libraries/data/apiHooks.test.tsx @@ -4,7 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import MockAdapter from 'axios-mock-adapter'; import { renderHook, waitFor } from '@testing-library/react'; import { getEntityLinksByDownstreamContextUrl } from './api'; -import { useEntityLinks, useUnpaginatedEntityLinks } from './apiHooks'; +import { useEntityLinks } from './apiHooks'; let axiosMock: MockAdapter; @@ -39,26 +39,11 @@ describe('course libraries api hooks', () => { axiosMock.reset(); }); - it('should return paginated links for course', async () => { - const courseId = 'course-v1:some+key'; - const url = getEntityLinksByDownstreamContextUrl(); - const expectedResult = { - next: null, results: [], previous: null, total: 0, - }; - axiosMock.onGet(url).reply(200, expectedResult); - const { result } = renderHook(() => useEntityLinks({ courseId }), { wrapper }); - await waitFor(() => { - expect(result.current.isLoading).toBeFalsy(); - }); - expect(result.current.data?.pages).toEqual([expectedResult]); - expect(axiosMock.history.get[0].url).toEqual(url); - }); - it('should return links for course', async () => { const courseId = 'course-v1:some+key'; const url = getEntityLinksByDownstreamContextUrl(); axiosMock.onGet(url).reply(200, []); - const { result } = renderHook(() => useUnpaginatedEntityLinks({ courseId }), { wrapper }); + const { result } = renderHook(() => useEntityLinks({ courseId }), { wrapper }); await waitFor(() => { expect(result.current.isLoading).toBeFalsy(); }); diff --git a/src/course-libraries/data/apiHooks.ts b/src/course-libraries/data/apiHooks.ts index 2d6b0c0444..093a63121e 100644 --- a/src/course-libraries/data/apiHooks.ts +++ b/src/course-libraries/data/apiHooks.ts @@ -1,8 +1,7 @@ import { - useInfiniteQuery, useQuery, } from '@tanstack/react-query'; -import { getEntityLinks, getEntityLinksSummaryByDownstreamContext, getUnpaginatedEntityLinks } from './api'; +import { getEntityLinks, getEntityLinksSummaryByDownstreamContext } from './api'; export const courseLibrariesQueryKeys = { all: ['courseLibraries'], @@ -29,39 +28,10 @@ export const courseLibrariesQueryKeys = { }; /** - * Hook to fetch publishable entity links by course key. + * Hook to fetch list of publishable entity links by course key. * (That is, get a list of the library components used in the given course.) */ export const useEntityLinks = ({ - courseId, readyToSync, upstreamUsageKey, pageSize, -}: { - courseId?: string, - readyToSync?: boolean, - upstreamUsageKey?: string, - pageSize?: number -}) => ( - useInfiniteQuery({ - queryKey: courseLibrariesQueryKeys.courseReadyToSyncLibraries({ - courseId, - readyToSync, - upstreamUsageKey, - }), - queryFn: ({ pageParam }) => getEntityLinks( - courseId, - readyToSync, - upstreamUsageKey, - pageParam, - pageSize, - ), - getNextPageParam: (lastPage) => lastPage.nextPageNum, - enabled: courseId !== undefined || upstreamUsageKey !== undefined || readyToSync !== undefined, - }) -); - -/** - * Hook to fetch unpaginated list of publishable entity links by course key. - */ -export const useUnpaginatedEntityLinks = ({ courseId, readyToSync, upstreamUsageKey, }: { courseId?: string, @@ -74,7 +44,7 @@ export const useUnpaginatedEntityLinks = ({ readyToSync, upstreamUsageKey, }), - queryFn: () => getUnpaginatedEntityLinks( + queryFn: () => getEntityLinks( courseId, readyToSync, upstreamUsageKey, diff --git a/src/course-libraries/messages.ts b/src/course-libraries/messages.ts index 803084f166..8dc7ab0980 100644 --- a/src/course-libraries/messages.ts +++ b/src/course-libraries/messages.ts @@ -116,11 +116,6 @@ const messages = defineMessages({ defaultMessage: 'Something went wrong! Could not fetch results.', description: 'Generic error message displayed when fetching link data fails.', }, - olderVersionPreviewAlert: { - id: 'course-authoring.course-libraries.reviw-tab.preview.old-version-alert', - defaultMessage: 'The old version preview is the previous library version', - description: 'Alert message stating that older version in preview is of library block', - }, }); export default messages; diff --git a/src/course-outline/CourseOutline.jsx b/src/course-outline/CourseOutline.jsx index e374388cde..2a5e33b09e 100644 --- a/src/course-outline/CourseOutline.jsx +++ b/src/course-outline/CourseOutline.jsx @@ -375,6 +375,7 @@ const CourseOutline = ({ courseId }) => { section, section.childInfo.children, )} + isSectionsExpanded={isSectionsExpanded} isSelfPaced={statusBarData.isSelfPaced} isCustomRelativeDatesActive={isCustomRelativeDatesActive} savingStatus={savingStatus} diff --git a/src/course-outline/CourseOutline.test.jsx b/src/course-outline/CourseOutline.test.jsx index 055f56cdb5..ed6b324000 100644 --- a/src/course-outline/CourseOutline.test.jsx +++ b/src/course-outline/CourseOutline.test.jsx @@ -11,7 +11,6 @@ import { cloneDeep } from 'lodash'; import { closestCorners } from '@dnd-kit/core'; import { useLocation } from 'react-router-dom'; -import userEvent from '@testing-library/user-event'; import { getCourseBestPracticesApiUrl, getCourseLaunchApiUrl, @@ -97,12 +96,6 @@ jest.mock('./data/api', () => ({ getTagsCount: () => jest.fn().mockResolvedValue({}), })); -jest.mock('../studio-home/hooks', () => ({ - useStudioHome: () => ({ - librariesV2Enabled: true, - }), -})); - // Mock ComponentPicker to call onComponentSelected on click jest.mock('../library-authoring/component-picker', () => ({ ComponentPicker: (props) => { @@ -160,7 +153,9 @@ describe('', () => { pathname: mockPathname, }); - store = initializeStore(); + store = initializeStore({ + studioHome: { studioHomeData: { librariesV2Enabled: true } }, + }); axiosMock = new MockAdapter(getAuthenticatedHttpClient()); axiosMock .onGet(getCourseOutlineIndexApiUrl(courseId)) @@ -179,6 +174,10 @@ describe('', () => { await executeThunk(fetchCourseOutlineIndexQuery(courseId), store.dispatch); }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it('render CourseOutline component correctly', async () => { const { getByText } = render(); @@ -289,13 +288,15 @@ describe('', () => { }); it('check that new section list is saved when dragged', async () => { - const { findAllByRole } = render(); - const courseBlockId = courseOutlineIndexMock.courseStructure.id; + const { findAllByRole, findByTestId } = render(); + const expandAllButton = await findByTestId('expand-collapse-all-button'); + fireEvent.click(expandAllButton); + const [section] = store.getState().courseOutline.sectionsList; const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' }); - const draggableButton = sectionsDraggers[6]; + const draggableButton = sectionsDraggers[1]; axiosMock - .onPut(getCourseBlockApiUrl(courseBlockId)) + .onPut(getCourseBlockApiUrl(section.id)) .reply(200, { dummy: 'value' }); const section1 = store.getState().courseOutline.sectionsList[0].id; @@ -314,13 +315,15 @@ describe('', () => { }); it('check section list is restored to original order when API call fails', async () => { - const { findAllByRole } = render(); - const courseBlockId = courseOutlineIndexMock.courseStructure.id; + const { findAllByRole, findByTestId } = render(); + const expandAllButton = await findByTestId('expand-collapse-all-button'); + fireEvent.click(expandAllButton); + const [section] = store.getState().courseOutline.sectionsList; const sectionsDraggers = await findAllByRole('button', { name: 'Drag to reorder' }); - const draggableButton = sectionsDraggers[6]; + const draggableButton = sectionsDraggers[1]; axiosMock - .onPut(getCourseBlockApiUrl(courseBlockId)) + .onPut(getCourseBlockApiUrl(section.id)) .reply(500); const section1 = store.getState().courseOutline.sectionsList[0].id; @@ -395,8 +398,6 @@ describe('', () => { const { findAllByTestId } = render(); const [sectionElement] = await findAllByTestId('section-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); - const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn'); - fireEvent.click(expandBtn); const units = await within(subsectionElement).findAllByTestId('unit-card'); expect(units.length).toBe(1); @@ -421,8 +422,6 @@ describe('', () => { render(); const [sectionElement] = await screen.findAllByTestId('section-card'); const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); - const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn'); - fireEvent.click(expandBtn); const units = await within(subsectionElement).findAllByTestId('unit-card'); expect(units.length).toBe(1); @@ -646,8 +645,6 @@ describe('', () => { await checkEditTitle(section, subsectionElement, subsection, 'New subsection name', 'subsection'); // check unit - const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn'); - fireEvent.click(expandBtn); const [unit] = subsection.childInfo.children; const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); await checkEditTitle(section, unitElement, unit, 'New unit name', 'unit'); @@ -660,8 +657,6 @@ describe('', () => { const [sectionElement] = await screen.findAllByTestId('section-card'); const [subsection] = section.childInfo.children; const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); - const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn'); - fireEvent.click(expandBtn); const [unit] = subsection.childInfo.children; const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); @@ -700,8 +695,6 @@ describe('', () => { const [sectionElement] = await findAllByTestId('section-card'); const [subsection] = section.childInfo.children; const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); - const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn'); - fireEvent.click(expandBtn); const [unit] = subsection.childInfo.children; const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); @@ -771,8 +764,6 @@ describe('', () => { const [sectionElement] = await findAllByTestId('section-card'); const [subsection] = section.childInfo.children; const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); - const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn'); - fireEvent.click(expandBtn); const [unit] = subsection.childInfo.children; const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); @@ -1481,8 +1472,6 @@ describe('', () => { const [firstSection] = await findAllByTestId('section-card'); const [firstSubsection] = await within(firstSection).findAllByTestId('subsection-card'); - const subsectionExpandButton = await within(firstSubsection).getByTestId('subsection-card-header__expanded-btn'); - fireEvent.click(subsectionExpandButton); const [firstUnit] = await within(firstSubsection).findAllByTestId('unit-card'); const unitDropdownButton = await within(firstUnit).findByTestId('unit-card-header__menu-button'); @@ -1842,8 +1831,6 @@ describe('', () => { const [, sectionElement] = await findAllByTestId('section-card'); const [, subsection] = section.childInfo.children; const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); - const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn'); - await act(async () => fireEvent.click(expandBtn)); const [, secondUnit] = subsection.childInfo.children; const [, unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); @@ -1883,8 +1870,6 @@ describe('', () => { const [, sectionElement] = await findAllByTestId('section-card'); const [firstSubsection, subsection] = section.childInfo.children; const [, subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); - const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn'); - await act(async () => fireEvent.click(expandBtn)); const [unit] = subsection.childInfo.children; const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); @@ -1920,8 +1905,6 @@ describe('', () => { const [subsection] = secondSection.childInfo.children; const firstSectionLastSubsection = firstSection.childInfo.children[firstSection.childInfo.children.length - 1]; const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); - const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn'); - await act(async () => fireEvent.click(expandBtn)); const [unit] = subsection.childInfo.children; const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); @@ -1966,8 +1949,6 @@ describe('', () => { const [, sectionElement] = await findAllByTestId('section-card'); const [firstSubsection, subsection] = section.childInfo.children; const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); - const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn'); - await act(async () => fireEvent.click(expandBtn)); const lastUnitIdx = firstSubsection.childInfo.children.length - 1; const unit = firstSubsection.childInfo.children[lastUnitIdx]; const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx]; @@ -2005,8 +1986,6 @@ describe('', () => { const secondSectionLastSubsection = secondSection.childInfo.children[lastSubIndex]; const thirdSectionFirstSubsection = thirdSection.childInfo.children[0]; const subsectionElement = (await within(sectionElement).findAllByTestId('subsection-card'))[lastSubIndex]; - const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn'); - await act(async () => fireEvent.click(expandBtn)); const lastUnitIdx = secondSectionLastSubsection.childInfo.children.length - 1; const unit = secondSectionLastSubsection.childInfo.children[lastUnitIdx]; const unitElement = (await within(subsectionElement).findAllByTestId('unit-card'))[lastUnitIdx]; @@ -2051,8 +2030,6 @@ describe('', () => { const sections = await findAllByTestId('section-card'); const [sectionElement] = sections; const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); - const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn'); - await act(async () => fireEvent.click(expandBtn)); // get first and only unit in the subsection const [firstUnit] = await within(subsectionElement).findAllByTestId('unit-card'); @@ -2072,8 +2049,6 @@ describe('', () => { const lastSection = sections[sections.length - 1]; // it has only one subsection const [lastSubsectionElement] = await within(lastSection).findAllByTestId('subsection-card'); - const lastExpandBtn = await within(lastSubsectionElement).findByTestId('subsection-card-header__expanded-btn'); - await act(async () => fireEvent.click(lastExpandBtn)); // get last and the only unit in the subsection const [lastUnit] = await within(lastSubsectionElement).findAllByTestId('unit-card'); @@ -2094,6 +2069,9 @@ describe('', () => { const { findAllByTestId } = render(); const [sectionElement] = await findAllByTestId('section-card'); + const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); + const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn'); + fireEvent.click(expandBtn); const [section] = store.getState().courseOutline.sectionsList; const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' }); const draggableButton = subsectionsDraggers[1]; @@ -2125,6 +2103,9 @@ describe('', () => { const { findAllByTestId } = render(); const [sectionElement] = await findAllByTestId('section-card'); + const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); + const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn'); + fireEvent.click(expandBtn); const [section] = store.getState().courseOutline.sectionsList; const subsectionsDraggers = within(sectionElement).getAllByRole('button', { name: 'Drag to reorder' }); const draggableButton = subsectionsDraggers[1]; @@ -2154,8 +2135,6 @@ describe('', () => { const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const section = store.getState().courseOutline.sectionsList[2]; const [subsection] = section.childInfo.children; - const expandBtn = within(subsectionElement).getByTestId('subsection-card-header__expanded-btn'); - fireEvent.click(expandBtn); const unitDraggers = await within(subsectionElement).findAllByRole('button', { name: 'Drag to reorder' }); const draggableButton = unitDraggers[1]; const sections = courseOutlineIndexMock.courseStructure.childInfo.children; @@ -2190,8 +2169,6 @@ describe('', () => { const [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); const section = store.getState().courseOutline.sectionsList[2]; const [subsection] = section.childInfo.children; - const expandBtn = within(subsectionElement).getByTestId('subsection-card-header__expanded-btn'); - fireEvent.click(expandBtn); const unitDraggers = await within(subsectionElement).findAllByRole('button', { name: 'Drag to reorder' }); const draggableButton = unitDraggers[1]; const sections = courseOutlineIndexMock.courseStructure.childInfo.children; @@ -2229,8 +2206,6 @@ describe('', () => { .onGet(getXBlockApiUrl(section.id)) .reply(200, courseSectionMock); let [subsectionElement] = await within(sectionElement).findAllByTestId('subsection-card'); - const expandBtn = await within(subsectionElement).findByTestId('subsection-card-header__expanded-btn'); - await userEvent.click(expandBtn); const [unit] = subsection.childInfo.children; const [unitElement] = await within(subsectionElement).findAllByTestId('unit-card'); diff --git a/src/course-outline/card-header/CardHeader.jsx b/src/course-outline/card-header/CardHeader.jsx index d4a0af75d7..46ac9015d6 100644 --- a/src/course-outline/card-header/CardHeader.jsx +++ b/src/course-outline/card-header/CardHeader.jsx @@ -120,7 +120,7 @@ const CardHeader = ({ value={titleValue} name="displayName" onChange={(e) => setTitleValue(e.target.value)} - aria-label="edit field" + aria-label={intl.formatMessage(messages.editFieldAriaLabel)} onBlur={() => onEditSubmit(titleValue)} onKeyDown={(e) => { if (e.key === 'Enter') { diff --git a/src/course-outline/card-header/CardHeader.scss b/src/course-outline/card-header/CardHeader.scss index 8753847831..26a32bbe92 100644 --- a/src/course-outline/card-header/CardHeader.scss +++ b/src/course-outline/card-header/CardHeader.scss @@ -9,7 +9,7 @@ height: 1.5rem; margin-right: .25rem; background: transparent; - color: $black; + color: var(--pgn-color-black); } .item-card-button-icon { diff --git a/src/course-outline/card-header/TitleButton.jsx b/src/course-outline/card-header/TitleButton.jsx index 82d4d83e99..3413b74642 100644 --- a/src/course-outline/card-header/TitleButton.jsx +++ b/src/course-outline/card-header/TitleButton.jsx @@ -39,7 +39,7 @@ const TitleButton = ({ className="item-card-header__title-btn" onClick={onTitleClick} > - {title} + {title} ); diff --git a/src/course-outline/card-header/TitleLink.jsx b/src/course-outline/card-header/TitleLink.jsx index 32e68851f2..914f1d50ee 100644 --- a/src/course-outline/card-header/TitleLink.jsx +++ b/src/course-outline/card-header/TitleLink.jsx @@ -14,7 +14,7 @@ const TitleLink = ({ className="item-card-header__title-btn" to={titleLink} > - {title} + {title} ); diff --git a/src/course-outline/card-header/messages.js b/src/course-outline/card-header/messages.js index 4874525d5b..9114b77a83 100644 --- a/src/course-outline/card-header/messages.js +++ b/src/course-outline/card-header/messages.js @@ -1,6 +1,10 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ + editFieldAriaLabel: { + id: 'course-authoring.course-outline.card.edit-field.aria-label', + defaultMessage: 'Edit field', + }, expandTooltip: { id: 'course-authoring.course-outline.card.expandTooltip', defaultMessage: 'Collapse/Expand this card', diff --git a/src/course-outline/empty-placeholder/EmptyPlaceholder.scss b/src/course-outline/empty-placeholder/EmptyPlaceholder.scss index cf7c54ca41..2d8635751c 100644 --- a/src/course-outline/empty-placeholder/EmptyPlaceholder.scss +++ b/src/course-outline/empty-placeholder/EmptyPlaceholder.scss @@ -3,8 +3,8 @@ align-items: center; justify-content: center; gap: 1.25rem; - border: .0625rem solid $gray-200; + border: .0625rem solid var(--pgn-color-gray-200); border-radius: .375rem; - box-shadow: inset inset 0 1px .125rem 1px $gray-200; + box-shadow: inset inset 0 1px .125rem 1px var(--pgn-color-gray-200); padding: 2.5rem; } diff --git a/src/course-outline/header-navigations/HeaderNavigations.jsx b/src/course-outline/header-navigations/HeaderNavigations.jsx index a6d71bd5f0..9f7ee50e77 100644 --- a/src/course-outline/header-navigations/HeaderNavigations.jsx +++ b/src/course-outline/header-navigations/HeaderNavigations.jsx @@ -66,6 +66,8 @@ const HeaderNavigations = ({ {hasSections && ( } > {/* Content selection */} {videos && (videos.map( img => ( -
+
{img.externalUrl}
), ))} @@ -62,7 +59,7 @@ SelectVideoModal.propTypes = { fetchVideos: PropTypes.func.isRequired, }; -export const mapStateToProps = () => ({}); +export const mapStateToProps = null; export const mapDispatchToProps = { fetchVideos: thunkActions.app.fetchVideos, }; diff --git a/src/editors/containers/VideoEditor/components/SelectVideoModal.test.jsx b/src/editors/containers/VideoEditor/components/SelectVideoModal.test.jsx new file mode 100644 index 0000000000..6901b878a9 --- /dev/null +++ b/src/editors/containers/VideoEditor/components/SelectVideoModal.test.jsx @@ -0,0 +1,131 @@ +import React from 'react'; +import { + render, screen, fireEvent, waitFor, +} from '@testing-library/react'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { Provider } from 'react-redux'; +import configureStore from 'redux-mock-store'; +import { SelectVideoModal, useVideoList, useOnSelectClick } from './SelectVideoModal'; +import messages from './messages'; + +describe('SelectVideoModal', () => { + const mockStore = configureStore([]); + const mockFetchVideos = jest.fn(); + const mockSetSelection = jest.fn(); + const mockClose = jest.fn(); + let store; + + beforeEach(() => { + store = mockStore({}); + jest.clearAllMocks(); + }); + + it('renders the modal with the correct title', () => { + render( + + + + + , + ); + + expect(screen.getByText(messages.selectVideoModalTitle.defaultMessage)).toBeInTheDocument(); + }); + + it('calls close when the modal is closed', () => { + render( + + + + + , + ); + + fireEvent.click(screen.getByRole('button', { name: /close/i })); + expect(mockClose).toHaveBeenCalled(); + }); + + it('renders a div for each video in the videos array', () => { + const videos = [ + { externalUrl: 'video1.mp4' }, + { externalUrl: 'video2.mp4' }, + ]; + const fetchVideos = jest.fn(({ onSuccess }) => onSuccess(videos)); + + render( + + + + + , + ); + + // Assert that a div is rendered for each video + videos.forEach((video) => { + expect(screen.getByText(video.externalUrl)).toBeInTheDocument(); + }); + }); +}); + +describe('SelectVideoModal hooks', () => { + describe('useVideoList', () => { + it('fetches videos and sets them in state', async () => { + // eslint-disable-next-line react/prop-types + const TestComponent = ({ fetchVideos }) => { + const videos = useVideoList({ fetchVideos }); + return ( +
+ {videos ? videos.map((v) => v.externalUrl).join(', ') : 'Loading'} +
+ ); + }; + + const videos = [ + { externalUrl: 'video1.mp4' }, + { externalUrl: 'video2.mp4' }, + ]; + const fetchVideos = jest.fn(({ onSuccess }) => { + onSuccess(videos); + }); + + render(); + + await waitFor(() => expect(screen.getByTestId('videos').textContent).toBe(videos.map((v) => v.externalUrl).join(', '))); + + expect(fetchVideos).toHaveBeenCalled(); + }); + }); + + describe('useOnSelectClick', () => { + it('calls setSelection with the first video', () => { + const mockSetSelection = jest.fn(); + const videos = [ + { externalUrl: 'video1.mp4' }, + { externalUrl: 'video2.mp4' }, + ]; + + const onSelectClick = useOnSelectClick({ + setSelection: mockSetSelection, + videos, + }); + + onSelectClick(); + expect(mockSetSelection).toHaveBeenCalledWith(videos[0]); + }); + }); +}); diff --git a/src/editors/containers/VideoEditor/components/VideoEditorModal.tsx b/src/editors/containers/VideoEditor/components/VideoEditorModal.tsx index 66f95b0ab5..55d36c3556 100644 --- a/src/editors/containers/VideoEditor/components/VideoEditorModal.tsx +++ b/src/editors/containers/VideoEditor/components/VideoEditorModal.tsx @@ -7,7 +7,9 @@ import VideoSettingsModal from './VideoSettingsModal'; import { RequestKeys } from '../../../data/constants/requests'; interface Props { + onReturn?: (() => void); isLibrary: boolean; + onClose?: (() => void) | null; } export const { @@ -27,13 +29,15 @@ export const hooks = { const VideoEditorModal: React.FC = ({ isLibrary, + onClose, + onReturn, }) => { const dispatch = useDispatch(); const location = useLocation(); const searchParams = new URLSearchParams(location.search); const selectedVideoId = searchParams.get('selectedVideoId'); const selectedVideoUrl = searchParams.get('selectedVideoUrl'); - const onReturn = hooks.useReturnToGallery(); + const onSettingsReturn = onReturn || hooks.useReturnToGallery(); const isLoaded = useSelector( (state) => selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchVideos }), ); @@ -44,8 +48,9 @@ const VideoEditorModal: React.FC = ({ return ( ); diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDetails.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDetails.jsx index 375c287b02..95127bfd0d 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDetails.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/LicenseDetails.jsx @@ -30,17 +30,17 @@ const LicenseDetails = ({ // redux updateField, }) => ( - level !== LicenseLevel.course && details && license !== 'select' ? ( + level !== LicenseLevel.course && details && license !== 'select' && (
- {license === LicenseTypes.allRightsReserved ? ( + {license === LicenseTypes.allRightsReserved && (
- ) : null} + )} {license === LicenseTypes.creativeCommons ? ( @@ -55,7 +55,6 @@ const LicenseDetails = ({
@@ -80,7 +79,6 @@ const LicenseDetails = ({ noncommercial: e.target.checked, }, })} - aria-label="Checkbox" /> @@ -106,7 +104,6 @@ const LicenseDetails = ({ shareAlike: e.target.checked ? false : details.shareAlike, }, })} - aria-label="Checkbox" /> @@ -132,7 +129,6 @@ const LicenseDetails = ({ noDerivatives: e.target.checked ? false : details.noDerivatives, }, })} - aria-label="Checkbox" /> @@ -144,7 +140,7 @@ const LicenseDetails = ({ ) : null}
- ) : null + ) ); LicenseDetails.propTypes = { diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/LicenseDetails.test.jsx.snap b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/LicenseDetails.test.jsx.snap index 046cc017d3..9e608e8c68 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/LicenseDetails.test.jsx.snap +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/LicenseWidget/__snapshots__/LicenseDetails.test.jsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`LicenseDetails snapshots snapshots: renders as expected with default props 1`] = `null`; +exports[`LicenseDetails snapshots snapshots: renders as expected with default props 1`] = `false`; exports[`LicenseDetails snapshots snapshots: renders as expected with level set to block and license set to Creative Commons 1`] = `
@@ -71,7 +70,6 @@ exports[`LicenseDetails snapshots snapshots: renders as expected with level set @@ -102,7 +100,6 @@ exports[`LicenseDetails snapshots snapshots: renders as expected with level set @@ -131,7 +128,6 @@ exports[`LicenseDetails snapshots snapshots: renders as expected with level set @@ -179,7 +175,7 @@ exports[`LicenseDetails snapshots snapshots: renders as expected with level set
`; -exports[`LicenseDetails snapshots snapshots: renders as expected with level set to block and license set to select 1`] = `null`; +exports[`LicenseDetails snapshots snapshots: renders as expected with level set to block and license set to select 1`] = `false`; exports[`LicenseDetails snapshots snapshots: renders as expected with level set to library 1`] = `
void; isLibrary: boolean; + onClose?: (() => void) | null; } const VideoSettingsModal: React.FC = ({ onReturn, isLibrary, + onClose, }) => ( <> {!isLibrary && ( @@ -32,7 +34,7 @@ const VideoSettingsModal: React.FC = ({ variant="link" className="text-primary-500" size="sm" - onClick={onReturn} + onClick={onClose || onReturn} style={{ textDecoration: 'none', marginLeft: '3px', diff --git a/src/editors/containers/VideoEditor/components/messages.ts b/src/editors/containers/VideoEditor/components/messages.ts new file mode 100644 index 0000000000..9777ecbf8d --- /dev/null +++ b/src/editors/containers/VideoEditor/components/messages.ts @@ -0,0 +1,11 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + selectVideoModalTitle: { + id: 'authoring.videoEditor.selectVideoModal.title', + defaultMessage: 'Add a video', + description: 'Title for the select video modal.', + }, +}); + +export default messages; diff --git a/src/editors/containers/VideoEditor/index.tsx b/src/editors/containers/VideoEditor/index.tsx index 0b3018812d..b9d291022c 100644 --- a/src/editors/containers/VideoEditor/index.tsx +++ b/src/editors/containers/VideoEditor/index.tsx @@ -39,7 +39,7 @@ const VideoEditor: React.FC = ({ > {(isCreateWorkflow || studioViewFinished) ? (
- +
) : (
{ const [highlighted, setHighlighted] = React.useState(null); const [ @@ -128,7 +129,10 @@ export const useVideoListProps = ({ }, selectBtnProps: { onClick: () => { - if (highlighted) { + /* istanbul ignore next */ + if (returnFunction) { + returnFunction()(); + } else if (highlighted) { navigateTo(`/course/${learningContextId}/editor/video/${blockId}?selectedVideoId=${highlighted}`); } else { setShowSelectVideoError(true); @@ -138,10 +142,15 @@ export const useVideoListProps = ({ }; }; -export const useVideoUploadHandler = ({ replace }) => { +export const useVideoUploadHandler = ({ replace, uploadHandler }) => { const learningContextId = useSelector(selectors.app.learningContextId); const blockId = useSelector(selectors.app.blockId); const path = `/course/${learningContextId}/editor/video_upload/${blockId}`; + if (uploadHandler) { + return () => { + uploadHandler(); + }; + } if (replace) { return () => window.location.replace(path); } @@ -191,11 +200,12 @@ export const getstatusBadgeVariant = ({ status }) => { export const getStatusMessage = ({ status }) => Object.values(filterMessages).find((m) => m.defaultMessage === status); -export const useVideoProps = ({ videos }) => { +export const useVideoProps = ({ videos, uploadHandler, returnFunction }) => { const searchSortProps = useSearchAndSortProps(); const videoList = useVideoListProps({ searchSortProps, videos, + returnFunction, }); const { galleryError, @@ -203,7 +213,7 @@ export const useVideoProps = ({ videos }) => { inputError, selectBtnProps, } = videoList; - const fileInput = { click: useVideoUploadHandler({ replace: false }) }; + const fileInput = { click: useVideoUploadHandler({ replace: false, uploadHandler }) }; return { galleryError, diff --git a/src/editors/containers/VideoGallery/index.jsx b/src/editors/containers/VideoGallery/index.jsx index 5f5a8a8ca8..ac25c85268 100644 --- a/src/editors/containers/VideoGallery/index.jsx +++ b/src/editors/containers/VideoGallery/index.jsx @@ -1,5 +1,10 @@ -import React, { useEffect } from 'react'; -import { Image } from '@openedx/paragon'; +import React, { useCallback, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { + Image, useToggle, StandardModal, +} from '@openedx/paragon'; +import { useSearchParams } from 'react-router-dom'; import { useSelector } from 'react-redux'; import { selectors } from '../../data/redux'; import * as hooks from './hooks'; @@ -8,8 +13,11 @@ import { acceptedImgKeys } from './utils'; import messages from './messages'; import { RequestKeys } from '../../data/constants/requests'; import videoThumbnail from '../../data/images/videoThumbnail.svg'; +import VideoUploadEditor from '../VideoUploadEditor'; +import VideoEditor from '../VideoEditor'; -const VideoGallery = () => { +const VideoGallery = ({ returnFunction, onCancel }) => { + const intl = useIntl(); const rawVideos = useSelector(selectors.app.videos); const isLoaded = useSelector( (state) => selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchVideos }), @@ -21,14 +29,27 @@ const VideoGallery = () => { (state) => selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadVideo }), ); const videos = hooks.buildVideos({ rawVideos }); - const handleVideoUpload = hooks.useVideoUploadHandler({ replace: true }); + const [isVideoUploadModalOpen, showVideoUploadModal, closeVideoUploadModal] = useToggle(); + const [isVideoEditorModalOpen, showVideoEditorModal, closeVideoEditorModal] = useToggle(); + const setSearchParams = useSearchParams()[1]; useEffect(() => { - // If no videos exists redirects to the video upload screen + // If no videos exists opens to the video upload modal if (isLoaded && videos.length === 0) { - handleVideoUpload(); + showVideoUploadModal(); } }, [isLoaded]); + + const onVideoUpload = useCallback((videoUrl) => { + closeVideoUploadModal(); + showVideoEditorModal(); + setSearchParams({ selectedVideoUrl: videoUrl }); + }, [closeVideoUploadModal, showVideoEditorModal, setSearchParams]); + + const uploadHandler = useCallback(() => { + showVideoUploadModal(); + }); + const { galleryError, inputError, @@ -36,7 +57,7 @@ const VideoGallery = () => { galleryProps, searchSortProps, selectBtnProps, - } = hooks.useVideoProps({ videos }); + } = hooks.useVideoProps({ videos, uploadHandler, returnFunction }); const handleCancel = hooks.useCancelHandler(); const modalMessages = { @@ -60,8 +81,8 @@ const VideoGallery = () => { { isFetchError, }} /> + +
+ +
+
+ {isVideoEditorModalOpen && ( + + )}
); }; -VideoGallery.propTypes = {}; +VideoGallery.propTypes = { + onCancel: PropTypes.func, + returnFunction: PropTypes.func, +}; export default VideoGallery; diff --git a/src/editors/containers/VideoGallery/index.test.jsx b/src/editors/containers/VideoGallery/index.test.jsx index 1cffffea82..6e45a02d27 100644 --- a/src/editors/containers/VideoGallery/index.test.jsx +++ b/src/editors/containers/VideoGallery/index.test.jsx @@ -6,6 +6,8 @@ import React from 'react'; import { act, fireEvent, render, screen, } from '@testing-library/react'; +import * as reactRouterDom from 'react-router-dom'; +import * as reduxThunks from '../../data/redux'; import VideoGallery from './index'; @@ -120,11 +122,10 @@ describe('VideoGallery', () => { expect(screen.getByText(video.client_video_id)).toBeInTheDocument() )); }); - it('navigates to video upload page when there are no videos', async () => { - expect(window.location.replace).not.toHaveBeenCalled(); + it('renders video upload modal when there are no videos', async () => { updateState({ videos: [] }); await renderComponent(); - expect(window.location.replace).toHaveBeenCalled(); + expect(screen.getByRole('heading', { name: /upload or embed a new video/i })).toBeInTheDocument(); }); it.each([ [/newest/i, [2, 1, 3]], @@ -191,5 +192,36 @@ describe('VideoGallery', () => { expect(screen.queryByText('client_id_1')).not.toBeInTheDocument(); expect(screen.queryByText('client_id_3')).not.toBeInTheDocument(); }); + + it('calls onVideoUpload correctly when a video is uploaded', async () => { + // Mock useSearchParams + const setSearchParams = jest.fn(); + jest.spyOn(reactRouterDom, 'useSearchParams').mockReturnValue([{}, setSearchParams]); + + // Mock the uploadVideo thunk to immediately call postUploadRedirect + jest.spyOn(reduxThunks.thunkActions.video, 'uploadVideo').mockImplementation( + ({ postUploadRedirect }) => () => { + if (postUploadRedirect) { + postUploadRedirect('http://test.video/url.mp4'); + } + return { type: 'MOCK_UPLOAD_VIDEO' }; + }, + ); + + await renderComponent(); + + // Open the upload modal by clicking the button + const openModalButton = screen.getByRole('button', { name: /upload or embed a new video/i }); + fireEvent.click(openModalButton); + + // Wait for the input to appear in the modal + const urlInput = await screen.findByPlaceholderText('Paste your video ID or URL'); + fireEvent.change(urlInput, { target: { value: 'http://test.video/url.mp4' } }); + + const submitButton = screen.getByRole('button', { name: /submit/i }); + fireEvent.click(submitButton); + + expect(setSearchParams).toHaveBeenCalledWith({ selectedVideoUrl: 'http://test.video/url.mp4' }); + }); }); }); diff --git a/src/editors/containers/VideoGallery/messages.js b/src/editors/containers/VideoGallery/messages.js index e26dd63db3..3dd446b7c9 100644 --- a/src/editors/containers/VideoGallery/messages.js +++ b/src/editors/containers/VideoGallery/messages.js @@ -21,7 +21,16 @@ const messages = { defaultMessage: 'Upload or embed a new video', description: 'Label for upload button', }, - + videoUploadModalTitle: { + id: 'authoring.selectvideomodal.upload.title', + defaultMessage: 'Upload or embed a new video', + description: 'Label for upload modal', + }, + videoEditorModalTitle: { + id: 'authoring.selectvideomodal.edit.title', + defaultMessage: 'Edit selected video', + description: 'Label for editor modal', + }, // Sort Dropdown sortByDateNewest: { id: 'authoring.selectvideomodal.sort.datenewest.label', diff --git a/src/editors/containers/VideoUploadEditor/VideoUploader.jsx b/src/editors/containers/VideoUploadEditor/VideoUploader.jsx index 028d1c085a..09d943db83 100644 --- a/src/editors/containers/VideoUploadEditor/VideoUploader.jsx +++ b/src/editors/containers/VideoUploadEditor/VideoUploader.jsx @@ -10,9 +10,9 @@ import { thunkActions } from '../../data/redux'; import * as hooks from './hooks'; import messages from './messages'; -const URLUploader = () => { +const URLUploader = ({ onUpload }) => { const [textInputValue, setTextInputValue] = React.useState(''); - const onURLUpload = hooks.onVideoUpload('selectedVideoUrl'); + const onURLUpload = hooks.onVideoUpload('selectedVideoUrl', onUpload); const intl = useIntl(); return (
@@ -58,16 +58,16 @@ const URLUploader = () => { ); }; -export const VideoUploader = ({ setLoading }) => { +export const VideoUploader = ({ setLoading, onUpload, onClose }) => { const dispatch = useDispatch(); const intl = useIntl(); - const goBack = hooks.useHistoryGoBack(); + const goBack = onClose || hooks.useHistoryGoBack(); const handleProcessUpload = ({ fileData }) => { dispatch(thunkActions.video.uploadVideo({ supportedFiles: [fileData], setLoadSpinner: setLoading, - postUploadRedirect: hooks.onVideoUpload('selectedVideoId'), + postUploadRedirect: hooks.onVideoUpload('selectedVideoId', onUpload), })); }; @@ -85,14 +85,20 @@ export const VideoUploader = ({ setLoading }) => { } + inputComponent={} />
); }; +URLUploader.propTypes = { + onUpload: PropTypes.func, +}; + VideoUploader.propTypes = { setLoading: PropTypes.func.isRequired, + onUpload: PropTypes.func, + onClose: PropTypes.func, }; export default VideoUploader; diff --git a/src/editors/containers/VideoUploadEditor/hooks.js b/src/editors/containers/VideoUploadEditor/hooks.js index a2774d9c60..3cc1f8468e 100644 --- a/src/editors/containers/VideoUploadEditor/hooks.js +++ b/src/editors/containers/VideoUploadEditor/hooks.js @@ -11,15 +11,20 @@ export const { navigateTo, } = appHooks; -export const postUploadRedirect = (storeState, uploadType = 'selectedVideoUrl') => { +export const postUploadRedirect = (storeState, uploadType = 'selectedVideoUrl', onUpload = null) => { const learningContextId = selectors.app.learningContextId(storeState); const blockId = selectors.app.blockId(storeState); + if (onUpload) { + return (videoUrl) => { + onUpload(videoUrl, learningContextId, blockId); + }; + } return (videoUrl) => navigateTo(`/course/${learningContextId}/editor/video/${blockId}?${uploadType}=${videoUrl}`); }; -export const onVideoUpload = (uploadType) => { +export const onVideoUpload = (uploadType, onUpload) => { const storeState = store.getState(); - return module.postUploadRedirect(storeState, uploadType); + return module.postUploadRedirect(storeState, uploadType, onUpload); }; export const useUploadVideo = async ({ diff --git a/src/editors/containers/VideoUploadEditor/index.jsx b/src/editors/containers/VideoUploadEditor/index.jsx index ae2be7b5fb..91664d3e0a 100644 --- a/src/editors/containers/VideoUploadEditor/index.jsx +++ b/src/editors/containers/VideoUploadEditor/index.jsx @@ -1,17 +1,18 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Spinner } from '@openedx/paragon'; import './index.scss'; import messages from './messages'; import { VideoUploader } from './VideoUploader'; -const VideoUploadEditor = () => { +const VideoUploadEditor = ({ onUpload, onClose }) => { const [loading, setLoading] = React.useState(false); const intl = useIntl(); return (!loading) ? (
- +
) : (
{ ); }; +VideoUploadEditor.propTypes = { + onUpload: PropTypes.func, + onClose: PropTypes.func, +}; + export default VideoUploadEditor; diff --git a/src/editors/data/redux/thunkActions/app.js b/src/editors/data/redux/thunkActions/app.js index 8da9f24b71..4c5079a5a9 100644 --- a/src/editors/data/redux/thunkActions/app.js +++ b/src/editors/data/redux/thunkActions/app.js @@ -125,6 +125,16 @@ export const saveBlock = (content, returnToUnit) => (dispatch) => { content, onSuccess: (response) => { dispatch(actions.app.setSaveResponse(response)); + const parsedData = JSON.parse(response.config.data); + if (parsedData?.has_changes) { + const storageKey = 'courseRefreshTriggerOnComponentEditSave'; + localStorage.setItem(storageKey, Date.now()); + + window.dispatchEvent(new StorageEvent('storage', { + key: storageKey, + newValue: Date.now().toString(), + })); + } returnToUnit(response.data); }, })); diff --git a/src/editors/data/redux/thunkActions/app.test.js b/src/editors/data/redux/thunkActions/app.test.js index 35debc7f3c..3f8dc10c9e 100644 --- a/src/editors/data/redux/thunkActions/app.test.js +++ b/src/editors/data/redux/thunkActions/app.test.js @@ -352,7 +352,11 @@ describe('app thunkActions', () => { }); it('dispatches actions.app.setSaveResponse with response and then calls returnToUnit', () => { dispatch.mockClear(); - const response = 'testRESPONSE'; + const mockParsedData = { has_changes: true }; + const response = { + config: { data: JSON.stringify(mockParsedData) }, + data: {}, + }; calls[1][0].saveBlock.onSuccess(response); expect(dispatch).toHaveBeenCalledWith(actions.app.setSaveResponse(response)); expect(returnToUnit).toHaveBeenCalled(); diff --git a/src/editors/data/redux/thunkActions/problem.test.ts b/src/editors/data/redux/thunkActions/problem.test.ts index 17f7f85b0a..3c7edbe52c 100644 --- a/src/editors/data/redux/thunkActions/problem.test.ts +++ b/src/editors/data/redux/thunkActions/problem.test.ts @@ -10,7 +10,6 @@ import { } from './problem'; import { checkboxesOLXWithFeedbackAndHintsOLX, advancedProblemOlX, blankProblemOLX } from '../../../containers/ProblemEditor/data/mockData/olxTestData'; import { ProblemTypeKeys } from '../../constants/problem'; -import * as requests from './requests'; const mockOlx = 'SOmEVALue'; const mockBuildOlx = jest.fn(() => mockOlx); @@ -72,22 +71,13 @@ describe('problem thunkActions', () => { ); }); test('switchToMarkdownEditor dispatches correct actions', () => { - switchToMarkdownEditor()(dispatch, getState); + switchToMarkdownEditor()(dispatch); expect(dispatch).toHaveBeenCalledWith( actions.problem.updateField({ isMarkdownEditorEnabled: true, }), ); - - expect(dispatch).toHaveBeenCalledWith( - requests.saveBlock({ - content: { - settings: { markdown_edited: true }, - olx: blockValue.data.data, - }, - }), - ); }); describe('switchEditor', () => { @@ -110,7 +100,7 @@ describe('problem thunkActions', () => { test('dispatches switchToMarkdownEditor when editorType is markdown', () => { switchEditor('markdown')(dispatch, getState); - expect(switchToMarkdownEditorMock).toHaveBeenCalledWith(dispatch, getState); + expect(switchToMarkdownEditorMock).toHaveBeenCalledWith(dispatch); }); }); diff --git a/src/editors/data/redux/thunkActions/problem.ts b/src/editors/data/redux/thunkActions/problem.ts index 28ba7b34fc..74876fdb1d 100644 --- a/src/editors/data/redux/thunkActions/problem.ts +++ b/src/editors/data/redux/thunkActions/problem.ts @@ -24,17 +24,17 @@ export const switchToAdvancedEditor = () => (dispatch, getState) => { dispatch(actions.problem.updateField({ problemType: ProblemTypeKeys.ADVANCED, rawOLX })); }; -export const switchToMarkdownEditor = () => (dispatch, getState) => { - const state = getState(); +export const switchToMarkdownEditor = () => (dispatch) => { dispatch(actions.problem.updateField({ isMarkdownEditorEnabled: true })); - const { blockValue } = state.app; - const olx = get(blockValue, 'data.data', ''); - const content = { settings: { markdown_edited: true }, olx }; - // Sending a request to save the problem block with the updated markdown_edited value - dispatch(requests.saveBlock({ content })); }; -export const switchEditor = (editorType) => (dispatch, getState) => (editorType === 'advanced' ? switchToAdvancedEditor : switchToMarkdownEditor)()(dispatch, getState); +export const switchEditor = (editorType) => (dispatch, getState) => { + if (editorType === 'advanced') { + switchToAdvancedEditor()(dispatch, getState); + } else { + switchToMarkdownEditor()(dispatch); + } +}; export const isBlankProblem = ({ rawOLX }) => { if (['', ''].includes(rawOLX.replace(/\s/g, ''))) { diff --git a/src/editors/setupEditorTest.js b/src/editors/setupEditorTest.js index 88129bd9a9..4ccd927f87 100644 --- a/src/editors/setupEditorTest.js +++ b/src/editors/setupEditorTest.js @@ -101,7 +101,9 @@ jest.mock('@openedx/paragon', () => jest.requireActual('./testUtils').mockNested Spinner: 'Spinner', Stack: 'Stack', Toast: 'Toast', - Truncate: 'Truncate', + Truncate: { + Deprecated: 'Truncate.Deprecated', + }, useWindowSize: { height: '500px' }, })); diff --git a/src/editors/sharedComponents/CodeEditor/hooks.js b/src/editors/sharedComponents/CodeEditor/hooks.js index 0eb25a7cc0..a15e64bf15 100644 --- a/src/editors/sharedComponents/CodeEditor/hooks.js +++ b/src/editors/sharedComponents/CodeEditor/hooks.js @@ -100,7 +100,7 @@ export const createCodeMirrorDomNode = ({ }) => { // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { - const languageExtension = CODEMIRROR_LANGUAGES[lang](); + const languageExtension = CODEMIRROR_LANGUAGES[lang] ? CODEMIRROR_LANGUAGES[lang]() : xml(); const cleanText = cleanHTML({ initialText }); const newState = EditorState.create({ doc: cleanText, diff --git a/src/editors/sharedComponents/CodeEditor/index.scss b/src/editors/sharedComponents/CodeEditor/index.scss index e24ef75397..a179844788 100644 --- a/src/editors/sharedComponents/CodeEditor/index.scss +++ b/src/editors/sharedComponents/CodeEditor/index.scss @@ -1,2 +1,2 @@ .cm-editor { height: 100% } -.cm-scroller { overflow: auto } \ No newline at end of file +.cm-scroller { overflow: auto } diff --git a/src/editors/sharedComponents/SelectableBox/index.scss b/src/editors/sharedComponents/SelectableBox/index.scss index 1038286af2..35d24dfd7a 100644 --- a/src/editors/sharedComponents/SelectableBox/index.scss +++ b/src/editors/sharedComponents/SelectableBox/index.scss @@ -3,7 +3,7 @@ .pgn__selectable_box-set { display: grid; grid-auto-rows: 1fr; - grid-gap: $selectable-box-space; + grid-gap: var(--pgn-spacing-selectable-box-box-space); @for $i from $min-cols-number through $max-cols-number { &.pgn__selectable_box-set--#{$i} { @@ -19,21 +19,21 @@ .pgn__selectable_box { position: relative; height: 100%; - padding: $selectable-box-padding; - box-shadow: $level-1-box-shadow; - border-radius: $selectable-box-border-radius; + padding: var(--pgn-spacing-selectable-box-padding); + box-shadow: var(--pgn-elevation-box-shadow-level-1); + border-radius: var(--pgn-spacing-selectable-box-border-radius); text-align: start; - background: $white; + background: var(--pgn-color-white); &:focus-visible { - outline: 1px solid $primary-700; + outline: 1px solid var(--pgn-color-primary-700); } .pgn__form-radio, .pgn__form-checkbox { position: absolute; - top: $selectable-box-padding; - inset-inline-end: $selectable-box-padding; + top: var(--pgn-spacing-selectable-box-padding); + inset-inline-end: var(--pgn-spacing-selectable-box-padding); input { margin-inline-end: 0; @@ -46,9 +46,9 @@ } .pgn__selectable_box-active { - outline: 2px solid $primary-500; + outline: 2px solid var(--pgn-color-primary-500); } .pgn__selectable_box-invalid { - outline: 2px solid $danger-300; + outline: 2px solid var(--pgn-color-danger-300); } diff --git a/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx b/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx index 4ffcb63af2..a9f6f5a6e6 100644 --- a/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx +++ b/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx @@ -66,7 +66,7 @@ const GalleryCard = ({

- {asset.displayName} + {asset.displayName}

{ asset.transcripts && (
diff --git a/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap b/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap index f14a235f3e..96d7b7b40e 100644 --- a/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap +++ b/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap @@ -59,9 +59,9 @@ exports[`GalleryCard component snapshot with duration badge 1`] = `

- + props.img.displayName - +

- + props.img.displayName - +

- + props.img.displayName - +

- + props.img.displayName - +

- + props.img.displayName - +

- + props.img.displayName - +

diff --git a/src/editors/sharedComponents/SourceCodeModal/index.jsx b/src/editors/sharedComponents/SourceCodeModal/index.jsx index 88f7ff9f8e..9282a180c4 100644 --- a/src/editors/sharedComponents/SourceCodeModal/index.jsx +++ b/src/editors/sharedComponents/SourceCodeModal/index.jsx @@ -1,4 +1,3 @@ -import React from 'react'; import PropTypes from 'prop-types'; import { @@ -41,6 +40,7 @@ const SourceCodeModal = ({
diff --git a/src/editors/sharedComponents/TinyMceWidget/hooks.js b/src/editors/sharedComponents/TinyMceWidget/hooks.js index 5afbba011c..3730e809b5 100644 --- a/src/editors/sharedComponents/TinyMceWidget/hooks.js +++ b/src/editors/sharedComponents/TinyMceWidget/hooks.js @@ -304,6 +304,7 @@ export const editorConfig = ({ updateContent, content, minHeight, + maxHeight, learningContextId, staticRootUrl, enableImageUpload, @@ -335,6 +336,7 @@ export const editorConfig = ({ content_css: false, content_style: tinyMCEStyles + a11ycheckerCss, min_height: minHeight, + max_height: maxHeight, contextmenu: 'link table', directionality: isLocaleRtl ? 'rtl' : 'ltr', document_base_url: baseURL, diff --git a/src/files-and-videos/files-page/FileInfoModalSidebar.jsx b/src/files-and-videos/files-page/FileInfoModalSidebar.jsx index ce2db6c628..2b4246d4b8 100644 --- a/src/files-and-videos/files-page/FileInfoModalSidebar.jsx +++ b/src/files-and-videos/files-page/FileInfoModalSidebar.jsx @@ -57,9 +57,9 @@ const FileInfoModalSidebar = ({
- + {asset?.portableUrl} - +
ActiveColumn({ row, pageLoadStatus: loadingStatus }), Filter: CheckboxFilter, @@ -106,7 +107,7 @@ const FilesPage = ({ }; const accessColumn = { id: 'lockStatus', - Header: 'Access', + Header: intl.formatMessage(messages.fileAccessColumn), accessor: 'lockStatus', Cell: ({ row }) => AccessColumn({ row }), Filter: CheckboxFilter, @@ -122,7 +123,7 @@ const FilesPage = ({ }; const fileSizeColumn = { id: 'fileSize', - Header: 'File size', + Header: intl.formatMessage(messages.fileSizeColumn), accessor: 'fileSize', Cell: ({ row }) => { const { fileSize } = row.original; @@ -133,12 +134,12 @@ const FilesPage = ({ const tableColumns = [ { ...thumbnailColumn }, { - Header: 'File name', + Header: intl.formatMessage(messages.fileNameColumn), accessor: 'displayName', }, { ...fileSizeColumn }, { - Header: 'Type', + Header: intl.formatMessage(messages.fileTypeColumn), accessor: 'wrapperType', Filter: CheckboxFilter, filter: 'includesValue', diff --git a/src/files-and-videos/files-page/FilesPage.scss b/src/files-and-videos/files-page/FilesPage.scss new file mode 100644 index 0000000000..6b27f995ae --- /dev/null +++ b/src/files-and-videos/files-page/FilesPage.scss @@ -0,0 +1,5 @@ +.files-table { + .pgn__data-table-container { + overflow-x: visible; + } +} diff --git a/src/files-and-videos/files-page/FilesPage.test.jsx b/src/files-and-videos/files-page/FilesPage.test.jsx index 80bb8f1f8c..c554646567 100644 --- a/src/files-and-videos/files-page/FilesPage.test.jsx +++ b/src/files-and-videos/files-page/FilesPage.test.jsx @@ -70,15 +70,6 @@ const mockStore = async ( } renderComponent(); await executeThunk(fetchAssets(courseId), store.dispatch); - - // Finish loading the expected files into the data table before returning, - // because loading new files can disrupt things like accessing file menus. - if (status === RequestStatus.SUCCESSFUL) { - const numFiles = skipNextPageFetch ? 13 : 15; - await waitFor(() => { - expect(screen.getByText(`Showing ${numFiles} of ${numFiles}`)).toBeInTheDocument(); - }); - } }; const emptyMockStore = async (status) => { @@ -205,7 +196,7 @@ describe('FilesAndUploads', () => { await mockStore(RequestStatus.SUCCESSFUL); axiosMock.onGet(`${getAssetsUrl(courseId)}?display_name=download.png&page_size=1`).reply(200, { assets: [] }); axiosMock.onPost(getAssetsUrl(courseId)).reply(200, generateNewAssetApiResponse()); - const addFilesButton = screen.getByLabelText('file-input'); + const addFilesButton = screen.getByLabelText(messages.fileInputAriaLabel.defaultMessage); userEvent.upload(addFilesButton, file); await executeThunk(validateAssetFiles(courseId, [file]), store.dispatch); await waitFor(() => { @@ -221,7 +212,7 @@ describe('FilesAndUploads', () => { axiosMock.onGet( `${getAssetsUrl(courseId)}?display_name=mOckID6&page_size=1`, ).reply(200, { assets: [{ display_name: 'mOckID6' }] }); - const addFilesButton = screen.getByLabelText('file-input'); + const addFilesButton = screen.getByLabelText(messages.fileInputAriaLabel.defaultMessage); userEvent.upload(addFilesButton, file); await executeThunk(validateAssetFiles(courseId, [file]), store.dispatch); expect(screen.getByText(filesPageMessages.overwriteConfirmMessage.defaultMessage)).toBeVisible(); @@ -242,7 +233,7 @@ describe('FilesAndUploads', () => { }; axiosMock.onPost(getAssetsUrl(courseId)).reply(200, responseData); - const addFilesButton = screen.getByLabelText('file-input'); + const addFilesButton = screen.getByLabelText(messages.fileInputAriaLabel.defaultMessage); userEvent.upload(addFilesButton, file); await executeThunk(validateAssetFiles(courseId, [file]), store.dispatch); @@ -266,7 +257,7 @@ describe('FilesAndUploads', () => { axiosMock.onGet( `${getAssetsUrl(courseId)}?display_name=mOckID6&page_size=1`, ).reply(200, { assets: [{ display_name: 'mOckID6' }] }); - const addFilesButton = screen.getByLabelText('file-input'); + const addFilesButton = screen.getByLabelText(messages.fileInputAriaLabel.defaultMessage); userEvent.upload(addFilesButton, file); await executeThunk(validateAssetFiles(courseId, [file]), store.dispatch); @@ -558,7 +549,7 @@ describe('FilesAndUploads', () => { await mockStore(RequestStatus.SUCCESSFUL); axiosMock.onGet(`${getAssetsUrl(courseId)}?display_name=download.png&page_size=1`).reply(200, { assets: [] }); axiosMock.onPost(getAssetsUrl(courseId)).reply(413, { error: errorMessage }); - const addFilesButton = screen.getByLabelText('file-input'); + const addFilesButton = screen.getByLabelText(messages.fileInputAriaLabel.defaultMessage); userEvent.upload(addFilesButton, file); await waitFor(() => { const addStatus = store.getState().assets.addingStatus; @@ -571,7 +562,7 @@ describe('FilesAndUploads', () => { it('404 validation should show error', async () => { await mockStore(RequestStatus.SUCCESSFUL); axiosMock.onGet(`${getAssetsUrl(courseId)}?display_name=download.png&page_size=1`).reply(404); - const addFilesButton = screen.getByLabelText('file-input'); + const addFilesButton = screen.getByLabelText(messages.fileInputAriaLabel.defaultMessage); userEvent.upload(addFilesButton, file); await executeThunk(addAssetFile(courseId, file, 1), store.dispatch); const addStatus = store.getState().assets.addingStatus; @@ -584,7 +575,7 @@ describe('FilesAndUploads', () => { await mockStore(RequestStatus.SUCCESSFUL); axiosMock.onGet(`${getAssetsUrl(courseId)}?display_name=download.png&page_size=1`).reply(200, { assets: [] }); axiosMock.onPost(getAssetsUrl(courseId)).reply(404); - const addFilesButton = screen.getByLabelText('file-input'); + const addFilesButton = screen.getByLabelText(messages.fileInputAriaLabel.defaultMessage); userEvent.upload(addFilesButton, file); await executeThunk(addAssetFile(courseId, file, 1), store.dispatch); const addStatus = store.getState().assets.addingStatus; diff --git a/src/files-and-videos/files-page/data/slice.js b/src/files-and-videos/files-page/data/slice.js index 3a96779185..4fbe4915c9 100644 --- a/src/files-and-videos/files-page/data/slice.js +++ b/src/files-and-videos/files-page/data/slice.js @@ -28,7 +28,7 @@ const slice = createSlice({ if (isEmpty(state.assetIds)) { state.assetIds = payload.assetIds; } else { - state.assetIds = [...state.assetIds, ...payload.assetIds]; + state.assetIds = [...new Set([...state.assetIds, ...payload.assetIds])]; } }, setSortedAssetIds: (state, { payload }) => { diff --git a/src/files-and-videos/files-page/messages.js b/src/files-and-videos/files-page/messages.js index a6082edcb0..c5d2771063 100644 --- a/src/files-and-videos/files-page/messages.js +++ b/src/files-and-videos/files-page/messages.js @@ -120,6 +120,31 @@ const messages = defineMessages({ in your course and signed in to access the file.`, description: 'Tooltip message for the lock icon in the table view of files', }, + fileNameColumn: { + id: 'course-authoring.files-and-uploads.file-info.fileName.column', + defaultMessage: 'File name', + description: 'Title of the File Name column in the file list view', + }, + fileSizeColumn: { + id: 'course-authoring.files-and-uploads.file-info.fileSize.column', + defaultMessage: 'File size', + description: 'Title of the File size column in the file list view', + }, + fileTypeColumn: { + id: 'course-authoring.files-and-uploads.file-info.fileType.column', + defaultMessage: 'Type', + description: 'Title of the Type column in the file list view', + }, + fileActiveColumn: { + id: 'course-authoring.files-and-uploads.file-info.fileActive.column', + defaultMessage: 'Active', + description: 'Title of the Active column in the file list view', + }, + fileAccessColumn: { + id: 'course-authoring.files-and-uploads.file-info.fileAccess.column', + defaultMessage: 'Access', + description: 'Title of the Access column in the file list view', + }, }); export default messages; diff --git a/src/files-and-videos/generic/DeleteConfirmationModal.jsx b/src/files-and-videos/generic/DeleteConfirmationModal.jsx index bbcb3ab7e1..b6f4627855 100644 --- a/src/files-and-videos/generic/DeleteConfirmationModal.jsx +++ b/src/files-and-videos/generic/DeleteConfirmationModal.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { getConfig } from '@edx/frontend-platform'; @@ -7,6 +7,7 @@ import { AlertModal, Button, Collapsible, + DataTableContext, Hyperlink, Truncate, } from '@openedx/paragon'; @@ -22,6 +23,13 @@ const DeleteConfirmationModal = ({ // injected intl, }) => { + const { clearSelection } = useContext(DataTableContext); + + const handleConfirmDeletion = () => { + handleBulkDelete(); + clearSelection(); + }; + const firstSelectedRow = selectedRows[0]?.original; let activeContentRows = []; if (Array.isArray(selectedRows)) { @@ -35,9 +43,9 @@ const DeleteConfirmationModal = ({ styling="basic" title={(

- + {original.displayName} - +

)} data-testid={`collapsible-${original.id}`} @@ -73,7 +81,7 @@ const DeleteConfirmationModal = ({ -
diff --git a/src/files-and-videos/generic/FileInput.jsx b/src/files-and-videos/generic/FileInput.jsx index c953994d22..9e7de8a9d7 100644 --- a/src/files-and-videos/generic/FileInput.jsx +++ b/src/files-and-videos/generic/FileInput.jsx @@ -1,6 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { useIntl } from '@edx/frontend-platform/i18n'; import { getSupportedFormats } from '../videos-page/data/utils'; +import messages from './messages'; export const useFileInput = ({ onAddFile, @@ -23,17 +25,20 @@ export const useFileInput = ({ }; }; -const FileInput = ({ fileInput: hook, supportedFileFormats, allowMultiple }) => ( - -); +const FileInput = ({ fileInput: hook, supportedFileFormats, allowMultiple }) => { + const intl = useIntl(); + return ( + + ); +}; FileInput.propTypes = { fileInput: PropTypes.shape({ diff --git a/src/files-and-videos/generic/FileTable.jsx b/src/files-and-videos/generic/FileTable.jsx index ded6884a83..219148fd7e 100644 --- a/src/files-and-videos/generic/FileTable.jsx +++ b/src/files-and-videos/generic/FileTable.jsx @@ -273,6 +273,16 @@ const FileTable = ({ setSelectedRows={setSelectedRows} fileType={fileType} /> + + {!isEmpty(selectedRows) && ( @@ -286,15 +296,7 @@ const FileTable = ({ sidebar={infoModalSidebar} /> )} - +
); }; diff --git a/src/files-and-videos/generic/InfoModal.jsx b/src/files-and-videos/generic/InfoModal.jsx index 14276a4c2f..39d4d79618 100644 --- a/src/files-and-videos/generic/InfoModal.jsx +++ b/src/files-and-videos/generic/InfoModal.jsx @@ -48,9 +48,9 @@ const InfoModal = ({
- + {file?.displayName} - +
diff --git a/src/files-and-videos/generic/messages.js b/src/files-and-videos/generic/messages.js index 9930ca6b56..bb4badf86f 100644 --- a/src/files-and-videos/generic/messages.js +++ b/src/files-and-videos/generic/messages.js @@ -209,6 +209,11 @@ const messages = defineMessages({ defaultMessage: 'Upload error', description: 'Title for upload error alert', }, + fileInputAriaLabel: { + id: 'course-authoring.files-and-uploads.fileInput.ariaLabel', + defaultMessage: 'Upload a file', + description: 'Accessible (screen reader) label for file input', + }, }); export default messages; diff --git a/src/files-and-videos/generic/table-components/GalleryCard.jsx b/src/files-and-videos/generic/table-components/GalleryCard.jsx index fa21a182cf..c51820a96e 100644 --- a/src/files-and-videos/generic/table-components/GalleryCard.jsx +++ b/src/files-and-videos/generic/table-components/GalleryCard.jsx @@ -67,9 +67,9 @@ const GalleryCard = ({ />
- + {original.displayName} - +
diff --git a/src/files-and-videos/generic/table-components/TableActions.jsx b/src/files-and-videos/generic/table-components/TableActions.jsx index 3e813e6c4c..0663ea8498 100644 --- a/src/files-and-videos/generic/table-components/TableActions.jsx +++ b/src/files-and-videos/generic/table-components/TableActions.jsx @@ -26,13 +26,18 @@ const TableActions = ({ intl, }) => { const [isSortOpen, openSort, closeSort] = useToggle(false); - const { state } = useContext(DataTableContext); + const { state, clearSelection } = useContext(DataTableContext); // This useEffect saves DataTable state so it can persist after table re-renders due to data reload. useEffect(() => { setInitialState(state); }, [state]); + const handleOpenFileSelector = () => { + fileInputControl.click(); + clearSelection(); + }; + return ( <> diff --git a/src/files-and-videos/generic/table-components/TableActions.test.jsx b/src/files-and-videos/generic/table-components/TableActions.test.jsx new file mode 100644 index 0000000000..d97f3b2bcb --- /dev/null +++ b/src/files-and-videos/generic/table-components/TableActions.test.jsx @@ -0,0 +1,138 @@ +import React from 'react'; +import { screen, fireEvent } from '@testing-library/react'; +import { DataTableContext } from '@openedx/paragon'; +import { initializeMocks, render } from '../../../testUtils'; +import TableActions from './TableActions'; +import messages from '../messages'; + +const defaultProps = { + selectedFlatRows: [], + fileInputControl: { click: jest.fn() }, + handleOpenDeleteConfirmation: jest.fn(), + handleBulkDownload: jest.fn(), + encodingsDownloadUrl: null, + handleSort: jest.fn(), + fileType: 'video', + setInitialState: jest.fn(), + intl: { + formatMessage: (msg, values) => msg.defaultMessage.replace('{fileType}', values?.fileType ?? ''), + }, +}; + +const mockColumns = [ + { + id: 'wrapperType', + Header: 'Type', + accessor: 'wrapperType', + filter: 'includes', + }, +]; + +const renderWithContext = (props = {}, contextOverrides = {}) => { + const contextValue = { + state: { + selectedRowIds: {}, + filters: [], + ...contextOverrides.state, + }, + clearSelection: jest.fn(), + gotoPage: jest.fn(), + setAllFilters: jest.fn(), + columns: mockColumns, + ...contextOverrides, + }; + + return render( + + + , + ); +}; + +describe('TableActions', () => { + beforeEach(() => { + initializeMocks(); + jest.clearAllMocks(); + }); + + test('renders buttons and dropdown', () => { + renderWithContext(); + + expect(screen.getByRole('button', { name: messages.sortButtonLabel.defaultMessage })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: messages.addFilesButtonLabel.defaultMessage.replace('{fileType}', 'video') })).toBeInTheDocument(); + }); + + test('disables bulk and delete actions if no rows selected', () => { + renderWithContext(); + + fireEvent.click(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage })); + + const downloadOption = screen.getByText(messages.downloadTitle.defaultMessage); + const deleteButton = screen.getByTestId('open-delete-confirmation-button'); + + expect(downloadOption).toHaveAttribute('aria-disabled', 'true'); + expect(downloadOption).toHaveClass('disabled'); + + expect(deleteButton).toHaveAttribute('aria-disabled', 'true'); + expect(deleteButton).toHaveClass('disabled'); + }); + + test('enables bulk and delete actions when rows are selected', () => { + renderWithContext({ + selectedFlatRows: [{ original: { id: '1', displayName: 'Video 1', wrapperType: 'video' } }], + }); + + fireEvent.click(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage })); + expect(screen.getByText(messages.downloadTitle.defaultMessage)).not.toBeDisabled(); + expect(screen.getByTestId('open-delete-confirmation-button')).not.toBeDisabled(); + }); + + test('calls file input click and clears selection when add button clicked', () => { + const mockClick = jest.fn(); + const mockClear = jest.fn(); + + renderWithContext({ fileInputControl: { click: mockClick } }, {}, mockClear); + fireEvent.click(screen.getByRole('button', { name: messages.addFilesButtonLabel.defaultMessage.replace('{fileType}', 'video') })); + expect(mockClick).toHaveBeenCalled(); + }); + + test('opens sort modal when sort button clicked', () => { + renderWithContext(); + fireEvent.click(screen.getByRole('button', { name: messages.sortButtonLabel.defaultMessage })); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + + test('calls handleBulkDownload when selected and clicked', () => { + const handleBulkDownload = jest.fn(); + renderWithContext({ + selectedFlatRows: [{ original: { id: '1', displayName: 'Video 1', wrapperType: 'video' } }], + handleBulkDownload, + }); + + fireEvent.click(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage })); + fireEvent.click(screen.getByText(messages.downloadTitle.defaultMessage)); + expect(handleBulkDownload).toHaveBeenCalled(); + }); + + test('calls handleOpenDeleteConfirmation when clicked', () => { + const handleOpenDeleteConfirmation = jest.fn(); + const selectedFlatRows = [{ original: { id: '1', displayName: 'Video 1', wrapperType: 'video' } }]; + renderWithContext({ + selectedFlatRows, + handleOpenDeleteConfirmation, + }); + + fireEvent.click(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage })); + fireEvent.click(screen.getByTestId('open-delete-confirmation-button')); + expect(handleOpenDeleteConfirmation).toHaveBeenCalledWith(selectedFlatRows); + }); + + test('shows encoding download link when provided', () => { + const encodingsDownloadUrl = '/some/path/to/encoding.zip'; + renderWithContext({ encodingsDownloadUrl }); + + fireEvent.click(screen.getByRole('button', { name: messages.actionsButtonLabel.defaultMessage })); + expect(screen.getByRole('link', { name: messages.downloadEncodingsTitle.defaultMessage })).toHaveAttribute('href', expect.stringContaining(encodingsDownloadUrl)); + }); +}); diff --git a/src/files-and-videos/generic/table-components/sort-and-filter-modal/SortAndFilterModal.jsx b/src/files-and-videos/generic/table-components/sort-and-filter-modal/SortAndFilterModal.jsx index b45c29e80b..ac246352ec 100644 --- a/src/files-and-videos/generic/table-components/sort-and-filter-modal/SortAndFilterModal.jsx +++ b/src/files-and-videos/generic/table-components/sort-and-filter-modal/SortAndFilterModal.jsx @@ -91,7 +91,7 @@ const SortAndFilterModal = ({ className="text-center" value="displayName,asc" type="radio" - aria-label="name descending radio" + aria-label={intl.formatMessage(messages.sortByNameAscendingAriaLabel)} > @@ -99,7 +99,7 @@ const SortAndFilterModal = ({ className="text-center" value="dateAdded,desc" type="radio" - aria-label="date added descending radio" + aria-label={intl.formatMessage(messages.sortByNewestAriaLabel)} > @@ -107,7 +107,7 @@ const SortAndFilterModal = ({ className="text-center" value="fileSize,desc" type="radio" - aria-label="file size descending radio" + aria-label={intl.formatMessage(messages.sortBySizeDescendingAriaLabel)} > @@ -115,7 +115,7 @@ const SortAndFilterModal = ({ className="text-center" value="displayName,desc" type="radio" - aria-label="name ascending radio" + aria-label={intl.formatMessage(messages.sortByNameDescendingAriaLabel)} > @@ -123,7 +123,7 @@ const SortAndFilterModal = ({ className="text-center" value="dateAdded,asc" type="radio" - aria-label="date added ascending radio" + aria-label={intl.formatMessage(messages.sortByOldestAriaLabel)} > @@ -131,7 +131,7 @@ const SortAndFilterModal = ({ className="text-center" value="fileSize,asc" type="radio" - aria-label="file size ascending radio" + aria-label={intl.formatMessage(messages.sortBySizeAscendingAriaLabel)} > diff --git a/src/files-and-videos/generic/table-components/sort-and-filter-modal/messages.js b/src/files-and-videos/generic/table-components/sort-and-filter-modal/messages.js index 13165bde50..86ec8c1e0e 100644 --- a/src/files-and-videos/generic/table-components/sort-and-filter-modal/messages.js +++ b/src/files-and-videos/generic/table-components/sort-and-filter-modal/messages.js @@ -22,31 +22,61 @@ const messages = defineMessages({ defaultMessage: 'Cancel', }, sortByNameAscending: { - id: 'course-authoring..files-and-videos.sort-and-filter.modal.sortByNameAscendingButton.label', + id: 'course-authoring.files-and-videos.sort-and-filter.modal.sortByNameAscendingButton.label', defaultMessage: 'Name (A-Z)', }, + sortByNameAscendingAriaLabel: { + id: 'course-authoring.files-and-videos.sort-and-filter.modal.sortByNameAscendingButton.aria-label', + defaultMessage: 'name descending radio', + description: 'Accessible (screen reader) label for "name descending" filter', + }, sortByNewest: { - id: 'course-authoring..files-and-videos.sort-and-filter.modal.sortByNewestButton.label', + id: 'course-authoring.files-and-videos.sort-and-filter.modal.sortByNewestButton.label', defaultMessage: 'Newest', }, + sortByNewestAriaLabel: { + id: 'course-authoring.files-and-videos.sort-and-filter.modal.sortByNewestButton.label-label', + defaultMessage: 'date added descending radio', + description: 'Accessible (screen reader) label for "date added descending" filter', + }, sortBySizeDescending: { - id: 'course-authoring..files-and-videos.sort-and-filter.modal.sortBySizeDescendingButton.label', + id: 'course-authoring.files-and-videos.sort-and-filter.modal.sortBySizeDescendingButton.label', defaultMessage: 'File size (High to low)', }, + sortBySizeDescendingAriaLabel: { + id: 'course-authoring.files-and-videos.sort-and-filter.modal.sortBySizeDescendingButton.aria-label', + defaultMessage: 'file size descending radio', + description: 'Accessible (screen reader) label for "file size descending" filter', + }, sortByNameDescending: { - id: 'course-authoring..files-and-videos.sort-and-filter.modal.sortByNameDescendingButton.label', + id: 'course-authoring.files-and-videos.sort-and-filter.modal.sortByNameDescendingButton.label', defaultMessage: 'Name (Z-A)', }, + sortByNameDescendingAriaLabel: { + id: 'course-authoring.files-and-videos.sort-and-filter.modal.sortByNameDescendingButton.aria-label', + defaultMessage: 'name ascending radio', + description: 'Accessible (screen reader) label for "name ascending" filter', + }, sortByOldest: { - id: 'course-authoring..files-and-videos.sort-and-filter.modal.sortByOldestButton.label', + id: 'course-authoring.files-and-videos.sort-and-filter.modal.sortByOldestButton.label', defaultMessage: 'Oldest', }, + sortByOldestAriaLabel: { + id: 'course-authoring.files-and-videos.sort-and-filter.modal.sortByOldestButton.aria-label', + defaultMessage: 'date added ascending radio', + description: 'Accessible (screen reader) label for "date added ascending" filter', + }, sortBySizeAscending: { - id: 'course-authoring..files-and-videos.sort-and-filter.modal.sortBySizeAscendingButton.label', + id: 'course-authoring.files-and-videos.sort-and-filter.modal.sortBySizeAscendingButton.label', defaultMessage: 'File size (Low to high)', }, + sortBySizeAscendingAriaLabel: { + id: 'course-authoring.files-and-videos.sort-and-filter.modal.sortBySizeAscendingButton.aria-label', + defaultMessage: 'file size ascending radio', + description: 'Accessible (screen reader) label for "file size ascending" filter', + }, applySortButton: { - id: 'course-authoring..files-and-videos.sort-and-filter.modal.applyySortButton.label', + id: 'course-authoring.files-and-videos.sort-and-filter.modal.applyySortButton.label', defaultMessage: 'Apply', }, }); diff --git a/src/files-and-videos/index.scss b/src/files-and-videos/index.scss index 0af5102913..94651e9b71 100644 --- a/src/files-and-videos/index.scss +++ b/src/files-and-videos/index.scss @@ -14,7 +14,7 @@ } .pgn__data-table-wrapper { - background-color: $light-200; + background-color: var(--pgn-color-light-200); box-shadow: 0 0 0; .pgn__data-table-status-bar { @@ -45,8 +45,7 @@ overflow-x: hidden; max-height: 500px; width: 321px; - - @include pgn-box-shadow(2, "down"); + box-shadow: var(--pgn-elevation-box-shadow-down-2); .pgn__menu-item { width: 100%; @@ -54,7 +53,7 @@ font-weight: 400; &:hover { - background-color: $light-300; + background-color: var(--pgn-color-light-300); } } } diff --git a/src/files-and-videos/videos-page/VideosPage.test.jsx b/src/files-and-videos/videos-page/VideosPage.test.jsx index 1a57468390..062ee712fa 100644 --- a/src/files-and-videos/videos-page/VideosPage.test.jsx +++ b/src/files-and-videos/videos-page/VideosPage.test.jsx @@ -244,7 +244,7 @@ describe('Videos page', () => { axiosUnauthenticateMock.onPut('http://testing.org').reply(200); axiosMock.onGet(getCourseVideosApiUrl(courseId)).reply(200, generateAddVideoApiResponse()); - const addFilesButton = screen.getAllByLabelText('file-input')[3]; + const addFilesButton = screen.getAllByLabelText(messages.fileInputAriaLabel.defaultMessage)[3]; await act(async () => { userEvent.upload(addFilesButton, file); }); @@ -263,7 +263,7 @@ describe('Videos page', () => { const setFailedSpy = jest.spyOn(api, 'sendVideoUploadStatus').mockImplementation(() => {}); uploadSpy.mockResolvedValue(new Promise(() => {})); - const addFilesButton = screen.getAllByLabelText('file-input')[3]; + const addFilesButton = screen.getAllByLabelText(messages.fileInputAriaLabel.defaultMessage)[3]; await act(async () => { userEvent.upload(addFilesButton, file); }); @@ -294,7 +294,7 @@ describe('Videos page', () => { const setFailedSpy = jest.spyOn(api, 'sendVideoUploadStatus').mockImplementation(() => {}); uploadSpy.mockResolvedValue(new Promise(() => {})); - const addFilesButton = screen.getAllByLabelText('file-input')[3]; + const addFilesButton = screen.getAllByLabelText(messages.fileInputAriaLabel.defaultMessage)[3]; await act(async () => { userEvent.upload(addFilesButton, file); }); @@ -610,7 +610,7 @@ describe('Videos page', () => { axiosMock.onPost(getCourseVideosApiUrl(courseId)).reply(413, { error: errorMessage }); axiosMock.onGet(getCourseVideosApiUrl(courseId)).reply(200, generateAddVideoApiResponse()); - const addFilesButton = screen.getAllByLabelText('file-input')[3]; + const addFilesButton = screen.getAllByLabelText(messages.fileInputAriaLabel.defaultMessage)[3]; await act(async () => { userEvent.upload(addFilesButton, file); }); @@ -627,7 +627,7 @@ describe('Videos page', () => { axiosMock.onPost(getCourseVideosApiUrl(courseId)).reply(404); axiosMock.onGet(getCourseVideosApiUrl(courseId)).reply(200, generateAddVideoApiResponse()); - const addFilesButton = screen.getAllByLabelText('file-input')[3]; + const addFilesButton = screen.getAllByLabelText(messages.fileInputAriaLabel.defaultMessage)[3]; await act(async () => { userEvent.upload(addFilesButton, file); }); @@ -658,7 +658,7 @@ describe('Videos page', () => { axiosMock.onPost(getCourseVideosApiUrl(courseId)).reply(204, generateNewVideoApiResponse()); axiosUnauthenticateMock.onPut('http://testing.org').reply(404); axiosMock.onGet(getCourseVideosApiUrl(courseId)).reply(200, generateAddVideoApiResponse()); - const addFilesButton = screen.getAllByLabelText('file-input')[3]; + const addFilesButton = screen.getAllByLabelText(messages.fileInputAriaLabel.defaultMessage)[3]; await act(async () => { userEvent.upload(addFilesButton, file); }); diff --git a/src/files-and-videos/videos-page/info-sidebar/TranscriptTab.test.jsx b/src/files-and-videos/videos-page/info-sidebar/TranscriptTab.test.jsx index d7a05d9ca5..75625eb17c 100644 --- a/src/files-and-videos/videos-page/info-sidebar/TranscriptTab.test.jsx +++ b/src/files-and-videos/videos-page/info-sidebar/TranscriptTab.test.jsx @@ -27,6 +27,7 @@ import { import { getApiBaseUrl } from '../data/api'; import messages from './messages'; +import genericMessages from '../../generic/messages'; import transcriptRowMessages from './transcript-item/messages'; import VideosPageProvider from '../VideosPageProvider'; import { deleteVideoTranscript } from '../data/thunks'; @@ -116,7 +117,7 @@ describe('TranscriptTab', () => { it('should upload new transcript', async () => { axiosMock.onPost(`${getApiBaseUrl()}/transcript_upload/`).reply(204); await act(async () => { - const addFileInput = screen.getByLabelText('file-input'); + const addFileInput = screen.getByLabelText(genericMessages.fileInputAriaLabel.defaultMessage); expect(addFileInput).toBeInTheDocument(); userEvent.upload(addFileInput, file); @@ -129,7 +130,7 @@ describe('TranscriptTab', () => { it('should show default error message', async () => { axiosMock.onPost(`${getApiBaseUrl()}/transcript_upload/`).reply(404); await act(async () => { - const addFileInput = screen.getByLabelText('file-input'); + const addFileInput = screen.getByLabelText(genericMessages.fileInputAriaLabel.defaultMessage); userEvent.upload(addFileInput, file); }); const addStatus = store.getState().videos.transcriptStatus; @@ -142,7 +143,7 @@ describe('TranscriptTab', () => { it('should show api provided error message', async () => { axiosMock.onPost(`${getApiBaseUrl()}/transcript_upload/`).reply(404, { error: 'api error' }); await act(async () => { - const addFileInput = screen.getByLabelText('file-input'); + const addFileInput = screen.getByLabelText(genericMessages.fileInputAriaLabel.defaultMessage); userEvent.upload(addFileInput, file); }); const addStatus = store.getState().videos.transcriptStatus; @@ -299,7 +300,7 @@ describe('TranscriptTab', () => { axiosMock.onPost(`${getApiBaseUrl()}/transcript_upload/`).reply(204); await act(async () => { - const addFileInput = screen.getAllByLabelText('file-input')[0]; + const addFileInput = screen.getAllByLabelText(genericMessages.fileInputAriaLabel.defaultMessage)[0]; userEvent.upload(addFileInput, file); }); const addStatus = store.getState().videos.transcriptStatus; @@ -315,7 +316,7 @@ describe('TranscriptTab', () => { axiosMock.onPost(`${getApiBaseUrl()}/transcript_upload/`).reply(404); await act(async () => { - const addFileInput = screen.getAllByLabelText('file-input')[0]; + const addFileInput = screen.getAllByLabelText(genericMessages.fileInputAriaLabel.defaultMessage)[0]; userEvent.upload(addFileInput, file); }); diff --git a/src/files-and-videos/videos-page/info-sidebar/transcript-item/LanguageSelect.scss b/src/files-and-videos/videos-page/info-sidebar/transcript-item/LanguageSelect.scss index e42eb28a11..9e893d971f 100644 --- a/src/files-and-videos/videos-page/info-sidebar/transcript-item/LanguageSelect.scss +++ b/src/files-and-videos/videos-page/info-sidebar/transcript-item/LanguageSelect.scss @@ -4,8 +4,7 @@ overflow-x: hidden; max-height: 275px; width: 300px; - - @include pgn-box-shadow(2, "down"); + box-shadow: var(--pgn-elevation-box-shadow-down-2); .pgn__menu-item { width: 100%; @@ -13,7 +12,7 @@ font-weight: 400; &:hover { - background-color: $light-300; + background-color: var(--pgn-color-light-300); } .pgn__menu-item-text { diff --git a/src/files-and-videos/videos-page/info-sidebar/transcript-item/TranscriptMenu.scss b/src/files-and-videos/videos-page/info-sidebar/transcript-item/TranscriptMenu.scss index 5a21a8d129..3b4a2f77a8 100644 --- a/src/files-and-videos/videos-page/info-sidebar/transcript-item/TranscriptMenu.scss +++ b/src/files-and-videos/videos-page/info-sidebar/transcript-item/TranscriptMenu.scss @@ -2,8 +2,7 @@ padding: 8px 0; margin: 0; width: 250px; - - @include pgn-box-shadow(2, "down"); + box-shadow: var(--pgn-elevation-box-shadow-down-2); .pgn__menu-item { width: 100%; @@ -11,7 +10,7 @@ font-weight: 400; &:hover { - background-color: $light-300; + background-color: var(--pgn-color-light-300); } } } diff --git a/src/files-and-videos/videos-page/transcript-settings/OrderTranscriptForm.jsx b/src/files-and-videos/videos-page/transcript-settings/OrderTranscriptForm.jsx index 28e344e36c..1f13871fc7 100644 --- a/src/files-and-videos/videos-page/transcript-settings/OrderTranscriptForm.jsx +++ b/src/files-and-videos/videos-page/transcript-settings/OrderTranscriptForm.jsx @@ -119,14 +119,14 @@ const OrderTranscriptForm = ({ > @@ -134,7 +134,7 @@ const OrderTranscriptForm = ({ diff --git a/src/files-and-videos/videos-page/transcript-settings/messages.js b/src/files-and-videos/videos-page/transcript-settings/messages.js index 6585cd8d7b..12062056d7 100644 --- a/src/files-and-videos/videos-page/transcript-settings/messages.js +++ b/src/files-and-videos/videos-page/transcript-settings/messages.js @@ -28,16 +28,31 @@ const messages = defineMessages({ defaultMessage: 'None', description: 'Label for order transcript None option', }, + noneAriaLabel: { + id: 'course-authoring.video-uploads.transcriptSettings.orderTranscripts.none.aria-label', + defaultMessage: 'none radio', + description: 'Accessible (screen reader) label for order transcript None option', + }, cieloLabel: { id: 'course-authoring.video-uploads.transcriptSettings.orderTranscripts.cielo24.label', defaultMessage: 'Cielo24', description: 'Label for order transcript Cieol24 option', }, + cieloAriaLabel: { + id: 'course-authoring.video-uploads.transcriptSettings.orderTranscripts.cielo24.aria-label', + defaultMessage: 'Cielo24 radio', + description: 'Accessible (screen reader) label for order transcript Cieol24 option', + }, threePlayMediaLabel: { id: 'course-authoring.video-uploads.transcriptSettings.orderTranscripts.3PlayMedia.label', defaultMessage: '3Play Media', description: 'Label for order transcript 3Play Media option', }, + threePlayMediaAriaLabel: { + id: 'course-authoring.video-uploads.transcriptSettings.orderTranscripts.3PlayMedia.aria-label', + defaultMessage: '3PlayMedia radio', + description: 'Accessible (screen reader) label for order transcript 3Play Media option', + }, updateSettingsLabel: { id: 'course-authoring.video-uploads.transcriptSettings.orderTranscripts.updateSettings.label', defaultMessage: 'Update settings', diff --git a/src/files-and-videos/videos-page/upload-modal/UploadProgressList.jsx b/src/files-and-videos/videos-page/upload-modal/UploadProgressList.jsx index fdecdb35b1..450de9d9e2 100644 --- a/src/files-and-videos/videos-page/upload-modal/UploadProgressList.jsx +++ b/src/files-and-videos/videos-page/upload-modal/UploadProgressList.jsx @@ -25,9 +25,9 @@ const UploadProgressList = ({ videosList }) => ( {bulletNumber}
- + {video.name} - +
diff --git a/src/generic/DraggableList/DraggableList.jsx b/src/generic/DraggableList/DraggableList.jsx index 1515f29a06..ef86c45f03 100644 --- a/src/generic/DraggableList/DraggableList.jsx +++ b/src/generic/DraggableList/DraggableList.jsx @@ -1,10 +1,9 @@ -import React, { useCallback } from 'react'; +import { useCallback } from 'react'; import PropTypes from 'prop-types'; import { createPortal } from 'react-dom'; import { DndContext, - closestCenter, KeyboardSensor, PointerSensor, useSensor, @@ -18,6 +17,7 @@ import { verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; +import { verticalSortableListCollisionDetection } from './verticalSortableList'; const DraggableList = ({ itemList, @@ -56,13 +56,20 @@ const DraggableList = ({ setActiveId?.(event.active.id); }, [setActiveId]); + const handleDragCancel = useCallback(() => { + setActiveId?.(null); + }, [setActiveId]); + return ( { @@ -45,14 +46,15 @@ const SortableItem = ({ }; return ( + /* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
{actions} @@ -93,6 +95,7 @@ SortableItem.propTypes = { isClickable: PropTypes.bool, onClick: PropTypes.func, disabled: PropTypes.bool, + cardClassName: PropTypes.string, // injected intl: intlShape.isRequired, }; diff --git a/src/generic/DraggableList/verticalSortableList.ts b/src/generic/DraggableList/verticalSortableList.ts new file mode 100644 index 0000000000..0d3409e32a --- /dev/null +++ b/src/generic/DraggableList/verticalSortableList.ts @@ -0,0 +1,80 @@ +/* istanbul ignore file */ +/** +This sorting strategy was copied over from https://github.com/clauderic/dnd-kit/pull/805 +to resolve issues with variable sized draggables. +*/ +import { CollisionDetection, DroppableContainer } from '@dnd-kit/core'; +import { sortBy } from 'lodash'; + +const collision = (dropppableContainer?: DroppableContainer) => ({ + id: dropppableContainer?.id ?? '', + value: dropppableContainer, +}); + +// Look for the first (/ furthest up / highest) droppable container that is at least +// 50% covered by the top edge of the dragging container. +const highestDroppableContainerMajorityCovered: CollisionDetection = ({ + droppableContainers, + collisionRect, +}) => { + const ascendingDroppabaleContainers = sortBy( + droppableContainers, + (c) => c?.rect.current?.top, + ); + + for (const droppableContainer of ascendingDroppabaleContainers) { + const { + rect: { current: droppableRect }, + } = droppableContainer; + + if (droppableRect) { + const coveredPercentage = (droppableRect.top + droppableRect.height - collisionRect.top) + / droppableRect.height; + + if (coveredPercentage > 0.5) { + return [collision(droppableContainer)]; + } + } + } + + // if we haven't found anything then we are off the top, so return the first item + return [collision(ascendingDroppabaleContainers[0])]; +}; + +// Look for the last (/ furthest down / lowest) droppable container that is at least +// 50% covered by the bottom edge of the dragging container. +const lowestDroppableContainerMajorityCovered: CollisionDetection = ({ + droppableContainers, + collisionRect, +}) => { + const descendingDroppabaleContainers = sortBy( + droppableContainers, + (c) => c?.rect.current?.top, + ).reverse(); + + for (const droppableContainer of descendingDroppabaleContainers) { + const { + rect: { current: droppableRect }, + } = droppableContainer; + + if (droppableRect) { + const coveredPercentage = (collisionRect.bottom - droppableRect.top) / droppableRect.height; + + if (coveredPercentage > 0.5) { + return [collision(droppableContainer)]; + } + } + } + + // if we haven't found anything then we are off the bottom, so return the last item + return [collision(descendingDroppabaleContainers[0])]; +}; + +export const verticalSortableListCollisionDetection: CollisionDetection = ( + args, +) => { + if (args.collisionRect.top < (args.active.rect.current?.initial?.top ?? 0)) { + return highestDroppableContainerMajorityCovered(args); + } + return lowestDroppableContainerMajorityCovered(args); +}; diff --git a/src/generic/WysiwygEditor.scss b/src/generic/WysiwygEditor.scss index 06a1226583..745cf2b635 100644 --- a/src/generic/WysiwygEditor.scss +++ b/src/generic/WysiwygEditor.scss @@ -1,5 +1,5 @@ .tox-dialog-wrap__backdrop { - background-color: $black !important; + background-color: var(--pgn-color-black) !important; opacity: .5; - z-index: $zindex-modal-backdrop; + z-index: var(--pgn-elevation-modal-backdrop-zindex); } diff --git a/src/generic/alert-message/index.scss b/src/generic/alert-message/index.scss new file mode 100644 index 0000000000..394e6d2598 --- /dev/null +++ b/src/generic/alert-message/index.scss @@ -0,0 +1,6 @@ +// TODO: remove this after upstream fix merging: https://github.com/openedx/paragon/pull/3562 +.alert { + .alert-message-content { + align-self: baseline; + } +} diff --git a/src/generic/block-type-utils/index.scss b/src/generic/block-type-utils/index.scss index 713509c0ba..34fbbd5aee 100644 --- a/src/generic/block-type-utils/index.scss +++ b/src/generic/block-type-utils/index.scss @@ -17,7 +17,7 @@ &:hover, &:active, &:focus { background-color: lighten(#005C9E, 20%); - border: 1px solid $primary; + border: 1px solid var(--pgn-color-primary-base); margin: -1px; } } @@ -42,7 +42,7 @@ &:hover, &:active, &:focus { background-color: lighten(#9747FF, 20%); - border: 1px solid $primary; + border: 1px solid var(--pgn-color-primary-base); margin: -1px; } } @@ -67,7 +67,7 @@ &:hover, &:active, &:focus { background-color: lighten(#FFCD29, 20%); - border: 1px solid $primary; + border: 1px solid var(--pgn-color-primary-base); margin: -1px; } } @@ -92,7 +92,7 @@ &:hover, &:active, &:focus { background-color: lighten(#358F0A, 20%); - border: 1px solid $primary; + border: 1px solid var(--pgn-color-primary-base); margin: -1px; } } @@ -117,7 +117,7 @@ &:hover, &:active, &:focus { background-color: lighten(#0B8E77, 20%); - border: 1px solid $primary; + border: 1px solid var(--pgn-color-primary-base); margin: -1px; } } @@ -142,7 +142,7 @@ &:hover, &:active, &:focus { background-color: lighten(#646464, 20%); - border: 1px solid $primary; + border: 1px solid var(--pgn-color-primary-base); margin: -1px; } } diff --git a/src/generic/clipboard/paste-component/PasteComponent.scss b/src/generic/clipboard/paste-component/PasteComponent.scss index c68ed4e4c6..af6d3b035e 100644 --- a/src/generic/clipboard/paste-component/PasteComponent.scss +++ b/src/generic/clipboard/paste-component/PasteComponent.scss @@ -10,7 +10,7 @@ } .whats-in-clipboard-text { - font-size: $font-size-sm; + font-size: var(--pgn-typography-font-size-sm); } } @@ -35,9 +35,9 @@ .clipboard-popover-detail-block-type { display: block; - font-size: $font-size-sm; + font-size: var(--pgn-typography-font-size-sm); line-height: 1.313rem; - color: $gray-700; + color: var(--pgn-color-gray-700); } .clipboard-popover-detail-course-name { diff --git a/src/generic/configure-modal/AdvancedTab.jsx b/src/generic/configure-modal/AdvancedTab.jsx index 73b7a48014..ba639099ee 100644 --- a/src/generic/configure-modal/AdvancedTab.jsx +++ b/src/generic/configure-modal/AdvancedTab.jsx @@ -5,7 +5,7 @@ import { Alert, Form, Hyperlink } from '@openedx/paragon'; import { Warning as WarningIcon, } from '@openedx/paragon/icons'; -import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n'; +import { FormattedMessage, injectIntl, useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; import PrereqSettings from './PrereqSettings'; @@ -32,6 +32,8 @@ const AdvancedTab = ({ } = values; let examTypeValue = 'none'; + const intl = useIntl(); + if (isTimeLimited && isProctoredExam) { if (isOnboardingExam) { examTypeValue = 'onboardingExam'; @@ -183,7 +185,7 @@ const AdvancedTab = ({ diff --git a/src/generic/configure-modal/messages.js b/src/generic/configure-modal/messages.js index 2d1574b763..7a141ad964 100644 --- a/src/generic/configure-modal/messages.js +++ b/src/generic/configure-modal/messages.js @@ -239,6 +239,11 @@ const messages = defineMessages({ id: 'course-authoring.course-outline.configure-modal.advanced-tab.time-allotted', defaultMessage: 'Time allotted (HH:MM):', }, + timeLimitPlaceholder: { + id: 'course-authoring.course-outline.configure-modal.advanced-tab.time-limit-placeholder', + defaultMessage: 'HH:MM', + description: 'The placeholder for the time limit input field, two digits for hours and two digits for minutes colons in between', + }, timeLimitDescription: { id: 'course-authoring.course-outline.configure-modal.advanced-tab.time-limit-description', defaultMessage: 'Select a time allotment for the exam. If it is over 24 hours, type in the amount of time. You can grant individual learners extra time to complete the exam through the Instructor Dashboard.', diff --git a/src/generic/course-stepper/CouseStepper.scss b/src/generic/course-stepper/CouseStepper.scss index 868f1c6f89..fcea339798 100644 --- a/src/generic/course-stepper/CouseStepper.scss +++ b/src/generic/course-stepper/CouseStepper.scss @@ -1,12 +1,12 @@ .course-stepper { .course-stepper__step { display: flex; - gap: $spacer; + gap: var(--pgn-spacing-spacer-base); padding: 1.25rem 0; opacity: .5; &:not(:last-child) { - border-bottom: 1px solid $gray-200; + border-bottom: 1px solid var(--pgn-color-gray-200); } .course-stepper__step-icon { @@ -35,7 +35,7 @@ .course-stepper__step-description { margin: 0; font-size: 1rem; - color: $gray-400; + color: var(--pgn-color-gray-400); } } @@ -52,7 +52,7 @@ & svg, .course-stepper__step-title { - color: $success-500; + color: var(--pgn-color-success-500); } } @@ -62,7 +62,7 @@ .course-stepper__step-title, .course-stepper__step-description, & svg { - color: $danger-300; + color: var(--pgn-color-danger-300); } } } diff --git a/src/generic/course-upload-image/CourseUploadImage.scss b/src/generic/course-upload-image/CourseUploadImage.scss index 966f592be7..97026e7b18 100644 --- a/src/generic/course-upload-image/CourseUploadImage.scss +++ b/src/generic/course-upload-image/CourseUploadImage.scss @@ -1,14 +1,13 @@ .image-preview { - @include pgn-box-shadow(1, "down"); - + box-shadow: var(--pgn-elevation-box-shadow-down-1); display: block; width: 23.4375rem; height: 12.5rem; overflow: hidden; margin: 0 auto; - border: .0625rem solid $gray-300; + border: .0625rem solid var(--pgn-color-gray-300); padding: .625rem; - background: $white; + background: var(--pgn-color-white); img { display: block; @@ -21,6 +20,6 @@ text-align: center; .pgn__dropzone { - background: $white; + background: var(--pgn-color-white); } } diff --git a/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.scss b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.scss index 5894c96ad2..95897e760c 100644 --- a/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.scss +++ b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.scss @@ -1,12 +1,12 @@ .create-or-rerun-course-form { .form-group-custom { &:not(:last-child) { - margin-bottom: $spacer; + margin-bottom: var(--pgn-spacing-spacer-base); } .pgn__form-label { - font: normal 1.125rem/1.75rem $font-family-base; - color: $gray-700; + font: normal 1.125rem/1.75rem var(--pgn-typography-font-family-base); + color: var(--pgn-color-gray-700); margin-bottom: .25rem; } diff --git a/src/generic/delete-modal/DeleteModal.jsx b/src/generic/delete-modal/DeleteModal.tsx similarity index 71% rename from src/generic/delete-modal/DeleteModal.jsx rename to src/generic/delete-modal/DeleteModal.tsx index 4159d9d3f2..cf81b96363 100644 --- a/src/generic/delete-modal/DeleteModal.jsx +++ b/src/generic/delete-modal/DeleteModal.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import { ActionRow, Button, @@ -9,17 +8,29 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; import LoadingButton from '../loading-button'; +interface DeleteModalProps { + isOpen: boolean; + close: () => void; + category?: string; + onDeleteSubmit: () => void | Promise; + title?: string; + description?: React.ReactNode | React.ReactNode[]; + variant?: string; + btnLabel?: string; + icon?: React.ElementType; +} + const DeleteModal = ({ - category, + category = '', isOpen, close, onDeleteSubmit, title, description, - variant, + variant = 'default', btnLabel, icon, -}) => { +}: DeleteModalProps) => { const intl = useIntl(); const modalTitle = title || intl.formatMessage(messages.title, { category }); @@ -62,28 +73,4 @@ const DeleteModal = ({ ); }; -DeleteModal.defaultProps = { - category: '', - title: '', - description: '', - variant: 'default', - btnLabel: '', - icon: null, -}; - -DeleteModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - close: PropTypes.func.isRequired, - category: PropTypes.string, - onDeleteSubmit: PropTypes.func.isRequired, - title: PropTypes.string, - description: PropTypes.oneOfType([ - PropTypes.element, - PropTypes.string, - ]), - variant: PropTypes.string, - btnLabel: PropTypes.string, - icon: PropTypes.elementType, -}; - export default DeleteModal; diff --git a/src/generic/delete-modal/messages.js b/src/generic/delete-modal/messages.ts similarity index 100% rename from src/generic/delete-modal/messages.js rename to src/generic/delete-modal/messages.ts diff --git a/src/generic/divider/Divider.scss b/src/generic/divider/Divider.scss index b78206689d..5f0b9cff6b 100644 --- a/src/generic/divider/Divider.scss +++ b/src/generic/divider/Divider.scss @@ -1,5 +1,5 @@ .divider { - border-top: $border-width solid $light-400; + border-top: var(--pgn-size-border-width) solid var(--pgn-color-light-400); height: 0; - margin: $spacer map-get($spacers, 0); + margin: var(--pgn-spacing-spacer-base) var(--pgn-spacing-spacer-0); } diff --git a/src/generic/help-sidebar/HelpSidebar.scss b/src/generic/help-sidebar/HelpSidebar.scss index 26f08f2e9a..c4b24300fd 100644 --- a/src/generic/help-sidebar/HelpSidebar.scss +++ b/src/generic/help-sidebar/HelpSidebar.scss @@ -1,17 +1,17 @@ .help-sidebar { .help-sidebar-about { .help-sidebar-about-title { - color: $black; + color: var(--pgn-color-black); margin-bottom: 1.25rem; } .help-sidebar-about-descriptions { - font: normal $font-weight-normal .875rem/1.5rem $font-family-base; + font: normal var(--pgn-typography-font-weight-normal) .875rem/1.5rem var(--pgn-typography-font-family-base); color: $text-color-base; } .help-sidebar-about-link { - font: normal $font-weight-normal .875rem/1.5rem $font-family-base; + font: normal var(--pgn-typography-font-weight-normal) .875rem/1.5rem var(--pgn-typography-font-family-base); } } @@ -20,15 +20,15 @@ } .help-sidebar-other-title { - font: normal $font-weight-bold 1.125rem/1.5rem $font-family-base; - color: $black; + font: normal var(--pgn-typography-font-weight-bold) 1.125rem/1.5rem var(--pgn-typography-font-family-base); + color: var(--pgn-color-black); margin-bottom: 1.25rem; } .sidebar-link { - font: normal $font-weight-normal .875rem/1.5rem $font-family-base; + font: normal var(--pgn-typography-font-weight-normal) .875rem/1.5rem var(--pgn-typography-font-family-base); line-height: 1.5rem; - color: $info-500; + color: var(--pgn-color-info-500); margin-bottom: .5rem; } } diff --git a/src/generic/hooks/tests/hooks.test.tsx b/src/generic/hooks/tests/hooks.test.tsx index bf15d83ba2..d2e83f06fc 100644 --- a/src/generic/hooks/tests/hooks.test.tsx +++ b/src/generic/hooks/tests/hooks.test.tsx @@ -96,7 +96,8 @@ describe('useIframeBehavior', () => { window.dispatchEvent(new MessageEvent('message', message)); }); - expect(setIframeHeight).toHaveBeenCalledWith(500); + // +10 padding + expect(setIframeHeight).toHaveBeenCalledWith(510); expect(setHasLoaded).toHaveBeenCalledWith(true); }); diff --git a/src/generic/hooks/useIframeBehavior.tsx b/src/generic/hooks/useIframeBehavior.tsx index 2c327a23dd..1e60a6f943 100644 --- a/src/generic/hooks/useIframeBehavior.tsx +++ b/src/generic/hooks/useIframeBehavior.tsx @@ -46,7 +46,8 @@ export const useIframeBehavior = ({ switch (type) { case iframeMessageTypes.resize: - setIframeHeight(payload.height); + // Adding 10px as padding + setIframeHeight(payload.height + 10); if (!hasLoaded && iframeHeight === 0 && payload.height > 0) { setHasLoaded(true); } diff --git a/src/generic/inplace-text-editor/InplaceTextEditor.test.tsx b/src/generic/inplace-text-editor/InplaceTextEditor.test.tsx index 8914ad6de5..9b7a55c281 100644 --- a/src/generic/inplace-text-editor/InplaceTextEditor.test.tsx +++ b/src/generic/inplace-text-editor/InplaceTextEditor.test.tsx @@ -1,6 +1,11 @@ import React from 'react'; import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { fireEvent, render as baseRender, screen } from '@testing-library/react'; +import { + act, + fireEvent, + render as baseRender, + screen, +} from '@testing-library/react'; import { InplaceTextEditor } from '.'; const mockOnSave = jest.fn(); @@ -24,8 +29,8 @@ describe('', () => { expect(screen.queryByRole('button', { name: /edit/ })).not.toBeInTheDocument(); }); - it('should render the edit button if alwaysShowEditButton is true', () => { - render(); + it('should render the edit button', () => { + render(); expect(screen.getByText('Test text')).toBeInTheDocument(); expect(screen.getByRole('button', { name: /edit/i })).toBeInTheDocument(); @@ -36,7 +41,10 @@ describe('', () => { const title = screen.getByText('Test text'); expect(title).toBeInTheDocument(); - fireEvent.click(title); + + const editButton = screen.getByRole('button', { name: /edit/i }); + expect(editButton).toBeInTheDocument(); + fireEvent.click(editButton); const textBox = screen.getByRole('textbox'); @@ -52,7 +60,10 @@ describe('', () => { const title = screen.getByText('Test text'); expect(title).toBeInTheDocument(); - fireEvent.click(title); + + const editButton = screen.getByRole('button', { name: /edit/i }); + expect(editButton).toBeInTheDocument(); + fireEvent.click(editButton); const textBox = screen.getByRole('textbox'); @@ -62,4 +73,62 @@ describe('', () => { expect(textBox).not.toBeInTheDocument(); expect(mockOnSave).not.toHaveBeenCalled(); }); + + it('should show the new text while processing and roolback in case of error', async () => { + let rejecter: (err: Error) => void; + const longMockOnSave = jest.fn().mockReturnValue( + new Promise((_resolve, reject) => { + rejecter = reject; + }), + ); + render(); + + const text = screen.getByText('Test text'); + expect(text).toBeInTheDocument(); + + const editButton = screen.getByRole('button', { name: /edit/i }); + expect(editButton).toBeInTheDocument(); + fireEvent.click(editButton); + + const textBox = screen.getByRole('textbox'); + + fireEvent.change(textBox, { target: { value: 'New text' } }); + fireEvent.keyDown(textBox, { key: 'Enter', code: 'Enter', charCode: 13 }); + + expect(textBox).not.toBeInTheDocument(); + expect(longMockOnSave).toHaveBeenCalledWith('New text'); + + // Show pending new text + const newText = screen.getByText('New text'); + expect(newText).toBeInTheDocument(); + + await act(async () => { rejecter(new Error('error')); }); + + // Remove pending new text on error + expect(newText).not.toBeInTheDocument(); + + // Show original text + expect(screen.getByText('Test text')).toBeInTheDocument(); + }); + + it('should disappear edit button while editing', async () => { + render(); + + const title = screen.getByText('Test text'); + expect(title).toBeInTheDocument(); + + const editButton = screen.getByRole('button', { name: /edit/i }); + expect(editButton).toBeInTheDocument(); + fireEvent.click(editButton); + + const textBox = screen.getByRole('textbox'); + expect(editButton).not.toBeInTheDocument(); + + fireEvent.change(textBox, { target: { value: 'New text' } }); + fireEvent.keyDown(textBox, { key: 'Enter', code: 'Enter', charCode: 13 }); + + expect(textBox).not.toBeInTheDocument(); + expect(mockOnSave).toHaveBeenCalledWith('New text'); + expect(await screen.findByRole('button', { name: /edit/i })).toBeInTheDocument(); + }); }); diff --git a/src/generic/inplace-text-editor/index.tsx b/src/generic/inplace-text-editor/index.tsx index 8caecd550f..cb07d2ffa9 100644 --- a/src/generic/inplace-text-editor/index.tsx +++ b/src/generic/inplace-text-editor/index.tsx @@ -1,14 +1,12 @@ import React, { useCallback, - useEffect, useState, - forwardRef, } from 'react'; import { Form, Icon, IconButton, - OverlayTrigger, + Truncate, Stack, } from '@openedx/paragon'; import { Edit } from '@openedx/paragon/icons'; @@ -16,33 +14,11 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; -interface IconWrapperProps { - popper: any; - children: React.ReactNode; - [key: string]: any; -} - -const IconWrapper = forwardRef(({ popper, children, ...props }, ref) => { - useEffect(() => { - // This is a workaround to force the popper to update its position when - // the editor is opened. - // Ref: https://react-bootstrap.netlify.app/docs/components/overlays/#updating-position-dynamically - popper.scheduleUpdate(); - }, [popper, children]); - - return ( -
- {children} -
- ); -}); - interface InplaceTextEditorProps { text: string; - onSave: (newText: string) => void; + onSave: (newText: string) => Promise; readOnly?: boolean; textClassName?: string; - alwaysShowEditButton?: boolean; } export const InplaceTextEditor: React.FC = ({ @@ -50,18 +26,29 @@ export const InplaceTextEditor: React.FC = ({ onSave, readOnly = false, textClassName, - alwaysShowEditButton = false, }) => { const intl = useIntl(); const [inputIsActive, setIsActive] = useState(false); + const [pendingSaveText, setPendingSaveText] = useState(); // state with the new text while updating const handleOnChangeText = useCallback( - (event) => { - const newText = event.target.value; - if (newText && newText !== text) { - onSave(newText); - } + async (event: React.ChangeEvent | React.KeyboardEvent) => { + const inputText = event.currentTarget.value; setIsActive(false); + if (inputText && inputText !== text) { + // NOTE: While using react query for optimistic updates would be the best approach, + // it could not be possible in some cases. For that reason, we use the `pendingSaveText` state + // to show the new text while saving. + setPendingSaveText(inputText); + try { + await onSave(inputText); + } catch { + // don't propagate the exception + } finally { + // reset the pending save text + setPendingSaveText(undefined); + } + } }, [text], ); @@ -78,86 +65,46 @@ export const InplaceTextEditor: React.FC = ({ } }; - if (readOnly) { - return ( - - {text} - - ); - } - - if (alwaysShowEditButton) { + // If we have the `pendingSaveText` state it means that we are in the process of saving the new text. + // In that case, we show the new text instead of the original in read-only mode as an optimistic update. + if (readOnly || pendingSaveText) { return ( - - {inputIsActive - ? ( - - ) - : ( - - {text} - - )} - - + + {pendingSaveText || text} + ); } return ( - - - - )} + -
- {inputIsActive - ? ( - - ) - : ( - + {inputIsActive + ? ( + + ) + : ( + <> + {text} - - )} -
-
+ + + + )} + ); }; diff --git a/src/generic/modal-dropzone/ModalDropzone.scss b/src/generic/modal-dropzone/ModalDropzone.scss index e5283f38c8..dbb64e35a1 100644 --- a/src/generic/modal-dropzone/ModalDropzone.scss +++ b/src/generic/modal-dropzone/ModalDropzone.scss @@ -7,7 +7,7 @@ min-height: 300px; .pgn__dropzone { - background: $white; + background: var(--pgn-color-white); height: 100%; min-height: 18.75rem; } diff --git a/src/generic/modal-iframe/index.scss b/src/generic/modal-iframe/index.scss index dd462ffa50..43c87a90f6 100644 --- a/src/generic/modal-iframe/index.scss +++ b/src/generic/modal-iframe/index.scss @@ -4,7 +4,7 @@ left: 0; width: 100%; height: 100%; - z-index: $zindex-modal; + z-index: var(--pgn-elevation-modal-zindex); iframe { width: inherit; diff --git a/src/generic/processing-notification/ProccessingNotification.scss b/src/generic/processing-notification/ProccessingNotification.scss index 455c16a407..0b92c35147 100644 --- a/src/generic/processing-notification/ProccessingNotification.scss +++ b/src/generic/processing-notification/ProccessingNotification.scss @@ -11,5 +11,5 @@ .toast-container { right: 1.25rem; left: unset; - z-index: $zindex-popover; + z-index: var(--pgn-elevation-popover-zindex); } diff --git a/src/generic/section-sub-header/SectionSubHeader.scss b/src/generic/section-sub-header/SectionSubHeader.scss index fc341029e3..cceb85fd8a 100644 --- a/src/generic/section-sub-header/SectionSubHeader.scss +++ b/src/generic/section-sub-header/SectionSubHeader.scss @@ -3,10 +3,10 @@ justify-content: space-between; align-items: center; margin-bottom: .75rem; - border-bottom: $border-width solid $light-400; + border-bottom: var(--pgn-size-border-width) solid var(--pgn-color-light-400); h2 { - color: $black; + color: var(--pgn-color-black); margin-bottom: .75rem; } } diff --git a/src/generic/styles.scss b/src/generic/styles.scss index 00ef459221..22756b9ad1 100644 --- a/src/generic/styles.scss +++ b/src/generic/styles.scss @@ -12,4 +12,5 @@ @import "./modal-dropzone/ModalDropzone"; @import "./configure-modal/ConfigureModal"; @import "./block-type-utils"; -@import "./modal-iframe" +@import "./modal-iframe"; +@import "./alert-message"; diff --git a/src/generic/sub-header/SubHeader.scss b/src/generic/sub-header/SubHeader.scss index dc76222220..ea3029b671 100644 --- a/src/generic/sub-header/SubHeader.scss +++ b/src/generic/sub-header/SubHeader.scss @@ -1,6 +1,6 @@ .sub-header { display: flex; - gap: map-get($spacers, 4\.5); + gap: var(--pgn-spacing-spacer-4-5); .sub-header-actions { margin-bottom: 1.75rem; @@ -9,25 +9,25 @@ } .sub-header-title { - font: normal $font-weight-bold 2rem/2.25rem $font-family-base; - color: $black; + font: normal var(--pgn-typography-font-weight-bold) 2rem/2.25rem var(--pgn-typography-font-family-base); + color: var(--pgn-color-black); .sub-header-title-subtitle, .sub-header-breadcrumbs { - font: normal $font-weight-normal .875rem/1.5rem $font-family-base; + font: normal var(--pgn-typography-font-weight-normal) .875rem/1.5rem var(--pgn-typography-font-family-base); display: block; color: $text-color-base; } } .sub-header-content-title { - font: normal $font-weight-normal 1.375rem/1.5 $font-family-base; + font: normal var(--pgn-typography-font-weight-normal) 1.375rem/1.5 var(--pgn-typography-font-family-base); margin-bottom: 0; - color: $black; + color: var(--pgn-color-black); } .sub-header-instructions { - font: normal $font-weight-normal .875rem/1.5rem $font-family-base; + font: normal var(--pgn-typography-font-weight-normal) .875rem/1.5rem var(--pgn-typography-font-family-base); color: $text-color-base; } diff --git a/src/grading-settings/assignment-section/AssignmentSection.scss b/src/grading-settings/assignment-section/AssignmentSection.scss index 69d17c3aee..2b73f97f0a 100644 --- a/src/grading-settings/assignment-section/AssignmentSection.scss +++ b/src/grading-settings/assignment-section/AssignmentSection.scss @@ -1,8 +1,8 @@ .course-grading-assignment-wrapper { - background-color: $white; - padding: map-get($spacers, 4); + background-color: var(--pgn-color-white); + padding: var(--pgn-spacing-spacer-4); text-align: right; - border: 1px solid $light-700; + border: 1px solid var(--pgn-color-light-700); .course-grading-assignment-items { list-style: none; @@ -33,8 +33,8 @@ .course-grading-assignment-item-alert-warning { .alert-heading { - font-size: $alert-font-size; - line-height: $alert-line-height; + font-size: var(--pgn-typography-alert-font-size); + line-height: var(--pgn-typography-alert-line-height); } .course-grading-assignment-item-alert-warning-list-label { @@ -51,8 +51,8 @@ } .course-grading-assignment-item-alert-success .alert-heading { - font-size: $alert-font-size; - line-height: $alert-line-height; + font-size: var(--pgn-typography-alert-font-size); + line-height: var(--pgn-typography-alert-line-height); margin-bottom: 0; } } diff --git a/src/grading-settings/grading-scale/GradingScale.scss b/src/grading-settings/grading-scale/GradingScale.scss index 1d65f7ceff..e8023f0bb5 100644 --- a/src/grading-settings/grading-scale/GradingScale.scss +++ b/src/grading-settings/grading-scale/GradingScale.scss @@ -2,20 +2,20 @@ display: flex; margin-top: 1.5rem; align-items: center; - margin-bottom: map-get($spacers, 6); + margin-bottom: var(--pgn-spacing-spacer-6); .grading-scale-segments-and-ticks { display: inline-block; height: 3.5rem; width: 100%; - border: 1px solid $black; + border: 1px solid var(--pgn-color-black); overflow: hidden; .grading-scale-tick { .grading-scale-tick-number { position: absolute; font-size: .6rem; - color: $black; + color: var(--pgn-color-black); top: 100%; transform: translate(-50%, 1.2rem); white-space: nowrap; @@ -25,7 +25,7 @@ content: ""; position: absolute; left: 0; - background-color: $gray-400; + background-color: var(--pgn-color-gray-400); height: .3125rem; width: .125rem; transform: translate(-50%, .7rem); @@ -40,10 +40,10 @@ height: 100%; padding: 0 .4375rem; width: .625rem; - z-index: $zindex-dropdown !important; + z-index: var(--pgn-elevation-dropdown-zindex) !important; &:disabled { - color: $black; + color: var(--pgn-color-black); } } @@ -86,7 +86,7 @@ } .grading-scale-segment-content-title { - font: normal $font-weight-semi-bold 1rem/1 $font-family-base; + font: normal var(--pgn-typography-font-weight-semi-bold) 1rem/1 var(--pgn-typography-font-family-base); border: none; outline: none; background-color: transparent; @@ -95,16 +95,16 @@ text-align: end; &:disabled { - color: $black; + color: var(--pgn-color-black); } } .grading-scale-segment-content-number { - font: normal $font-weight-normal .75rem/1 $font-family-base; - color: $black; + font: normal var(--pgn-typography-font-weight-normal) .75rem/1 var(--pgn-typography-font-family-base); + color: var(--pgn-color-black); &:disabled { - color: $black; + color: var(--pgn-color-black); } } } diff --git a/src/grading-settings/scss/GradingSettings.scss b/src/grading-settings/scss/GradingSettings.scss index 0de7790900..f265177091 100644 --- a/src/grading-settings/scss/GradingSettings.scss +++ b/src/grading-settings/scss/GradingSettings.scss @@ -8,12 +8,12 @@ } .grading-label { - font: normal $font-weight-bold .75rem/1.25rem $font-family-base; - color: $gray-500; + font: normal var(--pgn-typography-font-weight-bold) .75rem/1.25rem var(--pgn-typography-font-family-base); + color: var(--pgn-color-gray-500); } .grading-description { - font: normal .75rem/1.5rem $font-family-base; - color: $gray-700; + font: normal .75rem/1.5rem var(--pgn-typography-font-family-base); + color: var(--pgn-color-gray-700); } } diff --git a/src/group-configurations/GroupConfigurations.scss b/src/group-configurations/GroupConfigurations.scss index 6faa08f315..2bf5c904a6 100644 --- a/src/group-configurations/GroupConfigurations.scss +++ b/src/group-configurations/GroupConfigurations.scss @@ -13,12 +13,11 @@ } .configuration-card { - @include pgn-box-shadow(1, "down"); - - background: $white; + box-shadow: var(--pgn-elevation-box-shadow-down-1); + background: var(--pgn-color-white); border-radius: .375rem; - padding: map-get($spacers, 4); - margin-bottom: map-get($spacers, 4); + padding: var(--pgn-spacing-spacer-4); + margin-bottom: var(--pgn-spacing-spacer-4); .configuration-card-header { display: flex; @@ -30,7 +29,7 @@ align-items: flex-start; padding: 0; height: auto; - color: $black; + color: var(--pgn-color-black); &:focus::before { display: none; @@ -38,8 +37,8 @@ .pgn__icon { display: inline-block; - margin-right: map-get($spacers, 1); - margin-bottom: map-get($spacers, 2\.5); + margin-right: var(--pgn-spacing-spacer-1); + margin-bottom: var(--pgn-spacing-spacer-2-5); } .pgn__hstack { @@ -55,25 +54,25 @@ text-align: left; h3 { - margin-bottom: map-get($spacers, 2); + margin-bottom: var(--pgn-spacing-spacer-2); } } .configuration-card-header__badge { display: flex; - padding: .125rem map-get($spacers, 2); + padding: .125rem var(--pgn-spacing-spacer-2); justify-content: center; align-items: center; - border-radius: $border-radius; - border: .063rem solid $light-300; - background: $white; + border-radius: var(--pgn-size-border-radius-base); + border: .063rem solid var(--pgn-color-light-300); + background: var(--pgn-color-white); &:first-child { - margin-left: map-get($spacers, 2\.5); + margin-left: var(--pgn-spacing-spacer-2-5); } & span:last-child { - color: $primary-700; + color: var(--pgn-color-primary-700); } } @@ -83,18 +82,18 @@ } .configuration-card-content { - margin: 0 map-get($spacers, 2) 0 map-get($spacers, 4); + margin: 0 var(--pgn-spacing-spacer-2) 0 var(--pgn-spacing-spacer-4); .configuration-card-content__experiment-stack { display: flex; justify-content: space-between; - padding: map-get($spacers, 2\.5) 0; + padding: var(--pgn-spacing-spacer-2-5) 0; margin: 0; - color: $primary-500; - gap: $spacer; + color: var(--pgn-color-primary-500); + gap: var(--pgn-spacing-spacer-base); &:not(:last-child) { - border-bottom: .063rem solid $light-400; + border-bottom: .063rem solid var(--pgn-color-light-400); } } } @@ -105,20 +104,20 @@ .configuration-form-group { .pgn__form-label { - font: normal $font-weight-bold .875rem/1.25rem $font-family-base; - color: $gray-700; + font: normal var(--pgn-typography-font-weight-bold) .875rem/1.25rem var(--pgn-typography-font-family-base); + color: var(--pgn-color-gray-700); margin-bottom: .875rem; } .pgn__form-control-description, .pgn__form-text { - font: normal $font-weight-normal .75rem/1.25rem $font-family-base; - color: $gray-500; + font: normal var(--pgn-typography-font-weight-normal) .75rem/1.25rem var(--pgn-typography-font-family-base); + color: var(--pgn-color-gray-500); margin-top: .625rem; } .pgn__form-text-invalid { - color: $form-feedback-invalid-color; + color: var(--pgn-color-form-feedback-invalid); } } diff --git a/src/group-configurations/common/TitleButton.jsx b/src/group-configurations/common/TitleButton.jsx index 87d5d50016..d6edfb0a06 100644 --- a/src/group-configurations/common/TitleButton.jsx +++ b/src/group-configurations/common/TitleButton.jsx @@ -27,7 +27,7 @@ const TitleButton = ({ >

- {name} + {name}

{formatMessage(messages.titleId, { id })} diff --git a/src/group-configurations/empty-placeholder/EmptyPlaceholder.scss b/src/group-configurations/empty-placeholder/EmptyPlaceholder.scss index 1768ecac81..06b79c136b 100644 --- a/src/group-configurations/empty-placeholder/EmptyPlaceholder.scss +++ b/src/group-configurations/empty-placeholder/EmptyPlaceholder.scss @@ -1,6 +1,5 @@ .group-configurations-empty-placeholder { - @include pgn-box-shadow(1, "down"); - + box-shadow: var(--pgn-elevation-box-shadow-down-1); display: flex; align-items: center; justify-content: center; diff --git a/src/import-page/file-section/FileSection.jsx b/src/import-page/file-section/FileSection.jsx index bbc89a53bd..740e6d9e98 100644 --- a/src/import-page/file-section/FileSection.jsx +++ b/src/import-page/file-section/FileSection.jsx @@ -41,7 +41,7 @@ const FileSection = ({ intl, courseId }) => { handleError, )) } - accept={{ 'application/gzip': ['.tar.gz'] }} + accept={{ 'application/x-tar.gz': ['.tar.gz'] }} data-testid="dropzone" style={{ height: '200px' }} /> diff --git a/src/index.scss b/src/index.scss index c5d9bcb769..cd18b8836d 100644 --- a/src/index.scss +++ b/src/index.scss @@ -1,7 +1,5 @@ -@import "~@edx/brand/paragon/fonts"; -@import "~@edx/brand/paragon/variables"; -@import "~@openedx/paragon/scss/core/core"; -@import "~@edx/brand/paragon/overrides"; +@use "@openedx/paragon/styles/css/core/custom-media-breakpoints.css" as paragonCustomMediaBreakpoints; + @import "~@edx/frontend-component-header/dist/index"; @import "assets/scss/variables"; @import "assets/scss/form"; @@ -47,7 +45,7 @@ div.xblock-highlight { @keyframes glow { 0% { - box-shadow: 0 0 5px 5px $primary-500; + box-shadow: 0 0 5px 5px var(--pgn-color-primary-500); } 100% { @@ -56,10 +54,10 @@ div.xblock-highlight { } body { - background-color: $light-200; + background-color: var(--pgn-color-light-200); .editor-page { - background-color: $light-100; + background-color: var(--pgn-color-light-100); } } diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index ce07e0c70f..f0c0404c90 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -55,6 +55,10 @@ const path = '/library/:libraryId/*'; const libraryTitle = mockContentLibrary.libraryData.title; describe('', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + beforeEach(async () => { const mocks = initializeMocks(); axiosMock = mocks.axiosMock; @@ -78,6 +82,10 @@ describe('', () => { }); }); + afterAll(() => { + jest.useRealTimers(); + }); + const renderLibraryPage = async () => { render(, { path, params: { libraryId: mockContentLibrary.libraryId } }); @@ -362,7 +370,7 @@ describe('', () => { fireEvent.change(searchBox, { target: { value: 'words to find' } }); // Default sort option changes to "Most Relevant" - expect(screen.getAllByText('Most Relevant').length).toEqual(2); + expect((await screen.findAllByText('Most Relevant')).length).toEqual(2); await waitFor(() => { expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, { body: expect.stringContaining('"sort":[]'), @@ -392,7 +400,7 @@ describe('', () => { await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument()); }); - it('should open component sidebar, showing manage tab on clicking add to collection menu item (component)', async () => { + it('should open component sidebar, showing manage tab on clicking add to collection menu item - component', async () => { const mockResult0 = { ...mockResult }.results[0].hits[0]; const displayName = 'Introduction to Testing'; expect(mockResult0.display_name).toStrictEqual(displayName); @@ -407,9 +415,10 @@ describe('', () => { const sidebar = screen.getByTestId('library-sidebar'); - const { getByRole, queryByText } = within(sidebar); + const { getByRole, findByText } = within(sidebar); - await waitFor(() => expect(queryByText(displayName)).toBeInTheDocument()); + expect(await findByText(displayName)).toBeInTheDocument(); + jest.advanceTimersByTime(300); expect(getByRole('tab', { selected: true })).toHaveTextContent('Manage'); const closeButton = getByRole('button', { name: /close/i }); fireEvent.click(closeButton); @@ -417,7 +426,7 @@ describe('', () => { await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument()); }); - it('should open component sidebar, showing manage tab on clicking add to collection menu item (unit)', async () => { + it('should open component sidebar, showing manage tab on clicking add to collection menu item - unit', async () => { const displayName = 'Test Unit'; await renderLibraryPage(); @@ -430,10 +439,11 @@ describe('', () => { const sidebar = screen.getByTestId('library-sidebar'); - const { getByRole, queryByText } = within(sidebar); + const { getByRole, findByText } = within(sidebar); - await waitFor(() => expect(queryByText(displayName)).toBeInTheDocument()); - expect(getByRole('tab', { selected: true })).toHaveTextContent('Organize'); + expect(await findByText(displayName)).toBeInTheDocument(); + jest.advanceTimersByTime(300); + expect(getByRole('tab', { selected: true })).toHaveTextContent('Manage'); const closeButton = getByRole('button', { name: /close/i }); fireEvent.click(closeButton); diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 6ffc4182de..b49ae07e76 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -15,12 +15,11 @@ import { Breadcrumb, Button, Container, - Icon, Stack, Tab, Tabs, } from '@openedx/paragon'; -import { Add, ArrowBack, InfoOutline } from '@openedx/paragon/icons'; +import { Add, InfoOutline } from '@openedx/paragon/icons'; import { Link } from 'react-router-dom'; import Loading from '../generic/Loading'; @@ -32,7 +31,6 @@ import { ClearFiltersButton, FilterByBlockType, FilterByTags, - FilterByPublished, SearchContextProvider, SearchKeywordsField, SearchSortWidget, @@ -46,6 +44,7 @@ import { SidebarBodyComponentId, useSidebarContext } from './common/context/Side import { allLibraryPageTabs, ContentType, useLibraryRoutes } from './routes'; import messages from './messages'; +import LibraryFilterByPublished from './generic/filter-by-published'; const HeaderActions = () => { const intl = useIntl(); @@ -114,7 +113,7 @@ export const SubHeaderTitle = ({ title }: { title: ReactNode }) => { const showReadOnlyBadge = readOnly && !componentPickerMode; return ( - + {title} {showReadOnlyBadge && (
@@ -214,16 +213,11 @@ const LibraryAuthoringPage = ({ const breadcumbs = componentPickerMode && !restrictToLibrary ? ( } linkAs={Link} /> ) : undefined; @@ -246,6 +240,17 @@ const LibraryAuthoringPage = ({ extraFilter.push(activeTypeFilters[activeKey]); } + /* + + ( - + )); return ( @@ -299,7 +304,14 @@ const LibraryAuthoringPage = ({ {!(insideCollections || insideUnits) && } - + diff --git a/src/library-authoring/LibraryBlock/LibraryBlock.tsx b/src/library-authoring/LibraryBlock/LibraryBlock.tsx index c87091acdb..52e0794e03 100644 --- a/src/library-authoring/LibraryBlock/LibraryBlock.tsx +++ b/src/library-authoring/LibraryBlock/LibraryBlock.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { getConfig } from '@edx/frontend-platform'; @@ -16,6 +17,7 @@ interface LibraryBlockProps { view?: string; scrolling?: string; minHeight?: string; + scrollIntoView?: boolean; } /** * React component that displays an XBlock in a sandboxed IFrame. @@ -33,6 +35,7 @@ export const LibraryBlock = ({ view, minHeight, scrolling = 'no', + scrollIntoView = false, }: LibraryBlockProps) => { const { iframeRef, setIframeRef } = useIframe(); const xblockView = view ?? 'student_view'; @@ -49,6 +52,13 @@ export const LibraryBlock = ({ onBlockNotification, }); + useEffect(() => { + /* istanbul ignore next */ + if (scrollIntoView) { + iframeRef?.current?.scrollIntoView({ behavior: 'smooth' }); + } + }, [scrollIntoView]); + useIframeContent(iframeRef, setIframeRef); return ( diff --git a/src/library-authoring/__mocks__/library-search.json b/src/library-authoring/__mocks__/library-search.json index 9eee970312..ba27af2185 100644 --- a/src/library-authoring/__mocks__/library-search.json +++ b/src/library-authoring/__mocks__/library-search.json @@ -494,7 +494,7 @@ ], "created": 1742221203.895054, "modified": 1742221203.895054, - "usage_key": "lct:Axim:TEST:unit:test-unit-9284e2", + "usage_key": "lct:org:lib:unit:test-unit-9a207", "block_type": "unit", "context_key": "lib:Axim:TEST", "org": "Axim", @@ -512,12 +512,18 @@ ], "created": "1742221203.895054", "modified": "1742221203.895054", - "usage_key": "lct:Axim:TEST:unit:test-unit-9284e2", + "usage_key": "lct:org:lib:unit:test-unit-9a207", "block_type": "unit", "context_key": "lib:Axim:TEST", "org": "Axim", "access_id": "15", - "num_children": "0" + "num_children": "0", + "published": { + "display_name": "Published Test Unit" + } + }, + "published": { + "display_name": "Published Test Unit" } } ], diff --git a/src/library-authoring/add-content/AddContent.test.tsx b/src/library-authoring/add-content/AddContent.test.tsx index 8820981c0e..09f01bd174 100644 --- a/src/library-authoring/add-content/AddContent.test.tsx +++ b/src/library-authoring/add-content/AddContent.test.tsx @@ -272,7 +272,7 @@ describe('', () => { await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(pasteUrl)); await waitFor(() => expect(axiosMock.history.patch.length).toEqual(1)); await waitFor(() => expect(axiosMock.history.patch[0].url).toEqual(collectionComponentUrl)); - expect(mockShowToast).toHaveBeenCalledWith('There was an error linking the content to this collection.'); + expect(mockShowToast).toHaveBeenCalledWith('Failed to add content to collection.'); }); it('should stop user from pasting unsupported blocks and show toast', async () => { diff --git a/src/library-authoring/add-content/AddContent.tsx b/src/library-authoring/add-content/AddContent.tsx index 7610b743f6..8ddbdae354 100644 --- a/src/library-authoring/add-content/AddContent.tsx +++ b/src/library-authoring/add-content/AddContent.tsx @@ -29,6 +29,8 @@ import { useLibraryContext } from '../common/context/LibraryContext'; import { PickLibraryContentModal } from './PickLibraryContentModal'; import { blockTypes } from '../../editors/data/constants/app'; +import { ContentType as LibraryContentTypes } from '../routes'; +import genericMessages from '../generic/messages'; import messages from './messages'; import type { BlockTypeMetadata } from '../data/api'; import { getContainerTypeFromId, ContainerType } from '../../generic/key-utils'; @@ -114,6 +116,9 @@ const AddContentView = ({ blockType: 'libraryContent', }; + const extraFilter = unitId ? ['NOT block_type = "unit"', 'NOT type = "collections"'] : undefined; + const visibleTabs = unitId ? [LibraryContentTypes.components] : undefined; + return ( <> {(collectionId || unitId) && componentPicker && ( @@ -123,6 +128,8 @@ const AddContentView = ({ )} @@ -301,7 +308,7 @@ const AddContent = () => { const linkComponent = (opaqueKey: string) => { if (collectionId) { addComponentsToCollectionMutation.mutateAsync([opaqueKey]).catch(() => { - showToast(intl.formatMessage(messages.errorAssociateComponentToCollectionMessage)); + showToast(intl.formatMessage(genericMessages.manageCollectionsFailed)); }); } if (unitId) { diff --git a/src/library-authoring/add-content/PickLibraryContentModal.test.tsx b/src/library-authoring/add-content/PickLibraryContentModal.test.tsx index a73ce8118e..982b657e8b 100644 --- a/src/library-authoring/add-content/PickLibraryContentModal.test.tsx +++ b/src/library-authoring/add-content/PickLibraryContentModal.test.tsx @@ -92,7 +92,10 @@ describe('', () => { } }); expect(onClose).toHaveBeenCalled(); - expect(mockShowToast).toHaveBeenCalledWith('Content linked successfully.'); + const text = context === 'collection' + ? 'Content added to collection.' + : 'Content linked successfully.'; + expect(mockShowToast).toHaveBeenCalledWith(text); }); it(`show error when api call fails (${context})`, async () => { @@ -130,8 +133,10 @@ describe('', () => { } }); expect(onClose).toHaveBeenCalled(); - const name = context === 'collection' ? 'collection' : 'container'; - expect(mockShowToast).toHaveBeenCalledWith(`There was an error linking the content to this ${name}.`); + const text = context === 'collection' + ? 'Failed to add content to collection.' + : 'There was an error linking the content to this container.'; + expect(mockShowToast).toHaveBeenCalledWith(text); }); }); }); diff --git a/src/library-authoring/add-content/PickLibraryContentModal.tsx b/src/library-authoring/add-content/PickLibraryContentModal.tsx index f71f40d081..4f243f5bf1 100644 --- a/src/library-authoring/add-content/PickLibraryContentModal.tsx +++ b/src/library-authoring/add-content/PickLibraryContentModal.tsx @@ -6,6 +6,8 @@ import { ToastContext } from '../../generic/toast-context'; import { useLibraryContext } from '../common/context/LibraryContext'; import type { SelectedComponent } from '../common/context/ComponentPickerContext'; import { useAddItemsToCollection, useAddComponentsToContainer } from '../data/apiHooks'; +import genericMessages from '../generic/messages'; +import type { ContentType } from '../routes'; import messages from './messages'; interface PickLibraryContentModalFooterProps { @@ -32,12 +34,14 @@ interface PickLibraryContentModalProps { isOpen: boolean; onClose: () => void; extraFilter?: string[]; + visibleTabs?: ContentType[], } export const PickLibraryContentModal: React.FC = ({ isOpen, onClose, extraFilter, + visibleTabs, }) => { const intl = useIntl(); @@ -69,16 +73,16 @@ export const PickLibraryContentModal: React.FC = ( if (collectionId) { updateCollectionItemsMutation.mutateAsync(usageKeys) .then(() => { - showToast(intl.formatMessage(messages.successAssociateComponentMessage)); + showToast(intl.formatMessage(genericMessages.manageCollectionsSuccess)); }) .catch(() => { - showToast(intl.formatMessage(messages.errorAssociateComponentToCollectionMessage)); + showToast(intl.formatMessage(genericMessages.manageCollectionsFailed)); }); } if (unitId) { updateUnitComponentsMutation.mutateAsync(usageKeys) .then(() => { - showToast(intl.formatMessage(messages.successAssociateComponentMessage)); + showToast(intl.formatMessage(messages.successAssociateComponentToContainerMessage)); }) .catch(() => { showToast(intl.formatMessage(messages.errorAssociateComponentToContainerMessage)); @@ -109,6 +113,7 @@ export const PickLibraryContentModal: React.FC = ( componentPickerMode="multiple" onChangeComponentSelection={setSelectedComponents} extraFilter={extraFilter} + visibleTabs={visibleTabs} /> ); diff --git a/src/library-authoring/add-content/messages.ts b/src/library-authoring/add-content/messages.ts index 120b5896fb..cd7e688c5e 100644 --- a/src/library-authoring/add-content/messages.ts +++ b/src/library-authoring/add-content/messages.ts @@ -84,15 +84,10 @@ const messages = defineMessages({ + ' The {detail} text provides more information about the error.' ), }, - successAssociateComponentMessage: { - id: 'course-authoring.library-authoring.associate-collection-content.success.text', + successAssociateComponentToContainerMessage: { + id: 'course-authoring.library-authoring.associate-container-content.success.text', defaultMessage: 'Content linked successfully.', - description: 'Message when linking of content to a collection in library is success', - }, - errorAssociateComponentToCollectionMessage: { - id: 'course-authoring.library-authoring.associate-collection-content.error.text', - defaultMessage: 'There was an error linking the content to this collection.', - description: 'Message when linking of content to a collection in library fails', + description: 'Message when linking of content to a container in library is success', }, errorAssociateComponentToContainerMessage: { id: 'course-authoring.library-authoring.associate-container-content.error.text', diff --git a/src/library-authoring/collections/CollectionInfo.tsx b/src/library-authoring/collections/CollectionInfo.tsx index 277a6fc1c6..68d2dcb075 100644 --- a/src/library-authoring/collections/CollectionInfo.tsx +++ b/src/library-authoring/collections/CollectionInfo.tsx @@ -48,7 +48,8 @@ const CollectionInfo = () => { if (componentPickerMode) { setCollectionId(collectionId); } else { - navigateTo({ collectionId }); + /* istanbul ignore next */ + navigateTo({ collectionId, doubleClicked: true }); } }, [componentPickerMode, navigateTo]); diff --git a/src/library-authoring/collections/CollectionInfoHeader.tsx b/src/library-authoring/collections/CollectionInfoHeader.tsx index 8f476d35ef..b885d72da2 100644 --- a/src/library-authoring/collections/CollectionInfoHeader.tsx +++ b/src/library-authoring/collections/CollectionInfoHeader.tsx @@ -26,14 +26,16 @@ const CollectionInfoHeader = () => { const updateMutation = useUpdateCollection(libraryId, collectionId); const { showToast } = useContext(ToastContext); - const handleSaveTitle = (newTitle: string) => { - updateMutation.mutateAsync({ - title: newTitle, - }).then(() => { + const handleSaveTitle = async (newTitle: string) => { + try { + await updateMutation.mutateAsync({ + title: newTitle, + }); showToast(intl.formatMessage(messages.updateCollectionSuccessMsg)); - }).catch(() => { + } catch (err) { showToast(intl.formatMessage(messages.updateCollectionErrorMsg)); - }); + throw err; + } }; if (!collection) { @@ -46,7 +48,6 @@ const CollectionInfoHeader = () => { text={collection.title} readOnly={readOnly} textClassName="font-weight-bold m-1.5" - alwaysShowEditButton /> ); }; diff --git a/src/library-authoring/collections/LibraryCollectionPage.test.tsx b/src/library-authoring/collections/LibraryCollectionPage.test.tsx index 3c3bf45474..eb102d2ed4 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.test.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.test.tsx @@ -315,7 +315,7 @@ describe('', () => { fireEvent.change(searchBox, { target: { value: 'words to find' } }); // Default sort option changes to "Most Relevant" - expect(screen.getAllByText('Most Relevant').length).toEqual(2); + expect((await screen.findAllByText('Most Relevant')).length).toEqual(2); await waitFor(() => { expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, { body: expect.stringContaining('"sort":[]'), diff --git a/src/library-authoring/collections/LibraryCollectionPage.tsx b/src/library-authoring/collections/LibraryCollectionPage.tsx index de3c7ce234..943664788d 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.tsx @@ -22,7 +22,6 @@ import NotFoundAlert from '../../generic/NotFoundAlert'; import { ClearFiltersButton, FilterByBlockType, - FilterByPublished, FilterByTags, SearchContextProvider, SearchKeywordsField, @@ -36,6 +35,7 @@ import { SidebarBodyComponentId, useSidebarContext } from '../common/context/Sid import messages from './messages'; import { LibrarySidebar } from '../library-sidebar'; import LibraryCollectionComponents from './LibraryCollectionComponents'; +import LibraryFilterByPublished from '../generic/filter-by-published'; const HeaderActions = () => { const intl = useIntl(); @@ -218,7 +218,7 @@ const LibraryCollectionPage = () => { - + diff --git a/src/library-authoring/common/context/SidebarContext.tsx b/src/library-authoring/common/context/SidebarContext.tsx index 83e545e8eb..0657842876 100644 --- a/src/library-authoring/common/context/SidebarContext.tsx +++ b/src/library-authoring/common/context/SidebarContext.tsx @@ -36,7 +36,7 @@ export const isComponentInfoTab = (tab: string): tab is ComponentInfoTab => ( export const UNIT_INFO_TABS = { Preview: 'preview', - Organize: 'organize', + Manage: 'manage', Usage: 'usage', Settings: 'settings', } as const; @@ -63,7 +63,8 @@ export interface SidebarComponentInfo { } export enum SidebarActions { - JumpToAddCollections = 'jump-to-add-collections', + JumpToManageCollections = 'jump-to-manage-collections', + JumpToManageTags = 'jump-to-manage-tags', ManageTeam = 'manage-team', None = '', } diff --git a/src/library-authoring/component-info/ComponentDetails.test.tsx b/src/library-authoring/component-info/ComponentDetails.test.tsx index 53671cd7e4..7f7b1e3bb4 100644 --- a/src/library-authoring/component-info/ComponentDetails.test.tsx +++ b/src/library-authoring/component-info/ComponentDetails.test.tsx @@ -8,7 +8,7 @@ import { import { mockFetchIndexDocuments, mockContentSearchConfig } from '../../search-manager/data/api.mock'; import { mockContentLibrary, - mockGetUnpaginatedEntityLinks, + mockGetEntityLinks, mockLibraryBlockMetadata, mockXBlockAssets, mockXBlockOLX, @@ -21,7 +21,7 @@ mockContentLibrary.applyMock(); mockLibraryBlockMetadata.applyMock(); mockXBlockAssets.applyMock(); mockXBlockOLX.applyMock(); -mockGetUnpaginatedEntityLinks.applyMock(); +mockGetEntityLinks.applyMock(); mockFetchIndexDocuments.applyMock(); const render = (usageKey: string) => baseRender(, { diff --git a/src/library-authoring/component-info/ComponentInfo.test.tsx b/src/library-authoring/component-info/ComponentInfo.test.tsx index 2206be3e50..0427e1b345 100644 --- a/src/library-authoring/component-info/ComponentInfo.test.tsx +++ b/src/library-authoring/component-info/ComponentInfo.test.tsx @@ -7,7 +7,7 @@ import { import { mockContentLibrary, mockLibraryBlockMetadata, - mockGetUnpaginatedEntityLinks, + mockGetEntityLinks, } from '../data/api.mocks'; import { mockContentSearchConfig, mockFetchIndexDocuments } from '../../search-manager/data/api.mock'; import { LibraryProvider } from '../common/context/LibraryContext'; @@ -18,7 +18,7 @@ import { getXBlockPublishApiUrl } from '../data/api'; mockContentSearchConfig.applyMock(); mockContentLibrary.applyMock(); mockLibraryBlockMetadata.applyMock(); -mockGetUnpaginatedEntityLinks.applyMock(); +mockGetEntityLinks.applyMock(); mockFetchIndexDocuments.applyMock(); jest.mock('./ComponentPreview', () => ({ __esModule: true, // Required when mocking 'default' export diff --git a/src/library-authoring/component-info/ComponentInfo.tsx b/src/library-authoring/component-info/ComponentInfo.tsx index 1b542c6956..635e54cc7e 100644 --- a/src/library-authoring/component-info/ComponentInfo.tsx +++ b/src/library-authoring/component-info/ComponentInfo.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Button, @@ -17,7 +17,6 @@ import { useLibraryContext } from '../common/context/LibraryContext'; import { type ComponentInfoTab, COMPONENT_INFO_TABS, - SidebarActions, isComponentInfoTab, useSidebarContext, } from '../common/context/SidebarContext'; @@ -107,9 +106,9 @@ const ComponentInfo = () => { sidebarTab, setSidebarTab, sidebarComponentInfo, - sidebarAction, defaultTab, hiddenTabs, + resetSidebarAction, } = useSidebarContext(); const [ isPublishConfirmationOpen, @@ -117,20 +116,16 @@ const ComponentInfo = () => { closePublishConfirmation, ] = useToggle(false); - const jumpToCollections = sidebarAction === SidebarActions.JumpToAddCollections; - const tab: ComponentInfoTab = ( isComponentInfoTab(sidebarTab) ? sidebarTab : defaultTab.component ); - useEffect(() => { - // Show Manage tab if JumpToAddCollections action is set in sidebarComponentInfo - if (jumpToCollections) { - setSidebarTab(COMPONENT_INFO_TABS.Manage); - } - }, [jumpToCollections, setSidebarTab]); + const handleTabChange = (newTab: ComponentInfoTab) => { + resetSidebarAction(); + setSidebarTab(newTab); + }; const usageKey = sidebarComponentInfo?.id; // istanbul ignore if: this should never happen @@ -198,7 +193,7 @@ const ComponentInfo = () => { className="my-3 d-flex justify-content-around" defaultActiveKey={defaultTab.component} activeKey={tab} - onSelect={setSidebarTab} + onSelect={handleTabChange} > {renderTab(COMPONENT_INFO_TABS.Preview, , intl.formatMessage(messages.previewTabTitle))} {renderTab(COMPONENT_INFO_TABS.Manage, , intl.formatMessage(messages.manageTabTitle))} diff --git a/src/library-authoring/component-info/ComponentInfoHeader.tsx b/src/library-authoring/component-info/ComponentInfoHeader.tsx index 0757c9775d..11b3256945 100644 --- a/src/library-authoring/component-info/ComponentInfoHeader.tsx +++ b/src/library-authoring/component-info/ComponentInfoHeader.tsx @@ -26,16 +26,18 @@ const ComponentInfoHeader = () => { const updateMutation = useUpdateXBlockFields(usageKey); const { showToast } = useContext(ToastContext); - const handleSaveDisplayName = (newDisplayName: string) => { - updateMutation.mutateAsync({ - metadata: { - display_name: newDisplayName, - }, - }).then(() => { + const handleSaveDisplayName = async (newDisplayName: string) => { + try { + await updateMutation.mutateAsync({ + metadata: { + display_name: newDisplayName, + }, + }); showToast(intl.formatMessage(messages.updateComponentSuccessMsg)); - }).catch(() => { + } catch (err) { showToast(intl.formatMessage(messages.updateComponentErrorMsg)); - }); + throw err; + } }; if (!xblockFields) { @@ -48,7 +50,6 @@ const ComponentInfoHeader = () => { text={xblockFields?.displayName} readOnly={readOnly} textClassName="font-weight-bold m-1.5" - alwaysShowEditButton /> ); }; diff --git a/src/library-authoring/component-info/ComponentManagement.test.tsx b/src/library-authoring/component-info/ComponentManagement.test.tsx index 9e070cde96..0e44912652 100644 --- a/src/library-authoring/component-info/ComponentManagement.test.tsx +++ b/src/library-authoring/component-info/ComponentManagement.test.tsx @@ -8,7 +8,7 @@ import { waitFor, } from '../../testUtils'; import { LibraryProvider } from '../common/context/LibraryContext'; -import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext'; +import { SidebarActions, SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext'; import { mockContentLibrary, mockLibraryBlockMetadata } from '../data/api.mocks'; import ComponentManagement from './ComponentManagement'; @@ -19,6 +19,16 @@ jest.mock('../../content-tags-drawer', () => ({ ), })); +const mockSearchParam = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), // use actual for all non-hook parts + useSearchParams: () => [ + { getAll: (paramName: string) => mockSearchParam(paramName) }, + () => {}, + ], +})); + mockContentLibrary.applyMock(); mockLibraryBlockMetadata.applyMock(); mockContentTaxonomyTagsData.applyMock(); @@ -55,6 +65,11 @@ const render = (usageKey: string, libraryId?: string) => baseRender(', () => { beforeEach(() => { initializeMocks(); + mockSearchParam.mockResolvedValue([undefined, () => {}]); + }); + + afterEach(() => { + jest.clearAllMocks(); }); it('should render draft status', async () => { @@ -119,4 +134,34 @@ describe('', () => { render(mockLibraryBlockMetadata.usageKeyWithCollections); expect(await screen.findByText('Collections (1)')).toBeInTheDocument(); }); + + it('should open collection section when sidebarAction = JumpToManageCollections', async () => { + setConfig({ + ...getConfig(), + ENABLE_TAGGING_TAXONOMY_PAGES: 'true', + }); + mockSearchParam.mockReturnValue([SidebarActions.JumpToManageCollections]); + render(mockLibraryBlockMetadata.usageKeyWithCollections); + expect(await screen.findByText('Collections (1)')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Manage tags' })).not.toBeInTheDocument(); + const tagsSection = await screen.findByRole('button', { name: 'Tags (0)' }); + expect(tagsSection).toHaveAttribute('aria-expanded', 'false'); + const collectionsSection = await screen.findByRole('button', { name: 'Collections (1)' }); + expect(collectionsSection).toHaveAttribute('aria-expanded', 'true'); + }); + + it('should open tags section when sidebarAction = JumpToManageTags', async () => { + setConfig({ + ...getConfig(), + ENABLE_TAGGING_TAXONOMY_PAGES: 'true', + }); + mockSearchParam.mockReturnValue([SidebarActions.JumpToManageTags]); + render(mockLibraryBlockMetadata.usageKeyForTags); + expect(await screen.findByText('Collections (0)')).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Manage tags' })).not.toBeInTheDocument(); + const tagsSection = await screen.findByRole('button', { name: 'Tags (6)' }); + expect(tagsSection).toHaveAttribute('aria-expanded', 'true'); + const collectionsSection = await screen.findByRole('button', { name: 'Collections (0)' }); + expect(collectionsSection).toHaveAttribute('aria-expanded', 'false'); + }); }); diff --git a/src/library-authoring/component-info/ComponentManagement.tsx b/src/library-authoring/component-info/ComponentManagement.tsx index c0eccdc5e4..5c3b6a664b 100644 --- a/src/library-authoring/component-info/ComponentManagement.tsx +++ b/src/library-authoring/component-info/ComponentManagement.tsx @@ -18,7 +18,8 @@ const ComponentManagement = () => { const intl = useIntl(); const { readOnly, isLoadingLibraryData } = useLibraryContext(); const { sidebarComponentInfo, sidebarAction, resetSidebarAction } = useSidebarContext(); - const jumpToCollections = sidebarAction === SidebarActions.JumpToAddCollections; + const jumpToCollections = sidebarAction === SidebarActions.JumpToManageCollections; + const jumpToTags = sidebarAction === SidebarActions.JumpToManageTags; const [tagsCollapseIsOpen, setTagsCollapseOpen] = React.useState(!jumpToCollections); const [collectionsCollapseIsOpen, setCollectionsCollapseOpen] = React.useState(true); @@ -26,8 +27,11 @@ const ComponentManagement = () => { if (jumpToCollections) { setTagsCollapseOpen(false); setCollectionsCollapseOpen(true); + } else if (jumpToTags) { + setTagsCollapseOpen(true); + setCollectionsCollapseOpen(false); } - }, [jumpToCollections, tagsCollapseIsOpen, collectionsCollapseIsOpen]); + }, [jumpToCollections, jumpToTags]); useEffect(() => { // This is required to redo actions. diff --git a/src/library-authoring/component-info/ComponentPreview.scss b/src/library-authoring/component-info/ComponentPreview.scss index 0baf5ebcb2..a857c0b1c5 100644 --- a/src/library-authoring/component-info/ComponentPreview.scss +++ b/src/library-authoring/component-info/ComponentPreview.scss @@ -1,3 +1,3 @@ .component-preview-modal { - min-width: map-get($grid-breakpoints, "md"); + min-width: var(--pgn-size-breakpoint-md); } diff --git a/src/library-authoring/component-info/ComponentUsage.tsx b/src/library-authoring/component-info/ComponentUsage.tsx index 48c97fba8b..13e59565f1 100644 --- a/src/library-authoring/component-info/ComponentUsage.tsx +++ b/src/library-authoring/component-info/ComponentUsage.tsx @@ -2,7 +2,7 @@ import { getConfig } from '@edx/frontend-platform'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { Collapsible, Hyperlink, Stack } from '@openedx/paragon'; import { useMemo } from 'react'; -import { useUnpaginatedEntityLinks } from '../../course-libraries/data/apiHooks'; +import { useEntityLinks } from '../../course-libraries/data/apiHooks'; import AlertError from '../../generic/alert-error'; import Loading from '../../generic/Loading'; @@ -34,7 +34,7 @@ export const ComponentUsage = ({ usageKey }: ComponentUsageProps) => { isError: isErrorDownstreamLinks, error: errorDownstreamLinks, isLoading: isLoadingDownstreamLinks, - } = useUnpaginatedEntityLinks({ upstreamUsageKey: usageKey }); + } = useEntityLinks({ upstreamUsageKey: usageKey }); const downstreamKeys = useMemo( () => dataDownstreamLinks?.map(link => link.downstreamUsageKey) || [], diff --git a/src/library-authoring/component-picker/ComponentPicker.test.tsx b/src/library-authoring/component-picker/ComponentPicker.test.tsx index 8e9fbbaf9d..cd7f497a37 100644 --- a/src/library-authoring/component-picker/ComponentPicker.test.tsx +++ b/src/library-authoring/component-picker/ComponentPicker.test.tsx @@ -14,6 +14,7 @@ import { mockGetCollectionMetadata, mockGetContentLibraryV2List, mockLibraryBlockMetadata, + mockGetContainerMetadata, } from '../data/api.mocks'; import { ComponentPicker } from './ComponentPicker'; @@ -40,6 +41,7 @@ mockContentSearchConfig.applyMock(); mockGetCollectionMetadata.applyMock(); mockGetContentLibraryV2List.applyMock(); mockLibraryBlockMetadata.applyMock(); +mockGetContainerMetadata.applyMock(); let postMessageSpy: jest.SpyInstance; @@ -99,6 +101,24 @@ describe('', () => { }, '*'); }); + it('should open the unit sidebar', async () => { + render(); + + expect(await screen.findByText('Test Library 1')).toBeInTheDocument(); + fireEvent.click(screen.getByDisplayValue(/lib:sampletaxonomyorg1:tl1/i)); + + // Wait for the content library to load + await screen.findByText(/Change Library/i); + expect(await screen.findByText('Test Library 1')).toBeInTheDocument(); + + // Click on the unit card to open the sidebar + fireEvent.click((await screen.findByText('Published Test Unit'))); + + const sidebar = await screen.findByTestId('library-sidebar'); + expect(sidebar).toBeInTheDocument(); + await waitFor(() => expect(within(sidebar).getByText('Published Test Unit')).toBeInTheDocument()); + }); + it('should pick component inside a collection using the card', async () => { render(); @@ -302,4 +322,43 @@ describe('', () => { expect(screen.queryByRole('tab', { name: /collections/i })).not.toBeInTheDocument(); expect(screen.queryByRole('tab', { name: /components/i })).not.toBeInTheDocument(); }); + + it('should not display never published filter', async () => { + render(); + + expect(await screen.findByText('Test Library 1')).toBeInTheDocument(); + fireEvent.click(screen.getByDisplayValue(/lib:sampletaxonomyorg1:tl1/i)); + + // Wait for the content library to load + const filterButton = await screen.findByRole('button', { name: /publish status/i }); + fireEvent.click(filterButton); + + // Verify the filters. Note: It's hard to verify the `published` filter, + // because there are many components with that text on the screen, but that's not the important thing. + expect(screen.getByText(/modified since publish/i)).toBeInTheDocument(); + expect(screen.queryByText(/never published/i)).not.toBeInTheDocument(); + }); + + it('should not display never published filter in collection page', async () => { + render(); + + expect(await screen.findByText('Test Library 1')).toBeInTheDocument(); + fireEvent.click(screen.getByDisplayValue(/lib:sampletaxonomyorg1:tl1/i)); + + // Wait for the content library to load + await screen.findByText(/Change Library/i); + expect(await screen.findByText('Test Library 1')).toBeInTheDocument(); + + // Click on the collection card to open the sidebar + fireEvent.click(screen.queryAllByText('Collection 1')[0]); + + // Wait for the content library to load + const filterButton = await screen.findByRole('button', { name: /publish status/i }); + fireEvent.click(filterButton); + + // Verify the filters. Note: It's hard to verify the `published` filter, + // because there are many components with that text on the screen, but that's not the important thing. + expect(screen.getByText(/modified since publish/i)).toBeInTheDocument(); + expect(screen.queryByText(/never published/i)).not.toBeInTheDocument(); + }); }); diff --git a/src/library-authoring/components/BaseCard.scss b/src/library-authoring/components/BaseCard.scss index 3a13bd14cb..a8971f1202 100644 --- a/src/library-authoring/components/BaseCard.scss +++ b/src/library-authoring/components/BaseCard.scss @@ -2,24 +2,41 @@ .pgn__card { height: 100%; min-width: 15rem; - } - .library-item-header { - border-top-left-radius: .375rem; - border-top-right-radius: .375rem; - padding: 0 .5rem 0 1.25rem; + &::before { + border: none !important; // Remove default focus + } + + &.selected:not(:focus) { + outline: 2px var(--pgn-color-gray-700) solid; + } - .library-item-header-icon { - width: 2.3rem; - height: 2.3rem; + &.selected:focus { + outline: 3px var(--pgn-color-gray-700) solid; } - .pgn__card-header-content { - margin-top: .55rem; + &:not(.selected):focus { + outline: 1px var(--pgn-color-gray-200) solid; + outline-offset: 2px; } - .pgn__card-header-actions { - margin: .25rem 0 .25rem 1rem; + .library-item-header { + padding: 0 .5rem 0 1.25rem; + border-top-left-radius: .375rem; + border-top-right-radius: .375rem; + + .library-item-header-icon { + width: 2.3rem; + height: 2.3rem; + } + + .pgn__card-header-content { + margin-top: .55rem; + } + + .pgn__card-header-actions { + margin: .25rem 0 .25rem 1rem; + } } } diff --git a/src/library-authoring/components/BaseCard.tsx b/src/library-authoring/components/BaseCard.tsx index 2b15937891..ef7dc5828c 100644 --- a/src/library-authoring/components/BaseCard.tsx +++ b/src/library-authoring/components/BaseCard.tsx @@ -22,7 +22,8 @@ type BaseCardProps = { tags: ContentHitTags; actions: React.ReactNode; hasUnpublishedChanges?: boolean; - onSelect: () => void + onSelect: (e?: React.MouseEvent) => void; + selected?: boolean; }; const BaseCard = ({ @@ -33,6 +34,7 @@ const BaseCard = ({ tags, actions, onSelect, + selected = false, ...props } : BaseCardProps) => { const tagCount = useMemo(() => { @@ -47,7 +49,7 @@ const BaseCard = ({ const intl = useIntl(); return ( - + { const { componentPickerMode } = useComponentPickerContext(); - const { showOnlyPublished } = useLibraryContext(); - const { openCollectionInfoSidebar } = useSidebarContext(); + const { showOnlyPublished, setCollectionId } = useLibraryContext(); + const { openCollectionInfoSidebar, sidebarComponentInfo } = useSidebarContext(); const { type: itemType, @@ -132,12 +132,19 @@ const CollectionCard = ({ hit } : CollectionCardProps) => { const { displayName = '', description = '' } = formatted; + const selected = sidebarComponentInfo?.type === SidebarBodyComponentId.CollectionInfo + && sidebarComponentInfo.id === collectionId; + const { navigateTo } = useLibraryRoutes(); - const openCollection = useCallback(() => { + const openCollection = useCallback((e?: React.MouseEvent) => { openCollectionInfoSidebar(collectionId); + const doubleClicked = (e?.detail || 0) > 1; if (!componentPickerMode) { - navigateTo({ collectionId }); + navigateTo({ collectionId, doubleClicked }); + } else if (doubleClicked) { + /* istanbul ignore next */ + setCollectionId(collectionId); } }, [collectionId, navigateTo, openCollectionInfoSidebar]); @@ -154,6 +161,7 @@ const CollectionCard = ({ hit } : CollectionCardProps) => { )} onSelect={openCollection} + selected={selected} /> ); }; diff --git a/src/library-authoring/components/ComponentCard.test.tsx b/src/library-authoring/components/ComponentCard.test.tsx index 087b0ad20a..e0e2c44e20 100644 --- a/src/library-authoring/components/ComponentCard.test.tsx +++ b/src/library-authoring/components/ComponentCard.test.tsx @@ -11,6 +11,13 @@ import { ContentHit } from '../../search-manager'; import ComponentCard from './ComponentCard'; import { PublishStatus } from '../../search-manager/data/api'; +const mockNavigate = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), // use actual for all non-hook parts + useNavigate: () => mockNavigate, +})); + const contentHit: ContentHit = { id: '1', usageKey: 'lb:org1:demolib:html:a1fa8bdd-dc67-4976-9bf5-0ea75a9bca3d', @@ -41,6 +48,8 @@ const contentHit: ContentHit = { const libraryId = 'lib:org1:Demo_Course'; const render = () => baseRender(, { + path: '/library/:libraryId', + params: { libraryId }, extraWrapper: ({ children }) => ( { children } @@ -104,4 +113,24 @@ describe('', () => { expect(mockShowToast).toHaveBeenCalledWith('Error copying to clipboard'); }); }); + + it('should select component on clicking edit menu option', async () => { + initializeMocks(); + render(); + + // Open menu + const menu = await screen.findByTestId('component-card-menu-toggle'); + expect(menu).toBeInTheDocument(); + fireEvent.click(menu); + + // Click copy to clipboard + const editOption = await screen.findByRole('button', { name: 'Edit' }); + expect(editOption).toBeInTheDocument(); + fireEvent.click(editOption); + // Verify that the url is updated to component url i.e. component is selected + expect(mockNavigate).toHaveBeenCalledWith({ + pathname: `/library/${libraryId}/component/${contentHit.usageKey}`, + search: '', + }); + }); }); diff --git a/src/library-authoring/components/ComponentCard.tsx b/src/library-authoring/components/ComponentCard.tsx index 86dccbfd4f..4b58147fbf 100644 --- a/src/library-authoring/components/ComponentCard.tsx +++ b/src/library-authoring/components/ComponentCard.tsx @@ -6,7 +6,7 @@ import { import { type ContentHit, PublishStatus } from '../../search-manager'; import { useComponentPickerContext } from '../common/context/ComponentPickerContext'; import { useLibraryContext } from '../common/context/LibraryContext'; -import { useSidebarContext } from '../common/context/SidebarContext'; +import { SidebarBodyComponentId, useSidebarContext } from '../common/context/SidebarContext'; import { useLibraryRoutes } from '../routes'; import AddComponentWidget from './AddComponentWidget'; import BaseCard from './BaseCard'; @@ -18,7 +18,7 @@ type ComponentCardProps = { const ComponentCard = ({ hit }: ComponentCardProps) => { const { showOnlyPublished } = useLibraryContext(); - const { openComponentInfoSidebar } = useSidebarContext(); + const { openComponentInfoSidebar, sidebarComponentInfo } = useSidebarContext(); const { componentPickerMode } = useComponentPickerContext(); const { @@ -44,6 +44,9 @@ const ComponentCard = ({ hit }: ComponentCardProps) => { } }, [usageKey, navigateTo, openComponentInfoSidebar]); + const selected = sidebarComponentInfo?.type === SidebarBodyComponentId.ComponentInfo + && sidebarComponentInfo.id === usageKey; + return ( { )} hasUnpublishedChanges={publishStatus !== PublishStatus.Published} onSelect={openComponent} + selected={selected} /> ); }; diff --git a/src/library-authoring/components/ComponentDeleter.tsx b/src/library-authoring/components/ComponentDeleter.tsx index 2ea3a99e8e..c48dedec1b 100644 --- a/src/library-authoring/components/ComponentDeleter.tsx +++ b/src/library-authoring/components/ComponentDeleter.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useContext } from 'react'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; -import { Warning } from '@openedx/paragon/icons'; +import { Icon } from '@openedx/paragon'; +import { CalendarViewDay, School, Warning } from '@openedx/paragon/icons'; import { useSidebarContext } from '../common/context/SidebarContext'; import { useDeleteLibraryBlock, useLibraryBlockMetadata, useRestoreLibraryBlock } from '../data/apiHooks'; @@ -66,6 +67,22 @@ const ComponentDeleter = ({ usageKey, ...props }: Props) => { return null; } + const deleteText = intl.formatMessage(messages.deleteComponentConfirm, { + componentName: , + message: ( + <> +
+ + {intl.formatMessage(messages.deleteComponentConfirmMsg1)} +
+
+ + {intl.formatMessage(messages.deleteComponentConfirmMsg2)} +
+ + ), + }); + return ( { variant="warning" title={intl.formatMessage(messages.deleteComponentWarningTitle)} icon={Warning} - description={( - - ), - }} - /> -)} + description={deleteText} onDeleteSubmit={doDelete} /> ); diff --git a/src/library-authoring/components/ComponentEditorModal.tsx b/src/library-authoring/components/ComponentEditorModal.tsx index 74ffc85383..08df29a760 100644 --- a/src/library-authoring/components/ComponentEditorModal.tsx +++ b/src/library-authoring/components/ComponentEditorModal.tsx @@ -1,7 +1,9 @@ import { getConfig } from '@edx/frontend-platform'; import React from 'react'; - +import { useSelector } from 'react-redux'; import { useQueryClient } from '@tanstack/react-query'; + +import { getWaffleFlags } from '../../data/selectors'; import EditorPage from '../../editors/EditorPage'; import { getBlockType } from '../../generic/key-utils'; import { useLibraryContext } from '../common/context/LibraryContext'; @@ -21,6 +23,7 @@ export function canEditComponent(usageKey: string): boolean { export const ComponentEditorModal: React.FC> = () => { const { componentBeingEdited, closeComponentEditor, libraryId } = useLibraryContext(); const queryClient = useQueryClient(); + const { useReactMarkdownEditor } = useSelector(getWaffleFlags); if (componentBeingEdited === undefined) { return null; @@ -37,11 +40,11 @@ export const ComponentEditorModal: React.FC> = () => { courseId={libraryId} blockType={blockType} blockId={componentBeingEdited.usageKey} + isMarkdownEditorEnabledForCourse={useReactMarkdownEditor} studioEndpointUrl={getConfig().STUDIO_BASE_URL} lmsEndpointUrl={getConfig().LMS_BASE_URL} onClose={onClose} returnFunction={() => onClose} - fullScreen={false} /> ); }; diff --git a/src/library-authoring/components/ComponentMenu.tsx b/src/library-authoring/components/ComponentMenu.tsx index 589901326b..ba37117119 100644 --- a/src/library-authoring/components/ComponentMenu.tsx +++ b/src/library-authoring/components/ComponentMenu.tsx @@ -20,6 +20,8 @@ import { import { canEditComponent } from './ComponentEditorModal'; import ComponentDeleter from './ComponentDeleter'; import messages from './messages'; +import { useLibraryRoutes } from '../routes'; +import { useRunOnNextRender } from '../../utils'; export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { const intl = useIntl(); @@ -36,6 +38,7 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { closeLibrarySidebar, setSidebarAction, } = useSidebarContext(); + const { navigateTo } = useLibraryRoutes(); const canEdit = usageKey && canEditComponent(usageKey); const { showToast } = useContext(ToastContext); @@ -87,10 +90,28 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { }); }; + const handleEdit = useCallback(() => { + navigateTo({ componentId: usageKey }); + openComponentInfoSidebar(usageKey); + openComponentEditor(usageKey); + }, [usageKey]); + + const scheduleJumpToCollection = useRunOnNextRender(() => { + // TODO: Ugly hack to make sure sidebar shows add to collection section + // This needs to run after all changes to url takes place to avoid conflicts. + setTimeout(() => setSidebarAction(SidebarActions.JumpToManageCollections), 250); + }); + const showManageCollections = useCallback(() => { - setSidebarAction(SidebarActions.JumpToAddCollections); + navigateTo({ componentId: usageKey }); openComponentInfoSidebar(usageKey); - }, [setSidebarAction, openComponentInfoSidebar, usageKey]); + scheduleJumpToCollection(); + }, [ + scheduleJumpToCollection, + openComponentInfoSidebar, + usageKey, + navigateTo, + ]); return ( @@ -104,7 +125,7 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { data-testid="component-card-menu-toggle" /> - openComponentEditor(usageKey) } : { disabled: true })}> + @@ -123,11 +144,9 @@ export const ComponentMenu = ({ usageKey }: { usageKey: string }) => { )} - {!unitId && ( - - - - )} + + + diff --git a/src/library-authoring/components/ContainerCard.test.tsx b/src/library-authoring/components/ContainerCard.test.tsx index a76601e1d0..a8716d7ad1 100644 --- a/src/library-authoring/components/ContainerCard.test.tsx +++ b/src/library-authoring/components/ContainerCard.test.tsx @@ -6,7 +6,7 @@ import { fireEvent, } from '../../testUtils'; import { LibraryProvider } from '../common/context/LibraryContext'; -import { mockContentLibrary, mockGetContainerChildren } from '../data/api.mocks'; +import { mockContentLibrary } from '../data/api.mocks'; import { type ContainerHit, PublishStatus } from '../../search-manager'; import ContainerCard from './ContainerCard'; import { getLibraryContainerApiUrl, getLibraryContainerRestoreApiUrl } from '../data/api'; @@ -40,7 +40,6 @@ let axiosMock: MockAdapter; let mockShowToast; mockContentLibrary.applyMock(); -mockGetContainerChildren.applyMock(); const render = (ui: React.ReactElement, showOnlyPublished: boolean = false) => baseRender(ui, { extraWrapper: ({ children }) => ( @@ -155,29 +154,54 @@ describe('', () => { it('should render no child blocks in card preview', async () => { render(); - expect(screen.queryByTitle('text block')).not.toBeInTheDocument(); + expect(screen.queryByTitle('lb:org1:Demo_course:html:text-0')).not.toBeInTheDocument(); expect(screen.queryByText('+0')).not.toBeInTheDocument(); }); it('should render <=5 child blocks in card preview', async () => { const containerWith5Children = { ...containerHitSample, - usageKey: mockGetContainerChildren.fiveChildren, - }; + content: { + childUsageKeys: Array(5).fill('').map((_child, idx) => `lb:org1:Demo_course:html:text-${idx}`), + }, + } satisfies ContainerHit; render(); - expect((await screen.findAllByTitle(/text block */)).length).toBe(5); + expect((await screen.findAllByTitle(/lb:org1:Demo_course:html:text-*/)).length).toBe(5); expect(screen.queryByText('+0')).not.toBeInTheDocument(); }); it('should render >5 child blocks with +N in card preview', async () => { const containerWith6Children = { ...containerHitSample, - usageKey: mockGetContainerChildren.sixChildren, - }; + content: { + childUsageKeys: Array(6).fill('').map((_child, idx) => `lb:org1:Demo_course:html:text-${idx}`), + }, + } satisfies ContainerHit; render(); - expect((await screen.findAllByTitle(/text block */)).length).toBe(4); + expect((await screen.findAllByTitle(/lb:org1:Demo_course:html:text-*/)).length).toBe(4); expect(screen.queryByText('+2')).toBeInTheDocument(); }); + + it('should render published child blocks when rendering a published card preview', async () => { + const containerWithPublishedChildren = { + ...containerHitSample, + content: { + childUsageKeys: Array(6).fill('').map((_child, idx) => `lb:org1:Demo_course:html:text-${idx}`), + }, + published: { + content: { + childUsageKeys: Array(2).fill('').map((_child, idx) => `lb:org1:Demo_course:html:text-${idx}`), + }, + }, + } satisfies ContainerHit; + render( + , + true, + ); + + expect((await screen.findAllByTitle(/lb:org1:Demo_course:html:text-*/)).length).toBe(2); + expect(screen.queryByText('+2')).not.toBeInTheDocument(); + }); }); diff --git a/src/library-authoring/components/ContainerCard.tsx b/src/library-authoring/components/ContainerCard.tsx index 45268aa150..ebcbbbeaf3 100644 --- a/src/library-authoring/components/ContainerCard.tsx +++ b/src/library-authoring/components/ContainerCard.tsx @@ -12,17 +12,19 @@ import { MoreVert } from '@openedx/paragon/icons'; import { Link } from 'react-router-dom'; import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils'; +import { getBlockType } from '../../generic/key-utils'; import { ToastContext } from '../../generic/toast-context'; import { type ContainerHit, PublishStatus } from '../../search-manager'; import { useComponentPickerContext } from '../common/context/ComponentPickerContext'; import { useLibraryContext } from '../common/context/LibraryContext'; -import { SidebarActions, useSidebarContext } from '../common/context/SidebarContext'; -import { useContainerChildren, useRemoveItemsFromCollection } from '../data/apiHooks'; +import { SidebarActions, SidebarBodyComponentId, useSidebarContext } from '../common/context/SidebarContext'; +import { useRemoveItemsFromCollection } from '../data/apiHooks'; import { useLibraryRoutes } from '../routes'; import AddComponentWidget from './AddComponentWidget'; import BaseCard from './BaseCard'; import messages from './messages'; import ContainerDeleter from './ContainerDeleter'; +import { useRunOnNextRender } from '../../utils'; type ContainerMenuProps = { hit: ContainerHit, @@ -44,6 +46,7 @@ const ContainerMenu = ({ hit } : ContainerMenuProps) => { } = useSidebarContext(); const { showToast } = useContext(ToastContext); const [isConfirmingDelete, confirmDelete, cancelDelete] = useToggle(false); + const { navigateTo } = useLibraryRoutes(); const removeComponentsMutation = useRemoveItemsFromCollection(libraryId, collectionId); @@ -59,10 +62,17 @@ const ContainerMenu = ({ hit } : ContainerMenuProps) => { }); }; + const scheduleJumpToCollection = useRunOnNextRender(() => { + // TODO: Ugly hack to make sure sidebar shows add to collection section + // This needs to run after all changes to url takes place to avoid conflicts. + setTimeout(() => setSidebarAction(SidebarActions.JumpToManageCollections)); + }); + const showManageCollections = useCallback(() => { - setSidebarAction(SidebarActions.JumpToAddCollections); + navigateTo({ unitId: containerId }); openUnitInfoSidebar(containerId); - }, [setSidebarAction, openUnitInfoSidebar, containerId]); + scheduleJumpToCollection(); + }, [scheduleJumpToCollection, navigateTo, openUnitInfoSidebar, containerId]); return ( <> @@ -107,21 +117,17 @@ const ContainerMenu = ({ hit } : ContainerMenuProps) => { }; type ContainerCardPreviewProps = { - containerId: string; + childUsageKeys: Array; showMaxChildren?: number; }; -const ContainerCardPreview = ({ containerId, showMaxChildren = 5 }: ContainerCardPreviewProps) => { - const { data, isLoading, isError } = useContainerChildren(containerId); - if (isLoading || isError) { - return null; - } - - const hiddenChildren = data.length - showMaxChildren; +const ContainerCardPreview = ({ childUsageKeys, showMaxChildren = 5 }: ContainerCardPreviewProps) => { + const hiddenChildren = childUsageKeys.length - showMaxChildren; return ( { - data.slice(0, showMaxChildren).map(({ id, blockType, displayName }, idx) => { + childUsageKeys.slice(0, showMaxChildren).map((usageKey, idx) => { + const blockType = getBlockType(usageKey); let blockPreview: ReactNode; let classNames; @@ -133,7 +139,7 @@ const ContainerCardPreview = ({ containerId, showMaxChildren = 5 }: ContainerCar ); } else { @@ -147,7 +153,9 @@ const ContainerCardPreview = ({ containerId, showMaxChildren = 5 }: ContainerCar } return (
{blockPreview} @@ -166,7 +174,7 @@ type ContainerCardProps = { const ContainerCard = ({ hit } : ContainerCardProps) => { const { componentPickerMode } = useComponentPickerContext(); const { setUnitId, showOnlyPublished } = useLibraryContext(); - const { openUnitInfoSidebar } = useSidebarContext(); + const { openUnitInfoSidebar, sidebarComponentInfo } = useSidebarContext(); const { blockType: itemType, @@ -176,6 +184,7 @@ const ContainerCard = ({ hit } : ContainerCardProps) => { published, publishStatus, usageKey: unitId, + content, } = hit; const numChildrenCount = showOnlyPublished ? ( @@ -186,13 +195,22 @@ const ContainerCard = ({ hit } : ContainerCardProps) => { showOnlyPublished ? formatted.published?.displayName : formatted.displayName ) ?? ''; + const childUsageKeys: Array = ( + showOnlyPublished ? published?.content?.childUsageKeys : content?.childUsageKeys + ) ?? []; + + const selected = sidebarComponentInfo?.type === SidebarBodyComponentId.UnitInfo + && sidebarComponentInfo.id === unitId; + const { navigateTo } = useLibraryRoutes(); - const openContainer = useCallback(() => { + const openContainer = useCallback((e?: React.MouseEvent) => { if (itemType === 'unit') { openUnitInfoSidebar(unitId); setUnitId(unitId); - navigateTo({ unitId }); + if (!componentPickerMode) { + navigateTo({ unitId, doubleClicked: (e?.detail || 0) > 1 }); + } } }, [unitId, itemType, openUnitInfoSidebar, navigateTo]); @@ -200,7 +218,7 @@ const ContainerCard = ({ hit } : ContainerCardProps) => { } + preview={} tags={tags} numChildren={numChildrenCount} actions={( @@ -214,6 +232,7 @@ const ContainerCard = ({ hit } : ContainerCardProps) => { )} hasUnpublishedChanges={publishStatus !== PublishStatus.Published} onSelect={openContainer} + selected={selected} /> ); }; diff --git a/src/library-authoring/components/ContainerDeleter.tsx b/src/library-authoring/components/ContainerDeleter.tsx index 9b7d5db05a..a4a1affc15 100644 --- a/src/library-authoring/components/ContainerDeleter.tsx +++ b/src/library-authoring/components/ContainerDeleter.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useCallback, useContext } from 'react'; +import { useCallback, useContext } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { Icon } from '@openedx/paragon'; import { Warning, School, Widgets } from '@openedx/paragon/icons'; @@ -47,7 +47,7 @@ const ContainerDeleter = ({
), - }) as ReactNode as string; + }); const deleteSuccess = intl.formatMessage(messages.deleteUnitSuccess); const deleteError = intl.formatMessage(messages.deleteUnitFailed); const undoDeleteError = messages.undoDeleteUnitToastFailed; diff --git a/src/library-authoring/components/PublishConfirmationModal.tsx b/src/library-authoring/components/PublishConfirmationModal.tsx index 8c98e234fe..eeadc84e9a 100644 --- a/src/library-authoring/components/PublishConfirmationModal.tsx +++ b/src/library-authoring/components/PublishConfirmationModal.tsx @@ -5,7 +5,7 @@ import BaseModal from '../../editors/sharedComponents/BaseModal'; import messages from './messages'; import infoMessages from '../component-info/messages'; import { ComponentUsage } from '../component-info/ComponentUsage'; -import { useUnpaginatedEntityLinks } from '../../course-libraries/data/apiHooks'; +import { useEntityLinks } from '../../course-libraries/data/apiHooks'; interface PublishConfirmationModalProps { isOpen: boolean, @@ -29,7 +29,7 @@ const PublishConfirmationModal = ({ const { data: dataDownstreamLinks, isLoading: isLoadingDownstreamLinks, - } = useUnpaginatedEntityLinks({ upstreamUsageKey: usageKey }); + } = useEntityLinks({ upstreamUsageKey: usageKey }); const hasDownstreamUsages = !isLoadingDownstreamLinks && dataDownstreamLinks?.length !== 0; diff --git a/src/library-authoring/components/messages.ts b/src/library-authoring/components/messages.ts index 2507930770..8248b4f43c 100644 --- a/src/library-authoring/components/messages.ts +++ b/src/library-authoring/components/messages.ts @@ -68,9 +68,19 @@ const messages = defineMessages({ }, deleteComponentConfirm: { id: 'course-authoring.library-authoring.component.delete-confirmation-text', - defaultMessage: 'Delete {componentName}? If this component has been used in a course, those copies won\'t be deleted, but they will no longer receive updates from the library.', + defaultMessage: 'Delete {componentName}? {message}', description: 'Confirmation text to display before deleting a component', }, + deleteComponentConfirmMsg1: { + id: 'course-authoring.library-authoring.component.delete-confirmation-msg-1', + defaultMessage: 'If this component has been used in a course, those copies won\'t be deleted, but they will no longer receive updates from the library.', + description: 'First part of confirmation message to display before deleting a component', + }, + deleteComponentConfirmMsg2: { + id: 'course-authoring.library-authoring.component.delete-confirmation-msg-2', + defaultMessage: 'If this component has been used in any units, it will also be deleted from those units.', + description: 'Second part of confirmation message to display before deleting a component', + }, deleteComponentCancelButton: { id: 'course-authoring.library-authoring.component.cancel-delete-button', defaultMessage: 'Cancel', diff --git a/src/library-authoring/containers/ContainerEditableTitle.tsx b/src/library-authoring/containers/ContainerEditableTitle.tsx new file mode 100644 index 0000000000..5a1ea0f6df --- /dev/null +++ b/src/library-authoring/containers/ContainerEditableTitle.tsx @@ -0,0 +1,48 @@ +import { useIntl } from '@edx/frontend-platform/i18n'; +import { useContext } from 'react'; +import { InplaceTextEditor } from '../../generic/inplace-text-editor'; +import { ToastContext } from '../../generic/toast-context'; +import { useLibraryContext } from '../common/context/LibraryContext'; +import { useContainer, useUpdateContainer } from '../data/apiHooks'; +import messages from './messages'; + +interface EditableTitleProps { + containerId: string; + textClassName?: string; +} + +export const ContainerEditableTitle = ({ containerId, textClassName }: EditableTitleProps) => { + const intl = useIntl(); + + const { readOnly, showOnlyPublished } = useLibraryContext(); + + const { data: container } = useContainer(containerId); + + const updateMutation = useUpdateContainer(containerId); + const { showToast } = useContext(ToastContext); + + const handleSaveDisplayName = async (newDisplayName: string) => { + try { + await updateMutation.mutateAsync({ + displayName: newDisplayName, + }); + showToast(intl.formatMessage(messages.updateContainerSuccessMsg)); + } catch (err) { + showToast(intl.formatMessage(messages.updateContainerErrorMsg)); + } + }; + + // istanbul ignore if: this should never happen + if (!container) { + return null; + } + + return ( + + ); +}; diff --git a/src/library-authoring/containers/ContainerInfoHeader.tsx b/src/library-authoring/containers/ContainerInfoHeader.tsx index 39d590db6c..c47a4ee6b9 100644 --- a/src/library-authoring/containers/ContainerInfoHeader.tsx +++ b/src/library-authoring/containers/ContainerInfoHeader.tsx @@ -1,17 +1,7 @@ -import { useContext } from 'react'; -import { useIntl } from '@edx/frontend-platform/i18n'; - -import { InplaceTextEditor } from '../../generic/inplace-text-editor'; -import { ToastContext } from '../../generic/toast-context'; -import { useLibraryContext } from '../common/context/LibraryContext'; import { useSidebarContext } from '../common/context/SidebarContext'; -import { useContainer, useUpdateContainer } from '../data/apiHooks'; -import messages from './messages'; +import { ContainerEditableTitle } from './ContainerEditableTitle'; const ContainerInfoHeader = () => { - const intl = useIntl(); - - const { readOnly } = useLibraryContext(); const { sidebarComponentInfo } = useSidebarContext(); const containerId = sidebarComponentInfo?.id; @@ -20,32 +10,10 @@ const ContainerInfoHeader = () => { throw new Error('containerId is required'); } - const { data: container } = useContainer(containerId); - - const updateMutation = useUpdateContainer(containerId); - const { showToast } = useContext(ToastContext); - - const handleSaveDisplayName = (newDisplayName: string) => { - updateMutation.mutateAsync({ - displayName: newDisplayName, - }).then(() => { - showToast(intl.formatMessage(messages.updateContainerSuccessMsg)); - }).catch(() => { - showToast(intl.formatMessage(messages.updateContainerErrorMsg)); - }); - }; - - if (!container) { - return null; - } - return ( - ); }; diff --git a/src/library-authoring/containers/ContainerOrganize.tsx b/src/library-authoring/containers/ContainerOrganize.tsx index e6585785e2..6419bfd430 100644 --- a/src/library-authoring/containers/ContainerOrganize.tsx +++ b/src/library-authoring/containers/ContainerOrganize.tsx @@ -28,7 +28,7 @@ const ContainerOrganize = () => { const { readOnly } = useLibraryContext(); const { sidebarComponentInfo, sidebarAction } = useSidebarContext(); - const jumpToCollections = sidebarAction === SidebarActions.JumpToAddCollections; + const jumpToCollections = sidebarAction === SidebarActions.JumpToManageCollections; const containerId = sidebarComponentInfo?.id; // istanbul ignore if: this should never happen @@ -85,7 +85,7 @@ const ContainerOrganize = () => { > - {intl.formatMessage(messages.organizeTabTagsTitle, { count: tagsCount })} + {intl.formatMessage(messages.manageTabTagsTitle, { count: tagsCount })} @@ -113,7 +113,7 @@ const ContainerOrganize = () => { > - {intl.formatMessage(messages.organizeTabCollectionsTitle, { count: collectionsCount })} + {intl.formatMessage(messages.manageTabCollectionsTitle, { count: collectionsCount })} diff --git a/src/library-authoring/containers/UnitInfo.test.tsx b/src/library-authoring/containers/UnitInfo.test.tsx index 677063f851..e20e27f518 100644 --- a/src/library-authoring/containers/UnitInfo.test.tsx +++ b/src/library-authoring/containers/UnitInfo.test.tsx @@ -5,7 +5,7 @@ import { initializeMocks, render as baseRender, screen, waitFor, fireEvent, } from '../../testUtils'; -import { mockContentLibrary, mockGetContainerMetadata } from '../data/api.mocks'; +import { mockContentLibrary, mockGetContainerChildren, mockGetContainerMetadata } from '../data/api.mocks'; import { LibraryProvider } from '../common/context/LibraryContext'; import UnitInfo from './UnitInfo'; import { getLibraryContainerApiUrl, getLibraryContainerPublishApiUrl } from '../data/api'; @@ -14,26 +14,33 @@ import { SidebarBodyComponentId, SidebarProvider } from '../common/context/Sideb mockGetContainerMetadata.applyMock(); mockContentLibrary.applyMock(); mockGetContainerMetadata.applyMock(); +mockGetContainerChildren.applyMock(); const { libraryId } = mockContentLibrary; const { containerId } = mockGetContainerMetadata; -const render = () => baseRender(, { - extraWrapper: ({ children }) => ( - - { + const params: { libraryId: string, unitId?: string } = { libraryId, unitId: containerId }; + return baseRender(, { + path: '/library/:libraryId/:unitId?', + params, + extraWrapper: ({ children }) => ( + - {children} - - - ), -}); + + {children} + +
+ ), + }); +}; let axiosMock: MockAdapter; let mockShowToast; @@ -95,4 +102,26 @@ describe('', () => { }); expect(mockShowToast).toHaveBeenCalledWith('Failed to publish changes'); }); + + it('show only published content', async () => { + render(true); + expect(await screen.findByTestId('unit-info-menu-toggle')).toBeInTheDocument(); + expect(screen.getByText(/text block published 1/i)).toBeInTheDocument(); + }); + + it('shows the preview tab by default and the component are readonly', async () => { + render(); + const previewTab = await screen.findByText('Preview'); + expect(previewTab).toBeInTheDocument(); + expect(previewTab).toHaveAttribute('aria-selected', 'true'); + + // Check that there are no edit buttons for components titles + expect(screen.queryAllByRole('button', { name: /edit/i }).length).toBe(0); + + // Check that there are no drag handle for components + expect(screen.queryAllByRole('button', { name: 'Drag to reorder' }).length).toBe(0); + + // Check that there are no menu buttons for components + expect(screen.queryAllByRole('button', { name: /component actions menu/i }).length).toBe(0); + }); }); diff --git a/src/library-authoring/containers/UnitInfo.tsx b/src/library-authoring/containers/UnitInfo.tsx index 164962fcc6..cdf5c2d080 100644 --- a/src/library-authoring/containers/UnitInfo.tsx +++ b/src/library-authoring/containers/UnitInfo.tsx @@ -9,7 +9,7 @@ import { IconButton, useToggle, } from '@openedx/paragon'; -import React, { useEffect, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { Link } from 'react-router-dom'; import { MoreVert } from '@openedx/paragon/icons'; @@ -17,7 +17,6 @@ import { useComponentPickerContext } from '../common/context/ComponentPickerCont import { useLibraryContext } from '../common/context/LibraryContext'; import { type UnitInfoTab, - SidebarActions, UNIT_INFO_TABS, isUnitInfoTab, useSidebarContext, @@ -81,9 +80,8 @@ const UnitInfo = () => { sidebarTab, setSidebarTab, sidebarComponentInfo, - sidebarAction, + resetSidebarAction, } = useSidebarContext(); - const jumpToCollections = sidebarAction === SidebarActions.JumpToAddCollections; const { insideUnit } = useLibraryRoutes(); const tab: UnitInfoTab = ( @@ -96,6 +94,12 @@ const UnitInfo = () => { const showOpenUnitButton = !insideUnit && !componentPickerMode; + /* istanbul ignore next */ + const handleTabChange = (newTab: UnitInfoTab) => { + resetSidebarAction(); + setSidebarTab(newTab); + }; + const renderTab = useCallback((infoTab: UnitInfoTab, component: React.ReactNode, title: string) => { if (hiddenTabs.includes(infoTab)) { // For some reason, returning anything other than empty list breaks the tab style @@ -117,13 +121,6 @@ const UnitInfo = () => { } }, [publishContainer]); - useEffect(() => { - // Show Organize tab if JumpToAddCollections action is set in sidebarComponentInfo - if (jumpToCollections) { - setSidebarTab(UNIT_INFO_TABS.Organize); - } - }, [jumpToCollections, setSidebarTab]); - if (!container || !unitId) { return null; } @@ -163,10 +160,14 @@ const UnitInfo = () => { className="my-3 d-flex justify-content-around" defaultActiveKey={defaultTab.unit} activeKey={tab} - onSelect={setSidebarTab} + onSelect={handleTabChange} > - {renderTab(UNIT_INFO_TABS.Preview, , intl.formatMessage(messages.previewTabTitle))} - {renderTab(UNIT_INFO_TABS.Organize, , intl.formatMessage(messages.organizeTabTitle))} + {renderTab( + UNIT_INFO_TABS.Preview, + , + intl.formatMessage(messages.previewTabTitle), + )} + {renderTab(UNIT_INFO_TABS.Manage, , intl.formatMessage(messages.manageTabTitle))} {renderTab(UNIT_INFO_TABS.Settings, 'Unit Settings', intl.formatMessage(messages.settingsTabTitle))} diff --git a/src/library-authoring/containers/messages.ts b/src/library-authoring/containers/messages.ts index 9ebae29e1b..fe8e0139ed 100644 --- a/src/library-authoring/containers/messages.ts +++ b/src/library-authoring/containers/messages.ts @@ -11,20 +11,20 @@ const messages = defineMessages({ defaultMessage: 'Preview', description: 'Title for preview tab', }, - organizeTabTitle: { - id: 'course-authoring.library-authoring.container-sidebar.organize-tab.title', - defaultMessage: 'Organize', - description: 'Title for organize tab', + manageTabTitle: { + id: 'course-authoring.library-authoring.container-sidebar.manage-tab.title', + defaultMessage: 'Manage', + description: 'Title for manage tab', }, - organizeTabTagsTitle: { - id: 'course-authoring.library-authoring.container-sidebar.organize-tab.tags.title', + manageTabTagsTitle: { + id: 'course-authoring.library-authoring.container-sidebar.manage-tab.tags.title', defaultMessage: 'Tags ({count})', - description: 'Title for tags section in organize tab', + description: 'Title for tags section in manage tab', }, - organizeTabCollectionsTitle: { - id: 'course-authoring.library-authoring.container-sidebar.organize-tab.collections.title', + manageTabCollectionsTitle: { + id: 'course-authoring.library-authoring.container-sidebar.manage-tab.collections.title', defaultMessage: 'Collections ({count})', - description: 'Title for collections section in organize tab', + description: 'Title for collections section in manage tab', }, publishContainerButton: { id: 'course-authoring.library-authoring.container-sidebar.publish-button', diff --git a/src/library-authoring/data/api.mocks.ts b/src/library-authoring/data/api.mocks.ts index eb5f60188e..92220b140b 100644 --- a/src/library-authoring/data/api.mocks.ts +++ b/src/library-authoring/data/api.mocks.ts @@ -188,6 +188,7 @@ mockCreateLibraryBlock.newHtmlData = { id: 'lb:Axim:TEST:html:123', blockType: 'html', displayName: 'New Text Component', + publishedDisplayName: null, hasUnpublishedChanges: true, lastPublished: null, // or e.g. '2024-08-30T16:37:42Z', publishedBy: null, // or e.g. 'test_author', @@ -202,6 +203,7 @@ mockCreateLibraryBlock.newProblemData = { id: 'lb:Axim:TEST:problem:prob1', blockType: 'problem', displayName: 'New Problem', + publishedDisplayName: null, hasUnpublishedChanges: true, lastPublished: null, // or e.g. '2024-08-30T16:37:42Z', publishedBy: null, // or e.g. 'test_author', @@ -216,6 +218,7 @@ mockCreateLibraryBlock.newVideoData = { id: 'lb:Axim:TEST:video:vid1', blockType: 'video', displayName: 'New Video', + publishedDisplayName: null, hasUnpublishedChanges: true, lastPublished: null, // or e.g. '2024-08-30T16:37:42Z', publishedBy: null, // or e.g. 'test_author', @@ -348,6 +351,7 @@ mockLibraryBlockMetadata.dataNeverPublished = { id: 'lb:Axim:TEST1:html:571fe018-f3ce-45c9-8f53-5dafcb422fd1', blockType: 'html', displayName: 'Introduction to Testing 1', + publishedDisplayName: null, lastPublished: null, publishedBy: null, lastDraftCreated: null, @@ -363,6 +367,7 @@ mockLibraryBlockMetadata.dataPublished = { id: 'lb:Axim:TEST2:html:571fe018-f3ce-45c9-8f53-5dafcb422fd2', blockType: 'html', displayName: 'Introduction to Testing 2', + publishedDisplayName: 'Introduction to Testing 2', lastPublished: '2024-06-22T00:00:00', publishedBy: 'Luke', lastDraftCreated: null, @@ -391,6 +396,7 @@ mockLibraryBlockMetadata.dataWithCollections = { id: 'lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd', blockType: 'html', displayName: 'Introduction to Testing 2', + publishedDisplayName: null, lastPublished: '2024-06-21T00:00:00', publishedBy: 'Luke', lastDraftCreated: null, @@ -407,6 +413,7 @@ mockLibraryBlockMetadata.dataPublishedWithChanges = { id: 'lb:Axim:TEST2:html:571fe018-f3ce-45c9-8f53-5dafcb422fvv', blockType: 'html', displayName: 'Introduction to Testing 2', + publishedDisplayName: 'Introduction to Testing 3', lastPublished: '2024-06-22T00:00:00', publishedBy: 'Luke', lastDraftCreated: null, @@ -488,6 +495,7 @@ mockGetContainerMetadata.containerData = { id: 'lct:org:lib:unit:test-unit-9a2072', containerType: 'unit', displayName: 'Test Unit', + publishedDisplayName: 'Published Test Unit', created: '2024-09-19T10:00:00Z', createdBy: 'test_author', lastPublished: '2024-09-20T10:00:00Z', @@ -536,6 +544,7 @@ export async function mockGetContainerChildren(containerId: string): Promise jest.spyOn(api, 'getBlockTypes').mockImplementation(mockBlockTypesMetadata); -export async function mockGetUnpaginatedEntityLinks( +export async function mockGetEntityLinks( _downstreamContextKey?: string, _readyToSync?: boolean, upstreamUsageKey?: string, -): ReturnType { - const thisMock = mockGetUnpaginatedEntityLinks; +): ReturnType { + const thisMock = mockGetEntityLinks; switch (upstreamUsageKey) { case thisMock.upstreamUsageKey: return thisMock.response; case mockLibraryBlockMetadata.usageKeyPublishedWithChanges: return thisMock.response; @@ -688,8 +698,8 @@ export async function mockGetUnpaginatedEntityLinks( default: return []; } } -mockGetUnpaginatedEntityLinks.upstreamUsageKey = mockLibraryBlockMetadata.usageKeyPublished; -mockGetUnpaginatedEntityLinks.response = downstreamLinkInfo.results[0].hits.map((obj: { usageKey: any; }) => ({ +mockGetEntityLinks.upstreamUsageKey = mockLibraryBlockMetadata.usageKeyPublished; +mockGetEntityLinks.response = downstreamLinkInfo.results[0].hits.map((obj: { usageKey: any; }) => ({ id: 875, upstreamContextTitle: 'CS problems 3', upstreamVersion: 10, @@ -703,10 +713,10 @@ mockGetUnpaginatedEntityLinks.response = downstreamLinkInfo.results[0].hits.map( created: '2025-02-08T14:07:05.588484Z', updated: '2025-02-08T14:07:05.588484Z', })); -mockGetUnpaginatedEntityLinks.emptyUsageKey = 'lb:Axim:TEST1:html:empty'; -mockGetUnpaginatedEntityLinks.emptyComponentUsage = [] as courseLibApi.PublishableEntityLink[]; +mockGetEntityLinks.emptyUsageKey = 'lb:Axim:TEST1:html:empty'; +mockGetEntityLinks.emptyComponentUsage = [] as courseLibApi.PublishableEntityLink[]; -mockGetUnpaginatedEntityLinks.applyMock = () => jest.spyOn( +mockGetEntityLinks.applyMock = () => jest.spyOn( courseLibApi, - 'getUnpaginatedEntityLinks', -).mockImplementation(mockGetUnpaginatedEntityLinks); + 'getEntityLinks', +).mockImplementation(mockGetEntityLinks); diff --git a/src/library-authoring/data/api.test.ts b/src/library-authoring/data/api.test.ts index f5882ee678..ece0773f4d 100644 --- a/src/library-authoring/data/api.test.ts +++ b/src/library-authoring/data/api.test.ts @@ -142,4 +142,13 @@ describe('library data API', () => { await api.removeLibraryContainerChildren(containerId, ['test']); expect(axiosMock.history.delete[0].url).toEqual(url); }); + + it('getContentLibraryV2List', async () => { + const url = api.getContentLibraryV2ListApiUrl(); + + axiosMock.onGet(url).reply(200, { some: 'data' }); + + await api.getContentLibraryV2List({ type: 'complex' }); + expect(axiosMock.history.get[0].url).toEqual(url); + }); }); diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts index 220ce0a3f9..dd8a8f7430 100644 --- a/src/library-authoring/data/api.ts +++ b/src/library-authoring/data/api.ts @@ -119,7 +119,7 @@ export const getLibraryContainerRestoreApiUrl = (containerId: string) => `${getL /** * Get the URL for a single container children api. */ -export const getLibraryContainerChildrenApiUrl = (containerId: string) => `${getLibraryContainerApiUrl(containerId)}children/`; +export const getLibraryContainerChildrenApiUrl = (containerId: string, published: boolean = false) => `${getLibraryContainerApiUrl(containerId)}children/?published=${published}`; /** * Get the URL for library container collections. */ @@ -250,6 +250,7 @@ export interface LibraryBlockMetadata { id: string; blockType: string; displayName: string; + publishedDisplayName: string | null; lastPublished: string | null; publishedBy: string | null; lastDraftCreated: string | null; @@ -259,6 +260,9 @@ export interface LibraryBlockMetadata { modified: string | null; tagsCount: number; collections: CollectionMetadata[]; + // Local only variable set to true when a new block is added + // NOTE: Currently only updated when a new component is added inside a unit + isNew?: boolean; } export interface UpdateLibraryDataRequest { @@ -596,6 +600,7 @@ export interface Container { id: string; containerType: 'unit'; displayName: string; + publishedDisplayName: string; lastPublished: string | null; publishedBy: string | null; createdBy: string | null; @@ -649,8 +654,13 @@ export async function restoreContainer(containerId: string) { /** * Fetch a library container's children's metadata. */ -export async function getLibraryContainerChildren(containerId: string): Promise { - const { data } = await getAuthenticatedHttpClient().get(getLibraryContainerChildrenApiUrl(containerId)); +export async function getLibraryContainerChildren( + containerId: string, + published: boolean = false, +): Promise { + const { data } = await getAuthenticatedHttpClient().get( + getLibraryContainerChildrenApiUrl(containerId, published), + ); return camelCaseObject(data); } diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts index e9a5202b14..3fd1f0633e 100644 --- a/src/library-authoring/data/apiHooks.ts +++ b/src/library-authoring/data/apiHooks.ts @@ -5,6 +5,7 @@ import { useQueryClient, type Query, type QueryClient, + replaceEqualDeep, } from '@tanstack/react-query'; import { useCallback } from 'react'; @@ -90,7 +91,11 @@ export const xblockQueryKeys = { componentMetadata: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'componentMetadata'], componentDownstreamLinks: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'downstreamLinks'], - /** Predicate used to invalidate all metadata only */ + /** + * Predicate used to invalidate all metadata only (not OLX, fields, assets, etc.). + * Affects all libraries; we could do a more complex version that affects only one library, but it would require + * introspecting the usage keys. + */ allComponentMetadata: (query: Query) => query.queryKey[0] === 'xblock' && query.queryKey[2] === 'componentMetadata', }; @@ -207,23 +212,32 @@ export const useContentLibraryV2List = (customParams: api.GetLibrariesV2CustomPa }) ); +/** Publish all changes in the library. */ export const useCommitLibraryChanges = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: api.commitLibraryChanges, onSettled: (_data, _error, libraryId) => { + // Invalidate all content-related metadata and search results for the whole library. queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId) }); + queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) }); + // For XBlocks, the only thing we need to invalidate is the metadata which includes "has unpublished changes" + queryClient.invalidateQueries({ predicate: xblockQueryKeys.allComponentMetadata }); }, }); }; +/** Discard all un-published changes in the library */ export const useRevertLibraryChanges = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: api.revertLibraryChanges, onSettled: (_data, _error, libraryId) => { + // Invalidate all content-related metadata and search results for the whole library. queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId) }); queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) }); + // For XBlocks, the only thing we need to invalidate is the metadata which includes "has unpublished changes" + queryClient.invalidateQueries({ predicate: xblockQueryKeys.allComponentMetadata }); }, }); }; @@ -458,15 +472,28 @@ export const useCollection = (libraryId: string, collectionId: string) => ( */ export const useUpdateCollection = (libraryId: string, collectionId: string) => { const queryClient = useQueryClient(); + const collectionQueryKey = libraryAuthoringQueryKeys.collection(libraryId, collectionId); return useMutation({ mutationFn: (data: api.UpdateCollectionComponentsRequest) => ( api.updateCollectionMetadata(libraryId, collectionId, data) ), + onMutate: (data) => { + const previousData = queryClient.getQueryData(collectionQueryKey) as api.CollectionMetadata; + queryClient.setQueryData(collectionQueryKey, { + ...previousData, + ...data, + }); + + return { previousData }; + }, + onError: (_err, _data, context) => { + queryClient.setQueryData(collectionQueryKey, context?.previousData); + }, onSettled: () => { // NOTE: We invalidate the library query here because we need to update the library's // collection list. queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) }); - queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.collection(libraryId, collectionId) }); + queryClient.invalidateQueries({ queryKey: collectionQueryKey }); }, }); }; @@ -584,13 +611,26 @@ export const useContainer = (containerId?: string) => ( export const useUpdateContainer = (containerId: string) => { const libraryId = getLibraryId(containerId); const queryClient = useQueryClient(); + const containerQueryKey = libraryAuthoringQueryKeys.container(containerId); return useMutation({ mutationFn: (data: api.UpdateContainerDataRequest) => api.updateContainerMetadata(containerId, data), + onMutate: (data) => { + const previousData = queryClient.getQueryData(containerQueryKey) as api.Container; + queryClient.setQueryData(containerQueryKey, { + ...previousData, + ...data, + }); + + return { previousData }; + }, + onError: (_err, _data, context) => { + queryClient.setQueryData(containerQueryKey, context?.previousData); + }, onSettled: () => { // NOTE: We invalidate the library query here because we need to update the library's // container list. queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) }); - queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.container(containerId) }); + queryClient.invalidateQueries({ queryKey: containerQueryKey }); }, }); }; @@ -627,11 +667,27 @@ export const useRestoreContainer = (containerId: string) => { /** * Get the metadata and children for a container in a library */ -export const useContainerChildren = (containerId?: string) => ( +export const useContainerChildren = (containerId?: string, published: boolean = false) => ( useQuery({ enabled: !!containerId, queryKey: libraryAuthoringQueryKeys.containerChildren(containerId!), - queryFn: () => api.getLibraryContainerChildren(containerId!), + queryFn: () => api.getLibraryContainerChildren(containerId!, published), + structuralSharing: (oldData: api.LibraryBlockMetadata[], newData: api.LibraryBlockMetadata[]) => { + // This just sets `isNew` flag to new children components + if (oldData) { + const oldDataIds = oldData.map((obj) => obj.id); + // eslint-disable-next-line no-param-reassign + newData = newData.map((newObj) => { + if (!oldDataIds.includes(newObj.id)) { + // Set isNew = true if we have new child on refetch + // eslint-disable-next-line no-param-reassign + newObj.isNew = true; + } + return newObj; + }); + } + return replaceEqualDeep(oldData, newData); + }, }) ); @@ -642,13 +698,22 @@ export const useAddComponentsToContainer = (containerId?: string) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (componentIds: string[]) => { - if (containerId !== undefined) { - return api.addComponentsToContainer(containerId, componentIds); + // istanbul ignore if: this should never happen + if (!containerId) { + return undefined; } - return undefined; + return api.addComponentsToContainer(containerId, componentIds); }, onSettled: () => { - queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.containerChildren(containerId!) }); + // istanbul ignore if: this should never happen + if (!containerId) { + return; + } + // NOTE: We invalidate the library query here because we need to update the library's + // container list. + const libraryId = getLibraryId(containerId); + queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.containerChildren(containerId) }); + queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) }); }, }); }; diff --git a/src/library-authoring/generic/filter-by-published/index.tsx b/src/library-authoring/generic/filter-by-published/index.tsx new file mode 100644 index 0000000000..825ac56f4d --- /dev/null +++ b/src/library-authoring/generic/filter-by-published/index.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { useLibraryContext } from '../../common/context/LibraryContext'; +import { FilterByPublished, PublishStatus } from '../../../search-manager'; + +/** + * When browsing library content for insertion into a course, we only show published + * content. In that case, there is no need for a 'Never Published' filter, which will + * never show results. This component removes that option from FilterByPublished + * when not relevant. + */ +const LibraryFilterByPublished : React.FC> = () => { + const { showOnlyPublished } = useLibraryContext(); + + if (showOnlyPublished) { + return ( + + ); + } + + return ; +}; + +export default LibraryFilterByPublished; diff --git a/src/library-authoring/generic/history-widget/HistoryWidget.scss b/src/library-authoring/generic/history-widget/HistoryWidget.scss index 84e11cf60a..2a9f946bd0 100644 --- a/src/library-authoring/generic/history-widget/HistoryWidget.scss +++ b/src/library-authoring/generic/history-widget/HistoryWidget.scss @@ -1,5 +1,5 @@ .history-widget-bar { - border-left: 8px solid $info-300; + border-left: 8px solid var(--pgn-color-info-300); border-radius: 4px; padding-left: 1rem; } diff --git a/src/library-authoring/generic/manage-collections/ManageCollections.test.tsx b/src/library-authoring/generic/manage-collections/ManageCollections.test.tsx index b73dd0d837..7f7b79d436 100644 --- a/src/library-authoring/generic/manage-collections/ManageCollections.test.tsx +++ b/src/library-authoring/generic/manage-collections/ManageCollections.test.tsx @@ -77,7 +77,7 @@ describe('', () => { await waitFor(() => { expect(axiosMock.history.patch.length).toEqual(1); }); - expect(mockShowToast).toHaveBeenCalledWith('Item collections updated'); + expect(mockShowToast).toHaveBeenCalledWith('Content added to collection.'); expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({ collection_keys: ['my-first-collection', 'my-second-collection'], }); @@ -103,7 +103,7 @@ describe('', () => { await waitFor(() => { expect(axiosMock.history.patch.length).toEqual(1); }); - expect(mockShowToast).toHaveBeenCalledWith('Item collections updated'); + expect(mockShowToast).toHaveBeenCalledWith('Content added to collection.'); expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({ collection_keys: ['my-first-collection', 'my-second-collection'], }); @@ -133,7 +133,7 @@ describe('', () => { expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual({ collection_keys: ['my-second-collection'], }); - expect(mockShowToast).toHaveBeenCalledWith('Failed to update item collections'); + expect(mockShowToast).toHaveBeenCalledWith('Failed to add content to collection.'); expect(screen.queryByRole('search')).not.toBeInTheDocument(); }); diff --git a/src/library-authoring/generic/manage-collections/ManageCollections.tsx b/src/library-authoring/generic/manage-collections/ManageCollections.tsx index 41bc36ce7c..4cafadfa47 100644 --- a/src/library-authoring/generic/manage-collections/ManageCollections.tsx +++ b/src/library-authoring/generic/manage-collections/ManageCollections.tsx @@ -16,6 +16,7 @@ import { ToastContext } from '../../../generic/toast-context'; import { CollectionMetadata } from '../../data/api'; import { useLibraryContext } from '../../common/context/LibraryContext'; import { SidebarActions, useSidebarContext } from '../../common/context/SidebarContext'; +import genericMessages from '../messages'; import messages from './messages'; interface ManageCollectionsProps { @@ -50,9 +51,9 @@ const CollectionsSelectableBox = ({ const handleConfirmation = () => { setBtnState('pending'); updateCollectionsMutation.mutateAsync(selectedCollections).then(() => { - showToast(intl.formatMessage(messages.manageCollectionsToComponentSuccess)); + showToast(intl.formatMessage(genericMessages.manageCollectionsSuccess)); }).catch(() => { - showToast(intl.formatMessage(messages.manageCollectionsToComponentFailed)); + showToast(intl.formatMessage(genericMessages.manageCollectionsFailed)); }).finally(() => { setBtnState('default'); onClose(); @@ -205,7 +206,7 @@ const ManageCollections = ({ opaqueKey, collections, useUpdateCollectionsHook }: const collectionNames = collections.map((collection) => collection.title); return ( - sidebarAction === SidebarActions.JumpToAddCollections + sidebarAction === SidebarActions.JumpToManageCollections ? ( setSidebarAction(SidebarActions.JumpToAddCollections)} + onManageClick={() => setSidebarAction(SidebarActions.JumpToManageCollections)} /> ) ); diff --git a/src/library-authoring/generic/manage-collections/messages.ts b/src/library-authoring/generic/manage-collections/messages.ts index c9b998be47..1afefa4967 100644 --- a/src/library-authoring/generic/manage-collections/messages.ts +++ b/src/library-authoring/generic/manage-collections/messages.ts @@ -21,16 +21,6 @@ const messages = defineMessages({ defaultMessage: 'Collection selection', description: 'Aria label text for collection selection box', }, - manageCollectionsToComponentSuccess: { - id: 'course-authoring.library-authoring.manage-collections.add-success', - defaultMessage: 'Item collections updated', - description: 'Message to display on updating item collections', - }, - manageCollectionsToComponentFailed: { - id: 'course-authoring.library-authoring.manage-collections.add-failed', - defaultMessage: 'Failed to update item collections', - description: 'Message to display on failure of updating item collections', - }, manageCollectionsToComponentConfirmBtn: { id: 'course-authoring.library-authoring.manage-collections.add-confirm-btn', defaultMessage: 'Confirm', diff --git a/src/library-authoring/generic/messages.ts b/src/library-authoring/generic/messages.ts new file mode 100644 index 0000000000..e1aec050f0 --- /dev/null +++ b/src/library-authoring/generic/messages.ts @@ -0,0 +1,16 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + manageCollectionsSuccess: { + id: 'course-authoring.library-authoring.manage-collections.success', + defaultMessage: 'Content added to collection.', + description: 'Message to display on updating item collections', + }, + manageCollectionsFailed: { + id: 'course-authoring.library-authoring.manage-collections.failed', + defaultMessage: 'Failed to add content to collection.', + description: 'Message to display on failure of updating item collections', + }, +}); + +export default messages; diff --git a/src/library-authoring/generic/status-widget/StatusWidget.scss b/src/library-authoring/generic/status-widget/StatusWidget.scss index cdb1c4cf61..7253940ed1 100644 --- a/src/library-authoring/generic/status-widget/StatusWidget.scss +++ b/src/library-authoring/generic/status-widget/StatusWidget.scss @@ -5,8 +5,8 @@ } &.published-status { - background-color: $info-100; - border-top: 4px solid $info-400; + background-color: var(--pgn-color-info-100); + border-top: 4px solid var(--pgn-color-info-400); } } diff --git a/src/library-authoring/generic/status-widget/index.tsx b/src/library-authoring/generic/status-widget/index.tsx index 19fdc71444..3694e2836c 100644 --- a/src/library-authoring/generic/status-widget/index.tsx +++ b/src/library-authoring/generic/status-widget/index.tsx @@ -85,6 +85,7 @@ type StatusWidgedProps = { publishedBy: string | null; numBlocks?: number; onCommit?: () => void; + onCommitLabel?: string; onRevert?: () => void; }; @@ -114,6 +115,7 @@ const StatusWidget = ({ publishedBy, numBlocks, onCommit, + onCommitLabel, onRevert, }: StatusWidgedProps) => { const intl = useIntl(); @@ -188,7 +190,7 @@ const StatusWidget = ({ {onCommit && ( )} {onRevert && ( diff --git a/src/library-authoring/library-info/LibraryPublishStatus.tsx b/src/library-authoring/library-info/LibraryPublishStatus.tsx index 89ac4e8468..b6ee64738e 100644 --- a/src/library-authoring/library-info/LibraryPublishStatus.tsx +++ b/src/library-authoring/library-info/LibraryPublishStatus.tsx @@ -51,6 +51,7 @@ const LibraryPublishStatus = () => { void; +} + /** * Sidebar container for library pages. * @@ -24,9 +30,25 @@ import messages from '../messages'; * You can add more components in `bodyComponentMap`. * Use the returned actions to open and close this sidebar. */ -const LibrarySidebar = () => { +const LibrarySidebar = ({ onSidebarClose }: LibrarySidebarProps) => { const intl = useIntl(); - const { sidebarComponentInfo, closeLibrarySidebar } = useSidebarContext(); + const { + sidebarAction, + setSidebarTab, + sidebarComponentInfo, + closeLibrarySidebar, + } = useSidebarContext(); + const jumpToCollections = sidebarAction === SidebarActions.JumpToManageCollections; + const jumpToTags = sidebarAction === SidebarActions.JumpToManageTags; + + React.useEffect(() => { + // Show Manage tab if JumpToManageCollections or JumpToManageTags action is set + if (jumpToCollections || jumpToTags) { + // COMPONENT_INFO_TABS.Manage works for containers as well as its value + // is same as UNIT_INFO_TABS.Manage. + setSidebarTab(COMPONENT_INFO_TABS.Manage); + } + }, [jumpToCollections, setSidebarTab, jumpToTags]); const bodyComponentMap = { [SidebarBodyComponentId.AddContent]: , @@ -49,6 +71,11 @@ const LibrarySidebar = () => { const buildBody = () : React.ReactNode => bodyComponentMap[sidebarComponentInfo?.type || 'unknown']; const buildHeader = (): React.ReactNode => headerComponentMap[sidebarComponentInfo?.type || 'unknown']; + const handleSidebarClose = () => { + closeLibrarySidebar(); + onSidebarClose?.(); + }; + return ( @@ -58,7 +85,7 @@ const LibrarySidebar = () => { src={Close} iconAs={Icon} alt={intl.formatMessage(messages.closeButtonAlt)} - onClick={closeLibrarySidebar} + onClick={handleSidebarClose} size="inline" /> diff --git a/src/library-authoring/routes.test.tsx b/src/library-authoring/routes.test.tsx index 8a58f3a41f..ad03cbc4c3 100644 --- a/src/library-authoring/routes.test.tsx +++ b/src/library-authoring/routes.test.tsx @@ -15,6 +15,14 @@ jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useNavigate: () => mockNavigate, })); +jest.mock('./common/context/LibraryContext', () => ({ + ...jest.requireActual('./common/context/LibraryContext'), + useLibraryContext: () => ({ + setComponentId: jest.fn(), + setUnitId: jest.fn(), + setCollectionId: jest.fn(), + }), +})); mockContentLibrary.applyMock(); diff --git a/src/library-authoring/routes.ts b/src/library-authoring/routes.ts index 1bee5219ab..4615f229d3 100644 --- a/src/library-authoring/routes.ts +++ b/src/library-authoring/routes.ts @@ -11,6 +11,7 @@ import { useSearchParams, type PathMatch, } from 'react-router-dom'; +import { useLibraryContext } from './common/context/LibraryContext'; export const BASE_ROUTE = '/library/:libraryId'; @@ -48,6 +49,7 @@ export type NavigateToData = { collectionId?: string, contentType?: ContentType, unitId?: string, + doubleClicked?: boolean, }; export type LibraryRoutesData = { @@ -66,6 +68,7 @@ export const useLibraryRoutes = (): LibraryRoutesData => { const params = useParams(); const [searchParams] = useSearchParams(); const navigate = useNavigate(); + const { setComponentId, setUnitId, setCollectionId } = useLibraryContext(); const insideCollection = matchPath(BASE_ROUTE + ROUTES.COLLECTION, pathname); const insideCollections = matchPath(BASE_ROUTE + ROUTES.COLLECTIONS, pathname); @@ -78,6 +81,7 @@ export const useLibraryRoutes = (): LibraryRoutesData => { collectionId, unitId, contentType, + doubleClicked, }: NavigateToData = {}) => { const { collectionId: urlCollectionId, @@ -99,6 +103,18 @@ export const useLibraryRoutes = (): LibraryRoutesData => { }; let route: string; + // Update componentId, unitId, collectionId in library context if is not undefined. + // Ids can be cleared from route by passing in empty string so we need to set it. + if (componentId !== undefined) { + setComponentId(componentId); + } + if (unitId !== undefined) { + setUnitId(unitId); + } + if (collectionId !== undefined) { + setCollectionId(collectionId); + } + // Providing contentType overrides the current route so we can change tabs. if (contentType === ContentType.components) { route = ROUTES.COMPONENTS; @@ -111,7 +127,7 @@ export const useLibraryRoutes = (): LibraryRoutesData => { } else if (insideCollections) { // We're inside the Collections tab, route = ( - (collectionId && collectionId === (urlCollectionId || urlSelectedItemId)) + (collectionId && doubleClicked) // now open the previously-selected collection, ? ROUTES.COLLECTION // or stay there to list all collections, or a selected collection. @@ -128,7 +144,7 @@ export const useLibraryRoutes = (): LibraryRoutesData => { } else if (insideUnits) { // We're inside the units tab, route = ( - (unitId && unitId === (urlUnitId || urlSelectedItemId)) + (unitId && doubleClicked) // now open the previously-selected unit, ? ROUTES.UNIT // or stay there to list all units, or a selected unit. @@ -142,10 +158,10 @@ export const useLibraryRoutes = (): LibraryRoutesData => { // We're inside the All Content tab, so stay there, // and select a component. route = ROUTES.COMPONENT; - } else if (collectionId && collectionId === (urlCollectionId || urlSelectedItemId)) { + } else if (collectionId && doubleClicked) { // now open the previously-selected collection route = ROUTES.COLLECTION; - } else if (unitId && unitId === (urlUnitId || urlSelectedItemId)) { + } else if (unitId && doubleClicked) { // now open the previously-selected unit route = ROUTES.UNIT; } else { @@ -158,7 +174,15 @@ export const useLibraryRoutes = (): LibraryRoutesData => { pathname: newPath, search: searchParams.toString(), }); - }, [navigate, params, searchParams, pathname]); + }, [ + navigate, + params, + searchParams, + pathname, + setComponentId, + setUnitId, + setCollectionId, + ]); return { navigateTo, diff --git a/src/library-authoring/units/LibraryUnitBlocks.tsx b/src/library-authoring/units/LibraryUnitBlocks.tsx index 1ef20fa39a..e2d654cacd 100644 --- a/src/library-authoring/units/LibraryUnitBlocks.tsx +++ b/src/library-authoring/units/LibraryUnitBlocks.tsx @@ -1,12 +1,12 @@ import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { - ActionRow, Badge, Button, Icon, IconButton, Stack, useToggle, + ActionRow, Badge, Button, Icon, Stack, useToggle, } from '@openedx/paragon'; -import { Add, Description, DragIndicator } from '@openedx/paragon/icons'; -import { useQueryClient } from '@tanstack/react-query'; +import { Add, Description } from '@openedx/paragon/icons'; import classNames from 'classnames'; -import { useContext, useEffect, useState } from 'react'; -import { ContentTagsDrawerSheet } from '../../content-tags-drawer'; +import { + useCallback, useContext, useEffect, useState, +} from 'react'; import { blockTypes } from '../../editors/data/constants/app'; import DraggableList, { SortableItem } from '../../generic/DraggableList'; @@ -22,17 +22,17 @@ import { PickLibraryContentModal } from '../add-content'; import ComponentMenu from '../components'; import { LibraryBlockMetadata } from '../data/api'; import { - libraryAuthoringQueryKeys, useContainerChildren, useUpdateContainerChildren, useUpdateXBlockFields, } from '../data/apiHooks'; import { LibraryBlock } from '../LibraryBlock'; -import { useLibraryRoutes } from '../routes'; +import { useLibraryRoutes, ContentType } from '../routes'; import messages from './messages'; -import { useSidebarContext } from '../common/context/SidebarContext'; +import { SidebarActions, SidebarBodyComponentId, useSidebarContext } from '../common/context/SidebarContext'; import { ToastContext } from '../../generic/toast-context'; import { canEditComponent } from '../components/ComponentEditorModal'; +import { useRunOnNextRender } from '../../utils'; /** Components that need large min height in preview */ const LARGE_COMPONENTS = [ @@ -43,195 +43,247 @@ const LARGE_COMPONENTS = [ 'lti_consumer', ]; -interface BlockHeaderProps { - block: LibraryBlockMetadata; - onTagClick: () => void; +interface LibraryBlockMetadataWithUniqueId extends LibraryBlockMetadata { + originalId: string; +} + +interface ComponentBlockProps { + block: LibraryBlockMetadataWithUniqueId; + readOnly?: boolean; + isDragging?: boolean; } -/** Component header, split out to reuse in drag overlay */ -const BlockHeader = ({ block, onTagClick }: BlockHeaderProps) => { +/** Component header */ +const BlockHeader = ({ block, readOnly }: ComponentBlockProps) => { const intl = useIntl(); + const { showOnlyPublished } = useLibraryContext(); const { showToast } = useContext(ToastContext); + const { navigateTo } = useLibraryRoutes(); + const { openComponentInfoSidebar, setSidebarAction } = useSidebarContext(); - const updateMutation = useUpdateXBlockFields(block.id); + const updateMutation = useUpdateXBlockFields(block.originalId); - const handleSaveDisplayName = (newDisplayName: string) => { - updateMutation.mutateAsync({ - metadata: { - display_name: newDisplayName, - }, - }).then(() => { + const handleSaveDisplayName = async (newDisplayName: string) => { + try { + await updateMutation.mutateAsync({ + metadata: { + display_name: newDisplayName, + }, + }); showToast(intl.formatMessage(messages.updateComponentSuccessMsg)); - }).catch(() => { + } catch (err) { showToast(intl.formatMessage(messages.updateComponentErrorMsg)); - }); + throw err; + } + }; + + /* istanbul ignore next */ + const scheduleJumpToTags = useRunOnNextRender(() => { + // TODO: Ugly hack to make sure sidebar shows manage tags section + // This needs to run after all changes to url takes place to avoid conflicts. + setTimeout(() => setSidebarAction(SidebarActions.JumpToManageTags), 250); + }); + + /* istanbul ignore next */ + const jumpToManageTags = () => { + navigateTo({ componentId: block.originalId }); + openComponentInfoSidebar(block.originalId); + scheduleJumpToTags(); }; return ( <> - + e.stopPropagation()} + > - - {block.hasUnpublishedChanges && ( + e.stopPropagation()} + > + {!showOnlyPublished && block.hasUnpublishedChanges && ( - + )} - - + + {!readOnly && } ); }; -interface LibraryUnitBlocksProps { - /** set to true if it is rendered as preview - * This disables drag and drop - */ - preview?: boolean; -} - -export const LibraryUnitBlocks = ({ preview }: LibraryUnitBlocksProps) => { - const intl = useIntl(); - const [orderedBlocks, setOrderedBlocks] = useState([]); - const [isManageTagsDrawerOpen, openManageTagsDrawer, closeManageTagsDrawer] = useToggle(false); - const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle(); - - const [hidePreviewFor, setHidePreviewFor] = useState(null); +/** ComponentBlock to render preview of given component under Unit */ +const ComponentBlock = ({ block, readOnly, isDragging }: ComponentBlockProps) => { + const { showOnlyPublished } = useLibraryContext(); const { navigateTo } = useLibraryRoutes(); - const { showToast } = useContext(ToastContext); const { - unitId, - showOnlyPublished, - componentId, - readOnly, - setComponentId, - openComponentEditor, + unitId, collectionId, componentId, openComponentEditor, } = useLibraryContext(); - const { - openAddContentSidebar, - } = useSidebarContext(); - - const queryClient = useQueryClient(); - const orderMutator = useUpdateContainerChildren(unitId); - const { - data: blocks, - isLoading, - isError, - error, - } = useContainerChildren(unitId); - - useEffect(() => setOrderedBlocks(blocks || []), [blocks]); - - if (isLoading) { - return ; - } - - if (isError) { - // istanbul ignore next - return ; - } - - const handleReorder = () => async (newOrder: LibraryBlockMetadata[]) => { - const usageKeys = newOrder.map((o) => o.id); - try { - await orderMutator.mutateAsync(usageKeys); - showToast(intl.formatMessage(messages.orderUpdatedMsg)); - } catch (e) { - showToast(intl.formatMessage(messages.failedOrderUpdatedMsg)); - } - }; - - const onTagSidebarClose = () => { - queryClient.invalidateQueries(libraryAuthoringQueryKeys.containerChildren(unitId!)); - closeManageTagsDrawer(); - }; + const { openInfoSidebar, sidebarComponentInfo } = useSidebarContext(); - const handleComponentSelection = (block: LibraryBlockMetadata, numberOfClicks: number) => { - setComponentId(block.id); - navigateTo({ componentId: block.id }); - const canEdit = canEditComponent(block.id); + const handleComponentSelection = useCallback((numberOfClicks: number) => { + navigateTo({ componentId: block.originalId }); + const canEdit = canEditComponent(block.originalId); if (numberOfClicks > 1 && canEdit) { // Open editor on double click. - openComponentEditor(block.id); + openComponentEditor(block.originalId); + } else { + // open current component sidebar + openInfoSidebar(block.originalId, collectionId, unitId); } - }; + }, [block, collectionId, unitId, navigateTo, canEditComponent, openComponentEditor, openInfoSidebar]); + + useEffect(() => { + if (block.isNew) { + handleComponentSelection(1); + } + }, [block]); /* istanbul ignore next */ - const calculateMinHeight = (block: LibraryBlockMetadata) => { + const calculateMinHeight = () => { if (LARGE_COMPONENTS.includes(block.blockType)) { return '700px'; } return '200px'; }; - const renderOverlay = (activeId: string | null) => { - if (!activeId) { - return null; - } - const block = orderedBlocks?.find((val) => val.id === activeId); - if (!block) { - return null; + const getComponentStyle = useCallback(() => { + if (isDragging) { + return { + outline: '2px dashed gray', + maxHeight: '200px', + overflowY: 'hidden', + }; } - return ( - - - - - ); - }; + return {}; + }, [isDragging, componentId, block]); + + const selected = sidebarComponentInfo?.type === SidebarBodyComponentId.ComponentInfo + && sidebarComponentInfo?.id === block.originalId; - const renderedBlocks = orderedBlocks?.map((block) => ( - + return ( + } + componentStyle={getComponentStyle()} + actions={} actionStyle={{ borderRadius: '8px 8px 0px 0px', padding: '0.5rem 1rem', background: '#FBFAF9', borderBottom: 'solid 1px #E1DDDB', - outline: hidePreviewFor === block.id && '2px dashed gray', }} - isClickable - onClick={(e: { detail: number; }) => handleComponentSelection(block, e.detail)} - disabled={preview} + isClickable={!readOnly} + onClick={!readOnly ? (e: { detail: number; }) => handleComponentSelection(e.detail) : undefined} + disabled={readOnly} + cardClassName={selected ? 'selected' : undefined} > - {hidePreviewFor !== block.id && ( -
e.stopPropagation()} >
- )}
- )); + ); +}; + +interface LibraryUnitBlocksProps { + /** set to true if it is rendered as preview + * This disables drag and drop, title edit and menus + */ + readOnly?: boolean; +} + +export const LibraryUnitBlocks = ({ readOnly: componentReadOnly }: LibraryUnitBlocksProps) => { + const intl = useIntl(); + const [orderedBlocks, setOrderedBlocks] = useState([]); + const [isAddLibraryContentModalOpen, showAddLibraryContentModal, closeAddLibraryContentModal] = useToggle(); + + const [hidePreviewFor, setHidePreviewFor] = useState(null); + const { showToast } = useContext(ToastContext); + + const { unitId, readOnly: libraryReadOnly, showOnlyPublished } = useLibraryContext(); + + const readOnly = componentReadOnly || libraryReadOnly; + + const { openAddContentSidebar } = useSidebarContext(); + + const orderMutator = useUpdateContainerChildren(unitId); + const { + data: blocks, + isLoading, + isError, + error, + } = useContainerChildren(unitId, showOnlyPublished); + + const handleReorder = useCallback(() => async (newOrder?: LibraryBlockMetadataWithUniqueId[]) => { + if (!newOrder) { + return; + } + const usageKeys = newOrder.map((o) => o.originalId); + try { + await orderMutator.mutateAsync(usageKeys); + showToast(intl.formatMessage(messages.orderUpdatedMsg)); + } catch (e) { + showToast(intl.formatMessage(messages.failedOrderUpdatedMsg)); + } + }, [orderMutator]); + + useEffect(() => { + // Create new ids which are unique using index. + // This is required to support multiple components with same id under a unit. + const newBlocks = blocks?.map((block, idx) => { + const newBlock: LibraryBlockMetadataWithUniqueId = { + ...block, + id: `${block.id}----${idx}`, + originalId: block.id, + }; + return newBlock; + }); + return setOrderedBlocks(newBlocks || []); + }, [blocks, setOrderedBlocks]); + + if (isLoading) { + return ; + } + + if (isError) { + // istanbul ignore next + return ; + } return (
@@ -239,13 +291,22 @@ export const LibraryUnitBlocks = ({ preview }: LibraryUnitBlocksProps) => { itemList={orderedBlocks} setState={setOrderedBlocks} updateOrder={handleReorder} - renderOverlay={renderOverlay} activeId={hidePreviewFor} setActiveId={setHidePreviewFor} > - {renderedBlocks} + {orderedBlocks?.map((block, idx) => ( + // A container can have multiple instances of the same block + // eslint-disable-next-line react/no-array-index-key + + ))} - { !preview && ( + {!readOnly && (
)} -
); }; diff --git a/src/library-authoring/units/LibraryUnitPage.test.tsx b/src/library-authoring/units/LibraryUnitPage.test.tsx index f90dcedd12..18671ca562 100644 --- a/src/library-authoring/units/LibraryUnitPage.test.tsx +++ b/src/library-authoring/units/LibraryUnitPage.test.tsx @@ -42,14 +42,14 @@ mockContentLibrary.applyMock(); mockXBlockFields.applyMock(); mockLibraryBlockMetadata.applyMock(); -const closestCenter = jest.fn(); -jest.mock('@dnd-kit/core', () => ({ - ...jest.requireActual('@dnd-kit/core'), +const verticalSortableListCollisionDetection = jest.fn(); +jest.mock('../../generic/DraggableList/verticalSortableList', () => ({ + ...jest.requireActual('../../generic/DraggableList/verticalSortableList'), // Since jsdom (used by jest) does not support getBoundingClientRect function // which is required for drag-n-drop calculations, we mock closestCorners fn // from dnd-kit to return collided elements as per the test. This allows us to // test all drag-n-drop handlers. - closestCenter: () => closestCenter(), + verticalSortableListCollisionDetection: () => verticalSortableListCollisionDetection(), })); describe('', () => { @@ -106,12 +106,12 @@ describe('', () => { it('can rename unit', async () => { renderLibraryUnitPage(); expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument(); - // Unit title - const unitTitle = screen.getAllByRole( + + const editUnitTitleButton = screen.getAllByRole( 'button', - { name: mockGetContainerMetadata.containerData.displayName }, - )[0]; - fireEvent.click(unitTitle); + { name: /edit/i }, + )[0]; // 0 is the Unit Title, 1 is the first component on the list + fireEvent.click(editUnitTitleButton); const url = getLibraryContainerApiUrl(mockGetContainerMetadata.containerId); axiosMock.onPatch(url).reply(200); @@ -137,12 +137,12 @@ describe('', () => { it('show error if renaming unit fails', async () => { renderLibraryUnitPage(); expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument(); - // Unit title - const unitTitle = screen.getAllByRole( + + const editUnitTitleButton = screen.getAllByRole( 'button', - { name: mockGetContainerMetadata.containerData.displayName }, - )[0]; - fireEvent.click(unitTitle); + { name: /edit/i }, + )[0]; // 0 is the Unit Title, 1 is the first component on the list + fireEvent.click(editUnitTitleButton); const url = getLibraryContainerApiUrl(mockGetContainerMetadata.containerId); axiosMock.onPatch(url).reply(400); @@ -187,9 +187,9 @@ describe('', () => { it('should open and close component sidebar on component selection', async () => { renderLibraryUnitPage(); - const component = await screen.findByText('text block 0'); - userEvent.click(component); + // Card is 3 levels up the component name div + userEvent.click(component.parentElement!.parentElement!.parentElement!); const sidebar = await screen.findByTestId('library-sidebar'); const { findByRole, findByText } = within(sidebar); @@ -210,11 +210,11 @@ describe('', () => { // Wait loading of the component await screen.findByText('text block 0'); - const componentTitle = screen.getAllByRole( + const editButton = screen.getAllByRole( 'button', - { name: 'text block 0' }, - )[0]; - fireEvent.click(componentTitle); + { name: /edit/i }, + )[1]; // 0 is the Unit Title, 1 is the first component on the list + fireEvent.click(editButton); await waitFor(() => { expect(screen.getByRole('textbox', { name: /text input/i })).toBeInTheDocument(); @@ -244,11 +244,11 @@ describe('', () => { // Wait loading of the component await screen.findByText('text block 0'); - const componentTitle = screen.getAllByRole( + const editButton = screen.getAllByRole( 'button', - { name: 'text block 0' }, - )[0]; - fireEvent.click(componentTitle); + { name: /edit/i }, + )[1]; // 0 is the Unit Title, 1 is the first component on the list + fireEvent.click(editButton); await waitFor(() => { expect(screen.getByRole('textbox', { name: /text input/i })).toBeInTheDocument(); @@ -276,14 +276,26 @@ describe('', () => { axiosMock .onPatch(getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.containerId)) .reply(200); - closestCenter.mockReturnValue([{ id: 'lb:org1:Demo_course:html:text-1' }]); + verticalSortableListCollisionDetection.mockReturnValue([{ id: 'lb:org1:Demo_course:html:text-1----1' }]); await act(async () => { fireEvent.keyDown(firstDragHandle, { code: 'Space' }); }); + setTimeout(() => fireEvent.keyDown(firstDragHandle, { code: 'Space' })); + await waitFor(() => expect(mockShowToast).toHaveBeenLastCalledWith('Order updated')); + }); + + it('should cancel update order api on cancelling dragging component', async () => { + renderLibraryUnitPage(); + const firstDragHandle = (await screen.findAllByRole('button', { name: 'Drag to reorder' }))[0]; + axiosMock + .onPatch(getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.containerId)) + .reply(200); + verticalSortableListCollisionDetection.mockReturnValue([{ id: 'lb:org1:Demo_course:html:text-1----1' }]); await act(async () => { fireEvent.keyDown(firstDragHandle, { code: 'Space' }); }); - await waitFor(() => expect(mockShowToast).toHaveBeenLastCalledWith('Order updated')); + setTimeout(() => fireEvent.keyDown(firstDragHandle, { code: 'Escape' })); + await waitFor(() => expect(mockShowToast).not.toHaveBeenLastCalledWith('Order updated')); }); it('should show toast error message on update order failure', async () => { @@ -292,13 +304,11 @@ describe('', () => { axiosMock .onPatch(getLibraryContainerChildrenApiUrl(mockGetContainerMetadata.containerId)) .reply(500); - closestCenter.mockReturnValue([{ id: 'lb:org1:Demo_course:html:text-1' }]); - await act(async () => { - fireEvent.keyDown(firstDragHandle, { code: 'Space' }); - }); + verticalSortableListCollisionDetection.mockReturnValue([{ id: 'lb:org1:Demo_course:html:text-1----1' }]); await act(async () => { fireEvent.keyDown(firstDragHandle, { code: 'Space' }); }); + setTimeout(() => fireEvent.keyDown(firstDragHandle, { code: 'Space' })); await waitFor(() => expect(mockShowToast).toHaveBeenLastCalledWith('Failed to update components order')); }); @@ -311,7 +321,7 @@ describe('', () => { const menu = screen.getAllByRole('button', { name: /component actions menu/i })[0]; fireEvent.click(menu); - const removeButton = await screen.getByText('Remove from unit'); + const removeButton = await screen.findByText('Remove from unit'); fireEvent.click(removeButton); await waitFor(() => { @@ -342,7 +352,7 @@ describe('', () => { const menu = screen.getAllByRole('button', { name: /component actions menu/i })[0]; fireEvent.click(menu); - const removeButton = await screen.getByText('Remove from unit'); + const removeButton = await screen.findByText('Remove from unit'); fireEvent.click(removeButton); await waitFor(() => { @@ -360,7 +370,7 @@ describe('', () => { const menu = screen.getAllByRole('button', { name: /component actions menu/i })[0]; fireEvent.click(menu); - const removeButton = await screen.getByText('Remove from unit'); + const removeButton = await screen.findByText('Remove from unit'); fireEvent.click(removeButton); await waitFor(() => { @@ -388,7 +398,7 @@ describe('', () => { renderLibraryUnitPage(); const component = await screen.findByText('text block 0'); - userEvent.click(component); + userEvent.click(component.parentElement!.parentElement!.parentElement!); const sidebar = await screen.findByTestId('library-sidebar'); const { findByRole, findByText } = within(sidebar); @@ -409,7 +419,7 @@ describe('', () => { renderLibraryUnitPage(); const component = await screen.findByText('text block 0'); // trigger double click - userEvent.click(component, undefined, { clickCount: 2 }); + userEvent.click(component.parentElement!.parentElement!.parentElement!, undefined, { clickCount: 2 }); expect(await screen.findByRole('dialog', { name: 'Editor Dialog' })).toBeInTheDocument(); }); }); diff --git a/src/library-authoring/units/LibraryUnitPage.tsx b/src/library-authoring/units/LibraryUnitPage.tsx index e362cdb900..c8d337314b 100644 --- a/src/library-authoring/units/LibraryUnitPage.tsx +++ b/src/library-authoring/units/LibraryUnitPage.tsx @@ -41,14 +41,16 @@ const EditableTitle = ({ unitId }: EditableTitleProps) => { const updateMutation = useUpdateContainer(unitId); const { showToast } = useContext(ToastContext); - const handleSaveDisplayName = (newDisplayName: string) => { - updateMutation.mutateAsync({ - displayName: newDisplayName, - }).then(() => { + const handleSaveDisplayName = async (newDisplayName: string) => { + try { + await updateMutation.mutateAsync({ + displayName: newDisplayName, + }); showToast(intl.formatMessage(messages.updateContainerSuccessMsg)); - }).catch(() => { + } catch (err) { showToast(intl.formatMessage(messages.updateContainerErrorMsg)); - }); + throw err; + } }; // istanbul ignore if: this should never happen @@ -91,7 +93,7 @@ const HeaderActions = () => { } else { openUnitInfoSidebar(unitId); } - navigateTo({ unitId }); + navigateTo({ unitId, componentId: '' }); }, [unitId, infoSidebarIsOpen]); return ( @@ -123,21 +125,30 @@ export const LibraryUnitPage = () => { const { libraryId, unitId, - collectionId, componentId, + collectionId, } = useLibraryContext(); const { - sidebarComponentInfo, openInfoSidebar, + sidebarComponentInfo, setDefaultTab, setHiddenTabs, } = useSidebarContext(); + const { navigateTo } = useLibraryRoutes(); + + // Open unit or component sidebar on mount + useEffect(() => { + // includes componentId to open correct sidebar on page mount from url + openInfoSidebar(componentId, collectionId, unitId); + // avoid including componentId in dependencies to prevent flicker on closing sidebar. + // See below useEffect that clears componentId on closing sidebar. + }, [unitId, collectionId]); useEffect(() => { setDefaultTab({ collection: COLLECTION_INFO_TABS.Details, component: COMPONENT_INFO_TABS.Manage, - unit: UNIT_INFO_TABS.Organize, + unit: UNIT_INFO_TABS.Manage, }); setHiddenTabs([COMPONENT_INFO_TABS.Preview, UNIT_INFO_TABS.Preview]); return () => { @@ -150,10 +161,6 @@ export const LibraryUnitPage = () => { }; }, [setDefaultTab, setHiddenTabs]); - useEffect(() => { - openInfoSidebar(componentId, collectionId, unitId); - }, [componentId, unitId, collectionId]); - if (!unitId || !libraryId) { // istanbul ignore next - This shouldn't be possible; it's just here to satisfy the type checker. throw new Error('Rendered without unitId or libraryId URL parameter'); @@ -232,7 +239,7 @@ export const LibraryUnitPage = () => { className="library-authoring-sidebar box-shadow-left-1 bg-white" data-testid="library-sidebar" > - + navigateTo({ componentId: '' })} />
)}
diff --git a/src/library-authoring/units/index.scss b/src/library-authoring/units/index.scss index 91b5d78b1f..76f8148073 100644 --- a/src/library-authoring/units/index.scss +++ b/src/library-authoring/units/index.scss @@ -3,7 +3,8 @@ border-radius: 8px; padding: 0; margin-bottom: 1rem; - border: solid 1px $light-500; + border: solid 1px var(--pgn-color-light-500); + } .pgn__card.clickable { @@ -15,6 +16,23 @@ // this is required for clicks to be passed to underlying iframe component pointer-events: none; } + + &.selected:not(:focus) { + outline: 2px var(--pgn-color-gray-700) solid; + } + + &.selected:focus { + outline: 3px var(--pgn-color-gray-700) solid; + } + + &:not(.selected):focus { + outline: 1px var(--pgn-color-gray-200) solid; + outline-offset: 2px; + } + + &::before { + border: none !important; // Remove default focus + } } .pgn__action-row { diff --git a/src/optimizer-page/CourseOptimizerPage.test.js b/src/optimizer-page/CourseOptimizerPage.test.js index fd49dbbb75..46f1b2da3e 100644 --- a/src/optimizer-page/CourseOptimizerPage.test.js +++ b/src/optimizer-page/CourseOptimizerPage.test.js @@ -37,6 +37,24 @@ const OptimizerPage = () => ( ); +const setupOptimizerPage = async () => { + axiosMock.onGet(getLinkCheckStatusApiUrl(courseId)).reply(200, mockApiResponse); + const optimizerPage = render(); + + // Click the scan button + fireEvent.click(optimizerPage.getByText(messages.buttonTitle.defaultMessage)); + + // Wait for the scan results to load + await waitFor(() => { + expect(optimizerPage.getByText('Introduction to Programming')).toBeInTheDocument(); + }); + + // Click on filters button + fireEvent.click(optimizerPage.getByText(scanResultsMessages.filterButtonLabel.defaultMessage)); + + return optimizerPage; +}; + describe('CourseOptimizerPage', () => { describe('pollLinkCheckDuringScan', () => { let mockFetchLinkCheckStatus; @@ -71,6 +89,7 @@ describe('CourseOptimizerPage', () => { pollLinkCheckDuringScan(linkCheckInProgress, interval, dispatch, courseId); expect(interval.current).toBeTruthy(); }); + it('should not start polling if link check is not in progress', () => { const linkCheckInProgress = false; const interval = { current: null }; @@ -79,6 +98,7 @@ describe('CourseOptimizerPage', () => { pollLinkCheckDuringScan(linkCheckInProgress, interval, dispatch, courseId); expect(interval.current).toBeFalsy(); }); + it('should clear the interval if link check is finished', () => { const linkCheckInProgress = false; const interval = { current: 1 }; @@ -127,74 +147,180 @@ describe('CourseOptimizerPage', () => { }); }); - it('should list broken links results', async () => { - const { - getByText, queryAllByText, getAllByText, container, - } = render(); + it('should show no broken links found message', async () => { + axiosMock + .onGet(getLinkCheckStatusApiUrl(courseId)) + .reply(200, { LinkCheckStatus: 'Succeeded' }); + const { getByText } = render(); expect(getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument(); fireEvent.click(getByText(messages.buttonTitle.defaultMessage)); await waitFor(() => { - expect(getByText('5 broken links')).toBeInTheDocument(); - expect(getByText('5 locked links')).toBeInTheDocument(); + expect(getByText(scanResultsMessages.noBrokenLinksCard.defaultMessage)).toBeInTheDocument(); + }); + }); + + it('should show error message if request does not go through', async () => { + axiosMock + .onPost(postLinkCheckCourseApiUrl(courseId)) + .reply(500); + render(); + expect(screen.getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument(); + fireEvent.click(screen.getByText(messages.buttonTitle.defaultMessage)); + await waitFor(() => { + expect(screen.getByText(generalMessages.supportText.defaultMessage)).toBeInTheDocument(); }); + }); + + it('should show only broken links when brokenLinks filter is selected', async () => { + const { + getByText, + getByLabelText, + queryByText, + container, + } = await setupOptimizerPage(); + // Check if the modal is opened + expect(getByText('Broken')).toBeInTheDocument(); + // Select the broken links checkbox + fireEvent.click(getByLabelText(scanResultsMessages.brokenLabel.defaultMessage)); + const collapsibleTrigger = container.querySelector('.collapsible-trigger'); expect(collapsibleTrigger).toBeInTheDocument(); fireEvent.click(collapsibleTrigger); + await waitFor(() => { - expect(getAllByText(scanResultsMessages.brokenLinkStatus.defaultMessage)[0]).toBeInTheDocument(); - expect(queryAllByText(scanResultsMessages.lockedLinkStatus.defaultMessage)[0]).toBeInTheDocument(); - expect(queryAllByText(scanResultsMessages.recommendedManualCheckText.defaultMessage)[0]).toBeInTheDocument(); - const brokenLinks = getAllByText('https://example.com/broken-link-algo'); - expect(brokenLinks.length).toBeGreaterThan(0); - fireEvent.click(brokenLinks[0]); - const lockedLinks = getAllByText('https://example.com/locked-link-algo'); - expect(lockedLinks.length).toBeGreaterThan(0); - fireEvent.click(lockedLinks[0]); - fireEvent.click((getAllByText('Go to Block'))[0]); + expect(getByText('Test Broken Links')).toBeInTheDocument(); + expect(queryByText('Test Locked Links')).not.toBeInTheDocument(); + expect(queryByText('Test Manual Links')).not.toBeInTheDocument(); }); }); - it('should not list locked links results when show locked links is unchecked', async () => { + it('should show only manual links when manualLinks filter is selected and show all links when clicked again', async () => { const { - getByText, getAllByText, getByLabelText, queryAllByText, queryByText, container, - } = render(); - expect(getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument(); - fireEvent.click(getByText(messages.buttonTitle.defaultMessage)); + getByText, + getByLabelText, + queryByText, + container, + } = await setupOptimizerPage(); + // Check if the modal is opened + expect(getByText('Manual')).toBeInTheDocument(); + // Select the manual links checkbox + fireEvent.click(getByLabelText(scanResultsMessages.manualLabel.defaultMessage)); + + const collapsibleTrigger = container.querySelector('.collapsible-trigger'); + expect(collapsibleTrigger).toBeInTheDocument(); + fireEvent.click(collapsibleTrigger); + + await waitFor(() => { + expect(getByText('Test Manual Links')).toBeInTheDocument(); + expect(queryByText('Test Broken Links')).not.toBeInTheDocument(); + expect(queryByText('Test Locked Links')).not.toBeInTheDocument(); + }); + + // Click the manual links checkbox again to clear the filter + fireEvent.click(getByLabelText(scanResultsMessages.manualLabel.defaultMessage)); + + // Assert that all links are displayed after clearing the filter await waitFor(() => { - expect(getByText('5 broken links')).toBeInTheDocument(); + expect(getByText('Test Broken Links')).toBeInTheDocument(); + expect(getByText('Test Manual Links')).toBeInTheDocument(); + expect(getByText('Test Locked Links')).toBeInTheDocument(); }); - fireEvent.click(getByLabelText(scanResultsMessages.lockedCheckboxLabel.defaultMessage)); + }); + + it('should show only manual & locked links when manual & locked Links filters are selected, ignore broken links', async () => { + const { + getByText, + getByLabelText, + queryByText, + container, + } = await setupOptimizerPage(); + // Check if the modal is opened + expect(getByText('Manual')).toBeInTheDocument(); + expect(getByText('Locked')).toBeInTheDocument(); + // Select the manual & locked links checkbox + fireEvent.click(getByLabelText(scanResultsMessages.manualLabel.defaultMessage)); + fireEvent.click(getByLabelText(scanResultsMessages.lockedLabel.defaultMessage)); + const collapsibleTrigger = container.querySelector('.collapsible-trigger'); expect(collapsibleTrigger).toBeInTheDocument(); fireEvent.click(collapsibleTrigger); + await waitFor(() => { - expect(queryByText('5 locked links')).not.toBeInTheDocument(); - expect(getAllByText(scanResultsMessages.brokenLinkStatus.defaultMessage)[0]).toBeInTheDocument(); - expect(queryAllByText(scanResultsMessages.lockedLinkStatus.defaultMessage)?.[0]).toBeUndefined(); + expect(getByText('Test Manual Links')).toBeInTheDocument(); + expect(getByText('Test Locked Links')).toBeInTheDocument(); + expect(queryByText('Test Broken Links')).not.toBeInTheDocument(); }); }); - it('should show no broken links found message', async () => { - axiosMock - .onGet(getLinkCheckStatusApiUrl(courseId)) - .reply(200, { LinkCheckStatus: 'Succeeded' }); - const { getByText } = render(); - expect(getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument(); - fireEvent.click(getByText(messages.buttonTitle.defaultMessage)); + it('should show all links when all filters are selected', async () => { + const { + getByText, + getByLabelText, + container, + } = await setupOptimizerPage(); + // Check if the modal is opened + expect(getByText('Broken')).toBeInTheDocument(); + expect(getByText('Manual')).toBeInTheDocument(); + expect(getByText('Locked')).toBeInTheDocument(); + // Select the all checkboxes + fireEvent.click(getByLabelText(scanResultsMessages.brokenLabel.defaultMessage)); + fireEvent.click(getByLabelText(scanResultsMessages.lockedLabel.defaultMessage)); + fireEvent.click(getByLabelText(scanResultsMessages.manualLabel.defaultMessage)); + + const collapsibleTrigger = container.querySelector('.collapsible-trigger'); + expect(collapsibleTrigger).toBeInTheDocument(); + fireEvent.click(collapsibleTrigger); + await waitFor(() => { - expect(getByText(scanResultsMessages.noBrokenLinksCard.defaultMessage)).toBeInTheDocument(); + expect(getByText('Test Broken Links')).toBeInTheDocument(); + expect(getByText('Test Manual Links')).toBeInTheDocument(); + expect(getByText('Test Locked Links')).toBeInTheDocument(); }); }); - it('should show error message if request does not go through', async () => { - axiosMock - .onPost(postLinkCheckCourseApiUrl(courseId)) - .reply(500); - render(); - expect(screen.getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument(); - fireEvent.click(screen.getByText(messages.buttonTitle.defaultMessage)); + it('should show only manual links when the broken chip is clicked and show all links when clear filters button is clicked', async () => { + const { + getByText, + getByLabelText, + getByTestId, + queryByText, + container, + } = await setupOptimizerPage(); + // Select broken & manual link checkboxes + fireEvent.click(getByLabelText(scanResultsMessages.brokenLabel.defaultMessage)); + fireEvent.click(getByLabelText(scanResultsMessages.manualLabel.defaultMessage)); + + const collapsibleTrigger = container.querySelector('.collapsible-trigger'); + expect(collapsibleTrigger).toBeInTheDocument(); + fireEvent.click(collapsibleTrigger); + + // Assert that all links are displayed await waitFor(() => { - expect(screen.getByText(generalMessages.supportText.defaultMessage)).toBeInTheDocument(); + expect(getByText('Test Broken Links')).toBeInTheDocument(); + expect(getByText('Test Manual Links')).toBeInTheDocument(); + expect(queryByText('Test Locked Links')).not.toBeInTheDocument(); + }); + + // Click on the "Broken" chip to filter the results + const brokenChip = getByTestId('chip-brokenLinks'); + fireEvent.click(brokenChip); + + // Assert that only manual links are displayed + await waitFor(() => { + expect(queryByText('Test Broken Links')).not.toBeInTheDocument(); + expect(getByText('Test Manual Links')).toBeInTheDocument(); + expect(queryByText('Test Locked Links')).not.toBeInTheDocument(); + }); + + // Click the "Clear filters" button + const clearFiltersButton = getByText(scanResultsMessages.clearFilters.defaultMessage); + fireEvent.click(clearFiltersButton); + + // Assert that all links are displayed after clearing filters + await waitFor(() => { + expect(getByText('Test Broken Links')).toBeInTheDocument(); + expect(getByText('Test Manual Links')).toBeInTheDocument(); + expect(getByText('Test Locked Links')).toBeInTheDocument(); }); }); }); diff --git a/src/optimizer-page/CourseOptimizerPage.tsx b/src/optimizer-page/CourseOptimizerPage.tsx index 7fad8a7f76..97bc1977b6 100644 --- a/src/optimizer-page/CourseOptimizerPage.tsx +++ b/src/optimizer-page/CourseOptimizerPage.tsx @@ -132,8 +132,7 @@ const CourseOptimizerPage: FC<{ courseId: string }> = ({ courseId }) => { title={intl.formatMessage(messages.headingTitle)} subtitle={intl.formatMessage(messages.headingSubtitle)} /> -

{intl.formatMessage(messages.description1)}

-

{intl.formatMessage(messages.description2)}

+

{intl.formatMessage(messages.description)}

= ({ courseId }) => { /> {isShowExportButton && ( +

{lastScannedAt && `${intl.formatMessage(messages.lastScannedOn)} ${intl.formatDate(lastScannedAt, { year: 'numeric', month: 'long', day: 'numeric' })}`}

)} diff --git a/src/optimizer-page/messages.js b/src/optimizer-page/messages.js index 6bd588a60b..7b31032852 100644 --- a/src/optimizer-page/messages.js +++ b/src/optimizer-page/messages.js @@ -7,24 +7,15 @@ const messages = defineMessages({ }, headingTitle: { id: 'course-authoring.course-optimizer.heading.title', - defaultMessage: 'Course Optimizer', + defaultMessage: 'Course optimizer', }, headingSubtitle: { id: 'course-authoring.course-optimizer.heading.subtitle', defaultMessage: 'Tools', }, - description1: { - id: 'course-authoring.course-optimizer.description1', - defaultMessage: `This tool will scan the published version of your course for broken links. - Unpublished changes will not be included in the scan. - Note that this process will take more time for larger courses. - To update the scan after you have published new changes to your course, - click the "Start Scanning" button again. - `, - }, - description2: { - id: 'course-authoring.course-optimizer.description2', - defaultMessage: 'Broken links are links pointing to external websites, images, or videos that do not exist or are no longer available. These links can cause issues for learners when they try to access the content.', + description: { + id: 'course-authoring.course-optimizer.description', + defaultMessage: 'This tool will scan your course for broken links, and any links that point to pages in your previous course run. Unpublished changes will not be included in the scan. Note that this process will take more time for larger courses.', }, card1Title: { id: 'course-authoring.course-optimizer.card1.title', @@ -36,7 +27,7 @@ const messages = defineMessages({ }, buttonTitle: { id: 'course-authoring.course-optimizer.button.title', - defaultMessage: 'Start Scanning', + defaultMessage: 'Start scanning', }, preparingStepTitle: { id: 'course-authoring.course-optimizer.peparing-step.title', diff --git a/src/optimizer-page/mocks/mockApiResponse.js b/src/optimizer-page/mocks/mockApiResponse.js index 4d425c6e2a..339d996484 100644 --- a/src/optimizer-page/mocks/mockApiResponse.js +++ b/src/optimizer-page/mocks/mockApiResponse.js @@ -13,85 +13,103 @@ const mockApiResponse = { units: [ { id: 'unit-1-1-1', - displayName: 'Welcome Video', + displayName: 'Test Broken Links', blocks: [ { - id: 'block-1-1-1-1', + id: 'block-1-1-1-5', url: 'https://example.com/welcome-video', - brokenLinks: ['https://example.com/broken-link-algo'], - lockedLinks: ['https://example.com/locked-link-algo'], + brokenLinks: ['https://example.com/broken-link-algo1'], + lockedLinks: [], + externalForbiddenLinks: [], }, + ], + }, + { + id: 'unit-1-1-3', + displayName: 'Test Manual Links', + blocks: [ { - id: 'block-1-1-1-2', - url: 'https://example.com/intro-guide', - brokenLinks: ['https://example.com/broken-link-algo'], - lockedLinks: ['https://example.com/locked-link-algo'], + id: 'block-1-1-1-1', + url: 'https://example.com/welcome-video', + brokenLinks: [], + lockedLinks: [], externalForbiddenLinks: ['https://outsider.com/forbidden-link-algo'], }, ], }, { id: 'unit-1-1-2', - displayName: 'Course Overview', + displayName: 'Test Locked Links', blocks: [ { id: 'block-1-1-2-1', url: 'https://example.com/course-overview', - brokenLinks: ['https://example.com/broken-link-algo'], + brokenLinks: [], lockedLinks: ['https://example.com/locked-link-algo'], + externalForbiddenLinks: [], }, ], }, ], }, + ], + }, + { + id: 'section-2', + displayName: 'Introduction to Programming 2', + subsections: [ { id: 'subsection-1-2', - displayName: 'Basic Concepts', + displayName: 'Getting Started 2', units: [ { - id: 'unit-1-2-1', - displayName: 'Variables and Data Types', + id: 'unit-2-2-2', + displayName: 'unit', blocks: [ { - id: 'block-1-2-1-1', - url: 'https://example.com/variables', - brokenLinks: ['https://example.com/broken-link-algo'], - lockedLinks: ['https://example.com/locked-link-algo'], + id: 'block-1-1-1-6', + url: 'https://example.com/welcome-video', + brokenLinks: ['https://example.com/broken-link-algo1'], + lockedLinks: [], + externalForbiddenLinks: [], }, { - id: 'block-1-2-1-2', - url: 'https://example.com/broken-link', - brokenLinks: ['https://example.com/broken-link'], + id: 'block-1-1-1-6', + url: 'https://example.com/welcome-video', + brokenLinks: ['https://example.com/broken-link-algo1'], lockedLinks: ['https://example.com/locked-link-algo'], + externalForbiddenLinks: [], + }, + { + id: 'block-1-1-1-6', + url: 'https://example.com/welcome-video', + brokenLinks: ['https://example.com/broken-link-algo1'], + lockedLinks: [], + externalForbiddenLinks: ['https://outsider.com/forbidden-link-algo'], }, ], }, - ], - }, - ], - }, - { - id: 'section-2', - displayName: 'Advanced Topics', - subsections: [ - { - id: 'subsection-2-1', - displayName: 'Algorithms and Data Structures', - units: [ { - id: 'unit-2-1-1', - displayName: 'Sorting Algorithms', + id: 'unit-2-2-3', + displayName: 'unit', blocks: [ { - id: 'block-2-1-1-1', - url: 'https://example.com/sorting-algorithms', - brokenLinks: ['https://example.com/broken-link-algo'], - lockedLinks: ['https://example.com/locked-link-algo'], + id: 'block-1-1-1-7', + url: 'https://example.com/welcome-video', + brokenLinks: ['https://example.com/broken-link-algo1'], + lockedLinks: [], + externalForbiddenLinks: ['https://outsider.com/forbidden-link-algo'], }, + ], + }, + { + id: 'unit-2-2-4', + displayName: 'Test Locked Links', + blocks: [ { - id: 'block-2-1-1-2', - url: 'https://example.com/broken-link-algo', - brokenLinks: ['https://example.com/broken-link-algo'], + id: 'block-1-1-7-1', + url: 'https://example.com/course-overview', + brokenLinks: ['https://example.com/broken-link-algo1'], lockedLinks: ['https://example.com/locked-link-algo'], externalForbiddenLinks: ['https://outsider.com/forbidden-link-algo'], }, diff --git a/src/optimizer-page/scan-results/BrokenLinkTable.tsx b/src/optimizer-page/scan-results/BrokenLinkTable.tsx index 101afddff1..2453ac8657 100644 --- a/src/optimizer-page/scan-results/BrokenLinkTable.tsx +++ b/src/optimizer-page/scan-results/BrokenLinkTable.tsx @@ -1,14 +1,16 @@ import { - Card, Icon, OverlayTrigger, Table, Tooltip, + Card, Icon, DataTable, } from '@openedx/paragon'; import { - OpenInNew, Lock, LinkOff, InfoOutline, + ArrowForwardIos, + LinkOff, } from '@openedx/paragon/icons'; -import { useIntl } from '@edx/frontend-platform/i18n'; import { FC } from 'react'; -import { Unit } from '../types'; +import { Filters, Unit } from '../types'; import messages from './messages'; -import LockedInfoIcon from './LockedInfoIcon'; +import CustomIcon from './CustomIcon'; +import lockedIcon from './lockedIcon'; +import ManualIcon from './manualIcon'; const BrokenLinkHref: FC<{ href: string }> = ({ href }) => { const handleClick = (event: React.MouseEvent) => { @@ -25,145 +27,135 @@ const BrokenLinkHref: FC<{ href: string }> = ({ href }) => { ); }; -const GoToBlock: FC<{ block: { url: string } }> = ({ block }) => { +const GoToBlock: FC<{ block: { url: string, displayName: string } }> = ({ block }) => { const handleClick = (event: React.MouseEvent) => { event.preventDefault(); window.open(block.url, '_blank'); }; return ( - - - - Go to Block + ); }; -const RecommendedManualCheckHeading = () => { - const intl = useIntl(); - return ( - - {intl.formatMessage(messages.recommendedManualCheckText)} - - {intl.formatMessage(messages.recommendedManualCheckTooltip)} - - )} - > - - - - ); -}; +const LinksCol: FC<{ block: { url: string, displayName: string }, href: string }> = ({ block, href }) => ( + + + + + +); interface BrokenLinkTableProps { unit: Unit; - showLockedLinks: boolean; + filters: Filters; } type TableData = { - blockLink: JSX.Element; - brokenLink: JSX.Element; + Links: JSX.Element; status: JSX.Element; }[]; const BrokenLinkTable: FC = ({ unit, - showLockedLinks, + filters, }) => { - const intl = useIntl(); - return ( - -

{unit.displayName}

- { - const blockBrokenLinks = block.brokenLinks.map((link) => ({ - blockLink: , - blockDisplayName: block.displayName || '', - brokenLink: , - status: ( - - - - {intl.formatMessage(messages.brokenLinkStatus)} - - - ), - })); - acc.push(...blockBrokenLinks); + const brokenLinkList = unit.blocks.reduce( + ( + acc: TableData, + block, + ) => { + if ( + filters.brokenLinks + || (!filters.brokenLinks && !filters.externalForbiddenLinks && !filters.lockedLinks) + ) { + const blockBrokenLinks = block.brokenLinks.map((link) => ({ + Links: ( + + ), + status: ( + + ), + })); + acc.push(...blockBrokenLinks); + } - if (showLockedLinks) { - const blockLockedLinks = block.lockedLinks.map((link) => ({ - blockLink: , - blockDisplayName: block.displayName || '', - brokenLink: , - status: ( - - - {intl.formatMessage(messages.lockedLinkStatus)}{' '} - - - ), - })); + if ( + filters.lockedLinks + || (!filters.brokenLinks && !filters.externalForbiddenLinks && !filters.lockedLinks) + ) { + const blockLockedLinks = block.lockedLinks.map((link) => ({ + Links: ( + + ), + status: ( + + ), + })); - acc.push(...blockLockedLinks); - } + acc.push(...blockLockedLinks); + } - if (block.externalForbiddenLinks?.length > 0) { - const recommendedManualCheckHeading = { - blockLink:
, - blockDisplayName: , - brokenLink:
, - status:
, - }; - const externalForbiddenLinks = block.externalForbiddenLinks.map((link) => ({ - blockLink: , - blockDisplayName: block.displayName || '', - brokenLink: , - status:
, - })); + if ( + filters.externalForbiddenLinks + || (!filters.brokenLinks && !filters.externalForbiddenLinks && !filters.lockedLinks) + ) { + const externalForbiddenLinks = block.externalForbiddenLinks.map((link) => ({ + Links: ( + + ), + status: ( + + ), + })); - acc.push(recommendedManualCheckHeading); - acc.push(...externalForbiddenLinks); - } + acc.push(...externalForbiddenLinks); + } - return acc; - }, - [], - )} + return acc; + }, + [], + ); + + return ( + +

{unit.displayName}

+ diff --git a/src/optimizer-page/scan-results/CustomIcon.jsx b/src/optimizer-page/scan-results/CustomIcon.jsx new file mode 100644 index 0000000000..83e82b72a8 --- /dev/null +++ b/src/optimizer-page/scan-results/CustomIcon.jsx @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import { + Icon, + OverlayTrigger, + Tooltip, +} from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; + +const CustomIcon = ({ + icon, + message1, + message2, + placement = 'top', +}) => { + const intl = useIntl(); + + return ( + + {intl.formatMessage(message1)} + {message1 &&
} + {intl.formatMessage(message2)} + + )} + > + +
+ ); +}; + +const messagePropsType = { + id: PropTypes.string.isRequired, + defaultMessage: PropTypes.string.isRequired, +}; + +CustomIcon.propTypes = { + icon: PropTypes.elementType.isRequired, + message1: PropTypes.shape(messagePropsType).isRequired, + message2: PropTypes.shape(messagePropsType).isRequired, + placement: PropTypes.string, +}; + +export default CustomIcon; diff --git a/src/optimizer-page/scan-results/LockedInfoIcon.jsx b/src/optimizer-page/scan-results/LockedInfoIcon.jsx deleted file mode 100644 index 788dcb1301..0000000000 --- a/src/optimizer-page/scan-results/LockedInfoIcon.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import { - Icon, - OverlayTrigger, - Tooltip, -} from '@openedx/paragon'; -import { - Question, -} from '@openedx/paragon/icons'; -import { useIntl } from '@edx/frontend-platform/i18n'; -import messages from './messages'; - -const LockedInfoIcon = () => { - const intl = useIntl(); - - return ( - - {intl.formatMessage(messages.lockedInfoTooltip)} - - )} - > - - - ); -}; - -export default LockedInfoIcon; diff --git a/src/optimizer-page/scan-results/ScanResults.scss b/src/optimizer-page/scan-results/ScanResults.scss index e5b1978eb1..f523b03f48 100644 --- a/src/optimizer-page/scan-results/ScanResults.scss +++ b/src/optimizer-page/scan-results/ScanResults.scss @@ -3,30 +3,6 @@ display: none; } - .red-italics { - color: $brand-500; - margin-left: 2rem; - font-weight: 400; - font-size: 80%; - font-style: italic; - } - - .yellow-italics { - color: $warning-800; - margin-left: 2rem; - font-weight: 400; - font-size: 80%; - font-style: italic; - } - - .green-italics { - color: $green; - margin-left: 2rem; - font-weight: 400; - font-size: 80%; - font-style: italic; - } - .section { &.is-open { &:not(:first-child) { @@ -37,16 +13,11 @@ } } - .open-arrow { - transform: translate(-10px, 5px); - display: inline-block; - } - /* Section Header */ .subsection-header { font-size: 16px; /* Slightly smaller */ font-weight: 600; /* Reduced boldness */ - background-color: $dark-100; + background-color: var(--pgn-color-dark-100); padding: 10px; margin-bottom: 10px; @@ -67,7 +38,7 @@ font-size: 14px; font-weight: 700; margin-bottom: .75rem; - color: $primary-500; + color: var(--pgn-color-primary-500); } /* Block Links */ @@ -87,16 +58,32 @@ } .unit { - padding: 0 3rem; + padding: 0 1.5rem; + margin-right: -12px; } .broken-link { - color: $brand-500; + color: var(--pgn-color-blue); text-decoration: none; } + .links-container { + display: flex; + align-items: center; + gap: 1.5rem; + width:38rem; + } + + .go-to-block-link-container { + max-width: 20rem; + width: fit-content; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .broken-link-container { - max-width: 18rem; + max-width: 20rem; text-wrap: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -117,11 +104,117 @@ gap: .5rem; } - .broken-link-icon { - color: $brand-500; + .section-collapsible-header { + display: flex; + align-items: center; + justify-content: space-between; + } + + .section-collapsible-header-item { + display: flex; + align-items: center; + gap: .2rem; + } + + .section-collapsible-header-actions { + display: flex; + align-items: center; + font-size: small; + gap: 40px; + + span { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + color: var(--gray-700, #454545); + font-variant-numeric: lining-nums tabular-nums; + font-family: Inter, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 28px; + } + } + + .section-collapsible-item-container { + box-shadow: 0 1px 2px rgb(0 0 0 / .15); + } + + .section-collapsible-item { + margin-right: -24px; + } + + .scan-header-title-container { + margin-top: 32px; + } + + .scan-header-second-title-container { + margin-top: 24px; + margin-bottom: 32px; } - .lock-icon { - color: $warning-300; + .scan-header-title{ + color: #000000; + font-family: Inter, sans-serif; + font-size: 32px; + font-style: normal; + font-weight: 700; + line-height: 36px; + } + + .broken-links-header-title{ + color: #000000; + font-family: Inter, sans-serif; + font-size: 22px; + font-style: normal; + font-weight: 700; + line-height: 28px; + align-self: center; } } + +.opt-desc-mb { + margin-bottom: 40px; +} + +.arrow-forward-ios { + width: 10px; + height: 10px; + align-self: center; +} + +.scan-results-active-filters-container { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 24px; + margin-bottom: 24px; +} + +.scan-results-active-filters-chips { + display: flex; + padding: 2px 8px; + justify-content: center; + align-items: center; + gap: 4px; +} + +.scan-results-active-filters-chip { + border-radius: 6px; + border: 1px solid var(--light-300, #F2F0EF); + background: var(--extras-white, #FFFFFF); +} + +.clear-all-btn { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 28px; + padding-right: 0; +} + +.filter-modal { + margin-left: -1.5rem; + padding: 16px 16px 1px; +} diff --git a/src/optimizer-page/scan-results/ScanResults.tsx b/src/optimizer-page/scan-results/ScanResults.tsx index c0c32b98cc..c4dc73929c 100644 --- a/src/optimizer-page/scan-results/ScanResults.tsx +++ b/src/optimizer-page/scan-results/ScanResults.tsx @@ -1,15 +1,22 @@ import { useState, useMemo, FC } from 'react'; import { Card, - CheckBox, + Chip, + Button, + useCheckboxSetValues, + useToggle, } from '@openedx/paragon'; +import { + ArrowDropDown, + CloseSmall, +} from '@openedx/paragon/icons'; import { useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; import SectionCollapsible from './SectionCollapsible'; import BrokenLinkTable from './BrokenLinkTable'; -import LockedInfoIcon from './LockedInfoIcon'; import { LinkCheckResult } from '../types'; import { countBrokenLinks } from '../utils'; +import FilterModal from './filterModal'; const InfoCard: FC<{ text: string }> = ({ text }) => ( @@ -28,7 +35,14 @@ interface Props { const ScanResults: FC = ({ data }) => { const intl = useIntl(); - const [showLockedLinks, setShowLockedLinks] = useState(true); + const [isOpen, open, close] = useToggle(false); + const initialFilters = { + brokenLinks: false, + lockedLinks: false, + externalForbiddenLinks: false, + }; + const [filters, setFilters] = useState(initialFilters); + const [buttonRef, setButtonRef] = useState(null); const { brokenLinksCounts, @@ -36,55 +50,118 @@ const ScanResults: FC = ({ data }) => { externalForbiddenLinksCounts, } = useMemo(() => countBrokenLinks(data), [data?.sections]); + const activeFilters = Object.keys(filters).filter(key => filters[key]); + const [filterBy, { + add, remove, set, clear, + }] = useCheckboxSetValues(activeFilters); + if (!data?.sections) { return ; } const { sections } = data; + const filterOptions = [ + { name: intl.formatMessage(messages.brokenLabel), value: 'brokenLinks' }, + { name: intl.formatMessage(messages.manualLabel), value: 'externalForbiddenLinks' }, + { name: intl.formatMessage(messages.lockedLabel), value: 'lockedLinks' }, + ]; return (
-
+
+

{intl.formatMessage(messages.scanHeader)}

+
+
-

{intl.formatMessage(messages.scanHeader)}

- - { - setShowLockedLinks(!showLockedLinks); - }} - label={intl.formatMessage(messages.lockedCheckboxLabel)} - /> - - +

{intl.formatMessage(messages.brokenLinksHeader)}

+
+ + {activeFilters.length > 0 &&
} + {activeFilters.length > 0 && ( +
+ + {activeFilters.map(filter => ( + { + remove(filter); + const updatedFilters = { ...filters, [filter]: false }; + setFilters(updatedFilters); + }} + > + {filterOptions.find(option => option.value === filter)?.name} + + ))} + + +
+ )} {sections?.map((section, index) => ( {section.subsections.map((subsection) => ( <> -

- {subsection.displayName} -

- {subsection.units.map((unit) => ( -
- -
- ))} + {subsection.units.map((unit) => { + if ( + (!filters.brokenLinks && !filters.externalForbiddenLinks && !filters.lockedLinks) + || (filters.brokenLinks && unit.blocks.some(block => block.brokenLinks.length > 0)) + || (filters.externalForbiddenLinks + && unit.blocks.some(block => block.externalForbiddenLinks.length > 0)) + || (filters.lockedLinks && unit.blocks.some(block => block.lockedLinks.length > 0)) + ) { + return ( +
+ +
+ ); + } + return null; + })} ))}
diff --git a/src/optimizer-page/scan-results/SectionCollapsible.tsx b/src/optimizer-page/scan-results/SectionCollapsible.tsx index 80c40bf978..c854280f89 100644 --- a/src/optimizer-page/scan-results/SectionCollapsible.tsx +++ b/src/optimizer-page/scan-results/SectionCollapsible.tsx @@ -6,38 +6,57 @@ import { import { ArrowRight, ArrowDropDown, + LinkOff, } from '@openedx/paragon/icons'; +import CustomIcon from './CustomIcon'; +import messages from './messages'; +import lockedIcon from './lockedIcon'; +import ManualIcon from './manualIcon'; interface Props { title: string; children: React.ReactNode; - redItalics?: string; - yellowItalics?: string; - greenItalics?: string; + brokenNumber: number; + manualNumber: number; + lockedNumber: number; className?: string; } const SectionCollapsible: FC = ({ - title, children, redItalics = '', yellowItalics = '', greenItalics = '', className = '', + title, children, brokenNumber = 0, manualNumber = 0, lockedNumber = 0, className = '', }) => { const [isOpen, setIsOpen] = useState(false); - const styling = 'card-lg'; + const styling = 'card-lg rounded-sm shadow-outline'; const collapsibleTitle = (
- - {title} - {redItalics} - {yellowItalics} - {greenItalics} +
+ + {title} +
+
+ + + {brokenNumber} + + + + {manualNumber} + + + + {lockedNumber} + +
); return (
+

{collapsibleTitle}

)} diff --git a/src/optimizer-page/scan-results/filterModal.jsx b/src/optimizer-page/scan-results/filterModal.jsx new file mode 100644 index 0000000000..6a37b54b37 --- /dev/null +++ b/src/optimizer-page/scan-results/filterModal.jsx @@ -0,0 +1,92 @@ +import { ModalPopup, Form } from '@openedx/paragon'; +import { LinkOff } from '@openedx/paragon/icons'; +import { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import CustomIcon from './CustomIcon'; +import messages from './messages'; +import LockedIcon from './lockedIcon'; +import ManualIcon from './manualIcon'; + +const FilterModal = ({ + isOpen, + onClose, + onApply, + positionRef, + filterOptions, + initialFilters, + activeFilters, + filterBy, + add, + remove, + set, +}) => { + const [previousFilters, setPreviousFilters] = useState(activeFilters); + useEffect(() => { + if (JSON.stringify(activeFilters) !== JSON.stringify(previousFilters)) { + set(activeFilters); + setPreviousFilters(activeFilters); + } + }, [activeFilters]); + + const handleCheckboxChange = (e) => { + const { value, checked } = e.target; + + const updatedFilters = { ...initialFilters, [value]: checked }; + if (e.target.checked) { + add(e.target.value); + } else { + remove(e.target.value); + } + onApply(updatedFilters); + }; + + return ( + +
+ + + {filterOptions.map(({ name, value }) => ( + + + {name} + { value === 'brokenLinks' && } + { value === 'externalForbiddenLinks' && } + { value === 'lockedLinks' && } + + + ))} + + +
+
+ ); +}; + +FilterModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + onApply: PropTypes.func.isRequired, + positionRef: PropTypes.shape({ + current: PropTypes.instanceOf(Element), + }), + filterOptions: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + })).isRequired, + initialFilters: PropTypes.shape({ + brokenLinks: PropTypes.bool.isRequired, + lockedLinks: PropTypes.bool.isRequired, + externalForbiddenLinks: PropTypes.bool.isRequired, + }).isRequired, + activeFilters: PropTypes.arrayOf(PropTypes.string).isRequired, + filterBy: PropTypes.arrayOf(PropTypes.string).isRequired, + add: PropTypes.func.isRequired, + remove: PropTypes.func.isRequired, + set: PropTypes.func.isRequired, +}; + +export default FilterModal; diff --git a/src/optimizer-page/scan-results/lockedIcon.tsx b/src/optimizer-page/scan-results/lockedIcon.tsx new file mode 100644 index 0000000000..e5bc785322 --- /dev/null +++ b/src/optimizer-page/scan-results/lockedIcon.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +const LockedIcon: React.FC> = (props) => ( + + + +); + +export default LockedIcon; diff --git a/src/optimizer-page/scan-results/manualIcon.tsx b/src/optimizer-page/scan-results/manualIcon.tsx new file mode 100644 index 0000000000..673139d378 --- /dev/null +++ b/src/optimizer-page/scan-results/manualIcon.tsx @@ -0,0 +1,20 @@ +// frontend-app-course-authoring/src/assets/icons/ManualIcon.tsx +import React from 'react'; + +const ManualIcon: React.FC> = (props) => ( + + + +); + +export default ManualIcon; diff --git a/src/optimizer-page/scan-results/messages.js b/src/optimizer-page/scan-results/messages.js index e3b0c88d31..f4f6c7e781 100644 --- a/src/optimizer-page/scan-results/messages.js +++ b/src/optimizer-page/scan-results/messages.js @@ -15,43 +15,48 @@ const messages = defineMessages({ }, scanHeader: { id: 'course-authoring.course-optimizer.scanHeader', - defaultMessage: 'Broken Links Scan', + defaultMessage: 'Scan results', + }, + brokenLinksHeader: { + id: 'course-authoring.course-optimizer.brokenLinksHeader', + defaultMessage: 'Broken links', + }, + filterButtonLabel: { + id: 'course-authoring.course-optimizer.filterButtonLabel', + defaultMessage: 'Filters', }, lockedCheckboxLabel: { id: 'course-authoring.course-optimizer.lockedCheckboxLabel', defaultMessage: 'Show Locked Course Files', }, - brokenLinksNumber: { - id: 'course-authoring.course-optimizer.brokenLinksNumber', - defaultMessage: '{count} broken links', - }, - lockedLinksNumber: { - id: 'course-authoring.course-optimizer.lockedLinksNumber', - defaultMessage: '{count} locked links', - }, - externalForbiddenLinksNumber: { - id: 'course-authoring.course-optimizer.externalForbiddenLinksNumber', - defaultMessage: '{count} manual check', + lockedLabel: { + id: 'course-authoring.course-optimizer.lockedLabel', + defaultMessage: 'Locked', }, lockedInfoTooltip: { id: 'course-authoring.course-optimizer.lockedInfoTooltip', - defaultMessage: 'These course files are "locked", so we cannot verify if the link can access the file.', + defaultMessage: 'These course files are inaccessible for non-enrolled users so we cannot verify if the link can access the file.', + }, + brokenLabel: { + id: 'course-authoring.course-optimizer.brokenLabel', + defaultMessage: 'Broken', }, - brokenLinkStatus: { - id: 'course-authoring.course-optimizer.brokenLinkStatus', - defaultMessage: 'Status: Broken', + brokenInfoTooltip: { + id: 'course-authoring.course-optimizer.brokenInfoTooltip', + defaultMessage: `Links pointing to external websites, images, or videos that do not exist or are no longer available. + These links can cause issues for learners when they try to access the content.`, }, - lockedLinkStatus: { - id: 'course-authoring.course-optimizer.lockedLinkStatus', - defaultMessage: 'Status: Locked', + manualLabel: { + id: 'course-authoring.course-optimizer.manualLabel', + defaultMessage: 'Manual', }, - recommendedManualCheckText: { - id: 'course-authoring.course-optimizer.recommendedManualCheckText', - defaultMessage: 'Recommended Manual Check', + manualInfoTooltip: { + id: 'course-authoring.course-optimizer.manualInfoTooltip', + defaultMessage: 'We couldn\'t verify this link. Please check it manually.', }, - recommendedManualCheckTooltip: { - id: 'course-authoring.course-optimizer.recommendedManualCheckTooltip', - defaultMessage: 'For websites returning 403, websites often show 403 because they don\'t want bots accessing their content', + clearFilters: { + id: 'course-authoring.course-optimizer.clearFilters', + defaultMessage: 'Clear filters', }, }); diff --git a/src/optimizer-page/types.ts b/src/optimizer-page/types.ts index d874516312..9e8d946bcc 100644 --- a/src/optimizer-page/types.ts +++ b/src/optimizer-page/types.ts @@ -26,3 +26,9 @@ export interface Section { export interface LinkCheckResult { sections: Section[]; } + +export interface Filters { + brokenLinks: boolean, + lockedLinks: boolean, + externalForbiddenLinks: boolean, +} diff --git a/src/optimizer-page/utils.test.js b/src/optimizer-page/utils.test.js index dccd820b8e..eadabc8d02 100644 --- a/src/optimizer-page/utils.test.js +++ b/src/optimizer-page/utils.test.js @@ -6,9 +6,9 @@ describe('countBrokenLinks', () => { const data = mockApiResponse.LinkCheckOutput; expect(countBrokenLinks(data)).toStrictEqual( { - brokenLinksCounts: [5, 2], - lockedLinksCounts: [5, 2], - externalForbiddenLinksCounts: [1, 1], + brokenLinksCounts: [1, 5], + lockedLinksCounts: [1, 2], + externalForbiddenLinksCounts: [1, 3], }, ); }); diff --git a/src/pages-and-resources/PagesAndResources.jsx b/src/pages-and-resources/PagesAndResources.jsx index 2382be67a5..56ba8a63a7 100644 --- a/src/pages-and-resources/PagesAndResources.jsx +++ b/src/pages-and-resources/PagesAndResources.jsx @@ -92,7 +92,7 @@ const PagesAndResources = ({ courseId }) => { } /> - + } courseId={courseId} /> { (contentPermissionsPages.length > 0 || hasAdditionalCoursePlugin) && ( @@ -100,7 +100,7 @@ const PagesAndResources = ({ courseId }) => {

{intl.formatMessage(messages.contentPermissions)}

- + } /> ) } diff --git a/src/pages-and-resources/PagesAndResources.test.jsx b/src/pages-and-resources/PagesAndResources.test.jsx index 180c3f8674..0e662e3f86 100644 --- a/src/pages-and-resources/PagesAndResources.test.jsx +++ b/src/pages-and-resources/PagesAndResources.test.jsx @@ -1,16 +1,39 @@ import { screen, waitFor } from '@testing-library/react'; +import { getConfig, setConfig } from '@edx/frontend-platform'; +import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework'; import { PagesAndResources } from '.'; import { render } from './utils.test'; +const mockPlugin = (identifier) => ({ + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'mock-plugin-1', + type: DIRECT_PLUGIN, + priority: 1, + RenderWidget: () =>
HELLO
, + }, + }, + ], +}); + const courseId = 'course-v1:edX+TestX+Test_Course'; describe('PagesAndResources', () => { beforeEach(() => { jest.clearAllMocks(); + setConfig({ + ...getConfig(), + pluginSlots: { + 'org.openedx.frontend.authoring.additional_course_plugin.v1': mockPlugin('additional_course_plugin'), + 'org.openedx.frontend.authoring.additional_course_content_plugin.v1': mockPlugin('additional_course_content_plugin'), + }, + }); }); - it('doesn\'t show content permissions section if relevant apps are not enabled', () => { + it('doesn\'t show content permissions section if relevant apps are not enabled', async () => { const initialState = { models: { courseApps: {}, @@ -25,8 +48,11 @@ describe('PagesAndResources', () => { { preloadedState: initialState }, ); - expect(screen.queryByRole('heading', { name: 'Content permissions' })).not.toBeInTheDocument(); + await waitFor(() => expect(screen.queryByRole('heading', { name: 'Content permissions' })).not.toBeInTheDocument()); + await waitFor(() => expect(screen.queryByTestId('additional_course_plugin')).toBeInTheDocument()); + await waitFor(() => expect(screen.queryByTestId('additional_course_content_plugin')).not.toBeInTheDocument()); }); + it('show content permissions section if Learning Assistant app is enabled', async () => { const initialState = { models: { @@ -56,6 +82,8 @@ describe('PagesAndResources', () => { await waitFor(() => expect(screen.getByRole('heading', { name: 'Content permissions' })).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('Learning Assistant')).toBeInTheDocument()); + await waitFor(() => expect(screen.queryByTestId('additional_course_plugin')).toBeInTheDocument()); + await waitFor(() => expect(screen.queryByTestId('additional_course_content_plugin')).toBeInTheDocument()); }); it('show content permissions section if Xpert learning summaries app is enabled', async () => { @@ -89,5 +117,7 @@ describe('PagesAndResources', () => { await waitFor(() => expect(screen.getByRole('heading', { name: 'Content permissions' })).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('Xpert unit summaries')).toBeInTheDocument()); + await waitFor(() => expect(screen.queryByTestId('additional_course_plugin')).toBeInTheDocument()); + await waitFor(() => expect(screen.queryByTestId('additional_course_content_plugin')).toBeInTheDocument()); }); }); diff --git a/src/pages-and-resources/discussions/app-list/AppList.scss b/src/pages-and-resources/discussions/app-list/AppList.scss index 83d3c3c915..8a09744cc0 100644 --- a/src/pages-and-resources/discussions/app-list/AppList.scss +++ b/src/pages-and-resources/discussions/app-list/AppList.scss @@ -6,7 +6,7 @@ .pgn__data-table-container { tr { th { - background-color: $white; + background-color: var(--pgn-color-white); text-align: center; .d-flex, @@ -16,7 +16,7 @@ } td { - background-color: $white; + background-color: var(--pgn-color-white); .pgn__data-table-cell-wrap { max-width: unset; @@ -55,7 +55,7 @@ padding-top: 24px; h2 { - color: $primary-500; + color: var(--pgn-color-primary-500); line-height: 28px; font-size: 22px; } diff --git a/src/plugin-slots/AdditionalCourseContentPluginSlot/README.md b/src/plugin-slots/AdditionalCourseContentPluginSlot/README.md index e467dc5061..e132e646ca 100644 --- a/src/plugin-slots/AdditionalCourseContentPluginSlot/README.md +++ b/src/plugin-slots/AdditionalCourseContentPluginSlot/README.md @@ -3,4 +3,4 @@ ### Slot ID: `org.openedx.frontend.authoring.additional_course_content_plugin.v1` ### Slot ID Aliases -* `additional_course_content_plugin` +* `additional_course_content_plugin` \ No newline at end of file diff --git a/src/plugin-slots/AdditionalCourseContentPluginSlot/index.tsx b/src/plugin-slots/AdditionalCourseContentPluginSlot/index.tsx index 74a6f55b5a..c98ba0c725 100644 --- a/src/plugin-slots/AdditionalCourseContentPluginSlot/index.tsx +++ b/src/plugin-slots/AdditionalCourseContentPluginSlot/index.tsx @@ -1,5 +1,4 @@ import { PluginSlot } from '@openedx/frontend-plugin-framework/dist'; -import React from 'react'; export const AdditionalCourseContentPluginSlot = () => ( ( + + + slot props course + + )} + actions={} + size="sm" + /> + + + Additional course from slot props description. + Or anything else. + + + + ), + }, + }, + ] + } + }, +} + +export default config; +``` \ No newline at end of file diff --git a/src/plugin-slots/AdditionalCoursePluginSlot/images/additional-course-plugin-slot-example.png b/src/plugin-slots/AdditionalCoursePluginSlot/images/additional-course-plugin-slot-example.png new file mode 100644 index 0000000000..6dab83f755 Binary files /dev/null and b/src/plugin-slots/AdditionalCoursePluginSlot/images/additional-course-plugin-slot-example.png differ diff --git a/src/plugin-slots/AdditionalCoursePluginSlot/index.tsx b/src/plugin-slots/AdditionalCoursePluginSlot/index.tsx index a1cdfdc41d..b39f7603d6 100644 --- a/src/plugin-slots/AdditionalCoursePluginSlot/index.tsx +++ b/src/plugin-slots/AdditionalCoursePluginSlot/index.tsx @@ -1,5 +1,4 @@ import { PluginSlot } from '@openedx/frontend-plugin-framework/dist'; -import React from 'react'; export const AdditionalCoursePluginSlot = () => ( ( +
+ setIsAiTranslations(courseId === 'anyId')} + > + + Custom transcript 💬 + + + +
+); + +const config = { + pluginSlots: { + 'org.openedx.frontend.authoring.video_transcript_additional_translations_component.v1': { + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'custom_additional_translation_id', + type: DIRECT_PLUGIN, + RenderWidget: TranslationsBlock, + }, + }, + ], + }, + }, +} + +export default config; +``` diff --git a/src/plugin-slots/AdditionalTranslationsComponentSlot/images/additional-translation-example.png b/src/plugin-slots/AdditionalTranslationsComponentSlot/images/additional-translation-example.png new file mode 100644 index 0000000000..fa79a1610c Binary files /dev/null and b/src/plugin-slots/AdditionalTranslationsComponentSlot/images/additional-translation-example.png differ diff --git a/src/plugin-slots/CourseAuthoringUnitSidebarSlot/README.md b/src/plugin-slots/CourseAuthoringUnitSidebarSlot/README.md index ee00e7ba16..091af0d8df 100644 --- a/src/plugin-slots/CourseAuthoringUnitSidebarSlot/README.md +++ b/src/plugin-slots/CourseAuthoringUnitSidebarSlot/README.md @@ -1,9 +1,8 @@ # CourseAuthoringUnitSidebarSlot -### Slot ID: `org.openedx.frontend.authoring.course_unit_sidebar.v1` +### Slot ID: `org.openedx.frontend.authoring.course_unit_sidebar.v2` -### Slot ID Aliases -* `course_authoring_unit_sidebar_slot` +### Previous Version: [`org.openedx.frontend.authoring.course_unit_sidebar.v1`](./README.v1.md) ### Plugin Props: @@ -12,6 +11,8 @@ * `unitTitle` - String. The name of the current unit being viewed / edited. * `xBlocks` - Array of Objects. List of XBlocks in the Unit. Object structure defined in `index.tsx`. * `readOnly` - Boolean. True if the user should not be able to edit the contents of the unit. +* `isUnitVerticalType` - Boolean. If the unit category is `vertical`. +* `isSplitTestType` - Boolean. If the unit category is `split_test`. ## Description @@ -29,7 +30,7 @@ import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; const config = { pluginSlots: { - 'org.openedx.frontend.authoring.course_unit_sidebar.v1': { + 'org.openedx.frontend.authoring.course_unit_sidebar.v2': { keepDefault: true, plugins: [ { @@ -63,11 +64,11 @@ const ProblemBlocks = ({unitTitle, xBlocks}) => ( } -); +); const config = { pluginSlots: { - 'org.openedx.frontend.authoring.course_unit_sidebar.v1': { + 'org.openedx.frontend.authoring.course_unit_sidebar.v2': { keepDefault: true, plugins: [ { diff --git a/src/plugin-slots/CourseAuthoringUnitSidebarSlot/README.v1.md b/src/plugin-slots/CourseAuthoringUnitSidebarSlot/README.v1.md new file mode 100644 index 0000000000..aafccfb461 --- /dev/null +++ b/src/plugin-slots/CourseAuthoringUnitSidebarSlot/README.v1.md @@ -0,0 +1,95 @@ +# CourseAuthoringUnitSidebarSlot + +### Slot ID: `org.openedx.frontend.authoring.course_unit_sidebar.v1` + +### Slot ID Aliases: `course_authoring_unit_sidebar_slot` + +### Plugin Props: + +* `courseId` - String. +* `blockId` - String. The usage id of the current unit being viewed / edited. +* `unitTitle` - String. The name of the current unit being viewed / edited. +* `xBlocks` - Array of Objects. List of XBlocks in the Unit. Object structure defined in `index.tsx`. +* `readOnly` - Boolean. True if the user should not be able to edit the contents of the unit. + +### Description + +The slot wraps the sidebar that is displayed on the unit editor page. It can +be used to add additional sidebar components or modify the existing sidebar. + +> [!IMPORTANT] +> This document describes an older version `v1` of the `CourseAuthoringUnitSidebarSlot`. +> It is recommended to use the `org.openedx.frontend.authoring.course_unit_sidebar.v2` slot ID for new plugins. + +The `v1` slot has the following limitations compared to the `v2` version: +* It renders conditionally based on the `isUnitVerticalType` prop, which means the plugins won't be rendered in other scenarios like unit with library blocks. +* It does **not** wrap the `SplitTestSidebarInfo` component. So it can't be hidden from the sidebar by overriding the components in the slot. +* As it is not the primary child component of the sidebar, CSS styling for inserted components face limitations, such as an inability to be `sticky` or achieve 100% height. + +## Example 1 + +![Screenshot of the unit sidebar surrounded by border](./images/unit_sidebar_with_border.png) + +The following example configuration surrounds the sidebar in a border as shown above. + +```js +import { PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; + +const config = { + pluginSlots: { + 'org.openedx.frontend.authoring.course_unit_sidebar.v1': { + keepDefault: true, + plugins: [ + { + op: PLUGIN_OPERATIONS.Wrap, + widgetId: 'default_contents', + wrapper: ({ component }) => ( +
{component}
+ ), + }, + ], + }, + } +}; +export default config; +``` + +## Example 2 + +![Screenshot of the unit sidebar with an extra component listing all the problem blocks](./images/unit_sidebar_with_problem_blocks_list.png) + +```js +import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework'; + +const ProblemBlocks = ({unitTitle, xBlocks}) => ( + <> +

{unitTitle}: Problem Blocks

+
    + {xBlocks + .filter(block => block.blockType === "problem") + .map(block =>
  • {block.displayName}
  • ) + } +
+ +); + +const config = { + pluginSlots: { + 'org.openedx.frontend.authoring.course_unit_sidebar.v1': { + keepDefault: true, + plugins: [ + { + op: PLUGIN_OPERATIONS.Insert, + widget:{ + id: 'problem-blocks-list', + priority: 1, + type: DIRECT_PLUGIN, + RenderWidget: ProblemBlocks, + } + }, + ], + }, + } +}; +export default config; +``` diff --git a/src/plugin-slots/CourseAuthoringUnitSidebarSlot/index.tsx b/src/plugin-slots/CourseAuthoringUnitSidebarSlot/index.tsx index 0bf6de14ca..562cbccb0f 100644 --- a/src/plugin-slots/CourseAuthoringUnitSidebarSlot/index.tsx +++ b/src/plugin-slots/CourseAuthoringUnitSidebarSlot/index.tsx @@ -1,9 +1,11 @@ import { getConfig } from '@edx/frontend-platform'; import { PluginSlot } from '@openedx/frontend-plugin-framework/dist'; +import { Stack } from '@openedx/paragon'; import TagsSidebarControls from '../../content-tags-drawer/tags-sidebar-controls'; import Sidebar from '../../course-unit/sidebar'; import LocationInfo from '../../course-unit/sidebar/LocationInfo'; import PublishControls from '../../course-unit/sidebar/PublishControls'; +import SplitTestSidebarInfo from '../../course-unit/sidebar/SplitTestSidebarInfo'; export const CourseAuthoringUnitSidebarSlot = ( { @@ -12,26 +14,44 @@ export const CourseAuthoringUnitSidebarSlot = ( unitTitle, xBlocks, readOnly, + isUnitVerticalType, + isSplitTestType, }: CourseAuthoringUnitSidebarSlotProps, ) => ( - - - - {getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && ( - - - - )} - - - + + {isUnitVerticalType && ( + + + + + {getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && ( + + + + )} + + + + + )} + {isSplitTestType && ( + + + + )} + ); @@ -47,4 +67,6 @@ interface CourseAuthoringUnitSidebarSlotProps { unitTitle: string; xBlocks: XBlock[]; readOnly: boolean; + isUnitVerticalType: boolean; + isSplitTestType: boolean; } diff --git a/src/schedule-and-details/ScheduleAndDetails.scss b/src/schedule-and-details/ScheduleAndDetails.scss index bbaed92e71..99886b5b61 100644 --- a/src/schedule-and-details/ScheduleAndDetails.scss +++ b/src/schedule-and-details/ScheduleAndDetails.scss @@ -25,10 +25,10 @@ justify-content: space-between; align-items: center; margin-bottom: .75rem; - border-bottom: $border-width solid $light-400; + border-bottom: var(--pgn-size-border-width) solid var(--pgn-color-light-400); h2 { - color: $black; + color: var(--pgn-color-black); margin-bottom: .75rem; } } diff --git a/src/schedule-and-details/basic-section/BasicSection.scss b/src/schedule-and-details/basic-section/BasicSection.scss index 2d4fc39ea2..34f71808e6 100644 --- a/src/schedule-and-details/basic-section/BasicSection.scss +++ b/src/schedule-and-details/basic-section/BasicSection.scss @@ -8,9 +8,8 @@ } .pgn__pageBanner-component { - @include pgn-box-shadow(1, "down"); - - background-color: $white; + box-shadow: var(--pgn-elevation-box-shadow-down-1); + background-color: var(--pgn-color-white); border-radius: .375rem; } diff --git a/src/schedule-and-details/credit-section/CreditSection.scss b/src/schedule-and-details/credit-section/CreditSection.scss index 2cfbf1c963..269cc9f4fd 100644 --- a/src/schedule-and-details/credit-section/CreditSection.scss +++ b/src/schedule-and-details/credit-section/CreditSection.scss @@ -1,8 +1,8 @@ .credit-section { .credit-help-text { - padding-top: $spacer; + padding-top: var(--pgn-spacing-spacer-base); margin-bottom: .5rem; - font: normal .875rem/1.5rem $font-family-base; + font: normal .875rem/1.5rem var(--pgn-typography-font-family-base); } .credit-info-list { diff --git a/src/schedule-and-details/instructors-section/InstructorsSection.scss b/src/schedule-and-details/instructors-section/InstructorsSection.scss index 1581f8e1cc..eddab8aa25 100644 --- a/src/schedule-and-details/instructors-section/InstructorsSection.scss +++ b/src/schedule-and-details/instructors-section/InstructorsSection.scss @@ -6,6 +6,6 @@ gap: 1.875rem; padding-inline-start: 0; list-style-type: none; - padding-top: $spacer; + padding-top: var(--pgn-spacing-spacer-base); } } diff --git a/src/schedule-and-details/introducing-section/introduction-video/IntroductionVideo.scss b/src/schedule-and-details/introducing-section/introduction-video/IntroductionVideo.scss index 55dfae6e7e..40658278f0 100644 --- a/src/schedule-and-details/introducing-section/introduction-video/IntroductionVideo.scss +++ b/src/schedule-and-details/introducing-section/introduction-video/IntroductionVideo.scss @@ -1,6 +1,5 @@ .embed-video-container { - @include pgn-box-shadow(1, "down"); - + box-shadow: var(--pgn-elevation-box-shadow-down-1); border-radius: .1875rem; .introduction-video { diff --git a/src/schedule-and-details/learning-outcomes-section/LearningOutcomes.scss b/src/schedule-and-details/learning-outcomes-section/LearningOutcomes.scss index 13b761f01b..b40ef8937e 100644 --- a/src/schedule-and-details/learning-outcomes-section/LearningOutcomes.scss +++ b/src/schedule-and-details/learning-outcomes-section/LearningOutcomes.scss @@ -5,6 +5,6 @@ justify-content: space-between; padding-inline-start: 0; list-style-type: none; - padding-top: $spacer; + padding-top: var(--pgn-spacing-spacer-base); } } diff --git a/src/schedule-and-details/pacing-section/PacingSection.scss b/src/schedule-and-details/pacing-section/PacingSection.scss index 2d4c8a751f..5743339108 100644 --- a/src/schedule-and-details/pacing-section/PacingSection.scss +++ b/src/schedule-and-details/pacing-section/PacingSection.scss @@ -1,27 +1,27 @@ .pacing-section { .pgn__form-group { p.pgn__form-label { - font: normal .875rem/1.5rem $font-family-base; + font: normal .875rem/1.5rem var(--pgn-typography-font-family-base); } label.pgn__form-label { - font: normal $font-weight-bold .75rem/1.25rem $font-family-base; + font: normal var(--pgn-typography-font-weight-bold) .75rem/1.25rem var(--pgn-typography-font-family-base); color: $text-color-weak; } .pgn__form-control-description { - font: normal .75rem/1.5rem $font-family-base; - color: $black; + font: normal .75rem/1.5rem var(--pgn-typography-font-family-base); + color: var(--pgn-color-black); } } input.pgn__form-radio-input { padding: .5rem; - height: $spacer; - width: $spacer; + height: var(--pgn-spacing-spacer-base); + width: var(--pgn-spacing-spacer-base); } .pgn__form-control-set { - padding: $spacer 0 0 2.5rem; + padding: var(--pgn-spacing-spacer-base) 0 0 2.5rem; } } diff --git a/src/schedule-and-details/schedule-section/ScheduleSection.scss b/src/schedule-and-details/schedule-section/ScheduleSection.scss index 4d96dd0f64..8bb42dcd15 100644 --- a/src/schedule-and-details/schedule-section/ScheduleSection.scss +++ b/src/schedule-and-details/schedule-section/ScheduleSection.scss @@ -2,7 +2,7 @@ .schedule-date-list { display: flex; flex-direction: column; - padding-top: $spacer; + padding-top: var(--pgn-spacing-spacer-base); margin: 0; padding-inline-start: 0; list-style-type: none; @@ -32,7 +32,7 @@ .schedule-date-item-error { margin: 0; padding: 0; - color: $form-feedback-invalid-color; - font: normal $font-weight-normal .75rem/1.25rem $font-family-base; + color: var(--pgn-color-form-feedback-invalid); + font: normal var(--pgn-typography-font-weight-normal) .75rem/1.25rem var(--pgn-typography-font-family-base); } } diff --git a/src/schedule-and-details/schedule-section/certificate-display-row/CertificateDisplayRow.scss b/src/schedule-and-details/schedule-section/certificate-display-row/CertificateDisplayRow.scss index e715d7f18f..35e643cc0c 100644 --- a/src/schedule-and-details/schedule-section/certificate-display-row/CertificateDisplayRow.scss +++ b/src/schedule-and-details/schedule-section/certificate-display-row/CertificateDisplayRow.scss @@ -1,39 +1,39 @@ .dropdown-certificate { .dropdown-certificate-label { - font: normal $font-weight-bold .75rem/1.25rem $font-family-base; - color: $gray-500; + font: normal var(--pgn-typography-font-weight-bold) .75rem/1.25rem var(--pgn-typography-font-family-base); + color: var(--pgn-color-gray-500); margin-bottom: .5rem; } .dropdown-certificate-control { .dropdown-certificate-control-toggle { - font-size: $input-font-size; - font-weight: $input-font-weight; - line-height: $input-line-height; - background: $input-bg; - border-color: $input-border-color; - border-width: $input-border-width; - box-shadow: $input-box-shadow; - border-radius: $input-border-radius; - color: $input-color; - padding: $input-padding-y $input-padding-x; - height: $input-height; + font-size: var(--pgn-typography-form-input-font-size-base); + font-weight: var(--pgn-typography-form-input-font-weight); + line-height: var(--pgn-typography-form-input-line-height-base); + background: var(--pgn-color-form-input-bg-base); + border-color: var(--pgn-color-form-input-border); + border-width: var(--pgn-size-form-input-width-border); + box-shadow: var(--pgn-elevation-form-input-base); + border-radius: var(--pgn-size-form-input-radius-border-base); + color: var(--pgn-color-form-input-base); + padding: var(--pgn-spacing-form-input-padding-y-base) var(--pgn-spacing-form-input-padding-x-base); + height: var(--pgn-size-form-input-height-base); justify-content: space-between; width: 100%; &:not(:disabled, .disabled):active, &:not(:disabled, .disabled).active, .show & { - color: $input-color; - border-color: $input-bg; - background-color: $input-bg; + color: var(--pgn-color-form-input-base); + border-color: var(--pgn-color-form-input-bg-base); + background-color: var(--pgn-color-form-input-bg-base); } } } .pgn__form-control-description { - font: normal $font-weight-normal .75rem/1.25rem $font-family-base; - color: $gray-500; + font: normal var(--pgn-typography-font-weight-normal) .75rem/1.25rem var(--pgn-typography-font-family-base); + color: var(--pgn-color-gray-500); margin-top: .5rem; } } diff --git a/src/search-manager/FilterBy.scss b/src/search-manager/FilterBy.scss index 7579958f6f..b6e5ccad2e 100644 --- a/src/search-manager/FilterBy.scss +++ b/src/search-manager/FilterBy.scss @@ -7,7 +7,7 @@ } .clear-filter-button:hover { - color: $info-900 !important; + color: var(--pgn-color-info-900) !important; } .problem-menu-item { diff --git a/src/search-manager/FilterByPublished.tsx b/src/search-manager/FilterByPublished.tsx index f8ede2a956..079c6d43ee 100644 --- a/src/search-manager/FilterByPublished.tsx +++ b/src/search-manager/FilterByPublished.tsx @@ -10,12 +10,18 @@ import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; import SearchFilterWidget from './SearchFilterWidget'; import { useSearchContext } from './SearchManager'; -import { PublishStatus } from './data/api'; +import { allPublishFilters, PublishStatus } from './data/api'; + +interface FilterByPublishedProps { + visibleFilters?: PublishStatus[], +} /** * A button with a dropdown that allows filtering the current search by publish status */ -const FilterByPublished: React.FC> = () => { +const FilterByPublished = ({ + visibleFilters = allPublishFilters, +}: FilterByPublishedProps) => { const intl = useIntl(); const { publishStatus, @@ -42,6 +48,26 @@ const FilterByPublished: React.FC> = () => { }; const appliedFilters = publishStatusFilter.map(mode => ({ label: modeToLabel[mode] })); + const filterLabels = { + [PublishStatus.Published]: intl.formatMessage(messages.publishStatusPublished), + [PublishStatus.Modified]: intl.formatMessage(messages.publishStatusModified), + [PublishStatus.NeverPublished]: intl.formatMessage(messages.publishStatusNeverPublished), + }; + + const visibleFiltersToRender = visibleFilters.map((filter) => ( + { toggleFilterMode(filter); }} + > +
+ {filterLabels[filter]} + {publishStatus[filter] ?? 0} +
+
+ )); + return ( > = () => { value={publishStatusFilter} > - { toggleFilterMode(PublishStatus.Published); }} - > -
- {intl.formatMessage(messages.publishStatusPublished)} - {publishStatus[PublishStatus.Published] ?? 0} -
-
- { toggleFilterMode(PublishStatus.Modified); }} - > -
- {intl.formatMessage(messages.publishStatusModified)} - {publishStatus[PublishStatus.Modified] ?? 0} -
-
- { toggleFilterMode(PublishStatus.NeverPublished); }} - > -
- {intl.formatMessage(messages.publishStatusNeverPublished)} - {publishStatus[PublishStatus.NeverPublished] ?? 0} -
-
+ {visibleFiltersToRender}
diff --git a/src/search-manager/SearchKeywordsField.tsx b/src/search-manager/SearchKeywordsField.tsx index 90c09fdd93..bbe3b67f82 100644 --- a/src/search-manager/SearchKeywordsField.tsx +++ b/src/search-manager/SearchKeywordsField.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { SearchField } from '@openedx/paragon'; +import { debounce } from 'lodash'; import messages from './messages'; import { useSearchContext } from './SearchManager'; @@ -17,10 +18,15 @@ const SearchKeywordsField: React.FC<{ const defaultPlaceholder = usageKey ? messages.clearUsageKeyToSearch : messages.inputPlaceholder; const { placeholder = intl.formatMessage(defaultPlaceholder) } = props; + const handleSearch = React.useCallback( + debounce((term) => setSearchKeywords(term.trim()), 400), + [searchKeywords], + );// Perform search after 500ms + return ( setSearchKeywords('')} value={searchKeywords} className={props.className} diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts index 549054b3fe..d829b8a527 100644 --- a/src/search-manager/data/api.ts +++ b/src/search-manager/data/api.ts @@ -31,6 +31,8 @@ export enum PublishStatus { NeverPublished = 'never', } +export const allPublishFilters: PublishStatus[] = Object.values(PublishStatus); + /** * Get the content search configuration from the CMS. */ @@ -50,6 +52,7 @@ export const getContentSearchConfig = async (): Promise<{ url: string, indexName export interface ContentDetails { htmlContent?: string; capaContent?: string; + childUsageKeys?: Array; [k: string]: any; } @@ -151,9 +154,10 @@ export interface ContentHit extends BaseContentHit { * Defined in edx-platform/openedx/core/djangoapps/content/search/documents.py */ export interface ContentPublishedData { - description?: string, - displayName?: string, - numChildren?: number, + description?: string; + displayName?: string; + numChildren?: number; + content?: ContentDetails; } /** @@ -171,6 +175,9 @@ export interface CollectionHit extends BaseContentHit { * Information about a single container returned in the search results * Defined in edx-platform/openedx/core/djangoapps/content/search/documents.py */ +interface ContainerHitContent { + childUsageKeys?: string[], +} export interface ContainerHit extends BaseContentHit { type: 'library_container'; blockType: 'unit'; // This should be expanded to include other container types @@ -178,6 +185,7 @@ export interface ContainerHit extends BaseContentHit { published?: ContentPublishedData; publishStatus: PublishStatus; formatted: BaseContentHit['formatted'] & { published?: ContentPublishedData, }; + content?: ContainerHitContent; } export type HitType = ContentHit | CollectionHit | ContainerHit; diff --git a/src/search-modal/SearchModal.scss b/src/search-modal/SearchModal.scss index d14fdac832..f359b695b8 100644 --- a/src/search-modal/SearchModal.scss +++ b/src/search-modal/SearchModal.scss @@ -57,7 +57,7 @@ .search-result { &:hover { - background-color: $gray-100; + background-color: var(--pgn-color-gray-100); cursor: pointer; } @@ -66,4 +66,13 @@ background-color: unset; } } + + // Fix a bug with search modal: very long text is not truncated with an ellipsis + // https://github.com/openedx/frontend-app-authoring/issues/1900 + .hit-description { + display: -webkit-box; /* stylelint-disable-line value-no-vendor-prefix */ + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + } } diff --git a/src/search-modal/SearchResult.tsx b/src/search-modal/SearchResult.tsx index 032d5cda1a..6f82a0f045 100644 --- a/src/search-modal/SearchResult.tsx +++ b/src/search-modal/SearchResult.tsx @@ -181,7 +181,7 @@ const SearchResult: React.FC<{ hit: ContentHit }> = ({ hit }) => {
-
+
diff --git a/src/search-modal/SearchUI.test.tsx b/src/search-modal/SearchUI.test.tsx index 2990cd6e1e..0c18160586 100644 --- a/src/search-modal/SearchUI.test.tsx +++ b/src/search-modal/SearchUI.test.tsx @@ -480,7 +480,7 @@ describe('', () => { const expandButtonLabel = /Expand to show child tags of "ESDC Skills and Competencies"/i; await waitFor(() => { expect(getByLabelText(expandButtonLabel)).toBeInTheDocument(); }); - const input = getByLabelText('Search tags'); + const input = getByRole('searchbox'); fireEvent.change(input, { target: { value: 'Lightcast' } }); await waitFor(() => { expect(queryByLabelText(/^ESDC Skills and Competencies/i)).toBeNull(); }); diff --git a/src/setupTest.js b/src/setupTest.js index 6c24a75235..bb55d692ff 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -48,6 +48,7 @@ mergeConfig({ STUDIO_BASE_URL: process.env.STUDIO_BASE_URL || null, LMS_BASE_URL: process.env.LMS_BASE_URL || null, LIBRARY_UNSUPPORTED_BLOCKS: (process.env.LIBRARY_UNSUPPORTED_BLOCKS || 'conditional,step-builder,problem-builder').split(','), + PARAGON_THEME_URLS: process.env.PARAGON_THEME_URLS || null, }, 'CourseAuthoringConfig'); class ResizeObserver { diff --git a/src/studio-home/scss/StudioHome.scss b/src/studio-home/scss/StudioHome.scss index fd7cee3559..3f4320cc25 100644 --- a/src/studio-home/scss/StudioHome.scss +++ b/src/studio-home/scss/StudioHome.scss @@ -14,20 +14,20 @@ margin-bottom: 2.25rem; .organization-section-title { - color: $black; + color: var(--pgn-color-black); } .organization-section-form { - margin: $spacer 0 -8px; + margin: var(--pgn-spacing-spacer-base) 0 -8px; .organization-section-form-label { - color: $gray-700; + color: var(--pgn-color-gray-700); margin-bottom: 0; margin-right: .75rem; } .organization-section-form-control { - border-color: $gray-500; + border-color: var(--pgn-color-gray-500); .form-control { font-size: .875rem; @@ -43,7 +43,7 @@ margin-bottom: 1.625rem; .nav-link { - border-bottom: .125rem solid $light-400; + border-bottom: .125rem solid var(--pgn-color-light-400); } .nav-link.active { @@ -75,14 +75,14 @@ } .card-item-title { - font: normal $font-weight-normal 1.125rem/1.75rem $font-family-base; - color: $black; + font: normal var(--pgn-typography-font-weight-normal) 1.125rem/1.75rem var(--pgn-typography-font-family-base); + color: var(--pgn-color-black); margin-bottom: .1875rem; } .pgn__card-header-subtitle-md { - font: normal $font-weight-normal .75rem/1.25rem $font-family-base; - color: $gray-700; + font: normal var(--pgn-typography-font-weight-normal) .75rem/1.25rem var(--pgn-typography-font-family-base); + color: var(--pgn-color-gray-700); margin: 0; } } diff --git a/src/studio-home/tabs-section/courses-tab/courses-filters/__snapshots__/index.test.jsx.snap b/src/studio-home/tabs-section/courses-tab/courses-filters/__snapshots__/index.test.jsx.snap index a98777a5ed..7ea2152b74 100644 --- a/src/studio-home/tabs-section/courses-tab/courses-filters/__snapshots__/index.test.jsx.snap +++ b/src/studio-home/tabs-section/courses-tab/courses-filters/__snapshots__/index.test.jsx.snap @@ -26,15 +26,19 @@ exports[`CoursesFilters snapshot 1`] = ` search - +
+ +