diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index eff6bfea75..be55bfaacb 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -33,7 +33,7 @@ jobs:
path: coverage
merge-multiple: true
- name: Upload coverage
- uses: codecov/codecov-action@v5
+ uses: codecov/codecov-action@v6
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/package-lock.json b/package-lock.json
index 7d97aa32a8..8164581eb7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -42,7 +42,7 @@
"@openedx/paragon": "^23.5.0",
"@redux-devtools/extension": "^3.3.0",
"@reduxjs/toolkit": "2.11.2",
- "@tanstack/react-query": "5.90.21",
+ "@tanstack/react-query": "5.95.2",
"@tanstack/react-table": "^8.21.3",
"@tinymce/tinymce-react": "^6.0.0",
"classnames": "2.5.1",
@@ -96,7 +96,7 @@
"jest-canvas-mock": "^2.5.2",
"jest-expect-message": "^1.1.3",
"oxlint": "^1.42.0",
- "oxlint-tsgolint": "^0.16.0",
+ "oxlint-tsgolint": "^0.17.0",
"react-test-renderer": "^18.3.1",
"redux-mock-store": "^1.5.4"
}
@@ -178,7 +178,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz",
"integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.24.7",
@@ -2055,12 +2054,12 @@
}
},
"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==",
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"license": "ISC",
"dependencies": {
- "brace-expansion": "^2.0.1"
+ "brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -2329,21 +2328,21 @@
}
},
"node_modules/@codemirror/state": {
- "version": "6.5.4",
- "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz",
- "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==",
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz",
+ "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
- "version": "6.39.16",
- "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.16.tgz",
- "integrity": "sha512-m6S22fFpKtOWhq8HuhzsI1WzUP/hB9THbDj0Tl5KX4gbO6Y91hwBl7Yky33NdvB6IffuRFiBxf1R8kJMyXmA4Q==",
+ "version": "6.40.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.40.0.tgz",
+ "integrity": "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==",
"license": "MIT",
"dependencies": {
- "@codemirror/state": "^6.5.0",
+ "@codemirror/state": "^6.6.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
@@ -2396,7 +2395,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
},
@@ -2419,7 +2417,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
}
@@ -2496,7 +2493,6 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
@@ -2674,7 +2670,6 @@
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.5.5.tgz",
"integrity": "sha512-imExY37cxE7qzKYg3gaqcdfhc0rzpV1DEFmy6PPCJg4m+cycQNiXtAKl3nITkcQkzhV0JYh3qttEgq6d4a1QXw==",
"license": "AGPL-3.0",
- "peer": true,
"dependencies": {
"@cospired/i18n-iso-languages": "4.2.0",
"@formatjs/intl-pluralrules": "4.3.3",
@@ -3446,7 +3441,6 @@
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
@@ -5208,9 +5202,9 @@
"link": true
},
"node_modules/@openedx/frontend-build": {
- "version": "14.6.2",
- "resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.6.2.tgz",
- "integrity": "sha512-Iu4/GPq90Xr/MSWnonn2qX8VDhI89HN7KOYBZ0/sxmAQgvXXNc7OYNC7kumvzbYzKueJQTyZoUYS7UjKB/n1WA==",
+ "version": "14.6.3",
+ "resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.6.3.tgz",
+ "integrity": "sha512-6TVe8WWRuakErz/5wwN+CbaE2MItp8pKiJc2rB+3J0azRIjbWiEK40Vk0SKJVkdnZBlp0VlSSSQGZnlwbFF60g==",
"license": "AGPL-3.0",
"dependencies": {
"@babel/cli": "7.24.8",
@@ -5231,7 +5225,7 @@
"@types/jest": "29.5.12",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
- "autoprefixer": "10.4.20",
+ "autoprefixer": "10.4.27",
"babel-jest": "29.7.0",
"babel-loader": "9.2.1",
"babel-plugin-formatjs": "^10.4.0",
@@ -5412,11 +5406,10 @@
}
},
"node_modules/@openedx/paragon": {
- "version": "23.19.1",
- "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.19.1.tgz",
- "integrity": "sha512-c/cWnvZsGS7xyq0tJpssmv2oyfYG6Fuawy6EzWy8CYiQ4oD67EVuSwBInCfSJoNZhvvkUE+4B/YhDIRGUVDz5w==",
+ "version": "23.19.2",
+ "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.19.2.tgz",
+ "integrity": "sha512-4umD73Ujknvo4Bt1dr5X9QvwR1vlSkdoG/s6SaKmBlUa1eP1CicpIBqZiMC5/z3BUPcorxWedXQKp1Rdlv+73Q==",
"license": "Apache-2.0",
- "peer": true,
"workspaces": [
"example",
"component-generator",
@@ -5480,9 +5473,9 @@
}
},
"node_modules/@openedx/paragon/node_modules/axios": {
- "version": "0.30.2",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.30.2.tgz",
- "integrity": "sha512-0pE4RQ4UQi1jKY6p7u6i1Tkzqmu+d+/tHS7Q7rKunWLB9WyilBTpHHpXzPNMDj5hTbK0B0PTLSz07yqMBiF6xg==",
+ "version": "0.30.3",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.30.3.tgz",
+ "integrity": "sha512-5/tmEb6TmE/ax3mdXBc/Mi6YdPGxQsv+0p5YlciXWt3PHIn0VamqCXhRMtScnwY3lbgSXLneOuXAKUhgmSRpwg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.4",
@@ -5491,9 +5484,9 @@
}
},
"node_modules/@openedx/paragon/node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
+ "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -5512,7 +5505,7 @@
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
- "deprecated": "Glob versions prior to v9 are no longer supported",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
@@ -5529,9 +5522,9 @@
}
},
"node_modules/@openedx/paragon/node_modules/minimatch": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
- "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
@@ -5582,9 +5575,9 @@
}
},
"node_modules/@oxlint-tsgolint/darwin-arm64": {
- "version": "0.16.0",
- "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-0.16.0.tgz",
- "integrity": "sha512-WQt5lGwRPJBw7q2KNR0mSPDAaMmZmVvDlEEti96xLO7ONhyomQc6fBZxxwZ4qTFedjJnrHX94sFelZ4OKzS7UQ==",
+ "version": "0.17.2",
+ "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-0.17.2.tgz",
+ "integrity": "sha512-1/QmWTRB8g5273wUnmmQxQz+kEFLJq8MYS82uFdxulUa2sdggWEQphnRhDRAzcbOCtrsya8+xGohv41dqRM/hQ==",
"cpu": [
"arm64"
],
@@ -5596,9 +5589,9 @@
]
},
"node_modules/@oxlint-tsgolint/darwin-x64": {
- "version": "0.16.0",
- "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-0.16.0.tgz",
- "integrity": "sha512-VJo29XOzdkalvCTiE2v6FU3qZlgHaM8x8hUEVJGPU2i5W+FlocPpmn00+Ld2n7Q0pqIjyD5EyvZ5UmoIEJMfqg==",
+ "version": "0.17.2",
+ "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-0.17.2.tgz",
+ "integrity": "sha512-GjEvcZPm8e9N2QtRlH5ttRr4II1ph86iR+gj7P7u47NuxKs099GivV0ISAsRlG09uYgRG3lTe2x5JrnMknuI0Q==",
"cpu": [
"x64"
],
@@ -5610,9 +5603,9 @@
]
},
"node_modules/@oxlint-tsgolint/linux-arm64": {
- "version": "0.16.0",
- "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-0.16.0.tgz",
- "integrity": "sha512-MPfqRt1+XRHv9oHomcBMQ3KpTE+CSkZz14wUxDQoqTNdUlV0HWdzwIE9q65I3D9YyxEnqpM7j4qtDQ3apqVvbQ==",
+ "version": "0.17.2",
+ "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-0.17.2.tgz",
+ "integrity": "sha512-Ybo4npjDMXQ15MBoftOBut9/gOdHhbnIhRmphx9owBQcZBmwrIy1+PfLqHRBuTCJ8diUmxQxSRkvXrGb+ogGqQ==",
"cpu": [
"arm64"
],
@@ -5624,9 +5617,9 @@
]
},
"node_modules/@oxlint-tsgolint/linux-x64": {
- "version": "0.16.0",
- "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-0.16.0.tgz",
- "integrity": "sha512-XQSwVUsnwLokMhe1TD6IjgvW5WMTPzOGGkdFDtXWQmlN2YeTw94s/NN0KgDrn2agM1WIgAenEkvnm0u7NgwEyw==",
+ "version": "0.17.2",
+ "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-0.17.2.tgz",
+ "integrity": "sha512-bU3A7bg9qa1VeWUwYwbXaAcUCOW+fl+SndNMNoYpm2+nhsAzzr9k9jz5Qr7NeKwbYet3qETjmhCmmfqe1syiPA==",
"cpu": [
"x64"
],
@@ -5638,9 +5631,9 @@
]
},
"node_modules/@oxlint-tsgolint/win32-arm64": {
- "version": "0.16.0",
- "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-0.16.0.tgz",
- "integrity": "sha512-EWdlspQiiFGsP2AiCYdhg5dTYyAlj6y1nRyNI2dQWq4Q/LITFHiSRVPe+7m7K7lcsZCEz2icN/bCeSkZaORqIg==",
+ "version": "0.17.2",
+ "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-0.17.2.tgz",
+ "integrity": "sha512-MeM1tyeg8J4DoHxAO3geDllM0Zm0tQDieQ701OXiS/vFA4QK+v+qBEJALqUys5obbIlLR2scmhzGor89bOr2ug==",
"cpu": [
"arm64"
],
@@ -5652,9 +5645,9 @@
]
},
"node_modules/@oxlint-tsgolint/win32-x64": {
- "version": "0.16.0",
- "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-0.16.0.tgz",
- "integrity": "sha512-1ufk8cgktXJuJZHKF63zCHAkaLMwZrEXnZ89H2y6NO85PtOXqu4zbdNl0VBpPP3fCUuUBu9RvNqMFiv0VsbXWA==",
+ "version": "0.17.2",
+ "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-0.17.2.tgz",
+ "integrity": "sha512-XfmGnyosL9jDGPwZcoDqdYADQNXjzH5hZs0xoZFodBbQhI1oAuItw/XR6tgga6grjusPSMS7j373sSGLLrE3yg==",
"cpu": [
"x64"
],
@@ -5666,9 +5659,9 @@
]
},
"node_modules/@oxlint/binding-android-arm-eabi": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.56.0.tgz",
- "integrity": "sha512-IyfYPthZyiSKwAv/dLjeO18SaK8MxLI9Yss2JrRDyweQAkuL3LhEy7pwIwI7uA3KQc1Vdn20kdmj3q0oUIQL6A==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.57.0.tgz",
+ "integrity": "sha512-C7EiyfAJG4B70496eV543nKiq5cH0o/xIh/ufbjQz3SIvHhlDDsyn+mRFh+aW8KskTyUpyH2LGWL8p2oN6bl1A==",
"cpu": [
"arm"
],
@@ -5683,9 +5676,9 @@
}
},
"node_modules/@oxlint/binding-android-arm64": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.56.0.tgz",
- "integrity": "sha512-Ga5zYrzH6vc/VFxhn6MmyUnYEfy9vRpwTIks99mY3j6Nz30yYpIkWryI0QKPCgvGUtDSXVLEaMum5nA+WrNOSg==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.57.0.tgz",
+ "integrity": "sha512-9i80AresjZ/FZf5xK8tKFbhQnijD4s1eOZw6/FHUwD59HEZbVLRc2C88ADYJfLZrF5XofWDiRX/Ja9KefCLy7w==",
"cpu": [
"arm64"
],
@@ -5700,9 +5693,9 @@
}
},
"node_modules/@oxlint/binding-darwin-arm64": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.56.0.tgz",
- "integrity": "sha512-ogmbdJysnw/D4bDcpf1sPLpFThZ48lYp4aKYm10Z/6Nh1SON6NtnNhTNOlhEY296tDFItsZUz+2tgcSYqh8Eyw==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.57.0.tgz",
+ "integrity": "sha512-0eUfhRz5L2yKa9I8k3qpyl37XK3oBS5BvrgdVIx599WZK63P8sMbg+0s4IuxmIiZuBK68Ek+Z+gcKgeYf0otsg==",
"cpu": [
"arm64"
],
@@ -5717,9 +5710,9 @@
}
},
"node_modules/@oxlint/binding-darwin-x64": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.56.0.tgz",
- "integrity": "sha512-x8QE1h+RAtQ2g+3KPsP6Fk/tdz6zJQUv5c7fTrJxXV3GHOo+Ry5p/PsogU4U+iUZg0rj6hS+E4xi+mnwwlDCWQ==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.57.0.tgz",
+ "integrity": "sha512-UvrSuzBaYOue+QMAcuDITe0k/Vhj6KZGjfnI6x+NkxBTke/VoM7ZisaxgNY0LWuBkTnd1OmeQfEQdQ48fRjkQg==",
"cpu": [
"x64"
],
@@ -5734,9 +5727,9 @@
}
},
"node_modules/@oxlint/binding-freebsd-x64": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.56.0.tgz",
- "integrity": "sha512-6G+WMZvwJpMvY7my+/SHEjb7BTk/PFbePqLpmVmUJRIsJMy/UlyYqjpuh0RCgYYkPLcnXm1rUM04kbTk8yS1Yg==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.57.0.tgz",
+ "integrity": "sha512-wtQq0dCoiw4bUwlsNVDJJ3pxJA218fOezpgtLKrbQqUtQJcM9yP8z+I9fu14aHg0uyAxIY+99toL6uBa2r7nxA==",
"cpu": [
"x64"
],
@@ -5751,9 +5744,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm-gnueabihf": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.56.0.tgz",
- "integrity": "sha512-YYHBsk/sl7fYwQOok+6W5lBPeUEvisznV/HZD2IfZmF3Bns6cPC3Z0vCtSEOaAWTjYWN3jVsdu55jMxKlsdlhg==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.57.0.tgz",
+ "integrity": "sha512-qxFWl2BBBFcT4djKa+OtMdnLgoHEJXpqjyGwz8OhW35ImoCwR5qtAGqApNYce5260FQqoAHW8S8eZTjiX67Tsg==",
"cpu": [
"arm"
],
@@ -5768,9 +5761,9 @@
}
},
"node_modules/@oxlint/binding-linux-arm-musleabihf": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.56.0.tgz",
- "integrity": "sha512-+AZK8rOUr78y8WT6XkDb04IbMRqauNV+vgT6f8ZLOH8wnpQ9i7Nol0XLxAu+Cq7Sb+J9wC0j6Km5hG8rj47/yQ==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.57.0.tgz",
+ "integrity": "sha512-SQoIsBU7J0bDW15/f0/RvxHfY3Y0+eB/caKBQtNFbuerTiA6JCYx9P1MrrFTwY2dTm/lMgTSgskvCEYk2AtG/Q==",
"cpu": [
"arm"
],
@@ -5785,13 +5778,16 @@
}
},
"node_modules/@oxlint/binding-linux-arm64-gnu": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.56.0.tgz",
- "integrity": "sha512-urse2SnugwJRojUkGSSeH2LPMaje5Q50yQtvtL9HFckiyeqXzoFwOAZqD5TR29R2lq7UHidfFDM9EGcchcbb8A==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.57.0.tgz",
+ "integrity": "sha512-jqxYd1W6WMeozsCmqe9Rzbu3SRrGTyGDAipRlRggetyYbUksJqJKvUNTQtZR/KFoJPb+grnSm5SHhdWrywv3RQ==",
"cpu": [
"arm64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -5802,13 +5798,16 @@
}
},
"node_modules/@oxlint/binding-linux-arm64-musl": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.56.0.tgz",
- "integrity": "sha512-rkTZkBfJ4TYLjansjSzL6mgZOdN5IvUnSq3oNJSLwBcNvy3dlgQtpHPrRxrCEbbcp7oQ6If0tkNaqfOsphYZ9g==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.57.0.tgz",
+ "integrity": "sha512-i66WyEPVEvq9bxRUCJ/MP5EBfnTDN3nhwEdFZFTO5MmLLvzngfWEG3NSdXQzTT3vk5B9i6C2XSIYBh+aG6uqyg==",
"cpu": [
"arm64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -5819,13 +5818,16 @@
}
},
"node_modules/@oxlint/binding-linux-ppc64-gnu": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.56.0.tgz",
- "integrity": "sha512-uqL1kMH3u69/e1CH2EJhP3CP28jw2ExLsku4o8RVAZ7fySo9zOyI2fy9pVlTAp4voBLVgzndXi3SgtdyCTa2aA==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.57.0.tgz",
+ "integrity": "sha512-oMZDCwz4NobclZU3pH+V1/upVlJZiZvne4jQP+zhJwt+lmio4XXr4qG47CehvrW1Lx2YZiIHuxM2D4YpkG3KVA==",
"cpu": [
"ppc64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -5836,13 +5838,16 @@
}
},
"node_modules/@oxlint/binding-linux-riscv64-gnu": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.56.0.tgz",
- "integrity": "sha512-j0CcMBOgV6KsRaBdsebIeiy7hCjEvq2KdEsiULf2LZqAq0v1M1lWjelhCV57LxsqaIGChXFuFJ0RiFrSRHPhSg==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.57.0.tgz",
+ "integrity": "sha512-uoBnjJ3MMEBbfnWC1jSFr7/nSCkcQYa72NYoNtLl1imshDnWSolYCjzb8LVCwYCCfLJXD+0gBLD7fyC14c0+0g==",
"cpu": [
"riscv64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -5853,13 +5858,16 @@
}
},
"node_modules/@oxlint/binding-linux-riscv64-musl": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.56.0.tgz",
- "integrity": "sha512-7VDOiL8cDG3DQ/CY3yKjbV1c4YPvc4vH8qW09Vv+5ukq3l/Kcyr6XGCd5NvxUmxqDb2vjMpM+eW/4JrEEsUetA==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.57.0.tgz",
+ "integrity": "sha512-BdrwD7haPZ8a9KrZhKJRSj6jwCor+Z8tHFZ3PT89Y3Jq5v3LfMfEePeAmD0LOTWpiTmzSzdmyw9ijneapiVHKQ==",
"cpu": [
"riscv64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -5870,13 +5878,16 @@
}
},
"node_modules/@oxlint/binding-linux-s390x-gnu": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.56.0.tgz",
- "integrity": "sha512-JGRpX0M+ikD3WpwJ7vKcHKV6Kg0dT52BW2Eu2BupXotYeqGXBrbY+QPkAyKO6MNgKozyTNaRh3r7g+VWgyAQYQ==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.57.0.tgz",
+ "integrity": "sha512-BNs+7ZNsRstVg2tpNxAXfMX/Iv5oZh204dVyb8Z37+/gCh+yZqNTlg6YwCLIMPSk5wLWIGOaQjT0GUOahKYImw==",
"cpu": [
"s390x"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -5887,13 +5898,16 @@
}
},
"node_modules/@oxlint/binding-linux-x64-gnu": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.56.0.tgz",
- "integrity": "sha512-dNaICPvtmuxFP/VbqdofrLqdS3bM/AKJN3LMJD52si44ea7Be1cBk6NpfIahaysG9Uo+L98QKddU9CD5L8UHnQ==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.57.0.tgz",
+ "integrity": "sha512-AghS18w+XcENcAX0+BQGLiqjpqpaxKJa4cWWP0OWNLacs27vHBxu7TYkv9LUSGe5w8lOJHeMxcYfZNOAPqw2bg==",
"cpu": [
"x64"
],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -5904,13 +5918,16 @@
}
},
"node_modules/@oxlint/binding-linux-x64-musl": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.56.0.tgz",
- "integrity": "sha512-pF1vOtM+GuXmbklM1hV8WMsn6tCNPvkUzklj/Ej98JhlanbmA2RB1BILgOpwSuCTRTIYx2MXssmEyQQ90QF5aA==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.57.0.tgz",
+ "integrity": "sha512-E/FV3GB8phu/Rpkhz5T96hAiJlGzn91qX5yj5gU754P5cmVGXY1Jw/VSjDSlZBCY3VHjsVLdzgdkJaomEmcNOg==",
"cpu": [
"x64"
],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
"optional": true,
"os": [
@@ -5921,9 +5938,9 @@
}
},
"node_modules/@oxlint/binding-openharmony-arm64": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.56.0.tgz",
- "integrity": "sha512-bp8NQ4RE6fDIFLa4bdBiOA+TAvkNkg+rslR+AvvjlLTYXLy9/uKAYLQudaQouWihLD/hgkrXIKKzXi5IXOewwg==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.57.0.tgz",
+ "integrity": "sha512-xvZ2yZt0nUVfU14iuGv3V25jpr9pov5N0Wr28RXnHFxHCRxNDMtYPHV61gGLhN9IlXM96gI4pyYpLSJC5ClLCQ==",
"cpu": [
"arm64"
],
@@ -5938,9 +5955,9 @@
}
},
"node_modules/@oxlint/binding-win32-arm64-msvc": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.56.0.tgz",
- "integrity": "sha512-PxT4OJDfMOQBzo3OlzFb9gkoSD+n8qSBxyVq2wQSZIHFQYGEqIRTo9M0ZStvZm5fdhMqaVYpOnJvH2hUMEDk/g==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.57.0.tgz",
+ "integrity": "sha512-Z4D8Pd0AyHBKeazhdIXeUUy5sIS3Mo0veOlzlDECg6PhRRKgEsBJCCV1n+keUZtQ04OP+i7+itS3kOykUyNhDg==",
"cpu": [
"arm64"
],
@@ -5955,9 +5972,9 @@
}
},
"node_modules/@oxlint/binding-win32-ia32-msvc": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.56.0.tgz",
- "integrity": "sha512-PTRy6sIEPqy2x8PTP1baBNReN/BNEFmde0L+mYeHmjXE1Vlcc9+I5nsqENsB2yAm5wLkzPoTNCMY/7AnabT4/A==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.57.0.tgz",
+ "integrity": "sha512-StOZ9nFMVKvevicbQfql6Pouu9pgbeQnu60Fvhz2S6yfMaii+wnueLnqQ5I1JPgNF0Syew4voBlAaHD13wH6tw==",
"cpu": [
"ia32"
],
@@ -5972,9 +5989,9 @@
}
},
"node_modules/@oxlint/binding-win32-x64-msvc": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.56.0.tgz",
- "integrity": "sha512-ZHa0clocjLmIDr+1LwoWtxRcoYniAvERotvwKUYKhH41NVfl0Y4LNbyQkwMZzwDvKklKGvGZ5+DAG58/Ik47tQ==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.57.0.tgz",
+ "integrity": "sha512-6PuxhYgth8TuW0+ABPOIkGdBYw+qYGxgIdXPHSVpiCDm+hqTTWCmC739St1Xni0DJBt8HnSHTG67i1y6gr8qrA==",
"cpu": [
"x64"
],
@@ -6375,7 +6392,6 @@
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
- "peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@@ -6399,7 +6415,6 @@
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@standard-schema/utils": "^0.3.0",
@@ -6425,8 +6440,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@reduxjs/toolkit/node_modules/redux-thunk": {
"version": "3.1.0",
@@ -6674,7 +6688,6 @@
"resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz",
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0",
@@ -6774,9 +6787,9 @@
}
},
"node_modules/@tanstack/query-core": {
- "version": "5.90.20",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz",
- "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==",
+ "version": "5.95.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.95.2.tgz",
+ "integrity": "sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ==",
"license": "MIT",
"funding": {
"type": "github",
@@ -6784,12 +6797,12 @@
}
},
"node_modules/@tanstack/react-query": {
- "version": "5.90.21",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz",
- "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==",
+ "version": "5.95.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.95.2.tgz",
+ "integrity": "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA==",
"license": "MIT",
"dependencies": {
- "@tanstack/query-core": "5.90.20"
+ "@tanstack/query-core": "5.95.2"
},
"funding": {
"type": "github",
@@ -7000,7 +7013,8 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -7396,7 +7410,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -7408,7 +7421,6 @@
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
@@ -7547,7 +7559,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/regexpp": "^4.4.0",
"@typescript-eslint/scope-manager": "5.62.0",
@@ -7594,7 +7605,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
"license": "BSD-2-Clause",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "5.62.0",
"@typescript-eslint/types": "5.62.0",
@@ -8274,7 +8284,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -8364,7 +8373,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -8795,9 +8803,9 @@
}
},
"node_modules/autoprefixer": {
- "version": "10.4.20",
- "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
- "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
+ "version": "10.4.27",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz",
+ "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==",
"funding": [
{
"type": "opencollective",
@@ -8814,11 +8822,10 @@
],
"license": "MIT",
"dependencies": {
- "browserslist": "^4.23.3",
- "caniuse-lite": "^1.0.30001646",
- "fraction.js": "^4.3.7",
- "normalize-range": "^0.1.2",
- "picocolors": "^1.0.1",
+ "browserslist": "^4.28.1",
+ "caniuse-lite": "^1.0.30001774",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
@@ -8860,7 +8867,6 @@
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
@@ -9044,7 +9050,6 @@
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"cosmiconfig": "^7.0.0",
@@ -9241,9 +9246,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.10.8",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz",
- "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==",
+ "version": "2.10.12",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.12.tgz",
+ "integrity": "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
@@ -9421,7 +9426,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -9625,9 +9629,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001779",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001779.tgz",
- "integrity": "sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA==",
+ "version": "1.0.30001782",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001782.tgz",
+ "integrity": "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==",
"funding": [
{
"type": "opencollective",
@@ -9780,8 +9784,7 @@
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/clean-css": {
"version": "5.3.3",
@@ -11132,7 +11135,8 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/dom-converter": {
"version": "0.2.0",
@@ -11324,7 +11328,6 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/email-validator/-/email-validator-2.0.4.tgz",
"integrity": "sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==",
- "peer": true,
"engines": {
"node": ">4.0"
}
@@ -11669,7 +11672,6 @@
"integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==",
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
@@ -11726,7 +11728,6 @@
"resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz",
"integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==",
"license": "MIT",
- "peer": true,
"dependencies": {
"eslint-config-airbnb-base": "^15.0.0",
"object.assign": "^4.1.2",
@@ -11767,7 +11768,6 @@
"resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.1.0.tgz",
"integrity": "sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==",
"license": "MIT",
- "peer": true,
"dependencies": {
"eslint-config-airbnb-base": "^15.0.0"
},
@@ -12243,6 +12243,7 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"ms": "^2.1.1"
}
@@ -12252,6 +12253,7 @@
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"esutils": "^2.0.2"
},
@@ -12264,7 +12266,6 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz",
"integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.20.7",
"aria-query": "^5.1.3",
@@ -12301,7 +12302,6 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz",
"integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"array-includes": "^3.1.6",
"array.prototype.flatmap": "^1.3.1",
@@ -12332,7 +12332,6 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.1.tgz",
"integrity": "sha512-Ck77j8hF7l9N4S/rzSLOWEKpn994YH6iwUK8fr9mXIaQvGpQYmOnQLbiue1u5kI5T1y+gdgqosnEAO9NCz0DBg==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -13384,7 +13383,6 @@
}
],
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.1",
"deepmerge": "^2.1.1",
@@ -13418,15 +13416,15 @@
}
},
"node_modules/fraction.js": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
- "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
- "type": "patreon",
+ "type": "github",
"url": "https://github.com/sponsors/rawify"
}
},
@@ -15550,7 +15548,6 @@
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@@ -16959,8 +16956,7 @@
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.23",
@@ -17491,9 +17487,9 @@
"license": "ISC"
},
"node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -17550,7 +17546,6 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
- "peer": true,
"engines": {
"node": "*"
}
@@ -17796,15 +17791,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/normalize-range": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
- "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.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",
@@ -18130,9 +18116,9 @@
}
},
"node_modules/oxlint": {
- "version": "1.56.0",
- "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.56.0.tgz",
- "integrity": "sha512-Q+5Mj5PVaH/R6/fhMMFzw4dT+KPB+kQW4kaL8FOIq7tfhlnEVp6+3lcWqFruuTNlUo9srZUW3qH7Id4pskeR6g==",
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.57.0.tgz",
+ "integrity": "sha512-DGFsuBX5MFZX9yiDdtKjTrYPq45CZ8Fft6qCltJITYZxfwYjVdGf/6wycGYTACloauwIPxUnYhBVeZbHvleGhw==",
"dev": true,
"license": "MIT",
"bin": {
@@ -18145,25 +18131,25 @@
"url": "https://github.com/sponsors/Boshen"
},
"optionalDependencies": {
- "@oxlint/binding-android-arm-eabi": "1.56.0",
- "@oxlint/binding-android-arm64": "1.56.0",
- "@oxlint/binding-darwin-arm64": "1.56.0",
- "@oxlint/binding-darwin-x64": "1.56.0",
- "@oxlint/binding-freebsd-x64": "1.56.0",
- "@oxlint/binding-linux-arm-gnueabihf": "1.56.0",
- "@oxlint/binding-linux-arm-musleabihf": "1.56.0",
- "@oxlint/binding-linux-arm64-gnu": "1.56.0",
- "@oxlint/binding-linux-arm64-musl": "1.56.0",
- "@oxlint/binding-linux-ppc64-gnu": "1.56.0",
- "@oxlint/binding-linux-riscv64-gnu": "1.56.0",
- "@oxlint/binding-linux-riscv64-musl": "1.56.0",
- "@oxlint/binding-linux-s390x-gnu": "1.56.0",
- "@oxlint/binding-linux-x64-gnu": "1.56.0",
- "@oxlint/binding-linux-x64-musl": "1.56.0",
- "@oxlint/binding-openharmony-arm64": "1.56.0",
- "@oxlint/binding-win32-arm64-msvc": "1.56.0",
- "@oxlint/binding-win32-ia32-msvc": "1.56.0",
- "@oxlint/binding-win32-x64-msvc": "1.56.0"
+ "@oxlint/binding-android-arm-eabi": "1.57.0",
+ "@oxlint/binding-android-arm64": "1.57.0",
+ "@oxlint/binding-darwin-arm64": "1.57.0",
+ "@oxlint/binding-darwin-x64": "1.57.0",
+ "@oxlint/binding-freebsd-x64": "1.57.0",
+ "@oxlint/binding-linux-arm-gnueabihf": "1.57.0",
+ "@oxlint/binding-linux-arm-musleabihf": "1.57.0",
+ "@oxlint/binding-linux-arm64-gnu": "1.57.0",
+ "@oxlint/binding-linux-arm64-musl": "1.57.0",
+ "@oxlint/binding-linux-ppc64-gnu": "1.57.0",
+ "@oxlint/binding-linux-riscv64-gnu": "1.57.0",
+ "@oxlint/binding-linux-riscv64-musl": "1.57.0",
+ "@oxlint/binding-linux-s390x-gnu": "1.57.0",
+ "@oxlint/binding-linux-x64-gnu": "1.57.0",
+ "@oxlint/binding-linux-x64-musl": "1.57.0",
+ "@oxlint/binding-openharmony-arm64": "1.57.0",
+ "@oxlint/binding-win32-arm64-msvc": "1.57.0",
+ "@oxlint/binding-win32-ia32-msvc": "1.57.0",
+ "@oxlint/binding-win32-x64-msvc": "1.57.0"
},
"peerDependencies": {
"oxlint-tsgolint": ">=0.15.0"
@@ -18175,22 +18161,21 @@
}
},
"node_modules/oxlint-tsgolint": {
- "version": "0.16.0",
- "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-0.16.0.tgz",
- "integrity": "sha512-4RuJK2jP08XwqtUu+5yhCbxEauCm6tv2MFHKEMsjbosK2+vy5us82oI3VLuHwbNyZG7ekZA26U2LLHnGR4frIA==",
+ "version": "0.17.2",
+ "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-0.17.2.tgz",
+ "integrity": "sha512-W3gmZSOzNFGs9EwU8i3xlDpC0aqynQNtoDnaftdAZ3FE8cR7W625pPRbSmtsUOtTC0MPixx1i08R6uRVLfPp7g==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"tsgolint": "bin/tsgolint.js"
},
"optionalDependencies": {
- "@oxlint-tsgolint/darwin-arm64": "0.16.0",
- "@oxlint-tsgolint/darwin-x64": "0.16.0",
- "@oxlint-tsgolint/linux-arm64": "0.16.0",
- "@oxlint-tsgolint/linux-x64": "0.16.0",
- "@oxlint-tsgolint/win32-arm64": "0.16.0",
- "@oxlint-tsgolint/win32-x64": "0.16.0"
+ "@oxlint-tsgolint/darwin-arm64": "0.17.2",
+ "@oxlint-tsgolint/darwin-x64": "0.17.2",
+ "@oxlint-tsgolint/linux-arm64": "0.17.2",
+ "@oxlint-tsgolint/linux-x64": "0.17.2",
+ "@oxlint-tsgolint/win32-arm64": "0.17.2",
+ "@oxlint-tsgolint/win32-x64": "0.17.2"
}
},
"node_modules/p-limit": {
@@ -18794,7 +18779,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.1",
@@ -19514,7 +19498,6 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -19600,6 +19583,7 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -19615,6 +19599,7 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=10"
},
@@ -19627,7 +19612,8 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/process": {
"version": "0.11.10",
@@ -19662,7 +19648,6 @@
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@@ -19804,9 +19789,9 @@
}
},
"node_modules/purgecss/node_modules/minimatch": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
- "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
@@ -19936,7 +19921,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -20115,7 +20099,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -20158,7 +20141,6 @@
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz",
"integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.12.5"
},
@@ -20419,7 +20401,6 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.15.4",
"@types/react-redux": "^7.1.20",
@@ -20451,7 +20432,6 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.16.0.tgz",
"integrity": "sha512-FPvF2XxTSikpJxcr+bHut2H4gJ17+18Uy20D5/F+SKzFap62R3cM5wH6b8WN3LyGSYeQilLEcJcR1fjBSI2S1A==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -20541,7 +20521,6 @@
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
"integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@remix-run/router": "1.23.2",
"react-router": "6.30.3"
@@ -20909,7 +20888,6 @@
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.9.2"
}
@@ -21387,7 +21365,6 @@
"resolved": "https://registry.npmjs.org/sass/-/sass-1.85.1.tgz",
"integrity": "sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag==",
"license": "MIT",
- "peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@@ -21519,7 +21496,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -22527,7 +22503,6 @@
"integrity": "sha512-+xU0IA1StzqAqFs/QtXkK+XJa7wpS4X5H+JQccRKsRCElgeLGocFU1U/UMvMUylKFw6vwGV+Y/a2wb2pm5rFFQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@bundled-es-modules/deepmerge": "^4.3.1",
"@bundled-es-modules/glob": "^10.4.2",
@@ -22622,7 +22597,6 @@
"integrity": "sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@csstools/css-parser-algorithms": "^2.3.1",
"@csstools/css-tokenizer": "^2.2.0",
@@ -23249,7 +23223,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -23261,8 +23234,7 @@
"version": "5.10.9",
"resolved": "https://registry.npmjs.org/tinymce/-/tinymce-5.10.9.tgz",
"integrity": "sha512-5bkrors87X9LhYX2xq8GgPHrIgJYHl87YNs+kBcjQ5I3CiUgzo/vFcGvT3MZQ9QHsEeYMhYO6a5CLGGffR8hMg==",
- "license": "LGPL-2.1",
- "peer": true
+ "license": "LGPL-2.1"
},
"node_modules/tmp": {
"version": "0.2.5",
@@ -23404,7 +23376,6 @@
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.4.tgz",
"integrity": "sha512-YiHwDhSvCiItoAgsKtoLFCuakDzDsJ1DLDnSouTaTmdOcOwIkSzbLXduaQ6M5DRVhuZC/NYaaZ/mtHbWMv/S6Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"bs-logger": "0.x",
"fast-json-stable-stringify": "2.x",
@@ -23569,8 +23540,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD",
- "peer": true
+ "license": "0BSD"
},
"node_modules/tsutils": {
"version": "3.21.0",
@@ -23619,7 +23589,6 @@
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"license": "(MIT OR CC0-1.0)",
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -23719,7 +23688,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -23862,7 +23830,6 @@
"integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==",
"hasInstallScript": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"napi-postinstall": "^0.3.0"
},
@@ -24144,7 +24111,6 @@
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
- "peer": true,
"bin": {
"uuid": "dist/esm/bin/uuid"
}
@@ -24270,7 +24236,6 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz",
"integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -24375,7 +24340,6 @@
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz",
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^2.1.1",
@@ -24462,7 +24426,6 @@
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz",
"integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/bonjour": "^3.5.9",
"@types/connect-history-api-fallback": "^1.3.5",
@@ -24999,7 +24962,6 @@
"resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz",
"integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.15.4",
"@types/lodash": "^4.14.175",
diff --git a/package.json b/package.json
index 403c475780..9af1f85eb4 100644
--- a/package.json
+++ b/package.json
@@ -66,8 +66,8 @@
"@openedx/paragon": "^23.5.0",
"@redux-devtools/extension": "^3.3.0",
"@reduxjs/toolkit": "2.11.2",
+ "@tanstack/react-query": "5.95.2",
"@tanstack/react-table": "^8.21.3",
- "@tanstack/react-query": "5.90.21",
"@tinymce/tinymce-react": "^6.0.0",
"classnames": "2.5.1",
"codemirror": "^6.0.0",
@@ -120,7 +120,7 @@
"jest-canvas-mock": "^2.5.2",
"jest-expect-message": "^1.1.3",
"oxlint": "^1.42.0",
- "oxlint-tsgolint": "^0.16.0",
+ "oxlint-tsgolint": "^0.17.0",
"react-test-renderer": "^18.3.1",
"redux-mock-store": "^1.5.4"
}
diff --git a/src/authz/data/api.ts b/src/authz/data/api.ts
index 9801f8be88..6aca5e5307 100644
--- a/src/authz/data/api.ts
+++ b/src/authz/data/api.ts
@@ -5,7 +5,9 @@ import {
PermissionValidationRequestItem,
PermissionValidationResponseItem,
} from '@src/authz/types';
-import { getApiUrl } from './utils';
+import { getConfig } from '@edx/frontend-platform';
+
+export const getAuthzApiUrl = (path: string) => `${getConfig().STUDIO_BASE_URL}/api/authz/${path || ''}`;
export const validateUserPermissions = async (
query: PermissionValidationQuery,
@@ -14,7 +16,7 @@ export const validateUserPermissions = async (
const request: PermissionValidationRequestItem[] = Object.values(query);
const { data }: { data: PermissionValidationResponseItem[] } = await getAuthenticatedHttpClient().post(
- getApiUrl('/api/authz/v1/permissions/validate/me'),
+ getAuthzApiUrl('v1/permissions/validate/me'),
request,
);
diff --git a/src/authz/data/utils.ts b/src/authz/data/utils.ts
deleted file mode 100644
index 8676ba1abd..0000000000
--- a/src/authz/data/utils.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { getConfig } from '@edx/frontend-platform';
-
-export const getApiUrl = (path: string) => `${getConfig().LMS_BASE_URL}${path || ''}`;
-export const getStudioApiUrl = (path: string) => `${getConfig().STUDIO_BASE_URL}${path || ''}`;
diff --git a/src/constants.ts b/src/constants.ts
index 12e65d401d..be5eeca609 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -116,3 +116,9 @@ export const BROKEN = 'broken';
export const LOCKED = 'locked';
export const MANUAL = 'manual';
+
+export enum AgreementGated {
+ UPLOAD = 'upload',
+ UPLOAD_VIDEOS = 'upload.videos',
+ UPLOAD_FILES = 'upload.files',
+}
diff --git a/src/course-checklist/ChecklistSection/ChecklistItemBody.jsx b/src/course-checklist/ChecklistSection/ChecklistItemBody.jsx
index d42a5cb0d4..5d0234d0df 100644
--- a/src/course-checklist/ChecklistSection/ChecklistItemBody.jsx
+++ b/src/course-checklist/ChecklistSection/ChecklistItemBody.jsx
@@ -5,24 +5,21 @@ import { ActionRow, Button, Icon } from '@openedx/paragon';
import { CheckCircle, RadioButtonUnchecked } from '@openedx/paragon/icons';
import { getConfig } from '@edx/frontend-platform';
-import { useWaffleFlags } from '../../data/apiHooks';
+import { useWaffleFlags } from '@src/data/apiHooks';
+
import messages from './messages';
const getUpdateLinks = (courseId, waffleFlags) => {
const baseUrl = getConfig().STUDIO_BASE_URL;
- const isLegacyGradingUrl = !waffleFlags.useNewGradingPage;
const isLegacyCertificateUrl = !waffleFlags.useNewCertificatesPage;
- const isLegacyCourseDatesUrl = !waffleFlags.useNewScheduleDetailsPage;
const isLegacyOutlineUrl = !waffleFlags.useNewCourseOutlinePage;
return {
welcomeMessage: `/course/${courseId}/course_info`,
- gradingPolicy: isLegacyGradingUrl
- ? `${baseUrl}/settings/grading/${courseId}` : `/course/${courseId}/settings/grading`,
+ gradingPolicy: `/course/${courseId}/settings/grading`,
certificate: isLegacyCertificateUrl
? `${baseUrl}/certificates/${courseId}` : `/course/${courseId}/certificates`,
- courseDates: isLegacyCourseDatesUrl
- ? `${baseUrl}/settings/details/${courseId}#schedule` : `/course/${courseId}/settings/details/#schedule`,
+ courseDates: `/course/${courseId}/settings/details/#schedule`,
proctoringEmail: `${baseUrl}/pages-and-resources/proctoring/settings`,
outline: isLegacyOutlineUrl ? `${baseUrl}/course/${courseId}` : `/course/${courseId}`,
};
diff --git a/src/course-checklist/ChecklistSection/ChecklistSection.test.jsx b/src/course-checklist/ChecklistSection/ChecklistSection.test.jsx
index 2fc73e66d6..d6baed767d 100644
--- a/src/course-checklist/ChecklistSection/ChecklistSection.test.jsx
+++ b/src/course-checklist/ChecklistSection/ChecklistSection.test.jsx
@@ -2,8 +2,9 @@ import { camelCaseObject } from '@edx/frontend-platform';
import {
initializeMocks, render, screen, within,
-} from '../../testUtils';
-import { getApiWaffleFlagsUrl } from '../../data/api';
+} from '@src/testUtils';
+import { getApiWaffleFlagsUrl } from '@src/data/api';
+
import { generateCourseLaunchData } from '../factories/mockApiResponses';
import { checklistItems } from './utils/courseChecklistData';
import messages from './messages';
@@ -36,9 +37,7 @@ describe('ChecklistSection', () => {
axiosMock
.onGet(getApiWaffleFlagsUrl(courseId))
.reply(200, {
- useNewGradingPage: true,
useNewCertificatesPage: true,
- useNewScheduleDetailsPage: true,
useNewCourseOutlinePage: true,
});
});
diff --git a/src/course-outline/CourseOutline.tsx b/src/course-outline/CourseOutline.tsx
index 95d75627c9..0ae520a447 100644
--- a/src/course-outline/CourseOutline.tsx
+++ b/src/course-outline/CourseOutline.tsx
@@ -498,6 +498,11 @@ const CourseOutline = () => {
isOpen={isConfigureModalOpen}
onClose={handleConfigureModalClose}
onConfigureSubmit={handleConfigureItemSubmit}
+ /**
+ * Only sections need overflow visible (for the Release date datepicker, fixed in #2901);
+ * enabling it for subsection/unit modals causes the Visibility tab background to clip.
+ */
+ isOverflowVisible={itemCategory === COURSE_BLOCK_NAMES.chapter.id}
currentItemData={currentItemData}
enableProctoredExams={enableProctoredExams}
enableTimedExams={enableTimedExams}
diff --git a/src/course-outline/outline-sidebar/info-sidebar/CourseInfoSidebar.tsx b/src/course-outline/outline-sidebar/info-sidebar/CourseInfoSidebar.tsx
index e02afd49c3..a7344952ea 100644
--- a/src/course-outline/outline-sidebar/info-sidebar/CourseInfoSidebar.tsx
+++ b/src/course-outline/outline-sidebar/info-sidebar/CourseInfoSidebar.tsx
@@ -1,34 +1,36 @@
+import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
-import { useToggle } from '@openedx/paragon';
+import {
+ Tab,
+ Tabs,
+ useToggle,
+} from '@openedx/paragon';
import { SchoolOutline, Tag } from '@openedx/paragon/icons';
+import { useUserPermissions } from '@src/authz/data/apiHooks';
+import { COURSE_PERMISSIONS } from '@src/authz/constants';
import { ContentTagsDrawerSheet, ContentTagsSnippet } from '@src/content-tags-drawer';
+import { useCourseSettings, useWaffleFlags } from '@src/data/apiHooks';
import { ComponentCountSnippet } from '@src/generic/block-type-utils';
+import { HelpSidebarLink, otherLinkURLParams, messages as helpSidebarMessages } from '@src/generic/help-sidebar';
+import { SidebarContent, SidebarSection, SidebarTitle } from '@src/generic/sidebar';
import { useGetBlockTypes } from '@src/search-manager';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
-import { SidebarContent, SidebarSection, SidebarTitle } from '@src/generic/sidebar';
-
import { useCourseDetails } from '@src/course-outline/data/apiHooks';
import messages from '../messages';
-export const CourseInfoSidebar = () => {
+const DetailsTab = () => {
const intl = useIntl();
- const { courseId } = useCourseAuthoringContext();
- const { data: courseDetails } = useCourseDetails(courseId);
+ const { courseId } = useCourseAuthoringContext();
const { data: componentData } = useGetBlockTypes(
[`context_key = "${courseId}"`],
);
-
const [isManageTagsDrawerOpen, openManageTagsDrawer, closeManageTagsDrawer] = useToggle(false);
return (
-
-
+ <>
{
onClose={closeManageTagsDrawer}
showSheet={isManageTagsDrawerOpen}
/>
-
+ >
+ );
+};
+
+const SettingsTab = () => {
+ const intl = useIntl();
+ const { courseId } = useCourseAuthoringContext();
+ const { data: courseSettingsData } = useCourseSettings(courseId);
+
+ const {
+ grading,
+ courseTeam,
+ advancedSettings,
+ scheduleAndDetails,
+ groupConfigurations,
+ } = otherLinkURLParams;
+ const waffleFlags = useWaffleFlags(courseId);
+
+ const proctoredExamSettingsUrl = courseSettingsData?.mfeProctoredExamSettingsUrl;
+
+ /*
+ AuthZ for Course Authoring
+ If authz.enable_course_authoring flag is enabled, validate permissions using AuthZ API.
+ */
+ const isAuthzEnabled = waffleFlags.enableAuthzCourseAuthoring;
+ const { isLoading: isLoadingUserPermissions, data: userPermissions } = useUserPermissions({
+ canManageAdvancedSettings: {
+ action: COURSE_PERMISSIONS.MANAGE_ADVANCED_SETTINGS,
+ scope: courseId,
+ },
+ }, isAuthzEnabled);
+
+ // If it's still loading, don't show the Advanced Settings link, otherwise, use the permission to decide
+ const authzCanManageAdvancedSettings = isLoadingUserPermissions
+ ? false
+ : !!userPermissions?.canManageAdvancedSettings;
+
+ // When authz is enabled, use permission, otherwise it's always allowed (legacy behavior)
+ const canManageAdvancedSettings = isAuthzEnabled ? authzCanManageAdvancedSettings : true;
+
+ return (
+
+
+
+
+
+ {canManageAdvancedSettings && (
+
+ )}
+ {proctoredExamSettingsUrl && (
+
+ )}
+
+ );
+};
+
+export const CourseInfoSidebar = () => {
+ const intl = useIntl();
+ const { courseId } = useCourseAuthoringContext();
+ const { data: courseDetails } = useCourseDetails(courseId);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
);
};
diff --git a/src/course-outline/outline-sidebar/info-sidebar/InfoSidebar.test.tsx b/src/course-outline/outline-sidebar/info-sidebar/InfoSidebar.test.tsx
index a8a1d415b8..2d9359046f 100644
--- a/src/course-outline/outline-sidebar/info-sidebar/InfoSidebar.test.tsx
+++ b/src/course-outline/outline-sidebar/info-sidebar/InfoSidebar.test.tsx
@@ -1,5 +1,6 @@
import { initializeMocks, render, screen } from '@src/testUtils';
-import { SelectionState } from '@src/data/types';
+import { getCourseSettingsApiUrl } from '@src/data/api';
+import type { SelectionState } from '@src/data/types';
import { OutlineSidebarProvider } from '@src/course-outline/outline-sidebar/OutlineSidebarContext';
import { getXBlockApiUrl } from '@src/course-outline/data/api';
import userEvent from '@testing-library/user-event';
@@ -22,10 +23,12 @@ jest.mock('@src/course-outline/data/apiHooks', () => ({
}),
}));
+const courseId = '5';
+
const openPublishModal = jest.fn();
jest.mock('@src/CourseAuthoringContext', () => ({
useCourseAuthoringContext: () => ({
- courseId: 5,
+ courseId,
setCurrentSelection: jest.fn(),
openPublishModal,
getUnitUrl: jest.fn(),
@@ -50,6 +53,32 @@ describe('InfoSidebar component', () => {
expect(await screen.findByText('Course name')).toBeInTheDocument();
});
+ it('shows the settings link for the course', async () => {
+ const user = userEvent.setup();
+ renderComponent();
+ await user.click((await screen.findByRole('tab', { name: 'Settings' })));
+ const links = await screen.findAllByRole('link');
+ expect(links).toHaveLength(5);
+ expect(links[0]).toHaveTextContent('Schedule & details');
+ expect(links[1]).toHaveTextContent('Grading');
+ expect(links[2]).toHaveTextContent('Course team');
+ expect(links[3]).toHaveTextContent('Group configurations');
+ expect(links[4]).toHaveTextContent('Advanced settings');
+ });
+
+ it('shows the proctored exam settings link for the course if it exists', async () => {
+ const user = userEvent.setup();
+ const courseSettingsData = {
+ mfeProctoredExamSettingsUrl: 'https://example.com/proctored-exam-settings',
+ };
+ axiosMock
+ .onGet(getCourseSettingsApiUrl(courseId))
+ .reply(200, courseSettingsData);
+ renderComponent();
+ await user.click(await screen.findByRole('tab', { name: 'Settings' }));
+ expect(await screen.findByRole('link', { name: 'Proctored exam settings' })).toBeInTheDocument();
+ });
+
it('renders InfoSidebar with section info', async () => {
const user = userEvent.setup();
selectedContainerState = {
diff --git a/src/course-outline/page-alerts/PageAlerts.jsx b/src/course-outline/page-alerts/PageAlerts.jsx
index 7389263044..dad989010e 100644
--- a/src/course-outline/page-alerts/PageAlerts.jsx
+++ b/src/course-outline/page-alerts/PageAlerts.jsx
@@ -15,6 +15,8 @@ import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { Link, useNavigate } from 'react-router-dom';
import { usePasteFileNotices } from '@src/course-outline/data/apiHooks';
+import { AlertAgreementGatedFeature } from '@src/generic/agreement-gated-feature';
+import { AgreementGated } from '../../constants';
import CourseOutlinePageAlertsSlot from '../../plugin-slots/CourseOutlinePageAlertsSlot';
import advancedSettingsMessages from '../../advanced-settings/messages';
import { OutOfSyncAlert } from '../../course-libraries/OutOfSyncAlert';
@@ -441,6 +443,9 @@ const PageAlerts = ({
{conflictingFilesPasteAlert()}
{newFilesPasteAlert()}
{renderOutOfSyncAlert()}
+
>
);
diff --git a/src/course-outline/status-bar/LegacyStatusBar.tsx b/src/course-outline/status-bar/LegacyStatusBar.tsx
index 2673dd2143..5eb5449d14 100644
--- a/src/course-outline/status-bar/LegacyStatusBar.tsx
+++ b/src/course-outline/status-bar/LegacyStatusBar.tsx
@@ -5,16 +5,16 @@ import {
Button, Hyperlink, Form, Stack, useToggle,
} from '@openedx/paragon';
import { Link } from 'react-router-dom';
+import { type ReactNode } from 'react';
-import { ReactNode } from 'react';
import { CourseOutlineStatusBar } from '@src/course-outline/data/types';
import { ContentTagsDrawerSheet } from '@src/content-tags-drawer';
import TagCount from '@src/generic/tag-count';
import { useHelpUrls } from '@src/help-urls/hooks';
-import { useWaffleFlags } from '@src/data/apiHooks';
import { VIDEO_SHARING_OPTIONS } from '@src/course-outline/constants';
import { useContentTagsCount } from '@src/generic/data/apiHooks';
import { getVideoSharingOptionText } from '@src/course-outline/utils';
+
import messages from './messages';
interface StatusBarItemProps {
@@ -47,7 +47,6 @@ export const LegacyStatusBar = ({
handleVideoSharingOptionChange,
}: LegacyStatusBarProps) => {
const intl = useIntl();
- const waffleFlags = useWaffleFlags(courseId);
const {
courseReleaseDate,
@@ -67,7 +66,6 @@ export const LegacyStatusBar = ({
const courseReleaseDateObj = moment.utc(courseReleaseDate, 'MMM DD, YYYY [at] HH:mm UTC', true);
const checkListTitle = `${completedCourseLaunchChecks + completedCourseBestPracticesChecks}/${totalCourseLaunchChecks + totalCourseBestPracticesChecks}`;
- const scheduleDestination = () => new URL(`settings/details/${courseId}#schedule`, getConfig().STUDIO_BASE_URL).href;
const {
contentHighlights: contentHighlightsUrl,
@@ -88,7 +86,7 @@ export const LegacyStatusBar = ({
{courseReleaseDateObj.isValid() ? (
{
- const waffleFlags = useWaffleFlags(courseId);
-
const {
endDate,
courseReleaseDate,
@@ -189,7 +186,6 @@ export const StatusBar = ({
const courseReleaseDateObj = moment.utc(courseReleaseDate, 'MMM DD, YYYY [at] HH:mm UTC', true);
const endDateObj = moment.utc(endDate);
- const scheduleDestination = () => new URL(`settings/details/${courseId}#schedule`, getConfig().STUDIO_BASE_URL).href;
if (isLoading) {
return null;
@@ -203,7 +199,7 @@ export const StatusBar = ({
startDate={courseReleaseDateObj}
endDate={endDateObj}
startDateRaw={courseReleaseDate}
- datesLink={waffleFlags.useNewScheduleDetailsPage ? `/course/${courseId}/settings/details/#schedule` : scheduleDestination()}
+ datesLink={`/course/${courseId}/settings/details/#schedule`}
/>
diff --git a/src/course-unit/CourseUnit.test.tsx b/src/course-unit/CourseUnit.test.tsx
index 32ec0f4c05..9c6d38afef 100644
--- a/src/course-unit/CourseUnit.test.tsx
+++ b/src/course-unit/CourseUnit.test.tsx
@@ -713,15 +713,13 @@ describe('', () => {
await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch);
- await waitFor(async () => {
- const problemButton = screen.getByRole('button', {
- name: new RegExp(`problem ${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'),
- hidden: true,
- });
-
- await user.click(problemButton);
+ const problemButton = await screen.findByRole('button', {
+ name: new RegExp(`problem ${addComponentMessages.buttonText.defaultMessage} Problem`, 'i'),
+ hidden: true,
});
+ await user.click(problemButton);
+
axiosMock
.onGet(getCourseSectionVerticalApiUrl(blockId))
.reply(200, courseSectionVerticalMock);
diff --git a/src/custom-pages/CustomPages.test.tsx b/src/custom-pages/CustomPages.test.tsx
index fec0ebc220..f8acd09b1f 100644
--- a/src/custom-pages/CustomPages.test.tsx
+++ b/src/custom-pages/CustomPages.test.tsx
@@ -62,9 +62,7 @@ describe('CustomPages', () => {
axiosMock
.onGet(getApiWaffleFlagsUrl(courseId))
.reply(200, {
- useNewGradingPage: true,
useNewCertificatesPage: true,
- useNewScheduleDetailsPage: true,
useNewCourseOutlinePage: true,
});
});
diff --git a/src/data/api.ts b/src/data/api.ts
index 4f74a7196b..9501998bff 100644
--- a/src/data/api.ts
+++ b/src/data/api.ts
@@ -48,6 +48,8 @@ export const bulkModulestoreMigrateUrl = () => `${getStudioBaseUrl()}/api/module
*/
export const getPreviewModulestoreMigrationUrl = () => `${getStudioBaseUrl()}/api/modulestore_migrator/v1/migration_preview/`;
+export const getCourseSettingsApiUrl = (courseId: string) => `${getStudioBaseUrl()}/api/contentstore/v1/course_settings/${courseId}`;
+
export const getApiWaffleFlagsUrl = (courseId?: string): string => {
const baseUrl = getStudioBaseUrl();
const apiPath = '/api/contentstore/v1/course_waffle_flags';
@@ -76,9 +78,6 @@ export const waffleFlagDefaults = {
enableCourseOptimizerCheckPrevRunLinks: false,
useNewHomePage: true,
useNewCustomPages: true,
- useNewScheduleDetailsPage: true,
- useNewAdvancedSettingsPage: true,
- useNewGradingPage: true,
useNewUpdatesPage: true,
useNewImportPage: false,
useNewExportPage: true,
@@ -86,10 +85,8 @@ export const waffleFlagDefaults = {
useNewVideoUploadsPage: true,
useNewCourseOutlinePage: true,
useNewUnitPage: false,
- useNewCourseTeamPage: true,
useNewCertificatesPage: true,
useNewTextbooksPage: true,
- useNewGroupConfigurationsPage: true,
useReactMarkdownEditor: true,
useVideoGalleryFlow: false,
enableAuthzCourseAuthoring: false,
@@ -208,3 +205,66 @@ export async function getPreviewModulestoreMigration(
const { data } = await client.get(getPreviewModulestoreMigrationUrl(), { params });
return camelCaseObject(data);
}
+
+export const getUserAgreementRecordApi = (agreementType: string) => `${getConfig().LMS_BASE_URL}/api/agreements/v1/agreement_record/${agreementType}`;
+
+export async function getUserAgreementRecord(agreementType: string) {
+ const client = getAuthenticatedHttpClient();
+ const { data } = await client.get(getUserAgreementRecordApi(agreementType));
+ return camelCaseObject(data);
+}
+
+export async function updateUserAgreementRecord(agreementType: string) {
+ const client = getAuthenticatedHttpClient();
+ const { data } = await client.post(getUserAgreementRecordApi(agreementType));
+ return camelCaseObject(data);
+}
+
+export const getUserAgreementApi = (agreementType: string) => `${getConfig().LMS_BASE_URL}/api/agreements/v1/agreement/${agreementType}/`;
+
+export async function getUserAgreement(agreementType: string) {
+ const client = getAuthenticatedHttpClient();
+ const { data } = await client.get(getUserAgreementApi(agreementType));
+ return camelCaseObject(data);
+}
+
+export interface CourseSettingsData {
+ aboutPageEditable: boolean;
+ canShowCertificateAvailableDateField: boolean;
+ courseDisplayName: string;
+ courseDisplayNameWithDefault: string;
+ creditEligibilityEnabled: boolean;
+ enableExtendedCourseDetails: boolean;
+ enrollmentEndEditable: boolean;
+ isCreditCourse: boolean;
+ isEntranceExamsEnabled: boolean;
+ isPrerequisiteCoursesEnabled: boolean;
+ languageOptions: [string, string][];
+ lmsLinkForAboutPage: string;
+ licensingEnabled: boolean;
+ marketingEnabled: boolean;
+ mfeProctoredExamSettingsUrl: string;
+ platformName: string;
+ possiblePreRequisiteCourses: {
+ courseKey: string;
+ displayName: string;
+ lmsLink: string;
+ number: string;
+ org: string;
+ rerunLink: string;
+ run: string;
+ url: string;
+ }
+ shortDescriptionEditable: boolean;
+ showMinGradeWarning: boolean;
+ sidebarHtmlEnabled: boolean;
+ upgradeDeadline: string | null;
+}
+
+/**
+ * Get course settings.
+ */
+export async function getCourseSettings(courseId: string): Promise {
+ const { data } = await getAuthenticatedHttpClient().get(getCourseSettingsApiUrl(courseId));
+ return camelCaseObject(data);
+}
diff --git a/src/data/apiHooks.ts b/src/data/apiHooks.ts
index 211b9eede5..1a764e377c 100644
--- a/src/data/apiHooks.ts
+++ b/src/data/apiHooks.ts
@@ -1,16 +1,20 @@
-import {
- skipToken, useMutation, useQuery, useQueryClient,
-} from '@tanstack/react-query';
+import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
+import { UserAgreement, UserAgreementRecord } from '@src/data/types';
import { libraryAuthoringQueryKeys } from '@src/library-authoring/data/apiHooks';
import {
- getWaffleFlags,
- waffleFlagDefaults,
- bulkModulestoreMigrate,
- getModulestoreMigrationStatus,
+ skipToken, useMutation, useQueries, useQuery, useQueryClient, UseQueryOptions,
+} from '@tanstack/react-query';
+import {
BulkMigrateRequestData,
+ bulkModulestoreMigrate,
getCourseDetails,
- getPreviewModulestoreMigration,
+ getModulestoreMigrationStatus,
+ getPreviewModulestoreMigration, getUserAgreement,
+ getUserAgreementRecord,
+ getWaffleFlags, updateUserAgreementRecord,
+ waffleFlagDefaults,
+ getCourseSettings,
} from './api';
import { RequestStatus, RequestStatusType } from './constants';
@@ -165,3 +169,57 @@ export function createGlobalState(
return { data, setData, resetData };
};
}
+
+export const getGatingAgreementTypes = (gatingTypes: string[]): string[] => (
+ [...new Set(
+ gatingTypes
+ .flatMap(gatingType => getConfig().AGREEMENT_GATING?.[gatingType])
+ .filter(item => Boolean(item)),
+ )]
+);
+
+export const useUserAgreementRecord = (agreementType:string) => (
+ useQuery({
+ queryKey: ['agreement-record', agreementType],
+ queryFn: () => getUserAgreementRecord(agreementType),
+ retry: false,
+ })
+);
+
+export const useUserAgreementRecords = (agreementTypes:string[]) => (
+ useQueries({
+ queries: agreementTypes.map>(agreementType => ({
+ queryKey: ['agreement-record', agreementType],
+ queryFn: () => getUserAgreementRecord(agreementType),
+ retry: false,
+ })),
+ })
+);
+
+export const useUserAgreementRecordUpdater = (agreementType:string) => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async () => updateUserAgreementRecord(agreementType),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['agreement-record', agreementType] });
+ },
+ });
+};
+
+export const useUserAgreement = (agreementType:string) => (
+ useQuery({
+ queryKey: ['agreements', agreementType],
+ queryFn: () => getUserAgreement(agreementType),
+ retry: false,
+ })
+);
+
+/**
+ * Get the course settings
+ */
+export const useCourseSettings = (courseId: string) => (
+ useQuery({
+ queryKey: ['courseSettings', courseId],
+ queryFn: () => getCourseSettings(courseId),
+ })
+);
diff --git a/src/data/types.ts b/src/data/types.ts
index c13205a6a0..7dd267f4f8 100644
--- a/src/data/types.ts
+++ b/src/data/types.ts
@@ -201,3 +201,19 @@ export type AccessManagedXBlockDataTypes = {
onlineProctoringRules?: string;
discussionEnabled?: boolean;
};
+
+export interface UserAgreementRecord {
+ username: string;
+ agreementType: string;
+ acceptedAt: string | null;
+ isCurrent: boolean;
+}
+
+export interface UserAgreement {
+ type: string;
+ name: string;
+ summary: string;
+ hasText: boolean;
+ url: string;
+ updated: string;
+}
diff --git a/src/files-and-videos/files-page/CourseFilesTable.tsx b/src/files-and-videos/files-page/CourseFilesTable.tsx
index d71b90476e..1fc9e885eb 100644
--- a/src/files-and-videos/files-page/CourseFilesTable.tsx
+++ b/src/files-and-videos/files-page/CourseFilesTable.tsx
@@ -1,5 +1,6 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { CheckboxFilter } from '@openedx/paragon';
+import { AgreementGated, UPLOAD_FILE_MAX_SIZE } from '@src/constants';
import {
addAssetFile,
deleteAssetFile,
@@ -20,13 +21,13 @@ import {
FileTable,
ThumbnailColumn,
} from '@src/files-and-videos/generic';
+import { GatedComponentWrapper } from '@src/generic/agreement-gated-feature';
import { useModels } from '@src/generic/model-store';
import { DeprecatedReduxState } from '@src/store';
import { getFileSizeToClosestByte } from '@src/utils';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
-import { UPLOAD_FILE_MAX_SIZE } from '@src/constants';
export const CourseFilesTable = () => {
const intl = useIntl();
@@ -159,26 +160,28 @@ export const CourseFilesTable = () => {
return null;
}
return (
- <>
-
-
- >
+
+ <>
+
+
+ >
+
);
};
diff --git a/src/files-and-videos/files-page/FilesPage.jsx b/src/files-and-videos/files-page/FilesPage.jsx
index 41af98f34e..210c43bdc1 100644
--- a/src/files-and-videos/files-page/FilesPage.jsx
+++ b/src/files-and-videos/files-page/FilesPage.jsx
@@ -1,7 +1,7 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { Container } from '@openedx/paragon';
-import { useEffect } from 'react';
+import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
@@ -10,6 +10,8 @@ import Placeholder from '@src/editors/Placeholder';
import { RequestStatus } from '@src/data/constants';
import getPageHeadTitle from '@src/generic/utils';
import EditFileAlertsSlot from '@src/plugin-slots/EditFileAlertsSlot';
+import { AlertAgreementGatedFeature } from '@src/generic/agreement-gated-feature';
+import { AgreementGated } from '@src/constants';
import { EditFileErrors } from '../generic';
import { fetchAssets, resetErrors } from './data/thunks';
@@ -55,6 +57,9 @@ const FilesPage = () => {
updateFileStatus={updateAssetStatus}
loadingStatus={loadingStatus}
/>
+
{intl.formatMessage(messages.heading)}
diff --git a/src/files-and-videos/videos-page/CourseVideosTable.tsx b/src/files-and-videos/videos-page/CourseVideosTable.tsx
index d1667ef13f..fe14b2645b 100644
--- a/src/files-and-videos/videos-page/CourseVideosTable.tsx
+++ b/src/files-and-videos/videos-page/CourseVideosTable.tsx
@@ -2,6 +2,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import {
ActionRow, Button, CheckboxFilter, useToggle,
} from '@openedx/paragon';
+import { AgreementGated } from '@src/constants';
import { RequestStatus } from '@src/data/constants';
import {
ActiveColumn,
@@ -29,6 +30,7 @@ import messages from '@src/files-and-videos/videos-page/messages';
import TranscriptSettings from '@src/files-and-videos/videos-page/transcript-settings';
import UploadModal from '@src/files-and-videos/videos-page/upload-modal';
import VideoThumbnail from '@src/files-and-videos/videos-page/VideoThumbnail';
+import { GatedComponentWrapper } from '@src/generic/agreement-gated-feature';
import { useModels } from '@src/generic/model-store';
import { DeprecatedReduxState } from '@src/store';
import React, { useEffect, useRef } from 'react';
@@ -224,23 +226,24 @@ export const CourseVideosTable = () => {
];
return (
- <>
-
-
- {isVideoTranscriptEnabled ? (
-
- ) : null}
-
- {
+
+ <>
+
+
+ {isVideoTranscriptEnabled ? (
+
+ ) : null}
+
+ {
loadingStatus !== RequestStatus.FAILED && (
<>
{isVideoTranscriptEnabled && (
@@ -275,14 +278,15 @@ export const CourseVideosTable = () => {
>
)
}
-
- >
+
+ >
+
);
};
diff --git a/src/files-and-videos/videos-page/VideosPage.tsx b/src/files-and-videos/videos-page/VideosPage.tsx
index 53d32322c8..b2faa26e07 100644
--- a/src/files-and-videos/videos-page/VideosPage.tsx
+++ b/src/files-and-videos/videos-page/VideosPage.tsx
@@ -1,4 +1,6 @@
-import { useEffect } from 'react';
+import { AgreementGated } from '@src/constants';
+import { AlertAgreementGatedFeature } from '@src/generic/agreement-gated-feature';
+import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { useDispatch, useSelector } from 'react-redux';
@@ -57,6 +59,9 @@ const VideosPage = () => {
updateFileStatus={updateVideoStatus}
loadingStatus={loadingStatus}
/>
+
{intl.formatMessage(messages.heading)}
diff --git a/src/generic/agreement-gated-feature/AlertAgreementGatedFeature.test.tsx b/src/generic/agreement-gated-feature/AlertAgreementGatedFeature.test.tsx
new file mode 100644
index 0000000000..78c525b66f
--- /dev/null
+++ b/src/generic/agreement-gated-feature/AlertAgreementGatedFeature.test.tsx
@@ -0,0 +1,142 @@
+import { initializeMockApp, mergeConfig } from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { AppProvider } from '@edx/frontend-platform/react';
+import { AgreementGated } from '@src/constants';
+import { getUserAgreementApi, getUserAgreementRecordApi } from '@src/data/api';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import MockAdapter from 'axios-mock-adapter';
+import React from 'react';
+import { AlertAgreementGatedFeature } from './AlertAgreementGatedFeature';
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+});
+
+async function renderComponent(gatingTypes: AgreementGated[]) {
+ return render(
+
+
+
+ ,
+ ,
+ );
+}
+
+describe('AlertAgreementGatedFeature', () => {
+ let axiosMock;
+ beforeAll(async () => {
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 3,
+ username: 'abc123',
+ administrator: false,
+ roles: [],
+ },
+ });
+ axiosMock = new MockAdapter(getAuthenticatedHttpClient());
+ });
+ beforeEach(() => {
+ axiosMock.onGet(getUserAgreementApi('agreement1')).reply(200, {
+ type: 'agreement1',
+ name: 'agreement1',
+ summary: 'summary1',
+ has_text: true,
+ url: 'https://example.com/agreement1',
+ updated: '2023-01-01T00:00:00Z',
+ });
+ axiosMock.onGet(getUserAgreementApi('agreement2')).reply(200, {
+ type: 'agreement2',
+ name: 'agreement2',
+ summary: 'summary2',
+ has_text: true,
+ url: 'https://example.com/agreement2',
+ });
+ axiosMock.onGet(getUserAgreementApi('agreement3')).reply(404);
+ axiosMock.onGet(getUserAgreementRecordApi('agreement1')).reply(200, {});
+ axiosMock.onGet(getUserAgreementRecordApi('agreement2')).reply(200, {});
+ mergeConfig({
+ AGREEMENT_GATING: {
+ [AgreementGated.UPLOAD]: ['agreement1', 'agreement2'],
+ [AgreementGated.UPLOAD_VIDEOS]: ['agreement2'],
+ },
+ });
+ });
+ afterEach(() => {
+ axiosMock.reset();
+ });
+
+ it('renders no alerts when gatingTypes is empty', async () => {
+ await renderComponent([]);
+ await waitFor(() => expect(queryClient.isFetching()).toBe(0));
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+ });
+
+ it('renders no alerts when gatingTypes have no associated agreement', async () => {
+ await renderComponent([AgreementGated.UPLOAD_FILES]);
+ await waitFor(() => expect(queryClient.isFetching()).toBe(0));
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+ });
+
+ it('renders no alerts when associated agreement does not exist', async () => {
+ mergeConfig({
+ AGREEMENT_GATING: {
+ [AgreementGated.UPLOAD_FILES]: ['agreement3'],
+ },
+ });
+ await renderComponent([AgreementGated.UPLOAD_FILES]);
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+ });
+
+ it('renders an alert for each agreement type associated with the gating types', async () => {
+ const gatingTypes = [AgreementGated.UPLOAD];
+ await renderComponent(gatingTypes);
+ await waitFor(() => expect(queryClient.isFetching()).toBe(0));
+
+ expect(screen.queryAllByRole('alert')).toHaveLength(2);
+ expect(screen.getByText('agreement1')).toBeInTheDocument();
+ expect(screen.getByText('summary1')).toBeInTheDocument();
+ expect(screen.getByText('agreement2')).toBeInTheDocument();
+ expect(screen.getByText('summary2')).toBeInTheDocument();
+ });
+
+ it('renders skips alerts for agreements that have already been accepted', async () => {
+ const gatingTypes = [AgreementGated.UPLOAD];
+ axiosMock.onGet(getUserAgreementRecordApi('agreement2')).reply(200, { is_current: true });
+ await renderComponent(gatingTypes);
+ await waitFor(() => expect(queryClient.isFetching()).toBe(0));
+
+ expect(screen.queryAllByRole('alert')).toHaveLength(1);
+ expect(screen.getByText('agreement1')).toBeInTheDocument();
+ expect(screen.getByText('summary1')).toBeInTheDocument();
+ expect(screen.queryByText('agreement2')).not.toBeInTheDocument();
+ expect(screen.queryByText('summary2')).not.toBeInTheDocument();
+ });
+
+ it('does not duplicate alert if multiple gating types have the same agreement type', async () => {
+ const gatingTypes = [AgreementGated.UPLOAD, AgreementGated.UPLOAD_FILES];
+ await renderComponent(gatingTypes);
+ await waitFor(() => expect(queryClient.isFetching()).toBe(0));
+
+ expect(screen.queryAllByRole('alert')).toHaveLength(2);
+ expect(screen.getByText('agreement1')).toBeInTheDocument();
+ expect(screen.getByText('summary1')).toBeInTheDocument();
+ expect(screen.getByText('agreement2')).toBeInTheDocument();
+ expect(screen.getByText('summary2')).toBeInTheDocument();
+ });
+
+ it('posts a request to mark acceptance when user clicks Agree', async () => {
+ const user = userEvent.setup();
+ const gatingTypes = [AgreementGated.UPLOAD_VIDEOS];
+ await renderComponent(gatingTypes);
+ await waitFor(() => expect(queryClient.isFetching()).toBe(0));
+ axiosMock.onPost(new RegExp(getUserAgreementRecordApi('*'))).reply(201, {});
+ await user.click(screen.getByRole('button', { name: 'Agree' }));
+ expect(axiosMock.history.post[0].url).toBe(getUserAgreementRecordApi('agreement2'));
+ });
+});
diff --git a/src/generic/agreement-gated-feature/AlertAgreementGatedFeature.tsx b/src/generic/agreement-gated-feature/AlertAgreementGatedFeature.tsx
new file mode 100644
index 0000000000..6df1f30321
--- /dev/null
+++ b/src/generic/agreement-gated-feature/AlertAgreementGatedFeature.tsx
@@ -0,0 +1,66 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { Alert, Button, Hyperlink } from '@openedx/paragon';
+import { Policy } from '@openedx/paragon/icons';
+import { AgreementGated } from '@src/constants';
+import {
+ getGatingAgreementTypes,
+ useUserAgreement,
+ useUserAgreementRecord,
+ useUserAgreementRecordUpdater,
+} from '@src/data/apiHooks';
+import messages from './messages';
+
+const AlertAgreement = ({ agreementType }: { agreementType: string }) => {
+ const intl = useIntl();
+ const { data, isLoading, isError } = useUserAgreement(agreementType);
+ const mutation = useUserAgreementRecordUpdater(agreementType);
+ const showAlert = data && !isLoading && !isError;
+ const handleAcceptAgreement = async () => {
+ try {
+ await mutation.mutateAsync();
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error('Error accepting agreement', e);
+ }
+ };
+ if (!showAlert) { return null; }
+ const { url, name, summary } = data;
+ return (
+
{intl.formatMessage(messages.learnMoreLinkLabel)},
+ ,
+ ]}
+ >
+ {name}
+ {summary}
+
+ );
+};
+
+const AlertAgreementWrapper = (
+ { agreementType }: { agreementType: string },
+) => {
+ const { data, isLoading, isError } = useUserAgreementRecord(agreementType);
+ const showAlert = !data?.isCurrent && !isLoading && !isError;
+ if (!showAlert) { return null; }
+ return
;
+};
+
+export const AlertAgreementGatedFeature = (
+ { gatingTypes }: { gatingTypes: AgreementGated[] },
+) => {
+ const agreementTypes = getGatingAgreementTypes(gatingTypes);
+ return (
+ <>
+ {agreementTypes.map((agreementType) => (
+
+ ))}
+ >
+ );
+};
diff --git a/src/generic/agreement-gated-feature/GatedComponentWrapper.test.tsx b/src/generic/agreement-gated-feature/GatedComponentWrapper.test.tsx
new file mode 100644
index 0000000000..bd00233f92
--- /dev/null
+++ b/src/generic/agreement-gated-feature/GatedComponentWrapper.test.tsx
@@ -0,0 +1,79 @@
+import { initializeMockApp, mergeConfig } from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { AppProvider } from '@edx/frontend-platform/react';
+import { AgreementGated } from '@src/constants';
+import { getUserAgreementRecordApi } from '@src/data/api';
+import { GatedComponentWrapper } from '@src/generic/agreement-gated-feature/GatedComponentWrapper';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { render, screen, waitFor } from '@testing-library/react';
+import MockAdapter from 'axios-mock-adapter';
+import React from 'react';
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+});
+
+async function renderComponent(gatingTypes: AgreementGated[]) {
+ return render(
+
+
+
+
+
+ ,
+ ,
+ );
+}
+
+describe('GatedComponentWrapper', () => {
+ let axiosMock;
+ beforeAll(async () => {
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 3,
+ username: 'abc123',
+ administrator: false,
+ roles: [],
+ },
+ });
+ axiosMock = new MockAdapter(getAuthenticatedHttpClient());
+ });
+ beforeEach(() => {
+ axiosMock.onGet(getUserAgreementRecordApi('agreement1')).reply(200, {});
+ axiosMock.onGet(getUserAgreementRecordApi('agreement2')).reply(200, { is_current: true });
+ mergeConfig({
+ AGREEMENT_GATING: {
+ [AgreementGated.UPLOAD]: ['agreement1', 'agreement2'],
+ [AgreementGated.UPLOAD_VIDEOS]: ['agreement2'],
+ [AgreementGated.UPLOAD_FILES]: ['agreement1'],
+ },
+ });
+ });
+ afterEach(() => {
+ axiosMock.reset();
+ });
+
+ it('applies no gating when gatingTypes is empty', async () => {
+ await renderComponent([]);
+ await waitFor(() => expect(queryClient.isFetching()).toBe(0));
+ expect(screen.getByRole('button').parentNode).not.toHaveAttribute('aria-disabled', 'true');
+ });
+
+ it('applies no gating when associated agreement has been accepted', async () => {
+ await renderComponent([AgreementGated.UPLOAD_VIDEOS]);
+ await waitFor(() => expect(queryClient.isFetching()).toBe(0));
+ expect(screen.getByRole('button').parentNode).not.toHaveAttribute('aria-disabled', 'true');
+ });
+
+ it('applies gating when associated agreement has not been accepted', async () => {
+ await renderComponent([AgreementGated.UPLOAD_FILES]);
+ await waitFor(() => expect(queryClient.isFetching()).toBe(0));
+ expect(screen.getByRole('button').parentNode).toHaveAttribute('aria-disabled', 'true');
+ });
+});
diff --git a/src/generic/agreement-gated-feature/GatedComponentWrapper.tsx b/src/generic/agreement-gated-feature/GatedComponentWrapper.tsx
new file mode 100644
index 0000000000..bddd352e40
--- /dev/null
+++ b/src/generic/agreement-gated-feature/GatedComponentWrapper.tsx
@@ -0,0 +1,30 @@
+import { AgreementGated } from '@src/constants';
+import {
+ getGatingAgreementTypes,
+ useUserAgreementRecords,
+} from '@src/data/apiHooks';
+
+interface GatedComponentWrapperProps {
+ gatingTypes: AgreementGated[];
+ children: React.ReactElement;
+}
+
+export const GatedComponentWrapper = (
+ { gatingTypes, children }: GatedComponentWrapperProps,
+) => {
+ const agreementTypes = getGatingAgreementTypes(gatingTypes);
+ const results = useUserAgreementRecords(agreementTypes);
+ const isNotGated = results.every((result) => !!result?.data?.isCurrent);
+ return isNotGated ? children : (
+
+ {children}
+
+ );
+};
diff --git a/src/generic/agreement-gated-feature/index.ts b/src/generic/agreement-gated-feature/index.ts
new file mode 100644
index 0000000000..9c3265f27e
--- /dev/null
+++ b/src/generic/agreement-gated-feature/index.ts
@@ -0,0 +1,2 @@
+export { AlertAgreementGatedFeature } from './AlertAgreementGatedFeature';
+export { GatedComponentWrapper } from './GatedComponentWrapper';
diff --git a/src/generic/agreement-gated-feature/messages.ts b/src/generic/agreement-gated-feature/messages.ts
new file mode 100644
index 0000000000..c17ac47c75
--- /dev/null
+++ b/src/generic/agreement-gated-feature/messages.ts
@@ -0,0 +1,16 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ agreeButtonLabel: {
+ id: 'authoring.agreement-gated-feature.agree',
+ defaultMessage: 'Agree',
+ description: 'The label for the Agree button on an alert asking users to agree with terms.',
+ },
+ learnMoreLinkLabel: {
+ id: 'authoring.agreement-gated-feature.learn-more',
+ defaultMessage: 'Learn more',
+ description: 'The label for a "learn more" link on an alert asking users to agree with terms.',
+ },
+});
+
+export default messages;
diff --git a/src/generic/configure-modal/ConfigureModal.tsx b/src/generic/configure-modal/ConfigureModal.tsx
index abf9b101f6..5b2ac38bba 100644
--- a/src/generic/configure-modal/ConfigureModal.tsx
+++ b/src/generic/configure-modal/ConfigureModal.tsx
@@ -28,6 +28,7 @@ interface Props {
currentItemData?: AccessManagedXBlockDataTypes,
isXBlockComponent?: boolean,
isSelfPaced?: boolean,
+ isOverflowVisible?: boolean,
}
const ConfigureModal = ({
@@ -39,6 +40,7 @@ const ConfigureModal = ({
enableTimedExams = false,
isXBlockComponent = false,
isSelfPaced,
+ isOverflowVisible = false,
}: Props) => {
const intl = useIntl();
@@ -298,7 +300,7 @@ const ConfigureModal = ({
onClose={onClose}
hasCloseButton
isFullscreenOnMobile
- isOverflowVisible
+ isOverflowVisible={isOverflowVisible}
>
diff --git a/src/generic/help-sidebar/HelpSidebar.test.jsx b/src/generic/help-sidebar/HelpSidebar.test.tsx
similarity index 98%
rename from src/generic/help-sidebar/HelpSidebar.test.jsx
rename to src/generic/help-sidebar/HelpSidebar.test.tsx
index 8f97c4478e..64eb6d9345 100644
--- a/src/generic/help-sidebar/HelpSidebar.test.jsx
+++ b/src/generic/help-sidebar/HelpSidebar.test.tsx
@@ -1,9 +1,9 @@
-// @ts-check
-
import { waitFor } from '@testing-library/react';
+
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
import { useUserPermissions } from '@src/authz/data/apiHooks';
-import { initializeMocks, render } from '../../testUtils';
+import { initializeMocks, render } from '@src/testUtils';
+
import messages from './messages';
import { HelpSidebar } from '.';
diff --git a/src/generic/help-sidebar/HelpSidebar.jsx b/src/generic/help-sidebar/HelpSidebar.tsx
similarity index 64%
rename from src/generic/help-sidebar/HelpSidebar.jsx
rename to src/generic/help-sidebar/HelpSidebar.tsx
index 5a9745e8a8..074ab955e1 100644
--- a/src/generic/help-sidebar/HelpSidebar.jsx
+++ b/src/generic/help-sidebar/HelpSidebar.tsx
@@ -1,8 +1,7 @@
-import PropTypes from 'prop-types';
+import { type ReactNode } from 'react';
import { useLocation } from 'react-router-dom';
import classNames from 'classnames';
import { useIntl } from '@edx/frontend-platform/i18n';
-import { getConfig } from '@edx/frontend-platform';
import { useUserPermissions } from '@src/authz/data/apiHooks';
import { COURSE_PERMISSIONS } from '@src/authz/constants';
@@ -11,13 +10,21 @@ import { otherLinkURLParams } from './constants';
import messages from './messages';
import HelpSidebarLink from './HelpSidebarLink';
+interface HelpSidebarProps {
+ courseId: string;
+ showOtherSettings?: boolean;
+ proctoredExamSettingsUrl?: string;
+ children: ReactNode;
+ className?: string;
+}
+
const HelpSidebar = ({
courseId,
- showOtherSettings,
- proctoredExamSettingsUrl,
+ showOtherSettings = false,
+ proctoredExamSettingsUrl = '',
children,
className,
-}) => {
+}: HelpSidebarProps) => {
const intl = useIntl();
const { pathname } = useLocation();
const {
@@ -30,16 +37,6 @@ const HelpSidebar = ({
const waffleFlags = useWaffleFlags(courseId);
const showOtherLink = (params) => !pathname.includes(params);
- const generateLegacyURL = (urlParameter) => {
- const referObj = new URL(`${urlParameter}/${courseId}`, getConfig().STUDIO_BASE_URL);
- return referObj.href;
- };
-
- const scheduleAndDetailsDestination = generateLegacyURL(scheduleAndDetails);
- const gradingDestination = generateLegacyURL(grading);
- const courseTeamDestination = generateLegacyURL(courseTeam);
- const advancedSettingsDestination = generateLegacyURL(advancedSettings);
- const groupConfigurationsDestination = generateLegacyURL(groupConfigurations);
/*
AuthZ for Course Authoring
@@ -78,46 +75,41 @@ const HelpSidebar = ({
{showOtherLink(scheduleAndDetails) && (
)}
{showOtherLink(grading) && (
)}
{showOtherLink(courseTeam) && (
)}
{showOtherLink(groupConfigurations) && (
)}
{showOtherLink(advancedSettings) && canManageAdvancedSettings && (
)}
{proctoredExamSettingsUrl && (
@@ -137,19 +129,4 @@ const HelpSidebar = ({
);
};
-HelpSidebar.defaultProps = {
- proctoredExamSettingsUrl: '',
- className: undefined,
- courseId: undefined,
- showOtherSettings: false,
-};
-
-HelpSidebar.propTypes = {
- courseId: PropTypes.string,
- showOtherSettings: PropTypes.bool,
- proctoredExamSettingsUrl: PropTypes.string,
- children: PropTypes.node.isRequired,
- className: PropTypes.string,
-};
-
export default HelpSidebar;
diff --git a/src/generic/help-sidebar/HelpSidebarLink.jsx b/src/generic/help-sidebar/HelpSidebarLink.tsx
similarity index 64%
rename from src/generic/help-sidebar/HelpSidebarLink.jsx
rename to src/generic/help-sidebar/HelpSidebarLink.tsx
index 4a4ee91ac8..dd853c552f 100644
--- a/src/generic/help-sidebar/HelpSidebarLink.jsx
+++ b/src/generic/help-sidebar/HelpSidebarLink.tsx
@@ -1,10 +1,21 @@
+import React from 'react';
+
import { Link } from 'react-router-dom';
-import PropTypes from 'prop-types';
import { Hyperlink } from '@openedx/paragon';
+interface HelpSidebarLinkProps {
+ as?: React.ElementType;
+ isNewPage?: boolean;
+ pathToPage: string;
+ title: string;
+}
+
const HelpSidebarLink = ({
- as, pathToPage, title, isNewPage,
-}) => {
+ as = 'li',
+ isNewPage = true,
+ pathToPage,
+ title,
+}: HelpSidebarLinkProps) => {
const TagElement = as;
if (isNewPage) {
return (
@@ -29,16 +40,4 @@ const HelpSidebarLink = ({
);
};
-HelpSidebarLink.propTypes = {
- isNewPage: PropTypes.bool,
- pathToPage: PropTypes.string.isRequired,
- title: PropTypes.string.isRequired,
- as: PropTypes.string,
-};
-
-HelpSidebarLink.defaultProps = {
- as: 'li',
- isNewPage: true,
-};
-
export default HelpSidebarLink;
diff --git a/src/generic/help-sidebar/index.ts b/src/generic/help-sidebar/index.ts
index 7f0fc8374a..3b5b9d2463 100644
--- a/src/generic/help-sidebar/index.ts
+++ b/src/generic/help-sidebar/index.ts
@@ -1,2 +1,4 @@
export { default as HelpSidebar } from './HelpSidebar';
export { default as HelpSidebarLink } from './HelpSidebarLink';
+export { otherLinkURLParams } from './constants';
+export { default as messages } from './messages';
diff --git a/src/grading-settings/GradingSettings.jsx b/src/grading-settings/GradingSettings.jsx
index 56c0da6620..a359087db4 100644
--- a/src/grading-settings/GradingSettings.jsx
+++ b/src/grading-settings/GradingSettings.jsx
@@ -3,22 +3,23 @@ import {
Button, Container, Layout, StatefulButton,
} from '@openedx/paragon';
import { Add as IconAdd, CheckCircle, Warning } from '@openedx/paragon/icons';
-import {
- useCourseSettings,
- useGradingSettings,
- useGradingSettingUpdater,
-} from 'CourseAuthoring/grading-settings/data/apiHooks';
import { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
+
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
-import { STATEFUL_BUTTON_STATES } from '../constants';
-import AlertMessage from '../generic/alert-message';
-import InternetConnectionAlert from '../generic/internet-connection-alert';
+import { STATEFUL_BUTTON_STATES } from '@src/constants';
+import { useCourseSettings } from '@src/data/apiHooks';
+import ConnectionErrorAlert from '@src/generic/ConnectionErrorAlert';
+import SectionSubHeader from '@src/generic/section-sub-header';
+import SubHeader from '@src/generic/sub-header/SubHeader';
+import AlertMessage from '@src/generic/alert-message';
+import InternetConnectionAlert from '@src/generic/internet-connection-alert';
+import getPageHeadTitle from '@src/generic/utils';
-import ConnectionErrorAlert from '../generic/ConnectionErrorAlert';
-import SectionSubHeader from '../generic/section-sub-header';
-import SubHeader from '../generic/sub-header/SubHeader';
-import getPageHeadTitle from '../generic/utils';
+import {
+ useGradingSettings,
+ useGradingSettingUpdater,
+} from './data/apiHooks';
import AssignmentSection from './assignment-section';
import CreditSection from './credit-section';
import DeadlineSection from './deadline-section';
diff --git a/src/grading-settings/GradingSettings.test.jsx b/src/grading-settings/GradingSettings.test.jsx
index c524845acd..76a667de4c 100644
--- a/src/grading-settings/GradingSettings.test.jsx
+++ b/src/grading-settings/GradingSettings.test.jsx
@@ -2,9 +2,10 @@ import {
act, fireEvent, render, screen, initializeMocks,
} from '@src/testUtils';
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
+import { getCourseSettingsApiUrl } from '@src/data/api';
import gradingSettings from './__mocks__/gradingSettings';
-import { getCourseSettingsApiUrl, getGradingSettingsApiUrl } from './data/api';
+import { getGradingSettingsApiUrl } from './data/api';
import * as apiHooks from './data/apiHooks';
import GradingSettings from './GradingSettings';
import messages from './messages';
diff --git a/src/grading-settings/data/api.js b/src/grading-settings/data/api.js
index 8e73c720bb..d21b128f64 100644
--- a/src/grading-settings/data/api.js
+++ b/src/grading-settings/data/api.js
@@ -5,7 +5,6 @@ import { deepConvertingKeysToCamelCase, deepConvertingKeysToSnakeCase } from '..
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
export const getGradingSettingsApiUrl = (courseId) => `${getApiBaseUrl()}/api/contentstore/v1/course_grading/${courseId}`;
-export const getCourseSettingsApiUrl = (courseId) => `${getApiBaseUrl()}/api/contentstore/v1/course_settings/${courseId}`;
/**
* Get's grading setting for a course.
@@ -29,14 +28,3 @@ export async function sendGradingSettings(courseId, settings) {
.post(getGradingSettingsApiUrl(courseId), deepConvertingKeysToSnakeCase(settings));
return camelCaseObject(data);
}
-
-/**
- * Get course settings.
- * @param {string} courseId
- * @returns {Promise
+ );
+};
+
+interface ActionsMenuProps {
+ rowData: TagListRowData;
+ startSubtagDraft: () => void;
+ disableAddSubtag: boolean;
+}
+
+const ActionsMenu = ({ rowData, startSubtagDraft, disableAddSubtag }: ActionsMenuProps) => {
+ const intl = useIntl();
+
+ return (
+
+
+
+
+ {intl.formatMessage(messages.addSubtag)}
+
+
+
+ );
+};
+
function getColumns({
- intl,
setIsCreatingTopTag,
setCreatingParentId,
setEditingRowId,
@@ -62,10 +148,12 @@ function getColumns({
}: GetColumnsArgs): TreeColumnDef[] {
const canAddSubtag = (row: Row
) => row.depth < maxDepth;
const draftInProgressHintId = 'tag-list-draft-in-progress-hint';
+ const intl = useIntl();
return [
{
- header: intl.formatMessage(messages.tagListColumnValueHeader),
+ id: 'valueColumn',
+ header: () => ,
cell: ({ row }) => {
const {
value,
@@ -79,27 +167,23 @@ function getColumns({
);
},
},
+ {
+ id: 'count',
+ header: intl.formatMessage(messages.tagListColumnCountHeader),
+ cell: UsageCountDisplay,
+ },
{
id: 'actions',
header: () => (
-
- {intl.formatMessage(messages.createNewTagTooltip)}
}
- src={AddCircle}
- alt={intl.formatMessage(messages.createTagButtonLabel)}
- size="inline"
- onClick={() => {
- onStartDraft();
- setDraftError('');
- setIsCreatingTopTag(true);
- setEditingRowId(null);
- setActiveActionMenuRowId(null);
- }}
- disabled={hasOpenDraft}
- aria-describedby={hasOpenDraft ? draftInProgressHintId : undefined}
- />
-
+
),
cell: ({ row }) => {
const rowData = asTagListRowData(row);
@@ -121,26 +205,7 @@ function getColumns({
return (
-
-
-
-
- {intl.formatMessage(messages.addSubtag)}
-
-
-
+
);
},
diff --git a/src/taxonomy/tag-list/tagTree.test.ts b/src/taxonomy/tag-list/tagTree.test.ts
index f19885faf1..068005adb5 100644
--- a/src/taxonomy/tag-list/tagTree.test.ts
+++ b/src/taxonomy/tag-list/tagTree.test.ts
@@ -1,10 +1,12 @@
import { rawData, treeRowData } from './mockData';
import { TagTree } from './tagTree';
-import TagTreeError from './tagTreeError';
+import { TagTreeError } from './errors';
+import { TagData } from '../data/types';
-const newSubtagChildRow = {
+// For testing purposes, we define a new child node that can be added to the tree in various test cases.
+const newChildNode: TagData = {
value: 'newChild',
- externalId: null,
+ externalId: 'some-external-id',
canChangeTag: true,
canDeleteTag: true,
id: 8,
@@ -42,13 +44,13 @@ describe('TagTree', () => {
it('gets a deep copy when getting a node so that direct mutations do not affect the original tree', () => {
const tree = new TagTree(rawData);
const node = tree.getTagAsDeepCopy('ab');
- expect(node?.externalId).toBeNull();
+ expect(node?.externalId).toBe('some-external-id');
if (node) {
node.externalId = 'modified';
}
const originalNode = tree.getTagAsDeepCopy('ab');
- expect(originalNode?.externalId).toBeNull();
+ expect(originalNode?.externalId).toBe('some-external-id');
});
it('returns null for non-existent node', () => {
@@ -59,9 +61,9 @@ describe('TagTree', () => {
it('creates a new top-level row', () => {
const tree = new TagTree(rawData);
- const newRow = {
+ const newRow: TagData = {
value: 'newTopLevel',
- externalId: null,
+ externalId: 'some-external-id',
canChangeTag: true,
canDeleteTag: true,
id: 7,
@@ -77,24 +79,24 @@ describe('TagTree', () => {
it('creates a new child row', () => {
const tree = new TagTree(rawData);
- tree.addNode(newSubtagChildRow, 'ab');
+ tree.addNode(newChildNode, 'ab');
const parentNode = tree.getTagAsDeepCopy('ab');
- expect(parentNode?.subRows).toContainEqual(newSubtagChildRow);
+ expect(parentNode?.subRows).toContainEqual(newChildNode);
});
it('edits a node value', () => {
const tree = new TagTree(rawData);
- tree.addNode(newSubtagChildRow, 'ab');
+ tree.addNode(newChildNode, 'ab');
tree.editTagValue('ab', 'editedAb');
expect(tree.getTagAsDeepCopy('editedAb')).not.toBeNull();
expect(tree.getTagAsDeepCopy('ab')).toBeNull();
expect(tree.getTagAsDeepCopy('editedAb')?.value).toBe('editedAb');
- expect(tree.getTagAsDeepCopy('editedAb')?.subRows).toContainEqual(newSubtagChildRow);
+ expect(tree.getTagAsDeepCopy('editedAb')?.subRows).toContainEqual(newChildNode);
});
it('deletes a top-level node and its children', () => {
const tree = new TagTree(rawData);
- tree.addNode(newSubtagChildRow, 'ab');
+ tree.addNode(newChildNode, 'ab');
tree.removeNode('ab');
expect(tree.getTagAsDeepCopy('ab')).toBeNull();
expect(tree.getTagAsDeepCopy('newChild')).toBeNull();
@@ -102,10 +104,10 @@ describe('TagTree', () => {
it('deletes a child node', () => {
const tree = new TagTree(rawData);
- tree.addNode(newSubtagChildRow, 'ab');
+ tree.addNode(newChildNode, 'ab');
tree.removeNode('newChild', 'ab');
const parentNode = tree.getTagAsDeepCopy('ab');
- expect(parentNode?.subRows).not.toContainEqual(newSubtagChildRow);
+ expect(parentNode?.subRows).not.toContainEqual(newChildNode);
});
it('returns null and leaves tree unchanged when removing a non-existent node', () => {
@@ -132,7 +134,7 @@ describe('TagTree', () => {
const tree = new TagTree(rawData);
const rowCountBefore = tree.getAllAsDeepCopy().length;
- tree.addNode(newSubtagChildRow, 'missing-parent');
+ tree.addNode(newChildNode, 'missing-parent');
expect(tree.getAllAsDeepCopy()).toHaveLength(rowCountBefore);
expect(tree.getTagAsDeepCopy('newChild')).toBeNull();
@@ -142,7 +144,7 @@ describe('TagTree', () => {
const orphanData = [
{
value: 'orphan',
- externalId: null,
+ externalId: 'some-external-id',
canChangeTag: true,
canDeleteTag: true,
id: 900,
@@ -164,7 +166,7 @@ describe('TagTree', () => {
const duplicateValueData = [
{
value: 'dup',
- externalId: null,
+ externalId: 'some-external-id',
canChangeTag: true,
canDeleteTag: true,
id: 1001,
@@ -176,7 +178,7 @@ describe('TagTree', () => {
},
{
value: 'dup',
- externalId: null,
+ externalId: 'some-external-id',
canChangeTag: true,
canDeleteTag: true,
id: 1002,
@@ -195,7 +197,7 @@ describe('TagTree', () => {
const cyclicData = [
{
value: 'a',
- externalId: null,
+ externalId: 'some-external-id',
canChangeTag: true,
canDeleteTag: true,
id: 1101,
@@ -207,7 +209,7 @@ describe('TagTree', () => {
},
{
value: 'b',
- externalId: null,
+ externalId: 'some-external-id',
canChangeTag: true,
canDeleteTag: true,
id: 1102,
@@ -232,7 +234,7 @@ describe('TagTree', () => {
const tree = new TagTree(rawData);
const newNode = {
value: 'ab',
- externalId: null,
+ externalId: 'some-external-id',
canChangeTag: true,
canDeleteTag: true,
id: 999,
@@ -250,7 +252,7 @@ describe('TagTree', () => {
const tree = new TagTree(rawData);
const newNode = {
value: 'new row',
- externalId: null,
+ externalId: 'some-external-id',
canChangeTag: true,
canDeleteTag: true,
id: 1000,
@@ -266,7 +268,7 @@ describe('TagTree', () => {
expect(tree.getAllAsDeepCopy()[0]).toEqual(newNode);
const nextNewNode = {
value: 'another new row',
- externalId: null,
+ externalId: 'some-external-id',
canChangeTag: true,
canDeleteTag: true,
id: 1001,
@@ -285,7 +287,7 @@ describe('TagTree', () => {
const tree = new TagTree(rawData);
const newChild = {
value: 'new child',
- externalId: null,
+ externalId: 'some-external-id',
canChangeTag: true,
canDeleteTag: true,
id: 1002,
@@ -303,7 +305,7 @@ describe('TagTree', () => {
const nextNewChild = {
value: 'another new child',
- externalId: null,
+ externalId: 'some-external-id',
canChangeTag: true,
canDeleteTag: true,
id: 1003,
diff --git a/src/taxonomy/tag-list/tagTree.ts b/src/taxonomy/tag-list/tagTree.ts
index 992a5f2503..881e3dc623 100644
--- a/src/taxonomy/tag-list/tagTree.ts
+++ b/src/taxonomy/tag-list/tagTree.ts
@@ -1,19 +1,5 @@
-import TagTreeError from './tagTreeError';
-
-export interface TagData {
- childCount: number;
- descendantCount: number;
- depth: number;
- externalId?: string | null;
- canChangeTag?: boolean;
- canDeleteTag?: boolean;
- id: number;
- parentValue: string | null;
- subTagsUrl: string | null;
- value: string;
- usageCount?: number;
- _id?: string;
-}
+import { TagTreeError } from './errors';
+import type { TagData } from '../data/types';
export interface TagTreeNode extends TagData {
subRows?: TagTreeNode[];
@@ -37,10 +23,41 @@ export class TagTree {
this.buildTree();
}
- getAllFlattenedAsCopy(): TagTreeNode[] {
- const flatten = (nodes: TagTreeNode[], accumulator: TagTreeNode[] = []): TagTreeNode[] => {
+ /** Returns a flattened copy of all nodes in the tree.
+ * For example, this array is not nested even though it contains a parent and a child tag:
+ * [
+ * {
+ * value: 'parent tag name',
+ * externalId: null,
+ * childCount: 2,
+ * descendantCount: 4,
+ * depth: 0,
+ * parentValue: null,
+ * id: 1,
+ * subTagsUrl: 'http://example.com',
+ * canChangeTag: true,
+ * canDeleteTag: true,
+ * },
+ * {
+ * value: 'child tag name',
+ * externalId: null,
+ * childCount: 0,
+ * descendantCount: 0,
+ * depth: 1,
+ * parentValue: 'parent tag name',
+ * id: 2,
+ * subTagsUrl: 'http://example.com',
+ * canChangeTag: true,
+ * canDeleteTag: true,
+ * },
+ * // ... more tags
+ * ]
+ */
+ getAllFlattenedAsCopy(): TagData[] {
+ const flatten = (nodes: TagTreeNode[], accumulator: TagData[] = []): TagData[] => {
for (const node of nodes) {
- accumulator.push({ ...node, subRows: undefined });
+ const { subRows, ...tagData } = node;
+ accumulator.push({ ...tagData }); // Create a shallow copy of the tag data without subRows
if (node.subRows) {
flatten(node.subRows, accumulator);
}
@@ -54,21 +71,25 @@ export class TagTree {
return JSON.parse(JSON.stringify(this.rows));
}
+ /** For extra robustness, we verify that there are no duplicate values
+ * in the data. (The backend also guarantees this.)
+ */
private validateNoDuplicateValues(items: TagData[]) {
- // this should be case-sensitive to account for conceivable duplicates that have different cases in the backend.
const seenValues = new Set();
for (const item of items) {
- if (seenValues.has(item.value)) {
- throw new TagTreeError(`Duplicate tag value found: ${item.value}`);
+ const lowerCaseValue = item.value.toLowerCase();
+ if (seenValues.has(lowerCaseValue)) {
+ throw new TagTreeError(`Duplicate tag value found: ${lowerCaseValue}`);
}
- seenValues.add(item.value);
+ seenValues.add(lowerCaseValue);
}
}
+ /** For extra robustness, we verify that there are no cycles in the data. (The backend also guarantees this.) */
private validateNoCycles(items: TagData[]) {
const parentByValue: { [key: string]: string | null } = {};
for (const item of items) {
- parentByValue[item.value] = item.parentValue;
+ parentByValue[item.value.toLowerCase()] = item.parentValue ? item.parentValue.toLowerCase() : null;
}
const visitStatus: { [key: string]: number } = {};
@@ -94,7 +115,7 @@ export class TagTree {
};
for (const item of items) {
- if (detectCycle(item.value)) {
+ if (detectCycle(item.value.toLowerCase())) {
throw new TagTreeError('Cycle detected in tag hierarchy.');
}
}
diff --git a/src/taxonomy/tag-list/tagTreeError.ts b/src/taxonomy/tag-list/tagTreeError.ts
deleted file mode 100644
index 5e1615f257..0000000000
--- a/src/taxonomy/tag-list/tagTreeError.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export default class TagTreeError extends Error {
- constructor(message: string) {
- super(message);
- this.name = 'TagTreeError';
- }
-}
diff --git a/src/taxonomy/taxonomy-detail/constants.ts b/src/taxonomy/taxonomy-detail/constants.ts
index ae59021c7a..442573c7c3 100644
--- a/src/taxonomy/taxonomy-detail/constants.ts
+++ b/src/taxonomy/taxonomy-detail/constants.ts
@@ -1,5 +1,10 @@
/**
- * Warning: This must reflect the `TAXONOMY_MAX_DEPTH` used in the openedx-core backend.
+ * The maximum allowable depth for any tag in the taxonomy (0-indexed).
+ * * **Constraint**: A value of 3 allows levels 0, 1, 2, and 3. Creation of new subtags
+ * is disabled for any tag already at this depth to prevent exceeding the limit.
+ * * **Data Handling**: This is a UI safety gate, not a filter. If the backend returns
+ * tags exceeding this depth, they will still be rendered, but further nesting will be blocked.
+ * * **Sync Required**: This must match `TAXONOMY_MAX_DEPTH` in the openedx-core backend.
*/
const TAXONOMY_MAX_DEPTH = 3;
diff --git a/src/taxonomy/tree-table/EditableCell.tsx b/src/taxonomy/tree-table/EditableCell.tsx
index 507976e826..09a660c159 100644
--- a/src/taxonomy/tree-table/EditableCell.tsx
+++ b/src/taxonomy/tree-table/EditableCell.tsx
@@ -10,13 +10,23 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
import OptionalExpandLink from '../tag-list/OptionalExpandLink';
+/**
+ * Props for the EditableCell component.
+ */
interface EditableCellProps {
+ /** The initial value to display in the cell */
initialValue?: string;
+ /** Callback function triggered on keyboard events */
onKeyDown?: (event: React.KeyboardEvent) => void;
+ /** Callback function triggered when the input value changes */
onChange?: (event: React.ChangeEvent) => void;
+ /** Error message to display if validation fails */
errorMessage?: string;
+ /** Indicates whether the cell value is currently being saved to the server */
isSaving?: boolean;
+ /** If true, the input field will automatically receive focus when the cell enters edit mode */
autoFocus?: boolean;
+ /** Function that returns a validation message to be displayed based on the current input value. */
getInlineValidationMessage?: (value: string) => string;
}
@@ -30,24 +40,23 @@ const EditableCell = ({
autoFocus = false,
}: EditableCellProps) => {
const [value, setValue] = useState(initialValue);
+ const [validationMessage, setValidationMessage] = useState('');
const inputId = useId();
const inputRef = useRef(null);
const intl = useIntl();
useEffect(() => {
- if (autoFocus) {
- if (inputRef.current) {
- inputRef.current.focus();
- inputRef.current.select();
- }
+ if (autoFocus && inputRef.current) {
+ inputRef.current.focus();
+ inputRef.current.select();
}
- }, [autoFocus]);
+ }, [inputRef.current]); // autoFocus explicitly not a dependency, to avoid unexpected focus change.
useEffect(() => {
setValue(initialValue);
- }, [initialValue]);
+ setValidationMessage(getInlineValidationMessage(initialValue));
+ }, []); // initialValue explicitly not a dependency, to avoid overwriting user input.
- const validationMessage = getInlineValidationMessage(value);
const effectiveErrorMessage = errorMessage || validationMessage;
const errorMessageId = `${inputId}-error`;
@@ -61,6 +70,7 @@ const EditableCell = ({
value={value}
onChange={(e) => {
setValue(e.target.value);
+ setValidationMessage(getInlineValidationMessage(e.target.value));
onChange(e);
}}
size="sm"
diff --git a/src/taxonomy/tree-table/NestedRows.tsx b/src/taxonomy/tree-table/NestedRows.tsx
index bed055302e..8facb825b7 100644
--- a/src/taxonomy/tree-table/NestedRows.tsx
+++ b/src/taxonomy/tree-table/NestedRows.tsx
@@ -9,22 +9,50 @@ import type {
import { CreateRow } from './CreateRow';
interface NestedRowsProps {
+ /** The parent row object from TanStack React Table */
parentRow: TreeRow;
+ /** The value identifier of the parent row */
parentRowValue: string;
+ /** Whether a new child row is currently being created for this parent */
isCreating?: boolean;
+ /** Callback when a new child row is saved (receives value and parentRowValue) */
onSaveNewChildRow?: (value: string, parentRowValue: string) => void;
+ /** Callback when child row creation is cancelled */
onCancelCreation?: () => void;
+ /** Array of child row objects to render */
childRowsData?: TreeRow[];
+ /** Current nesting depth level (used for indentation calculation) */
depth?: number;
+ /** Error message to display in draft creation form */
draftError?: string;
+ /** Setter function for draft error state */
setDraftError?: (error: string) => void;
+ /** ID of the row currently in creation mode */
creatingParentId?: RowId | null;
+ /** Setter function for which row is in creation mode */
setCreatingParentId?: (value: RowId | null) => void;
+ /** Callback to set whether top-level row creation is active */
setIsCreatingTopRow: (isCreating: boolean) => void;
+ /** State object for the row creation mutation (isPending, isError, error) */
createRowMutation: CreateRowMutationState;
+ /** Validation function for new row values (receives value and optional 'soft' or 'hard' mode;
+ * in 'hard' mode an exception is thrown on validation failure) */
validate: (value: string, mode?: 'soft' | 'hard') => boolean;
}
+/**
+ * NestedRows
+ *
+ * Recursively renders nested child rows within a tree table structure. This component handles:
+ * - Display of child rows when a parent row is expanded
+ * - Indentation based on nesting depth
+ * - Creation of new child rows with validation
+ * - Management of draft state during row creation
+ * - Recursive rendering of grandchild rows and deeper levels
+ *
+ * The component uses the TanStack React Table library to render table cells and manages
+ * the creation flow by displaying a CreateRow form when a parent is in creation mode.
+ */
const NestedRows = ({
parentRow,
parentRowValue,