diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 06b5afd845..0000000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -build/ -coverage/ -dist/ -docs/ diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 9bf5e49a86..0000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "root": true, - "overrides": [ - { - "files": ["*.ts"], - "parserOptions": { - "project": ["tsconfig.*?.json"], - "createDefaultProgram": true - }, - "plugins": ["@typescript-eslint"], - "extends": [ - "plugin:@angular-eslint/recommended", - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended" - ], - "env": { - "browser": true, - "jasmine": true - }, - "rules": { - "@typescript-eslint/no-unused-vars": [ - "warn", - { - "args": "all", - "argsIgnorePattern": "^_", - "caughtErrors": "all", - "caughtErrorsIgnorePattern": "^_", - "destructuredArrayIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "ignoreRestSiblings": true - } - ], - "@angular-eslint/no-empty-lifecycle-method": "warn", - "@angular-eslint/component-class-suffix": "warn", - "@angular-eslint/no-output-on-prefix": "warn", - "@typescript-eslint/no-inferrable-types": "off", - "@angular-eslint/directive-selector": [ - "warn", - { - "type": "attribute", - "prefix": "f", - "style": "camelCase" - } - ], - "@angular-eslint/component-selector": [ - "warn", - { - "type": "element", - "prefix": "f", - "style": "kebab-case" - } - ], - "@typescript-eslint/ban-types": "warn", - "@typescript-eslint/no-empty-function": "warn", - "@typescript-eslint/no-inferrable-types": "off", - "@typescript-eslint/no-this-alias": "warn", - "no-dupe-class-members": "warn", - "no-prototype-builtins": "warn", - "no-unused-vars": "off", - "no-useless-escape": "warn", - "no-var": "warn", - "quotes": [ - "warn", - "single", - { - "allowTemplateLiterals": true - } - ], - "prefer-const": "warn", - "prettier/prettier": "warn" - } - }, - { - "files": ["*.component.html"], - "extends": ["plugin:@angular-eslint/template/recommended", "plugin:prettier/recommended"], - "rules": { - "max-len": [ - "warn", - { - "code": 140 - } - ], - "prettier/prettier": "warn" - } - }, - { - "files": ["*.component.ts"], - "extends": ["plugin:@angular-eslint/template/process-inline-templates"] - } - ] -} diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000..260dc5e2fd --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,5 @@ +# 8 June 2026: Repository-wide formatting and lint configuration +26b4962794d16e90587fb179aab966b39459050c + +# 17 June 2026: Repository-wide sorting of HTML attributes +1ffc7ad7b9dc18f22be32bf6313bfde97957dd2f diff --git a/.github/workflows/deployment-institution.yml b/.github/workflows/deployment-institution.yml new file mode 100644 index 0000000000..d9f6075048 --- /dev/null +++ b/.github/workflows/deployment-institution.yml @@ -0,0 +1,71 @@ +name: create-institution-deployment +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + docker-web-server: + environment: deployment-secrets + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + submodules: recursive + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + - name: Check custom institution deployment secrets + id: custom_config + env: + INSTITUTION_DOCKERHUB_USERNAME: ${{ secrets.INSTITUTION_DOCKERHUB_USERNAME }} + INSTITUTION_DOCKERHUB_TOKEN: ${{ secrets.INSTITUTION_DOCKERHUB_TOKEN }} + INSTITUTION: ${{ secrets.INSTITUTION }} + run: | + if [ -n "$INSTITUTION_DOCKERHUB_USERNAME" ] && [ -n "$INSTITUTION_DOCKERHUB_TOKEN" ] && [ -n "$INSTITUTION" ]; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + else + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "Skipping custom institution Docker publish because one or more institution secrets are not set." + fi + - name: Login to DockerHub + uses: docker/login-action@v4 + if: github.event_name != 'pull_request' && steps.custom_config.outputs.enabled == 'true' + with: + username: ${{ secrets.INSTITUTION_DOCKERHUB_USERNAME }} + password: ${{ secrets.INSTITUTION_DOCKERHUB_TOKEN }} + - name: Setup meta for custom institution web server + id: docker_meta + uses: docker/metadata-action@v6 + if: steps.custom_config.outputs.enabled == 'true' + with: + images: ${{ secrets.INSTITUTION_DOCKERHUB_USERNAME }}/${{ secrets.INSTITUTION }} + tags: | + type=ref,event=tag + type=ref,event=branch + type=semver,pattern=prod-{{version}} + type=semver,pattern=prod-{{major}}.{{minor}} + type=semver,pattern=prod-{{major}} + - name: Build and push custom web server + id: docker_build + uses: docker/build-push-action@v7 + if: steps.custom_config.outputs.enabled == 'true' + with: + file: deploy.Dockerfile + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.docker_meta.outputs.tags }} + labels: ${{ steps.docker_meta.outputs.labels }} + build-args: | + SENTRY_DSN=${{ secrets.SENTRY_DSN }} + SENTRY_ORG=${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT=${{ secrets.SENTRY_PROJECT }} + SENTRY_RELEASE=${{ github.ref_name }} + SENTRY_DIST=${{ github.run_number }} + UPLOAD_SENTRY_SOURCEMAPS=true + secrets: | + sentry_auth_token=${{ secrets.SENTRY_AUTH_TOKEN }} + - name: Image digest + if: steps.custom_config.outputs.enabled == 'true' + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 40d35b9eee..6c986fcff7 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -50,20 +50,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: recursive - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v4 if: github.event_name != 'pull_request' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Setup meta for web server id: docker_meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: lmsdoubtfire/doubtfire-web tags: | @@ -74,7 +74,7 @@ jobs: type=semver,pattern=prod-{{major}} - name: Build and push web server id: docker_build - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: file: deploy.Dockerfile context: . diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000000..0bb56ce5ad --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,23 @@ +name: Lint CI + +on: + push: + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [22] + + steps: + - uses: actions/checkout@v6 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v5 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci + - run: npm run lint + - run: npm run typecheck diff --git a/.github/workflows/nodejs-ci.yml b/.github/workflows/nodejs-ci.yml index 3ef6877d51..df3f516514 100644 --- a/.github/workflows/nodejs-ci.yml +++ b/.github/workflows/nodejs-ci.yml @@ -13,16 +13,13 @@ jobs: strategy: matrix: - node-version: [20] + node-version: [22] steps: - - uses: actions/checkout@v4 - - uses: browser-actions/setup-chrome@latest + - uses: actions/checkout@v6 + - uses: browser-actions/setup-chrome@v2 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v5 with: node-version: ${{ matrix.node-version }} - - run: npm install -g @angular/cli - run: npm ci - # - run: npm run lint - # - run: npm run test:ci - run: npm run build --if-present diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..0254469205 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,19 @@ +name: Test CI + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - name: Use Node.js 22 + uses: actions/setup-node@v5 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run test:ci diff --git a/.gitignore b/.gitignore index c5c729aef4..9912f41615 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ dist/ .angulardoc.json .nx .idea + +# Sentry Config File +.sentryclirc diff --git a/.husky/commit-msg b/.husky/commit-msg index fe4c17a22d..70bd3dd23d 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npx --no-install commitlint --edit "" +npx --no-install commitlint --edit "$1" diff --git a/.husky/pre-commit b/.husky/pre-commit index 6700f51282..e69de29bb2 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" diff --git a/.postcssrc.json b/.postcssrc.json new file mode 100644 index 0000000000..865e00b364 --- /dev/null +++ b/.postcssrc.json @@ -0,0 +1,6 @@ +{ + "syntax": "postcss-scss", + "plugins": { + "@tailwindcss/postcss": {} + } +} diff --git a/.prettierignore b/.prettierignore index 25257e586f..d541041bd2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,3 +4,12 @@ yarn.lock dist build e2e +CHANGELOG.md +JPlag-Report-Viewer/ +.github/ +karma/ +Gruntfile.js +karma.conf.js +polyfills.ts +test.ts +main.ts diff --git a/.prettierrc b/.prettierrc index d73829a932..021153e85a 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,9 +1,15 @@ { "printWidth": 100, "tabWidth": 2, - "tabs": false, "singleQuote": true, - "semicolon": true, + "semi": true, "quoteProps": "preserve", - "bracketSpacing": false + "endOfLine": "lf", + "trailingComma": "all", + "bracketSpacing": false, + "plugins": ["@trivago/prettier-plugin-sort-imports"], + "importOrder": ["^@angular/(.*)$", "^rxjs$", "^rxjs/(.*)$", "^src/app/(.*)$", "^[./]"], + "importOrderSeparation": false, + "importOrderSortSpecifiers": true, + "importOrderParserPlugins": ["typescript", "decorators-legacy"] } diff --git a/.tool-versions b/.tool-versions index c2ca3d3d25..42bb250e67 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -nodejs 20.9.0 +nodejs 22.22.3 diff --git a/.vscode/settings.json b/.vscode/settings.json index 8582900e71..7e6882bfa3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "files.eol": "\n" + "files.eol": "\n" } diff --git a/CHANGELOG.md b/CHANGELOG.md index fd5b782d47..52ce354d6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,424 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [11.0.0-45](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-44...v11.0.0-45) (2026-07-13) + + +### Features + +* add label for starting tasks ([46100f5](https://github.com/b0ink/doubtfire-deploy/commit/46100f50ca8ddac11668e97deb482994f47f87f9)) +* add task list filters and sorting ([c65279c](https://github.com/b0ink/doubtfire-deploy/commit/c65279cb1900b5198e764d53b671719512486a83)) +* show all tasks in task planner ([eb87251](https://github.com/b0ink/doubtfire-deploy/commit/eb87251dbef37d5cca8d11c29816c42722605764)) + + +### Bug Fixes + +* dont reset task list sidebar when switching between project and task dashboard ([acc3ac2](https://github.com/b0ink/doubtfire-deploy/commit/acc3ac2625137075262c44239772d121e52c3a34)) +* lint ([dab681d](https://github.com/b0ink/doubtfire-deploy/commit/dab681d0e3d0e025804d3477eb68727ab42330ed)) +* use task weighting system as default sorting ([aa3c261](https://github.com/b0ink/doubtfire-deploy/commit/aa3c2613f9c1027eea761f51297380a4443a42b2)) + +## [11.0.0-44](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-43...v11.0.0-44) (2026-07-08) + + +### Features + +* display submitted grade in portfolio view ([a76cecf](https://github.com/b0ink/doubtfire-deploy/commit/a76cecfe1e4f3a78becf9507900e621e87447d69)) + + +### Bug Fixes + +* ensure accurate tutor views ([3b5e693](https://github.com/b0ink/doubtfire-deploy/commit/3b5e69397abcc53136942428fb97ac81a5cddb3d)) +* only call d2l mapping as convenor ([d6ecd44](https://github.com/b0ink/doubtfire-deploy/commit/d6ecd44658f6a668dde409d399944f53793c1c35)) +* persist portfolio filters in url ([322eef2](https://github.com/b0ink/doubtfire-deploy/commit/322eef29e57d13267a2466da4bcb4a55a7b49db6)) +* reduce threshold for mobile view in inbox dashboard ([9479351](https://github.com/b0ink/doubtfire-deploy/commit/9479351619479fed2ac26199f77946ac64903e7c)) + +## [11.0.0-43](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-42...v11.0.0-43) (2026-07-03) + + +### Bug Fixes + +* regenerate ngsw cache after sourcemaps removed ([be85d84](https://github.com/b0ink/doubtfire-deploy/commit/be85d8443b56f20a6866c0fc8d65915d5fe63a2a)) + +## [11.0.0-42](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-41...v11.0.0-42) (2026-07-03) + + +### Features + +* collapse task comments in narrow screen sizes ([380f277](https://github.com/b0ink/doubtfire-deploy/commit/380f2773b44d6582b62b13785b90a146a7002123)) +* render ansi output to html ([732aedd](https://github.com/b0ink/doubtfire-deploy/commit/732aedd2414dcfe3d74a78b4b9ca0bc17d4f5521)) + + +### Bug Fixes + +* add tooltip to collapsed task list item ([c81e61e](https://github.com/b0ink/doubtfire-deploy/commit/c81e61ef48b83f076ba7dac2d039b4bade5c9717)) +* ensure project is valid ([095315b](https://github.com/b0ink/doubtfire-deploy/commit/095315b212fff98cbe661df7a33a7b091cb66b1e)) + +## [11.0.0-41](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-40...v11.0.0-41) (2026-07-02) + + +### Bug Fixes + +* add padding when user cant view submission ([f16d9dc](https://github.com/b0ink/doubtfire-deploy/commit/f16d9dc6ee815e93022f9b8446dd32d1852ce088)) +* avoid repeated unit fetching ([cfe04dc](https://github.com/b0ink/doubtfire-deploy/commit/cfe04dc29f78ab815a17312f929e6deb052a88ed)) +* remove hash prefix ([51794b8](https://github.com/b0ink/doubtfire-deploy/commit/51794b800d2d731d6445cd21b5b592302e43defd)) +* tutorials route transition error ([db1294a](https://github.com/b0ink/doubtfire-deploy/commit/db1294a37e7333612202a8413256288ae97c05aa)) + +## [11.0.0-40](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-39...v11.0.0-40) (2026-07-01) + + +### Bug Fixes + +* reveal overflow task claims to convenors only ([c17a6d2](https://github.com/b0ink/doubtfire-deploy/commit/c17a6d239b71f446300b305de5877372c206028d)) + +## [11.0.0-39](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-38...v11.0.0-39) (2026-07-01) + + +### Bug Fixes + +* dynamic hint sizing to prevent overlap ([518b58b](https://github.com/b0ink/doubtfire-deploy/commit/518b58ba30c23dc4456a98ebfbe41cb48446e715)) + +## [11.0.0-38](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-37...v11.0.0-38) (2026-06-29) + +## [11.0.0-37](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-36...v11.0.0-37) (2026-06-29) + + +### Bug Fixes + +* fetch images on init ([b28bc69](https://github.com/b0ink/doubtfire-deploy/commit/b28bc6905b43d5843eee010cefff4938568edb10)) + +## [11.0.0-36](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-35...v11.0.0-36) (2026-06-29) + + +### Bug Fixes + +* modify grey border ([59c102d](https://github.com/b0ink/doubtfire-deploy/commit/59c102d7708e4c28a5584cc3614046e8e8f5fd52)) + +## [11.0.0-35](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-34...v11.0.0-35) (2026-06-29) + + +### Features + +* add skeleton loading ui to overseer image list ([b833a1c](https://github.com/b0ink/doubtfire-deploy/commit/b833a1c58ce07e6bf9b6a12c77a1bbea1635da10)) +* add skeleton ui for activities ([47ffe2b](https://github.com/b0ink/doubtfire-deploy/commit/47ffe2b261d26be39e93e7b89204c65b5d5b1f57)) + + +### Bug Fixes + +* reveal tii threshold if tii enabled ([ab1a401](https://github.com/b0ink/doubtfire-deploy/commit/ab1a401a3f67db83f7a55aa25060f2a29116eae1)) +* use correct api url ([ed4dba4](https://github.com/b0ink/doubtfire-deploy/commit/ed4dba4552ebf088893dfa58fffac2546690cedd)) + +## [11.0.0-34](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-33...v11.0.0-34) (2026-06-25) + + +### Bug Fixes + +* align icons ([8af5f5a](https://github.com/b0ink/doubtfire-deploy/commit/8af5f5ab177a8057e6b894ae7b039b8b8203c19f)) +* set correct button size ([ec8eef4](https://github.com/b0ink/doubtfire-deploy/commit/ec8eef49ca4275f00502872dab5b0cf23251c337)) + +## [11.0.0-33](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-32...v11.0.0-33) (2026-06-25) + + +### Bug Fixes + +* ensure session replays are captured ([bc35164](https://github.com/b0ink/doubtfire-deploy/commit/bc351643d65104ff85d716cff0e0d405cdb10170)) + +## [11.0.0-32](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-31...v11.0.0-32) (2026-06-25) + + +### Bug Fixes + +* ensure sentry replays work ([b2a8cdd](https://github.com/b0ink/doubtfire-deploy/commit/b2a8cdd2bcb15d409047c346b93136fd2f605b5f)) + +## [11.0.0-31](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-30...v11.0.0-31) (2026-06-25) + +## [11.0.0-30](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-29...v11.0.0-30) (2026-06-25) + + +### Bug Fixes + +* tunnel sentry requests ([2d24ebc](https://github.com/b0ink/doubtfire-deploy/commit/2d24ebc9e611dbf7ca0cbdcd8507b238538d9ea3)) + +## [11.0.0-29](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-28...v11.0.0-29) (2026-06-25) + + +### Features + +* sentry ([#1305](https://github.com/b0ink/doubtfire-deploy/issues/1305)) ([865faa2](https://github.com/b0ink/doubtfire-deploy/commit/865faa286016bf61fdb0ae0e300df48e92045bbb)) + +## [11.0.0-28](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-27...v11.0.0-28) (2026-06-24) + +## [11.0.0-27](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-26...v11.0.0-27) (2026-06-24) + + +### Features + +* customisable grades ([#1271](https://github.com/b0ink/doubtfire-deploy/issues/1271)) ([c2b68ac](https://github.com/b0ink/doubtfire-deploy/commit/c2b68aca0b5b0873443823795fc50bbcdc81fa4d)) + + +### Bug Fixes + +* allow empty strings to fix create user dialog ([efb4e5d](https://github.com/b0ink/doubtfire-deploy/commit/efb4e5d97f9641843f3e3e50f1fd38bb560fe853)) +* center div ([1c20dd7](https://github.com/b0ink/doubtfire-deploy/commit/1c20dd73befb5c0e204874140ca494c968487bd3)) +* close dialog after creating teachiing period ([af9ab4d](https://github.com/b0ink/doubtfire-deploy/commit/af9ab4d692f3e4c5e84a10e54a7f63d289a8092b)) +* ensure overseer assessment is valid before expanding ([6cad43c](https://github.com/b0ink/doubtfire-deploy/commit/6cad43c1c6aac6cf7552ed9db937d6a73cb17369)) +* ensure user is authenticated before loading scorm ([1b066c0](https://github.com/b0ink/doubtfire-deploy/commit/1b066c0e08cffc7a1176c7ec7f1b23da4ce6cf3f)) +* extend reply height to avoid text cutoff ([b4af5d1](https://github.com/b0ink/doubtfire-deploy/commit/b4af5d1b64dc32bea17b65c817aeaf56139be0fb)) +* hide quality pts if negative ([e43b96d](https://github.com/b0ink/doubtfire-deploy/commit/e43b96d77f21ebc654b4ca9e0b5a1cfeb5b8058c)) +* only show status icon if set ([46f494c](https://github.com/b0ink/doubtfire-deploy/commit/46f494c926b604f0c1fd396b59678f5ee74a7492)) +* truncate portfolio task list item correctly ([a02fe9e](https://github.com/b0ink/doubtfire-deploy/commit/a02fe9ebe57ae4157b9124d8a8a1c7aea364ba60)) +* update husky hooks for v9 ([5961860](https://github.com/b0ink/doubtfire-deploy/commit/596186040329ee198e1ef703d76676d15ad3ce89)) +* use correct scorm url ([d95c4ca](https://github.com/b0ink/doubtfire-deploy/commit/d95c4ca400a7aee7dd50f816e1b342e3754db87a)) + +## [11.0.0-26](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-25...v11.0.0-26) (2026-06-19) + + +### Bug Fixes + +* avoid converting colons in urls to emojis [#706](https://github.com/b0ink/doubtfire-deploy/issues/706) ([550b867](https://github.com/b0ink/doubtfire-deploy/commit/550b867df0e52026825e594a72d8544c9505a22b)) +* improve global loading logic for staff ([d81ac52](https://github.com/b0ink/doubtfire-deploy/commit/d81ac52cf1b90e139a34bf13ed08142919b6cc53)) +* prevent multiple global fetches ([f1528d1](https://github.com/b0ink/doubtfire-deploy/commit/f1528d1aedc7d6485b094ed3c4923b44c786d45b)) + +## [11.0.0-25](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-24...v11.0.0-25) (2026-06-18) + + +### Features + +* add day of week to gantt chart ([1fb4637](https://github.com/b0ink/doubtfire-deploy/commit/1fb4637cd2386f1aeff227043e0002bf9b77837b)) +* engagement passport ([#1257](https://github.com/b0ink/doubtfire-deploy/issues/1257)) ([7a754dc](https://github.com/b0ink/doubtfire-deploy/commit/7a754dcc82ca228f5688abab4480774df75a007e)) +* show students name if viewing other project ([38c0e85](https://github.com/b0ink/doubtfire-deploy/commit/38c0e85ebe698c71ea087f3aeecc6d7ca24c86d7)) + + +### Bug Fixes + +* ensure dashboard dropdown switches from task details view ([1ba5d1e](https://github.com/b0ink/doubtfire-deploy/commit/1ba5d1e42d302218520f8be4c0875673ad5d7a3f)) +* web calendar ([#1282](https://github.com/b0ink/doubtfire-deploy/issues/1282)) ([83b6d5e](https://github.com/b0ink/doubtfire-deploy/commit/83b6d5ea381604ae88585195314eafd239880632)) + +## [11.0.0-24](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-23...v11.0.0-24) (2026-06-17) + + +### Features + +* check access token expiry locally before attempting request ([#1270](https://github.com/b0ink/doubtfire-deploy/issues/1270)) ([4ff5562](https://github.com/b0ink/doubtfire-deploy/commit/4ff55627566f9004872c915658bd91bc348dd8e8)) +* submission history ([#1269](https://github.com/b0ink/doubtfire-deploy/issues/1269)) ([4cdae07](https://github.com/b0ink/doubtfire-deploy/commit/4cdae07eb5bef0f9866927892e92b6b80dde0c74)) +* upgrade gantt chart and add screenshotting ability ([#1263](https://github.com/b0ink/doubtfire-deploy/issues/1263)) ([08a8eee](https://github.com/b0ink/doubtfire-deploy/commit/08a8eee9337331aa328960e206722f8905c2a0f9)) + + +### Bug Fixes + +* use either portfolio available field ([f8d5142](https://github.com/b0ink/doubtfire-deploy/commit/f8d514261c7eca5465ac3af95da33b95a39d3f23)) + +## [11.0.0-23](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-22...v11.0.0-23) (2026-06-11) + + +### Features + +* display list of tasks still being processed ([#1264](https://github.com/b0ink/doubtfire-deploy/issues/1264)) ([55ef4c0](https://github.com/b0ink/doubtfire-deploy/commit/55ef4c0409cf5a066e91763a00f2bb428670e520)) + + +### Bug Fixes + +* apply status color ([df6a6ec](https://github.com/b0ink/doubtfire-deploy/commit/df6a6ecc73d01dd2ef449270bc7c6de446d93884)) +* avoid loading rendering all students at the same time ([ba7910a](https://github.com/b0ink/doubtfire-deploy/commit/ba7910a59460a4d4139273b272e99625e5efc518)) +* ensure entire task list can be scrolled through ([8ce79a8](https://github.com/b0ink/doubtfire-deploy/commit/8ce79a8f33c82231f7f3c4033a4d37039514c015)) + +## [11.0.0-22](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-21...v11.0.0-22) (2026-06-09) + + +### Features + +* communications system ([#1239](https://github.com/b0ink/doubtfire-deploy/issues/1239)) ([b55bcfc](https://github.com/b0ink/doubtfire-deploy/commit/b55bcfc8f84f381beb7d62f41031db3ba6a38193)) + +## [11.0.0-21](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-20...v11.0.0-21) (2026-06-08) + + +### Bug Fixes + +* ensure unit dates map correctly ([532f185](https://github.com/b0ink/doubtfire-deploy/commit/532f185d2bc6839549a06a2f9e625c676cc280a6)) + +## [11.0.0-20](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-19...v11.0.0-20) (2026-06-04) + +## [11.0.0-19](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-18...v11.0.0-19) (2026-06-03) + + +### Bug Fixes + +* ensure unit is fetched in unit task editor ([54b1cf0](https://github.com/b0ink/doubtfire-deploy/commit/54b1cf03ed662447e7027a61d084f62f2548c996)) + +## [11.0.0-18](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-17...v11.0.0-18) (2026-06-03) + +## [11.0.0-17](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-16...v11.0.0-17) (2026-06-03) + + +### Features + +* support zip file submissions ([#1240](https://github.com/b0ink/doubtfire-deploy/issues/1240)) ([99d546f](https://github.com/b0ink/doubtfire-deploy/commit/99d546f1544feeab31fc6b2434e330b22544b56f)) + +## [11.0.0-16](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-15...v11.0.0-16) (2026-06-03) + +## [11.0.0-15](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-14...v11.0.0-15) (2026-06-03) + + +### Bug Fixes + +* display past due date on same day ([cd47f5c](https://github.com/b0ink/doubtfire-deploy/commit/cd47f5c0e4c351e2421afd94647a1297b3b5ec75)) +* prevent reload of task list component when switching back to dashboard ([be780b1](https://github.com/b0ink/doubtfire-deploy/commit/be780b14bc875c05e1083353c6d447315bc179e7)) + +## [11.0.0-14](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-13...v11.0.0-14) (2026-06-03) + +## [11.0.0-13](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-12...v11.0.0-13) (2026-06-03) + +## [11.0.0-12](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-11...v11.0.0-12) (2026-06-02) + + +### Bug Fixes + +* correctly apply task tutorial filters ([924c267](https://github.com/b0ink/doubtfire-deploy/commit/924c2676c6739820cc066d046b7ace71ee2d2f9e)) + +## [11.0.0-11](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-10...v11.0.0-11) (2026-05-19) + +## [11.0.0-10](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-9...v11.0.0-10) (2026-05-18) + +## [11.0.0-9](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-8...v11.0.0-9) (2026-05-18) + + +### Bug Fixes + +* ensure unit is loaded first before querying inbox ([6d7c9fb](https://github.com/b0ink/doubtfire-deploy/commit/6d7c9fb8ff7fe714844ebc8f0e04f2ef9a40d4d0)) + +## [11.0.0-8](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-7...v11.0.0-8) (2026-05-18) + +## [11.0.0-7](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-6...v11.0.0-7) (2026-05-17) + +## [11.0.0-6](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-5...v11.0.0-6) (2026-05-14) + +## [11.0.0-5](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-4...v11.0.0-5) (2026-05-14) + + +### Bug Fixes + +* ensure access to tutor notes ([5e0ec7c](https://github.com/b0ink/doubtfire-deploy/commit/5e0ec7cf6ccf04286aa6865e5d84d14487f68f7f)) +* ensure valid selected task ([deaae7f](https://github.com/b0ink/doubtfire-deploy/commit/deaae7fb819ea8dfc01d6ad67415f50c2e4258a2)) + +## [11.0.0-4](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-3...v11.0.0-4) (2026-05-14) + + +### Features + +* add route auth guards ([fb2e781](https://github.com/b0ink/doubtfire-deploy/commit/fb2e78113b13782dbbafe38c21f27c5d63be69c6)) + + +### Bug Fixes + +* enable inbox access for tutors ([05d3eda](https://github.com/b0ink/doubtfire-deploy/commit/05d3eda76bdcfb491649f5d83e69213db3de0885)) + +## [11.0.0-3](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-2...v11.0.0-3) (2026-05-14) + + +### Bug Fixes + +* add padding ([94f4216](https://github.com/b0ink/doubtfire-deploy/commit/94f4216e588d6dfb6abc4d10804971cb0e8f8678)) +* unlock task status selection for staff ([#1222](https://github.com/b0ink/doubtfire-deploy/issues/1222)) ([e4080d0](https://github.com/b0ink/doubtfire-deploy/commit/e4080d03a629654518bc362f48e8e0fbb790dd96)) + +## [11.0.0-2](https://github.com/b0ink/doubtfire-deploy/compare/v11.0.0-1...v11.0.0-2) (2026-05-13) + +## [11.0.0-1](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.1-21...v11.0.0-1) (2026-05-13) + + +### Features + +* (wip) add name to new student experience skeleton ([7b9ace4](https://github.com/b0ink/doubtfire-deploy/commit/7b9ace47132f0237bc621d3d0e45c3031e7fac05)) +* add burndown replacement ([837658a](https://github.com/b0ink/doubtfire-deploy/commit/837658afff090e52d53cefa7c2eed20a46c1ac01)) +* add new progress component ([8beef70](https://github.com/b0ink/doubtfire-deploy/commit/8beef701e6b7fc0dbea42f8849dc02e79a665fb7)) +* add new student experience ([9bb77ab](https://github.com/b0ink/doubtfire-deploy/commit/9bb77ab108e50b637329bd739925d1d9566d9203)) +* allow paste attachment comment ([#1165](https://github.com/b0ink/doubtfire-deploy/issues/1165)) ([0f24980](https://github.com/b0ink/doubtfire-deploy/commit/0f24980e6edcc5d0f81e015990adaf14afea400e)) +* batch upload feedback csv ([#1175](https://github.com/b0ink/doubtfire-deploy/issues/1175)) ([9f95987](https://github.com/b0ink/doubtfire-deploy/commit/9f959877b6f47d878d87b468f4aab73f536d598b)) +* bulk import staff via emails ([#1195](https://github.com/b0ink/doubtfire-deploy/issues/1195)) ([c91ab47](https://github.com/b0ink/doubtfire-deploy/commit/c91ab478ae1d1cd459040290f6f7a44c7dce3efe)) +* confirm recursive fix in mobile tutor view ([b64601a](https://github.com/b0ink/doubtfire-deploy/commit/b64601a425c7a64bccfaf47c1e4fccb0343b1589)) +* confirmation modal to reassign tutorials when removing staff ([5f90e90](https://github.com/b0ink/doubtfire-deploy/commit/5f90e9085d69913eced55ad54ce9ac09431d3822)) +* discussed in class refactor ([#1145](https://github.com/b0ink/doubtfire-deploy/issues/1145)) ([4d8bb5b](https://github.com/b0ink/doubtfire-deploy/commit/4d8bb5b82d89caa02ed4936819be601b3f6977fc)) +* display icon for tasks escalated by student ([dfcfd47](https://github.com/b0ink/doubtfire-deploy/commit/dfcfd472305ef2971fa72734c18e77253b98fa11)) +* display portfolio submission time ([d717b27](https://github.com/b0ink/doubtfire-deploy/commit/d717b270eb80c0b0245e6b39f0b3f0567c379512)) +* display sso redirecting state ([248c992](https://github.com/b0ink/doubtfire-deploy/commit/248c992f46488f61916e00a87ca32ed987721f31)) +* edit comments ([#1194](https://github.com/b0ink/doubtfire-deploy/issues/1194)) ([976b6ac](https://github.com/b0ink/doubtfire-deploy/commit/976b6ac4fa3bd2d5e954eebcae3fcb40dd8d1f0e)) +* enable task pinning in explorer ([1647e2b](https://github.com/b0ink/doubtfire-deploy/commit/1647e2bcc86ac6fb08c82be9795a06939c57bb6e)) +* improve look of task status count ([475c316](https://github.com/b0ink/doubtfire-deploy/commit/475c3165db099e1412e30fb6cf18bbaddd516e75)) +* pause feedback threshold during teaching period breaks ([#1138](https://github.com/b0ink/doubtfire-deploy/issues/1138)) ([12fbf81](https://github.com/b0ink/doubtfire-deploy/commit/12fbf8147107dc185a9a9cbea940efac93a0f316)) +* require discussion before marking complete ([#1103](https://github.com/b0ink/doubtfire-deploy/issues/1103)) ([86ae886](https://github.com/b0ink/doubtfire-deploy/commit/86ae8865268ee19e4b3430ff679358cc075e1346)) +* staff note and similarity indicators ([4a65a9b](https://github.com/b0ink/doubtfire-deploy/commit/4a65a9b21d615ecf78bf09743c9cf8c3026ff621)) +* visualisations ([f1cc39e](https://github.com/b0ink/doubtfire-deploy/commit/f1cc39e59dd224c55ee0e8b5f1d50855263d147e)) + + +### Bug Fixes + +* avoid rendering the staff list twice ([9e5be59](https://github.com/b0ink/doubtfire-deploy/commit/9e5be591b9a0cf72d01efa5e28e5b2b9e7eba043)) +* burndown chart visualisation ([#942](https://github.com/b0ink/doubtfire-deploy/issues/942)) ([8487b8d](https://github.com/b0ink/doubtfire-deploy/commit/8487b8d20e838fa1b2a2238bb856f73edc86442c)) +* check for valid unit ([d9560c3](https://github.com/b0ink/doubtfire-deploy/commit/d9560c3b68df0a09f577cdab990d252b5cd24c58)) +* complete student enrolment modal ([efa89c3](https://github.com/b0ink/doubtfire-deploy/commit/efa89c344069d386f2411a417e570d4158af91b9)) +* debounce duplicate task submission requests ([80f92e2](https://github.com/b0ink/doubtfire-deploy/commit/80f92e2277c48978e1738340063a7223c04fddb8)) +* display groups only when a group set is selected ([dde1e76](https://github.com/b0ink/doubtfire-deploy/commit/dde1e7697f8a9a93577bb0e18e3e81a3ebd1ace2)) +* duplicate files ([da51e8e](https://github.com/b0ink/doubtfire-deploy/commit/da51e8e0cc9518a2a76447a48079b42f144064fc)) +* ensure authorisation active in angular ([deaa1e9](https://github.com/b0ink/doubtfire-deploy/commit/deaa1e940f8295ff962c111f1dbac75ed5ff51fa)) +* ensure loading screen removed in sign in component ([6954ac6](https://github.com/b0ink/doubtfire-deploy/commit/6954ac62627e058b6a73c0e83dc8dc8a1740698d)) +* ensure pdf viewer is visible [#1186](https://github.com/b0ink/doubtfire-deploy/issues/1186) ([6bc0752](https://github.com/b0ink/doubtfire-deploy/commit/6bc075228fe2a48dcf91aa5350b5f15f602e2dc8)) +* ensure selected group is valid ([95cb9ac](https://github.com/b0ink/doubtfire-deploy/commit/95cb9ace6615b45dd75161fd02d6dd130420817b)) +* fix pdf viewer for portfolios ([456cf46](https://github.com/b0ink/doubtfire-deploy/commit/456cf466bf49b86467b269ec3d6fb4e63f6a059f)) +* get new visualisations to build ([f828fd4](https://github.com/b0ink/doubtfire-deploy/commit/f828fd42fafaf09d7f807cea8f6a1e14ec56ecc5)) +* link task list to definitions for project dashboard ([90853e9](https://github.com/b0ink/doubtfire-deploy/commit/90853e93c109430669e3f80e4324649cd2d0dba6)) +* new burndown and task status count ([bfecd08](https://github.com/b0ink/doubtfire-deploy/commit/bfecd086eeeae33e9bbba430c79bcc4f21506a07)) +* only render if submission date is valid ([04986a0](https://github.com/b0ink/doubtfire-deploy/commit/04986a0e501f0185dbd199b8d2554133217f7e75)) +* open report in turnitin ([091aaf8](https://github.com/b0ink/doubtfire-deploy/commit/091aaf8ba744bbf677f9b8f51a58bc16b7d631ab)) +* remove hardcoded chart view size ([1b31930](https://github.com/b0ink/doubtfire-deploy/commit/1b319303ac969ef63617b2362297233bc79bb916)) +* remove markdown filter from learning outcomes ([1de217c](https://github.com/b0ink/doubtfire-deploy/commit/1de217c0bc19b8dbedb21cf2e89e195772ae2a02)) +* set task data for project dashboard state ([ef75340](https://github.com/b0ink/doubtfire-deploy/commit/ef7534064ef92c06e459953076bc30b6eff4647d)) +* support building on windows ([b2aa7ae](https://github.com/b0ink/doubtfire-deploy/commit/b2aa7ae6ea7a456953ed4f2a8c63f784ac3870b5)) +* switch staff to unit roles in unit service ([0e5d9de](https://github.com/b0ink/doubtfire-deploy/commit/0e5d9de9df2eaf16e473f4f8fde920ffb59ce765)) +* task route transition race when switching from inbox ([63c52dc](https://github.com/b0ink/doubtfire-deploy/commit/63c52dc4cdb2f2fbc701f69c64b7273f5778c868)) + +### [10.0.1-35](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.1-34...v10.0.1-35) (2026-04-28) + + +### Features + +* bulk import staff via emails ([#1195](https://github.com/b0ink/doubtfire-deploy/issues/1195)) ([c91ab47](https://github.com/b0ink/doubtfire-deploy/commit/c91ab478ae1d1cd459040290f6f7a44c7dce3efe)) +* edit comments ([#1194](https://github.com/b0ink/doubtfire-deploy/issues/1194)) ([976b6ac](https://github.com/b0ink/doubtfire-deploy/commit/976b6ac4fa3bd2d5e954eebcae3fcb40dd8d1f0e)) + + +### Bug Fixes + +* task route transition race when switching from inbox ([63c52dc](https://github.com/b0ink/doubtfire-deploy/commit/63c52dc4cdb2f2fbc701f69c64b7273f5778c868)) + +### [10.0.1-34](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.1-33...v10.0.1-34) (2026-04-27) + +### [10.0.1-33](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.1-32...v10.0.1-33) (2026-04-23) + + +### Features + +* allow paste attachment comment ([#1165](https://github.com/b0ink/doubtfire-deploy/issues/1165)) ([0f24980](https://github.com/b0ink/doubtfire-deploy/commit/0f24980e6edcc5d0f81e015990adaf14afea400e)) + + +### Bug Fixes + +* open report in turnitin ([091aaf8](https://github.com/b0ink/doubtfire-deploy/commit/091aaf8ba744bbf677f9b8f51a58bc16b7d631ab)) + +### [10.0.1-32](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.1-31...v10.0.1-32) (2026-04-18) + +### [10.0.1-31](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.1-30...v10.0.1-31) (2026-04-18) + +### [10.0.1-30](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.1-29...v10.0.1-30) (2026-04-18) + + +### Features + +* batch upload feedback csv ([#1175](https://github.com/b0ink/doubtfire-deploy/issues/1175)) ([9f95987](https://github.com/b0ink/doubtfire-deploy/commit/9f959877b6f47d878d87b468f4aab73f536d598b)) +* display icon for tasks escalated by student ([dfcfd47](https://github.com/b0ink/doubtfire-deploy/commit/dfcfd472305ef2971fa72734c18e77253b98fa11)) +* enable task pinning in explorer ([1647e2b](https://github.com/b0ink/doubtfire-deploy/commit/1647e2bcc86ac6fb08c82be9795a06939c57bb6e)) + + +### Bug Fixes + +* debounce duplicate task submission requests ([80f92e2](https://github.com/b0ink/doubtfire-deploy/commit/80f92e2277c48978e1738340063a7223c04fddb8)) + ### [10.0.1-29](https://github.com/b0ink/doubtfire-deploy/compare/v10.0.1-28...v10.0.1-29) (2026-04-15) diff --git a/CROSS_UNIT_STATUS.md b/CROSS_UNIT_STATUS.md new file mode 100644 index 0000000000..b83cfc162e --- /dev/null +++ b/CROSS_UNIT_STATUS.md @@ -0,0 +1,41 @@ +# Cross-Unit (Cross-Project) Dashboard — starting-point status + +**Branch:** `feature/cross-unit` (off `11.0.x`) · **Repos:** `doubtfire-web` + `doubtfire-api` +**State:** skeleton ported onto v11.0 and wired — *not yet built/tested* (see Verification). + +## What this feature is +A single dashboard that shows a student's tasks across **all** their units in one view +(`/dashboard`, Student-only). Currently a bare unit→task list — the visual/UX build sits on top. + +## What landed in this port +Ported from the old `Feature/Cross-Unit` branch (forked ~`10.0.0`, ~561 web / 169 api commits +behind) and **re-homed onto v11.0** — not a git rebase. + +### doubtfire-web +- **Clean adds (7 files):** `src/app/dashboard/**` — `f-cross-dashboard`, `list-item/dashboard-list-item`, + `list-item/expanded-list-item/*`. Verified to compile against v11's `GlobalStateService` + (`onLoad`, `currentUserProjects.values`). +- **Wiring (4 edits), translated UI-Router → Angular Router:** + - `app.routes.ts` — new top-level `{path: 'dashboard', component: CrossDashboardComponent, + canActivate: [roleWhitelistGuard], data: {roleWhitelist: ['Student']}}`. + - `doubtfire-angular.module.ts` — registered the 3 components in `declarations`. + ⚠️ **Shared file — coordinate with `feature/notifications` and `peer-progress`.** + - `home/states/home/home.component.html` — added a "View all" (`/dashboard`) button beside + "View previous" (`/view-all-projects`); converted `uiSref` → `routerLink`. + - `projects/states/index/global-state.service.ts` — projects query now sends + `include_task_definitions: true` (also fixed the latent `include_in_active` → `include_inactive` + param typo so web matches the API). + +### doubtfire-api (`feature/cross-unit` branch there) +- `app/api/projects_api.rb` — new `include_task_definitions` param on `GET /projects`. +- `app/api/entities/project_entity.rb` — expose `tasks` when `include_task_definitions`. +- `app/api/entities/minimal/minimal_unit_entity.rb` — expose `task_definitions` when requested. + +## Verification (NOT yet run — do before relying on this) +- web: `npm ci && npm run build && npm run lint` +- api: `bundle exec rubocop` + relevant tests +- manual: log in as a Student → home shows the new button → `/dashboard` renders the unit list. + +## What the team builds on top +Task status/colours, due dates, sorting/filtering, Material styling, empty/loading states. +The port only gets the compiling skeleton onto v11 as a shared starting point. diff --git a/Dockerfile b/Dockerfile index 18590c09e9..0a0b3f0c7c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20 +FROM node:22 ENV DEBIAN_FRONTEND noninteractive ENV USER=node diff --git a/MIGRATION-GUIDE.md b/MIGRATION-GUIDE.md index 049c5d2e52..3c3800c24f 100644 --- a/MIGRATION-GUIDE.md +++ b/MIGRATION-GUIDE.md @@ -57,7 +57,7 @@ Notice the naming convention. When migrating a component we use the format _name Add the start of the typescript using something based on the following: ```typescript -import { Component, Input, Inject } from '@angular/core'; +import {Component, Inject, Input} from '@angular/core'; @Component({ selector: 'task-description-card', @@ -96,7 +96,6 @@ We want to make sure we can see our progress as quickly as possible. So lets sta There are a few files we need to update to achieve this. - Remove link to component from the angular module. - - Open the component's CoffeeScript file and make a note of the name of the module. ```coffeescript @@ -111,7 +110,7 @@ There are a few files we need to update to achieve this. - Setup the new component in **doubtfire-angular.module.ts** - Import like this: ```ts - import { TaskDescriptionCardComponent } from './projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component'; + import {TaskDescriptionCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component'; ``` - Then add the component name to the list of **declarations**. Now the component will be available in Angular. - Remove the old and downgrade the new in **doubtfire-angularjs.module.ts** @@ -124,24 +123,31 @@ There are a few files we need to update to achieve this. ```typescript DoubtfireAngularJSModule.directive( 'taskDescriptionCard', - downgradeComponent({ component: TaskDescriptionCardComponent }) + downgradeComponent({component: TaskDescriptionCardComponent}), ); ``` - Update attributes on the new component usage. - - Search for all of the places where the component was already used (i.e. search for the component HTML tag). - Update the property binding style to use the Angular form which is `[property]="value"`. For example: ```html - + ``` Needs to change to: ```html - + ``` - Add matching inputs into your components typescript declaration. These use the syntax `@Input() name: type;`. For the task description card we use: diff --git a/README.md b/README.md index 5f4e969966..b050cbf4a6 100644 --- a/README.md +++ b/README.md @@ -1,238 +1,20 @@ -![Doubtfire Logo](src/assets/icons/android-chrome-192x192.png) +

+ OnTrack logo +

-# Doubtfire Web [![CI](https://img.shields.io/github/workflow/status/doubtfire-lms/doubtfire-web/Node.js%20CI?label=CI&logo=GitHub)](https://github.com/doubtfire-lms/doubtfire-web/actions/workflows/nodejs-ci.yml) +# OnTrack Web [![CI](https://img.shields.io/github/workflow/status/doubtfire-lms/doubtfire-web/Node.js%20CI?label=CI&logo=GitHub)](https://github.com/doubtfire-lms/doubtfire-web/actions/workflows/nodejs-ci.yml) A modern, lightweight learning management system. -> ## 🛠 Migration Status: In Development -> -> Doubtfire web migration from AngularJS/Coffeescript to Angular/Typescript, including refactoring all components, is currently in development. -> -> See the progress of component migration below. - -## Migration Progress - -SUMMARY: - -74 / 132 components migrated - -MIGRATED: - -- [x] ./src/app/home/splash-screen/splash-screen.component.ts -- [x] ./src/app/home/states/home/home.component.ts -- [x] ./src/app/tasks/task-submission-history/task-submission-history.component.ts -- [x] ./src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.ts -- [x] ./src/app/tasks/task-comments-viewer/extension-comment/extension-comment.component.ts -- [x] ./src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.ts -- [x] ./src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-recorder/intelligent-discussion-recorder.component.ts -- [x] ./src/app/tasks/task-comments-viewer/pdf-image-comment/pdf-image-comment.component.ts -- [x] ./src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.ts -- [x] ./src/app/tasks/task-comments-viewer/task-comments-viewer.component.ts -- [x] ./src/app/tasks/task-comment-composer/task-comment-composer.component.ts -- [x] ./src/app/tasks/task-comment-composer/discussion-prompt-composer/discussion-prompt-composer.component.ts -- [x] ./src/app/projects/states/dashboard/directives/student-task-list/create-portfolio-task-list-item/create-portfolio-task-list-item.component.ts -- [x] ./src/app/projects/states/dashboard/directives/student-task-list/task-list-item/task-list-item.component.ts -- [x] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component.ts -- [x] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component.ts -- [x] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component.ts -- [x] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.ts -- [x] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component.ts -- [x] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component.ts -- [x] ./src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.ts -- [x] ./src/app/admin/institution-settings/overseer-images/overseer-image-list.component.ts -- [x] ./src/app/admin/institution-settings/institution-settings.component.ts -- [x] ./src/app/admin/institution-settings/campuses/campus-list/campus-list.component.ts -- [x] ./src/app/admin/institution-settings/activity-type-list/activity-type-list.component.ts -- [x] ./src/app/admin/tii-action-log/tii-action-log.component.ts -- [x] ./src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.ts -- [x] ./src/app/admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog.ts -- [x] ./src/app/eula/accept-eula/accept-eula.component.ts -- [x] ./src/app/welcome/welcome.component.ts -- [x] ./src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts -- [x] ./src/app/units/states/tasks/inbox/inbox.component.ts -- [x] ./src/app/units/states/edit/directives/unit-students-editor/student-tutorial-select/student-tutorial-select.component.ts -- [x] ./src/app/units/states/edit/directives/unit-students-editor/unit-students-editor.component.ts -- [x] ./src/app/units/states/edit/directives/unit-students-editor/student-campus-select/student-campus-select.component.ts -- [x] ./src/app/units/states/edit/directives/unit-tutorials-manager/unit-tutorials-manager.component.ts -- [x] ./src/app/units/states/edit/directives/unit-tutorials-list/unit-tutorials-list.component.ts -- [x] ./src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts -- [x] ./src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-who/task-definition-who.component.ts -- [x] ./src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-dates/task-definition-dates.component.ts -- [x] ./src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-options/task-definition-options.component.ts -- [x] ./src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-resources/task-definition-resources.component.ts -- [x] ./src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-overseer/task-definition-overseer.component.ts -- [x] ./src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.ts -- [x] ./src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-general/task-definition-general.component.ts -- [x] ./src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component.ts -- [x] ./src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts -- [x] ./src/app/units/states/analytics/unit-analytics-route.component.ts -- [x] ./src/app/common/footer/footer.component.ts -- [x] ./src/app/common/audio-recorder/audio/audio-comment-recorder/audio-comment-recorder.ts -- [x] ./src/app/common/audio-recorder/audio/microphone-tester/microphone-tester.component.ts -- [x] ./src/app/common/audio-player/audio-player.component.ts -- [x] ./src/app/common/edit-profile-form/edit-profile-form.component.ts -- [x] ./src/app/common/file-drop/file-drop.component.ts -- [x] ./src/app/common/modals/extension-modal/extension-modal.component.ts -- [x] ./src/app/common/modals/about-doubtfire-modal/about-doubtfire-modal.component.ts -- [x] ./src/app/common/modals/calendar-modal/calendar-modal.component.ts -- [x] ./src/app/common/modals/task-assessment-modal/task-assessment-modal.component.ts -- [x] ./src/app/common/pdf-viewer/pdf-viewer.component.ts -- [x] ./src/app/common/obect-select/object-select.component.ts -- [x] ./src/app/common/hero-sidebar/hero-sidebar.component.ts -- [x] ./src/app/common/project-progress-bar/project-progress-bar.component.ts -- [x] ./src/app/common/f-chip/f-chip.component.ts -- [x] ./src/app/common/status-icon/status-icon.component.ts -- [x] ./src/app/common/user-badge/user-badge.component.ts -- [x] ./src/app/common/file-viewer/file-viewer.component.ts -- [x] ./src/app/common/user-icon/user-icon.component.ts -- [x] ./src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.ts -- [x] ./src/app/common/header/header.component.ts -- [x] ./src/app/common/header/task-dropdown/task-dropdown.component.ts -- [x] ./src/app/common/header/unit-dropdown/unit-dropdown.component.ts -- [x] ./src/app/common/services/alert.service.ts -- [x] ./src/app/sessions/states/sign-in/sign-in.component.ts -- [x] ./src/app/account/edit-profile/edit-profile.component.ts -- [x] ./src/app/groups/group-set-selector/group-set-selector.component.ts -- [x] ./src/app/admin/modals/create-unit-modal/create-unit-modal.coffee - -TODO: - -- [ ] ./src/app/visualisations/alignment-bar-chart.coffee -- [ ] ./src/app/visualisations/summary-task-status-scatter.coffee -- [ ] ./src/app/visualisations/target-grade-pie-chart.coffee -- [ ] ./src/app/visualisations/achievement-custom-bar-chart.coffee -- [ ] ./src/app/visualisations/student-task-status-pie-chart.coffee -- [ ] ./src/app/visualisations/alignment-bullet-chart.coffee -- [ ] ./src/app/visualisations/progress-burndown-chart.coffee -- [ ] ./src/app/visualisations/task-status-pie-chart.coffee -- [ ] ./src/app/visualisations/achievement-box-plot.coffee -- [ ] ./src/app/visualisations/task-completion-box-plot.coffee -- [ ] ./src/app/visualisations/visualisations.coffee -- [ ] ./src/app/tasks/task-status-selector/task-status-selector.coffee -- [ ] ./src/app/tasks/tasks.coffee -- [ ] ./src/app/tasks/modals/modals.coffee -- [ ] ./src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee -- [ ] ./src/app/tasks/modals/grade-task-modal/grade-task-modal.coffee -- [ ] ./src/app/tasks/task-definition-selector/task-definition-selector.coffee -- [ ] ./src/app/tasks/project-tasks-list/project-tasks-list.coffee -- [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.coffee -- [ ] ./src/app/tasks/task-ilo-alignment/modals/task-ilo-alignment-modal/task-ilo-alignment-modal.coffee -- [ ] ./src/app/tasks/task-ilo-alignment/modals/task-ilo-alignment.coffee -- [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.coffee -- [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment.coffee -- [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment-viewer/task-ilo-alignment-viewer.coffee -- [ ] ./src/app/config/privacy-policy/privacy-policy.coffee -- [ ] ./src/app/config/config.coffee -- [ ] ./src/app/config/runtime/runtime.coffee -- [ ] ./src/app/config/root-controller/root-controller.coffee -- [ ] ./src/app/config/local-storage/local-storage.coffee -- [ ] ./src/app/config/routing/routing.coffee -- [ ] ./src/app/config/vendor-dependencies/vendor-dependencies.coffee -- [ ] ./src/app/config/analytics/analytics.coffee -- [ ] ./src/app/config/debug/debug.coffee -- [ ] ./src/app/projects/projects.coffee -- [ ] ./src/app/projects/project-progress-dashboard/project-progress-dashboard.coffee -- [ ] ./src/app/projects/states/states.coffee -- [ ] ./src/app/projects/states/all/directives/directives.coffee -- [ ] ./src/app/projects/states/all/directives/all-projects-list/all-projects-list.coffee -- [ ] ./src/app/projects/states/all/all.coffee -- [ ] ./src/app/projects/states/groups/groups.coffee -- [ ] ./src/app/projects/states/feedback/feedback.coffee -- [ ] ./src/app/projects/states/dashboard/directives/directives.coffee -- [ ] ./src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.coffee -- [ ] ./src/app/projects/states/dashboard/directives/student-task-list/student-task-list.coffee -- [ ] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/directives.coffee -- [ ] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/task-outcomes-card/task-outcomes-card.coffee -- [ ] ./src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.coffee -- [ ] ./src/app/projects/states/dashboard/dashboard.coffee -- [ ] ./src/app/projects/states/outcomes/outcomes.coffee -- [ ] ./src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.coffee -- [ ] ./src/app/projects/states/portfolio/directives/directives.coffee -- [ ] ./src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.coffee -- [ ] ./src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.coffee -- [ ] ./src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee -- [ ] ./src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.coffee -- [ ] ./src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.coffee -- [ ] ./src/app/projects/states/portfolio/portfolio.coffee -- [ ] ./src/app/projects/states/index/index.coffee -- [ ] ./src/app/projects/states/tutorials/tutorials.coffee -- [ ] ./src/app/projects/project-outcome-alignment/project-outcome-alignment.coffee -- [ ] ./src/app/admin/modals/modals.coffee -- [ ] ./src/app/admin/modals/create-unit-modal/create-unit-modal.coffee -- [ ] ./src/app/admin/states/states.coffee -- [ ] ./src/app/admin/states/units/units.coffee -- [ ] ./src/app/admin/states/users/users.coffee -- [ ] ./src/app/admin/admin.coffee -- [ ] ./src/app/groups/group-selector/group-selector.coffee -- [ ] ./src/app/groups/group-set-manager/group-set-manager.coffee -- [ ] ./src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.coffee -- [ ] ./src/app/groups/group-member-list/group-member-list.coffee -- [ ] ./src/app/groups/tutor-group-manager/tutor-group-manager.coffee -- [ ] ./src/app/groups/groups.coffee -- [ ] ./src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.coffee -- [ ] ./src/app/units/modals/modals.coffee -- [ ] ./src/app/units/modals/unit-ilo-edit-modal/unit-ilo-edit-modal.coffee -- [ ] ./src/app/units/units.coffee -- [ ] ./src/app/units/states/states.coffee -- [ ] ./src/app/units/states/tasks/inbox/inbox.coffee -- [ ] ./src/app/units/states/tasks/tasks.coffee -- [ ] ./src/app/units/states/tasks/viewer/directives/directives.coffee -- [ ] ./src/app/units/states/tasks/viewer/directives/task-sheet-view/task-sheet-view.coffee -- [ ] ./src/app/units/states/tasks/viewer/directives/task-details-view/task-details-view.coffee -- [ ] ./src/app/units/states/tasks/viewer/directives/unit-task-list/unit-task-list.coffee -- [ ] ./src/app/units/states/tasks/viewer/viewer.coffee -- [ ] ./src/app/units/states/tasks/definition/definition.coffee -- [ ] ./src/app/units/states/portfolios/portfolios.coffee -- [ ] ./src/app/units/states/all/directives/all-units-list/all-units-list.coffee -- [ ] ./src/app/units/states/all/directives/directives.coffee -- [ ] ./src/app/units/states/all/all.coffee -- [ ] ./src/app/units/states/groups/groups.coffee -- [ ] ./src/app/units/states/edit/directives/directives.coffee -- [ ] ./src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.coffee -- [ ] ./src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.coffee -- [ ] ./src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.coffee -- [ ] ./src/app/units/states/edit/edit.coffee -- [ ] ./src/app/units/states/rollover/directives/directives.coffee -- [ ] ./src/app/units/states/rollover/directives/unit-dates-selector/unit-dates-selector.coffee -- [ ] ./src/app/units/states/rollover/rollover.coffee -- [ ] ./src/app/units/states/index/index.coffee -- [ ] ./src/app/units/states/students-list/students-list.coffee -- [ ] ./src/app/units/states/analytics/analytics.coffee -- [ ] ./src/app/common/filters/filters.coffee -- [ ] ./src/app/common/content-editable/content-editable.coffee -- [ ] ./src/app/common/alert-list/alert-list.coffee -- [ ] ./src/app/common/modals/confirmation-modal/confirmation-modal.coffee -- [ ] ./src/app/common/modals/comments-modal/comments-modal.coffee -- [ ] ./src/app/common/modals/modals.coffee -- [ ] ./src/app/common/modals/csv-result-modal/csv-result-modal.coffee -- [ ] ./src/app/common/modals/progress-modal/progress-modal.coffee -- [ ] ./src/app/common/grade-icon/grade-icon.coffee -- [ ] ./src/app/common/file-uploader/file-uploader.coffee -- [ ] ./src/app/common/common.coffee -- [ ] ./src/app/common/services/grade-service.coffee -- [ ] ./src/app/common/services/date-service.coffee -- [ ] ./src/app/common/services/alert-service.coffee -- [ ] ./src/app/common/services/media-service.coffee -- [ ] ./src/app/common/services/recorder-service.coffee -- [ ] ./src/app/common/services/outcome-service.coffee -- [ ] ./src/app/common/services/listener-service.coffee -- [ ] ./src/app/common/services/analytics-service.coffee -- [ ] ./src/app/common/services/services.coffee -- [ ] ./src/app/sessions/auth/http-auth-injector.coffee -- [ ] ./src/app/sessions/sessions.coffee -- [ ] ./src/app/errors/errors.coffee -- [ ] ./src/app/errors/states/states.coffee -- [ ] ./src/app/errors/states/unauthorised/unauthorised.coffee -- [ ] ./src/app/errors/states/not-found/not-found.coffee -- [ ] ./src/app/errors/states/timeout/timeout.coffee - ## Table of Contents -1. [Getting Started](#getting-started) -2. [Resources](#resources) -3. [Contributing](#contributing) -4. [Deployment](#deployment) -5. [License](#license) +- [Doubtfire Web ![CI](https://github.com/doubtfire-lms/doubtfire-web/actions/workflows/nodejs-ci.yml)](#doubtfire-web-) + - [Table of Contents](#table-of-contents) + - [Getting Started](#getting-started) + - [Deployment](#deployment) + - [Resources](#resources) + - [Contributing](#contributing) + - [License](#license) ## Getting Started @@ -319,18 +101,11 @@ You may prefix this command with the following environment variables: ## Resources -Doubtfire Web is an [Angular](http://angularjs.org) application built using [Bootstrap](http://getbootstrap.com). It uses many Open Source libraries, which you can read up on: +Doubtfire Web is an [Angular](https://angular.dev) application built using [Material UI]https://material.angular.dev). It uses many Open Source libraries, which you can read up on: - [Lodash](http://lodash.com/docs) - [Moment.js](http://momentjs.com) -- [Font Awesome](http://fontawesome.io) -- [UI Router](https://github.com/angular-ui/ui-router) -- [UI Bootstrap](http://angular-ui.github.io/bootstrap/versioned-docs/0.13.4/) -- [UI Select](https://github.com/angular-ui/ui-select) - [NVD3 Charts](http://krispo.github.io/angular-nvd3/#/) -- [Angular X-Editable](http://vitalets.github.io/angular-xeditable/) -- [Angular Filters](https://github.com/a8m/angular-filter) -- [Angular Markdown Filter](https://github.com/vpegado/angular-markdown-filter) ## Contributing diff --git a/angular.json b/angular.json index 6a528bd8c1..029aa7eafc 100644 --- a/angular.json +++ b/angular.json @@ -10,15 +10,18 @@ "prefix": "f", "schematics": { "@schematics/angular:application": { - "strict": false + "strict": false, + "standalone": false, + "style": "scss", + "skipTests": true } }, "architect": { "build": { - "builder": "@angular-devkit/build-angular:application", + "builder": "@angular/build:application", "options": { "outputPath": "dist", - "index": "build/index.html", + "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["src/polyfills.ts"], "tsConfig": "src/tsconfig.app.json", @@ -39,29 +42,18 @@ "loader": { ".ttf": "binary" }, + "stylePreprocessorOptions": { + "includePaths": ["src"] + }, "styles": [ "src/theme.scss", "src/styles.scss", - "./node_modules/bootstrap/dist/css/bootstrap.css", - "./node_modules/ngx-bootstrap/datepicker/bs-datepicker.css", - "./build/assets/doubtfire.css", - "./build/assets/node_modules/angular-xeditable/dist/css/xeditable.css", - "./build/assets/node_modules/codemirror/lib/codemirror.css", - "./build/assets/node_modules/codemirror/theme/xq-light.css", - "./build/assets/node_modules/nvd3/build/nv.d3.css", "node_modules/@ctrl/ngx-emoji-mart/picker.css", "node_modules/@worktile/gantt/styles/index.scss" ], "scripts": [ "node_modules/moment/moment.js", - "node_modules/d3/d3.js", - "node_modules/lodash/lodash.js", - "node_modules/underscore.string/dist/underscore.string.min.js", - "node_modules/nvd3/build/nv.d3.js", - "node_modules/es5-shim/es5-shim.js", - "node_modules/codemirror/lib/codemirror.js", - "node_modules/codemirror/addon/display/placeholder.js", - "node_modules/codemirror/mode/markdown/markdown.js", + "node_modules/d3/dist/d3.min.js", "node_modules/canvas-confetti/dist/confetti.browser.js" ], "extractLicenses": false, @@ -97,6 +89,13 @@ "extractLicenses": false, "sourceMap": true }, + "testing": { + "aot": false, + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "polyfills": ["zone.js"] + }, "devcontainer": { "optimization": false, "extractLicenses": false, @@ -111,7 +110,7 @@ } }, "serve": { - "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular/build:dev-server", "options": { "buildTarget": "doubtfire:build", "port": 4200, @@ -135,30 +134,24 @@ } }, "extract-i18n": { - "builder": "@angular-devkit/build-angular:extract-i18n", + "builder": "@angular/build:extract-i18n", "options": { "buildTarget": "doubtfire:build" } }, "test": { - "builder": "@angular-devkit/build-angular:karma", + "builder": "@angular/build:unit-test", "options": { - "main": "src/test.ts", - "polyfills": "src/polyfills.ts", + "buildTarget": "doubtfire:build:testing", "tsConfig": "src/tsconfig.spec.json", - "karmaConfig": "src/karma.conf.js", - "styles": [ - "./node_modules/bootstrap/dist/css/bootstrap.css", - "./node_modules/ngx-bootstrap/datepicker/bs-datepicker.css" - ], - "scripts": [], - "assets": ["src/favicon.ico", "src/assets", "src/manifest.webmanifest"] + "runner": "vitest", + "setupFiles": ["src/vitest-setup.ts"] } }, "lint": { "builder": "@angular-eslint/builder:lint", "options": { - "lintFilePatterns": ["src/**/*.ts", "src/**/*.component.html"] + "lintFilePatterns": ["src/**/*.ts", "src/**/*.component.html", "src/**/*.html"] } }, "eslint": { @@ -171,14 +164,35 @@ } }, "schematics": { - "@schematics/angular:component": { - "style": "scss" - }, "@angular-eslint/schematics:application": { "setParserOptionsProject": true }, "@angular-eslint/schematics:library": { "setParserOptionsProject": true + }, + "@schematics/angular:component": { + "type": "component" + }, + "@schematics/angular:directive": { + "type": "directive" + }, + "@schematics/angular:service": { + "type": "service" + }, + "@schematics/angular:guard": { + "typeSeparator": "." + }, + "@schematics/angular:interceptor": { + "typeSeparator": "." + }, + "@schematics/angular:module": { + "typeSeparator": "." + }, + "@schematics/angular:pipe": { + "typeSeparator": "." + }, + "@schematics/angular:resolver": { + "typeSeparator": "." } }, "cli": { diff --git a/build.config.js b/build.config.js index 2f9b32b333..ffb10f91a4 100644 --- a/build.config.js +++ b/build.config.js @@ -24,9 +24,9 @@ module.exports = { api: { src: ['build/src/app/api/api-url.js', 'build/src/app/config/external-name/external-name.js'], options: { - inline: true - } - } + inline: true, + }, + }, }, /** @@ -38,31 +38,15 @@ module.exports = { * app's unit tests. */ app_files: { - js: [ - 'src/**/*.js', - '!src/**/*.spec.js', - '!src/assets/**/*.js' - ], - jsunit: [ - 'src/**/*.spec.js' - ], + js: ['src/**/*.js', '!src/**/*.spec.js', '!src/assets/**/*.js'], + jsunit: ['src/**/*.spec.js'], - coffee: [ - 'src/**/*.coffee', - '!src/**/*.spec.coffee', - '!src/**/*.old.coffee' - ], - coffeeunit: [ - 'src/**/*.spec.coffee' - ], + coffee: ['src/**/*.coffee', '!src/**/*.spec.coffee', '!src/**/*.old.coffee'], + coffeeunit: ['src/**/*.spec.coffee'], - atpl: [ - 'src/app/**/*.tpl.html' - ], + atpl: ['src/app/**/*.tpl.html'], - html: [ - 'src/index.html' - ], + html: ['src/index.html'], scss: [ // Do not modify the order @@ -70,8 +54,8 @@ module.exports = { 'src/styles/common/**/*.scss', 'src/styles/modules/**/*.scss', 'src/app/**/*.scss', - '!src/app/**/*.component.scss' - ] + '!src/app/**/*.component.scss', + ], }, /** @@ -92,26 +76,22 @@ module.exports = { */ vendor_files: { compile: { - js: [ - ], - jsmap: [ - ], + js: [], + jsmap: [], scss: [ 'node_modules/bootstrap-sass/**/_bootstrap.scss', - 'node_modules/font-awesome/**/font-awesome.scss' + 'node_modules/font-awesome/**/font-awesome.scss', ], }, copy: { - js: [ - ], - jsmap: [ - ], + js: [], + jsmap: [], css: [ - "node_modules/nvd3/build/nv.d3.css", - "node_modules/angular-xeditable/dist/css/xeditable.css", - "node_modules/codemirror/lib/codemirror.css", - "node_modules/codemirror/theme/xq-light.css" - ] - } - } + 'node_modules/nvd3/build/nv.d3.css', + 'node_modules/angular-xeditable/dist/css/xeditable.css', + 'node_modules/codemirror/lib/codemirror.css', + 'node_modules/codemirror/theme/xq-light.css', + ], + }, + }, }; diff --git a/commitlint.config.js b/commitlint.config.js index 28fe5c5bf9..3347cb961c 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1 +1 @@ -module.exports = {extends: ['@commitlint/config-conventional']} +module.exports = {extends: ['@commitlint/config-conventional']}; diff --git a/deploy.Dockerfile b/deploy.Dockerfile index 9aa69355e3..ce51c53b13 100644 --- a/deploy.Dockerfile +++ b/deploy.Dockerfile @@ -1,5 +1,9 @@ ### STAGE 1: Build ### -FROM node:20 AS build +FROM node:22 AS build + +RUN apt-get update && apt-get install -y --no-install-recommends \ + gettext-base \ + && rm -rf /var/lib/apt/lists/* USER node @@ -12,8 +16,29 @@ RUN npm ci --force --include=optional COPY --chown=node:node . . RUN chmod 777 src +ARG SENTRY_DSN +ARG SENTRY_ORG +ARG SENTRY_PROJECT +ARG SENTRY_RELEASE +ARG SENTRY_DIST +ARG UPLOAD_SENTRY_SOURCEMAPS=false +ENV SENTRY_DSN=$SENTRY_DSN +ENV SENTRY_ORG=$SENTRY_ORG +ENV SENTRY_PROJECT=$SENTRY_PROJECT +ENV SENTRY_RELEASE=$SENTRY_RELEASE +ENV SENTRY_DIST=$SENTRY_DIST +RUN envsubst '${SENTRY_DSN} ${SENTRY_RELEASE} ${SENTRY_DIST}' < src/environments/environment.prod.ts > src/environments/environment.prod.ts.tmp && mv src/environments/environment.prod.ts.tmp src/environments/environment.prod.ts + # Launch - build to dist folder -RUN npm run-script deploy +RUN --mount=type=secret,id=sentry_auth_token,uid=1000 \ + if [ "$UPLOAD_SENTRY_SOURCEMAPS" = "true" ]; then \ + npm run deploy:build2api:sourcemaps; \ + SENTRY_AUTH_TOKEN="$(cat /run/secrets/sentry_auth_token)" npm run sentry:sourcemaps; \ + find dist/browser -name '*.map' -delete; \ + npx ngsw-config dist/browser ngsw-config.json /; \ + else \ + npm run-script deploy; \ + fi ## STAGE 2: Host ### diff --git a/docker-compose.yml b/docker-compose.yml index 9019dbcc4b..ee16895e5d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: container_name: web-doubtfire-api image: lmsdoubtfire/doubtfire-api:7.0-dev ports: - - "3000:3000" + - '3000:3000' volumes: - ../data/tmp:/doubtfire/tmp - ../data/student-work:/student-work @@ -74,8 +74,8 @@ services: build: . command: /bin/bash -c 'npm install; npm start' ports: - - "4200:4200" - - "9876:9876" + - '4200:4200' + - '9876:9876' depends_on: - doubtfire-api volumes: diff --git a/docs/PULL_REQUEST_TEMPLATE.md b/docs/PULL_REQUEST_TEMPLATE.md index ba3fe15bdc..92d5dc9709 100644 --- a/docs/PULL_REQUEST_TEMPLATE.md +++ b/docs/PULL_REQUEST_TEMPLATE.md @@ -1,35 +1,41 @@ -_Any italic text should be deleted from the final Pull Request text, including this line_ +## Jira ticket -# Description +Ticket number or link: -_Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change._ +## Summary -Fixes # (issue) +Briefly explain what you changed and why. -## Type of change +## Target branch -_Please delete options that are not relevant._ +Which shared branch should this be merged into? -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] This change requires a documentation update +Example: `feature/email-notifications` -# How Has This Been Tested? +## Testing -_Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration_ +Explain how you tested the change. -## Testing Checklist: +Include any useful commands, screenshots, logs, or test results. -- [ ] Tested in latest Chrome -- [ ] Tested in latest Safari -- [ ] Tested in latest Firefox +## Security and privacy -# Checklist: +Does this change affect authentication, permissions, notifications, student data, +secrets, personal information, or privacy? -- [ ] My code follows the style guidelines of this project -- [ ] I have performed a self-review of my own code -- [ ] I have commented my code in hard-to-understand areas -- [ ] I have made corresponding changes to the documentation -- [ ] My changes generate no new warnings -- [ ] I have requested a review from @macite and @jakerenzella on the Pull Request +If there is no known impact, write: `No known security or privacy impact.` + +## Evidence + +Add any screenshots, test output, diagrams, or other evidence that will help the reviewer. + +## Checklist + +- [ ] I selected the correct base branch. +- [ ] My changes match the assigned Jira ticket. +- [ ] I kept the change within the agreed scope. +- [ ] I tested my changes. +- [ ] I did not include passwords, tokens, API keys, secrets, or real student data. +- [ ] I updated relevant documentation, or no documentation change was needed. +- [ ] I reviewed my own changes before requesting review. +- [ ] This pull request is ready for review. diff --git a/docs/cpd-data-source-map.md b/docs/cpd-data-source-map.md new file mode 100644 index 0000000000..83731cfe6a --- /dev/null +++ b/docs/cpd-data-source-map.md @@ -0,0 +1,132 @@ +**Cross-unit Dashboard (CPD): Data Sources & Ownership Rules** + +**Branch:** docs/cpd-data-source-map + +**Purpose:** Provide frontend, backend, and security contributors with a +single source of truth regarding dashboard data sources, role +permissions, cross-unit visibility rules. + +**Field-to-Source Mapping Table** + +This table maps user-facing fields on the Cross-unit Dashboard to their +underlying backend services and models: +| Dashboard Field / Element | Angular Component / Data Binding | API Endpoint & Method | Backend Model / Entity | Access Scope & Permissions | +| :--- | :--- | :--- | :--- | :--- | +| **Unit Code & Name** | `DashboardUnit.code`, `DashboardUnit.name` | `GET /projects` | `Unit#code`, `Unit#name` via `Project.eager_load(:unit)` | Authenticated Student (`for_user current_user`) | +| **Task Title & Subtitle** | `DashboardTask.title`, `DashboardTask.subtitle` | `GET /projects` (`include_task_definitions=true`) | `TaskDefinition#name`, `TaskDefinition#abbreviation`, `TaskDefinition#targetGradeText` | Authenticated Student (`current_user`) | +| **Task Description** | `DashboardTask.description` | `GET /projects` (`include_task_definitions=true`) | `TaskDefinition#description` | Authenticated Student (`current_user`) | +| **Task Status & Color** | `DashboardTask.status`, `statusLabel`, `color` | `GET /projects` | `Task#status`, mapped via `TaskStatus.STATUS_LABELS/COLORS` | Authenticated Student (`current_user`) | +| **New Comments Count** | `DashboardTask.comments` | `GET /projects` | `Task#numNewComments` | Authenticated Student (`current_user`) | +| **Due Date** | `DashboardTask.dueDate` | `GET /projects` | `TaskDefinition#targetDate` | Authenticated Student (`current_user`) | +| **Task Weight / Priority** | `DashboardTask.weight` | `GET /projects` | `Task#topWeight` (calculated via `project.calcTopTasks()`) | Authenticated Student (`current_user`) | +| **Project ID / Unit Key** | `DashboardUnit.projectId` | `GET /projects` | `Project#id` | Authenticated Student (`current_user`) | + +**Data Ownership, Enrolment & Visibility Rules** + +**Active vs. Inactive Enrolment Filtering** + +- **Backend Inactive Toggle:** GET /projects accepts an optional query + parameter. By default (false), only active unit projects for + current_user are fetched. + +- **Frontend Active Task Scope:** CrossDashboardComponent populates + unit tasks via project.activeTasks(), ensuring archived or inactive + tasks are excluded from the default view. + +**Client-Side State, Filtering & Sorting Rules** + +- **Hide Completed Filter (Filter.HideCompleted):** Managed via + private filters: Map\. When active, tasks with + task.status == \'complete\' (completedTypes) are removed from + unitsProcessed. + +- **Default Task Ordering:** In all sort modes, completed tasks are + pushed to the bottom of the list (completedTypes.includes(a.status) + evaluation). + +- **Due Date Sort (SortMode.SubmissionDate):** Orders active tasks + chronologically using a.dueDate.getTime() - b.dueDate.getTime() + (TaskDefinition#targetDate). + +- **Default Weight Sort (SortMode.Default):** Orders tasks by + calculated priority weight using a.weight - b.weight + (Task#topWeight). + +**Role-Based Access Control & Data Isolation** + +- **Student Access:** All dashboard data subscriptions execute through + GlobalStateService.currentUserProjects, which calls ProjectsApi + endpoints protected by authenticated?. Queries are strictly scoped + to the authenticated session (for_user current_user). + +- **Write Safeguards:** Modifying target grades or submitted grades + via PUT /projects/:id is restricted once portfolio_exists? evaluates + to true (returns HTTP 403 Forbidden). + +**Technical Gaps** + +During this documentation audit, the following technical gaps were +identified directly in the source code: + +1. **SortMode.Recommended Stub:** In + CrossDashboardComponent.processTasks(), the recommended sort mode is + stubbed out with // TODO: Connect to recommender\'s points and + returns 0. Recommended sorting currently falls back to default + ordering and is not connected to the backend Task Prioritisation + Recommender Service. + +2. **In-Memory Filtering & Performance:** Task filtering (Hide + Completed) and sorting are processed entirely client-side on the + Angular main thread (this.unitsProcessed = this.units.map(\...)). + Heavy task volumes across multiple units may impact frontend + rendering performance. + +3. **Missing Inactive Unit Filter Toggle in UI:** While ProjectsApi + supports include_inactive: Boolean, CrossDashboardComponent does not + currently expose an interface control to pass this query parameter + to GlobalStateService. + +**Data-Flow Diagram** + +```text + [ CrossDashboardComponent ] + | + | (1) Subscribes to currentUserProjects + v + [ Frontend: GlobalStateService ] + | + | (2) GET /projects?include_task_definitions=true + v + [ Backend API: ProjectsApi ] + | + | (3) Project.eager_load(:unit, :user).for_user(current_user) + v + [ PostgreSQL Database ] + | + | (4) Represents array via Entities::ProjectEntity + v + [ CrossDashboardComponent: mapProjects() & mapTasks() ] + | + | (5) In-Memory Filter/Sort via processTasks() + v + [ Rendered Dashboard Unit Cards ] +``` + +**Recommended Follow-Up Tickets** + +- **Integrate Task Prioritisation Recommender with + SortMode.Recommended:** Connect SortMode.Recommended in + CrossDashboardComponent to the backend recommender service priority + scores, replacing the current return 0 stub. + +- **Add UI Toggle for Inactive/Archived Units:** Add a filter option + to pass include_inactive=true to GlobalStateService, allowing + students to view historical unit projects. + +- **Batch Priority Points in ProjectEntity:** Extend + Entities::ProjectEntity to include pre-calculated recommender + weights per task definition to streamline frontend sorting. + +- **Unit Test Coverage for CrossDashboardComponent:** Write Angular + unit tests for toggleFilter(), setSort(), and processTasks() to + prevent sorting regressions. diff --git a/env.config.js b/env.config.js index 5bf43b8dbf..60f507f0f5 100644 --- a/env.config.js +++ b/env.config.js @@ -2,30 +2,30 @@ module.exports = { env: { options: { // Global options - EXTERNAL_NAME: process.env.DF_EXTERNAL_NAME || 'Doubtfire' + EXTERNAL_NAME: process.env.DF_EXTERNAL_NAME || 'Doubtfire', }, development: { // Use the computer's IP address -- if using localhost then // testing on mobile devices will fail as it cannot point to // localhost (this would be the device itself!) - API_URL: 'http://' + require('ip').address() + ':3000/api' + API_URL: 'http://' + require('ip').address() + ':3000/api', }, production: { // Provide a set API_URL, otherwise window.location will be used // to locate the API dynamically (see api-url.coffee) - API_URL: process.env.DF_API_URL + API_URL: process.env.DF_API_URL, }, docker: { // API URL should use the DF_DOCKER_MACHINE_IP set - API_URL: 'http://' + process.env.DF_DOCKER_MACHINE_IP + ':3000/api' + API_URL: 'http://' + process.env.DF_DOCKER_MACHINE_IP + ':3000/api', }, devcontainer: { // API URL should use the DF_DOCKER_MACHINE_IP set - API_URL: 'http://' + process.env.DF_DOCKER_MACHINE_IP + ':4200/api' - } - } -} + API_URL: 'http://' + process.env.DF_DOCKER_MACHINE_IP + ':4200/api', + }, + }, +}; diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000000..41886408ff --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,133 @@ +// @ts-check + +// Allows us to bring in the recommended core rules from eslint itself +const eslint = require('@eslint/js'); + +// Allows us to use the typed utility for our config, and to bring in the recommended rules for TypeScript projects from typescript-eslint +const tseslint = require('typescript-eslint'); + +// Allows us to bring in the recommended rules for Angular projects from angular-eslint +const angular = require('angular-eslint'); + +const prettierPlugin = require('eslint-plugin-prettier'); + +const tailwindcss = require('eslint-plugin-tailwindcss'); + +/** + * Scope Angular template configs to HTML files. + * + * The cast is intentionally loose because angular-eslint and typescript-eslint + * can resolve different copies of @typescript-eslint utility types. + * + * @param {unknown[]} configs + * @returns {any[]} + */ +const htmlTemplateConfigs = (configs) => + configs.map((config) => ({ + .../** @type {object} */ (config), + files: ['**/*.html'], + })); + +// Export our config array, which is composed together thanks to the typed utility function from typescript-eslint +module.exports = tseslint.config( + { + ignores: ['build/**', 'coverage/**', 'dist/**', 'docs/**', '**/*.tpl.html'], + }, + { + linterOptions: { + reportUnusedDisableDirectives: false, + }, + }, + { + // Everything in this config object targets our TypeScript files (Components, Directives, Pipes etc) + files: ['**/*.ts'], + extends: [ + // Apply the recommended core rules + eslint.configs.recommended, + // Apply the recommended TypeScript rules + ...tseslint.configs.recommended, + // Optionally apply stylistic rules from typescript-eslint that improve code consistency + ...tseslint.configs.recommended, + // Apply the recommended Angular rules + ...angular.configs.tsRecommended, + ], + // Set the custom processor which will allow us to have our inline Component templates extracted + // and treated as if they are HTML files (and therefore have the .html config below applied to them) + processor: angular.processInlineTemplates, + // Override specific rules for TypeScript files (these will take priority over the extended configs above) + rules: { + '@angular-eslint/component-max-inline-declarations': ['error', {template: 0, styles: 0}], + '@angular-eslint/prefer-inject': 'off', + '@angular-eslint/prefer-standalone': 'off', + '@typescript-eslint/consistent-generic-constructors': ['error', 'type-annotation'], + '@typescript-eslint/no-inferrable-types': 'off', + '@angular-eslint/prefer-on-push-component-change-detection': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + ignoreRestSiblings: true, + varsIgnorePattern: '^_', + }, + ], + 'prettier/prettier': 'warn', + 'curly': ['error', 'all'], + }, + }, + { + files: ['**/*.ts'], + plugins: {prettier: prettierPlugin}, + rules: { + 'prettier/prettier': 'warn', + }, + }, + { + files: ['**/*.component.html'], + plugins: {prettier: prettierPlugin}, + extends: [tailwindcss.configs.recommended], + settings: { + tailwindcss: { + cssConfigPath: './src/tailwind-intellisense.css', + }, + }, + rules: { + 'tailwindcss/classnames-order': 'warn', + 'tailwindcss/enforces-shorthand': 'warn', + 'tailwindcss/no-contradicting-classname': 'warn', + 'tailwindcss/no-custom-classname': 'off', + 'tailwindcss/no-unnecessary-arbitrary-value': 'warn', + 'prettier/prettier': 'warn', + }, + }, + ...htmlTemplateConfigs(angular.configs.templateRecommended), + ...htmlTemplateConfigs(angular.configs.templateAccessibility), + { + // Everything in this config object targets our HTML files (external templates, + // and inline templates as long as we have the `processor` set on our TypeScript config above) + files: ['**/*.html'], + rules: { + '@angular-eslint/template/attributes-order': [ + 'error', + { + alphabetical: true, + order: [ + 'STRUCTURAL_DIRECTIVE', + 'TEMPLATE_REFERENCE', + 'ATTRIBUTE_BINDING', + 'INPUT_BINDING', + 'TWO_WAY_BINDING', + 'OUTPUT_BINDING', + ], + }, + ], + '@angular-eslint/template/prefer-control-flow': 'error', + // TODO: remove below eslint rule ignores to improve accessibility + '@angular-eslint/template/label-has-associated-control': 'off', + '@angular-eslint/template/mouse-events-have-key-events': 'off', + '@angular-eslint/template/click-events-have-key-events': 'off', + '@angular-eslint/template/interactive-supports-focus': 'off', + }, + }, +); diff --git a/nginx.conf b/nginx.conf index 679f2082fc..0914d0a344 100644 --- a/nginx.conf +++ b/nginx.conf @@ -11,9 +11,8 @@ http { index index.html; listen 80; - add_header Content-Security-Policy "default-src https: 'unsafe-inline' 'unsafe-eval' blob: data: ws:" always; - # add_header Feature-Policy "microphone=(self),speaker=(self),fullscreen=(self),payment=(none);" always; - add_header Permissions-Policy "microphone=(self),speaker=(self),fullscreen=(self),payment=(none)" always; + add_header Content-Security-Policy "default-src https: 'unsafe-inline' 'unsafe-eval' blob: data: ws:; worker-src 'self' blob:; child-src 'self' blob:" always; + add_header Permissions-Policy "microphone=(self),fullscreen=(self),payment=()" always; location / { try_files $uri $uri/ $uri/index.html /index.html; diff --git a/ngsw-config.json b/ngsw-config.json index 5c8069284f..c1e00b6bf7 100644 --- a/ngsw-config.json +++ b/ngsw-config.json @@ -1,8 +1,7 @@ { "$schema": "./node_modules/@angular/service-worker/config/schema.json", "index": "/index.html", - "dataGroups": - [ + "dataGroups": [ { "name": "api", "urls": ["/api"], @@ -11,6 +10,16 @@ "maxAge": "0u", "strategy": "freshness" } + }, + { + "name": "google-fonts-cache", + "urls": ["https://fonts.googleapis.com/**", "https://fonts.gstatic.com/**"], + "cacheConfig": { + "strategy": "freshness", + "maxSize": 10, + "maxAge": "7d", + "timeout": "5s" + } } ], "assetGroups": [ @@ -18,15 +27,10 @@ "name": "app", "installMode": "prefetch", "resources": { - "files": [ - "/favicon.ico", - "/index.html", - "/manifest.webmanifest", - "/*.css", - "/*.js" - ] + "files": ["/favicon.ico", "/index.html", "/manifest.webmanifest", "/*.css", "/*.js"] } - }, { + }, + { "name": "assets", "installMode": "lazy", "updateMode": "prefetch", @@ -36,14 +40,11 @@ "/*.(eot|svg|cur|jpg|png|png?default=blank&size=25webp|gif|otf|ttf|woff|woff2|ani)" ] } - }, { + }, + { "name": "external_assets", "resources": { - "urls": [ - "https://maxcdn.bootstrapcdn.com/bootstrap/**", - "https://fonts.googleapis.com/**", - "https://www.gravatar.com/avatar/**" - ] + "urls": ["https://www.gravatar.com/avatar/**"] } } ], @@ -55,6 +56,10 @@ "!/JPlag/**", "!/JPlag", "!/sidekiq/**", - "!/sidekiq" + "!/sidekiq", + "!/beta/**", + "!/beta", + "!/legacy", + "!/legacy/**" ] } diff --git a/package-lock.json b/package-lock.json index 5e16479f53..d4ffed0764 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,476 +1,387 @@ { "name": "doubtfire", - "version": "10.0.1-29", + "version": "11.0.0-45", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "doubtfire", - "version": "10.0.1-29", + "version": "11.0.0-45", "license": "AGPL-3.0", "dependencies": { - "@angular/animations": "^17.3.6", - "@angular/cdk": "^17.3.6", - "@angular/cli": "^17.3.6", - "@angular/common": "^17.3.6", - "@angular/compiler": "^17.3.6", - "@angular/core": "^17.3.6", - "@angular/forms": "^17.3.6", - "@angular/material": "^17.3.10", - "@angular/material-date-fns-adapter": "^17.3.10", - "@angular/platform-browser": "^17.3.6", - "@angular/platform-browser-dynamic": "^17.3.6", - "@angular/router": "^17.3.6", - "@angular/service-worker": "^17.3.6", - "@angular/upgrade": "^17.3.6", + "@angular/animations": "^22.0.3", + "@angular/cdk": "^22.0.2", + "@angular/common": "^22.0.3", + "@angular/compiler": "^22.0.3", + "@angular/core": "^22.0.3", + "@angular/forms": "^22.0.3", + "@angular/material": "^22.0.2", + "@angular/material-date-fns-adapter": "^22.0.2", + "@angular/platform-browser": "^22.0.3", + "@angular/platform-browser-dynamic": "^22.0.3", + "@angular/router": "^22.0.3", + "@angular/service-worker": "^22.0.3", "@ctrl/ngx-emoji-mart": "^9.3.0", + "@eslint/js": "^10.0.1", "@ngneat/hotkeys": "^4.0.0", - "@ngstack/code-editor": "7.3.0", - "@uirouter/angular": "^13.0", - "@uirouter/angular-hybrid": "^17.1.0", - "@uirouter/angularjs": "^1.0.30", - "@uirouter/core": "^6.1.0", - "@uirouter/rx": "^1.0.0", - "@worktile/gantt": "^18.0.5", - "angular": "1.5.11", - "angular-calendar": "^0.31.1", - "angular-filter": "0.5.17", - "angular-markdown-filter": "1.3.2", - "angular-md5": "0.1.10", - "angular-mocks": "1.8.3", - "angular-nvd3": "1.0.9", - "angular-resource": "1.5.11", - "angular-sanitize": "1.5.11", - "angular-ui-bootstrap": "0.13.4", - "angular-ui-codemirror": "0.3.0", - "angular-xeditable": "0.9.0", - "angulartics": "~1.0.3", - "angulartics-google-analytics": "0.1.4", - "bootstrap": "~3.4", - "bootstrap-sass": "~3.4", - "canvas-confetti": "^1.6.0", - "codemirror": "5.65.0", - "core-js": "^3.21.1", - "d3": "3.5.17", - "date-fns": "^3.6.0", - "es5-shim": "^4.5.12", - "file-saver": "^2.0.5", - "font-awesome": "~4.7.0", - "html2canvas": "^1.4.1", + "@ngstack/code-editor": "^9.0.0", + "@sentry/angular": "^10.61.0", + "@sentry/cli": "^3.5.1", + "@swimlane/ngx-charts": "^20.5.0", + "@tailwindcss/postcss": "^4.3.2", + "@worktile/gantt": "^21.0.0", + "angular-calendar": "^0.32.2", + "ansi-to-html": "^0.7.2", + "canvas-confetti": "^1.9", + "d3": "^7.9.0", + "date-fns": "^4.4.0", + "dompurify": "^3.4.11", + "html2canvas": "^1.0.0-rc.7", "html5-qrcode": "^2.3.8", - "jquery": "2.1.4", "jszip": "^3.10.1", - "lodash": "~4.18", "lottie-web": "^5.13.0", - "marked": "^11.1.0", - "moment": "^2.29.4", - "monaco-editor": "^0.44.0", - "ng-csv": "0.2.3", - "ng-file-upload": "~5.0.9", + "marked": "^18.0.5", + "moment": "^2.30", + "monaco-editor": "^0.55.1", "ng-flex-layout": "^17.3.7-beta.1", "ng2-pdf-viewer": "10.2.2", - "ngx-bootstrap": "^6.1.0", - "ngx-entity-service": "^0.0.41", - "ngx-lottie": "^11.0.2", - "ngx-monaco-editor-v2": "^17.0.1", - "nvd3": "1.8.6", + "ngx-entity-service": "^0.0.44", + "ngx-lottie": "^22.0.0", + "ngx-monaco-editor-v2-alternative": "^22.0.0", + "ngx-skeleton-loader": "^13.0.0", "qrcode": "^1.5.4", "rxjs": "~7.8.2", - "ts-md5": "^1.3.1", - "tslib": "^2.6.2", - "underscore.string": "2.3.3", - "zone.js": "~0.14" + "tslib": "^2.8.1", + "typescript-eslint": "^8.62.0", + "zone.js": "~0.16.2" }, "devDependencies": { - "@angular-devkit/build-angular": "^17.3.6", - "@angular-eslint/builder": "^17.3.0", - "@angular-eslint/eslint-plugin": "^17.3.0", - "@angular-eslint/eslint-plugin-template": "^17.3.0", - "@angular-eslint/schematics": "^17.3.0", - "@angular-eslint/template-parser": "^17.3.0", - "@angular/compiler-cli": "^17.3.6", - "@angular/language-service": "^17.3.6", - "@commitlint/cli": "^20.5.0", - "@commitlint/config-conventional": "^20", - "@types/angular": "1.5.11", + "@angular-eslint/builder": "^22.0.0", + "@angular-eslint/eslint-plugin": "^22.0.0", + "@angular-eslint/eslint-plugin-template": "^22.0.0", + "@angular-eslint/schematics": "^22.0.0", + "@angular-eslint/template-parser": "^22.0.0", + "@angular/build": "^22.0.4", + "@angular/cli": "^22.0.4", + "@angular/compiler-cli": "^22.0.3", + "@angular/language-service": "^22.0.3", + "@commitlint/cli": "^21.1.0", + "@commitlint/config-conventional": "^21.1.0", + "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/canvas-confetti": "^1.6.0", - "@types/d3": "^3.5.17", - "@types/file-saver": "^2.0.1", - "@types/jasmine": "~6.0.0", - "@types/jasminewd2": "~2.0.3", - "@types/lodash": "^4.14.115", - "@types/node": "^20.9.0", - "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.2.0", - "autoprefixer": "~6", - "canonical-path": "0.0.2", - "concurrently": "^3.2.0", - "eslint": "^8.57.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-import": "2.26.0", - "eslint-plugin-jsdoc": "39.3.6", - "eslint-plugin-prefer-arrow": "1.2.3", - "eslint-plugin-prettier": "^5.0.1", - "grunt": "^1.0.4", - "grunt-bump": "0.8.0", - "grunt-coffeelint": "0.0.16", - "grunt-contrib-clean": "~1.0.0", - "grunt-contrib-coffee": "^1.0.0", - "grunt-contrib-concat": "~1.0.1", - "grunt-contrib-connect": "^1.0.2", - "grunt-contrib-copy": "~1.0.0", - "grunt-contrib-jshint": "~1.0.0", - "grunt-contrib-watch": "^1.1.0", - "grunt-env": "0.4.4", - "grunt-html2js": "^0.6.0", - "grunt-karma": "~2.0.0", - "grunt-newer": "^1.1.2", - "grunt-ng-annotate": "^3.0.0", - "grunt-postcss": "~0.8", - "grunt-preprocess": "5.1.0", - "grunt-sass": "^3.0.2", - "grunt-sass-globbing": "^1.4.0", - "husky": "~8", - "ip": "^1.1.2", - "jasmine-core": "~4.1.0", - "jasmine-spec-reporter": "~5.0.0", - "karma": "^6.3.4", - "karma-chrome-launcher": "~3.1.0", - "karma-coverage-istanbul-reporter": "~3.0.2", - "karma-jasmine": "~4.0.0", - "karma-jasmine-html-reporter": "^1.5.0", - "load-grunt-tasks": "^5.0.0", - "npm-run-all2": "^7.0", - "postcss": "^8.4.27", - "postcss-scss": "^0.1.7", - "prettier": "^3.1.0", - "protractor": "~7.0.0", - "sass": "^1.48.0", - "tailwindcss": "~3.3", + "@types/d3": "^7.4.3", + "@types/dompurify": "^3.0.5", + "@types/node": "^26.0.1", + "@typescript-eslint/eslint-plugin": "^8.60.1", + "@typescript-eslint/parser": "^8.62.1", + "angular-eslint": "^22.0.0", + "autoprefixer": "^10.5.2", + "concurrently": "^10.0.3", + "eslint": "^10.6.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.6", + "eslint-plugin-tailwindcss": "^4.0.4", + "husky": "^9.1.7", + "ip": "^2.0.1", + "jsdom": "^29.1.1", + "npm-run-all2": "^9.0.2", + "postcss": "^8.5.16", + "postcss-scss": "^4.0.9", + "prettier": "^3.8.4", + "sass": "^1.101.0", + "tailwindcss": "^4.3.1", "ts-node": "~10.9", - "typescript": "~5.2", - "underscore": "^1.8.3" + "typescript": "~6.0.3", + "vitest": "^4.1.9" }, "engines": { - "node": ">=20.9.0" + "node": ">=22.22.3" }, "optionalDependencies": { - "@nx/nx-darwin-arm64": "^18.0", - "@nx/nx-darwin-x64": "^18.0", - "@nx/nx-linux-arm64-gnu": "^18.0", - "@nx/nx-linux-x64-gnu": "^18.0", - "@nx/nx-win32-x64-msvc": "^18.0", - "@rollup/rollup-linux-arm64-gnu": "*", - "@rollup/rollup-linux-x64-gnu": "*" + "@nx/nx-darwin-arm64": "^23.0.1", + "@nx/nx-darwin-x64": "^23.0.1", + "@nx/nx-linux-arm64-gnu": "^23.0.1", + "@nx/nx-linux-x64-gnu": "^23.0.1", + "@nx/nx-win32-x64-msvc": "^23.0.1", + "@rollup/rollup-linux-arm64-gnu": "^4.62.2", + "@rollup/rollup-linux-x64-gnu": "^4.62.2" } }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", + "node_modules/@algolia/abtesting": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.18.0.tgz", + "integrity": "sha512-8siuLG+FIns1AjZ/g2SDVwHz9S+ObacDQISEJvS8XsNei1zl3FXqfqQrBpmrG7ACWCyesXHbicMJtvRbg00FEw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", + "node_modules/@algolia/client-abtesting": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.52.0.tgz", + "integrity": "sha512-wtwPgyPmO7b7sQPVgoK29c1VpfS08DnnJCmxX/oU1pV2DlMRJCzQcLN7JSloYpodyKHwM8+9wOzlAM0co3TDmA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { - "node": ">=6.0.0" + "node": ">= 14.0.0" } }, - "node_modules/@angular-devkit/architect": { - "version": "0.1703.7", + "node_modules/@algolia/client-analytics": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.52.0.tgz", + "integrity": "sha512-9KY36bRl4AH7RjqSeDDOKnjsz4IxQFBEOB8/fWmEbdQe+Isbs5jGzVJu9NEPQ1Tgwxlf8Uf07Swj3jZyMNUZ2g==", + "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "17.3.7", - "rxjs": "7.8.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "node": ">= 14.0.0" } }, - "node_modules/@angular-devkit/architect/node_modules/rxjs": { - "version": "7.8.1", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" + "node_modules/@algolia/client-common": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.52.0.tgz", + "integrity": "sha512-3a/qM3dzJqqfTx7Yrw7uGQ98I3Q0rDfb4Vkv0wEzko96l7YQMxfBVz/VbLq2N+c59GweYv6Vhp8mPeqnWJSITw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@angular-devkit/build-angular": { - "version": "17.3.7", + "node_modules/@algolia/client-insights": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.52.0.tgz", + "integrity": "sha512-Rki7ACbMcvbQW0BuM84x9dkGHY47ABmv4jU6tYssat2k02p3mIUms2YOLUAMeknhmnFsj6lb6ZzOXdMWMyc1sA==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1703.7", - "@angular-devkit/build-webpack": "0.1703.7", - "@angular-devkit/core": "17.3.7", - "@babel/core": "7.24.0", - "@babel/generator": "7.23.6", - "@babel/helper-annotate-as-pure": "7.22.5", - "@babel/helper-split-export-declaration": "7.22.6", - "@babel/plugin-transform-async-generator-functions": "7.23.9", - "@babel/plugin-transform-async-to-generator": "7.23.3", - "@babel/plugin-transform-runtime": "7.24.0", - "@babel/preset-env": "7.24.0", - "@babel/runtime": "7.24.0", - "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "17.3.7", - "@vitejs/plugin-basic-ssl": "1.1.0", - "ansi-colors": "4.1.3", - "autoprefixer": "10.4.18", - "babel-loader": "9.1.3", - "babel-plugin-istanbul": "6.1.1", - "browserslist": "^4.21.5", - "copy-webpack-plugin": "11.0.0", - "critters": "0.0.22", - "css-loader": "6.10.0", - "esbuild-wasm": "0.20.1", - "fast-glob": "3.3.2", - "http-proxy-middleware": "2.0.6", - "https-proxy-agent": "7.0.4", - "inquirer": "9.2.15", - "jsonc-parser": "3.2.1", - "karma-source-map-support": "1.4.0", - "less": "4.2.0", - "less-loader": "11.1.0", - "license-webpack-plugin": "4.0.2", - "loader-utils": "3.2.1", - "magic-string": "0.30.8", - "mini-css-extract-plugin": "2.8.1", - "mrmime": "2.0.0", - "open": "8.4.2", - "ora": "5.4.1", - "parse5-html-rewriting-stream": "7.0.0", - "picomatch": "4.0.1", - "piscina": "4.4.0", - "postcss": "8.4.35", - "postcss-loader": "8.1.1", - "resolve-url-loader": "5.0.0", - "rxjs": "7.8.1", - "sass": "1.71.1", - "sass-loader": "14.1.1", - "semver": "7.6.0", - "source-map-loader": "5.0.0", - "source-map-support": "0.5.21", - "terser": "5.29.1", - "tree-kill": "1.2.2", - "tslib": "2.6.2", - "undici": "6.11.1", - "vite": "5.1.7", - "watchpack": "2.4.0", - "webpack": "5.90.3", - "webpack-dev-middleware": "6.1.2", - "webpack-dev-server": "4.15.1", - "webpack-merge": "5.10.0", - "webpack-subresource-integrity": "5.1.0" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "optionalDependencies": { - "esbuild": "0.20.1" - }, - "peerDependencies": { - "@angular/compiler-cli": "^17.0.0", - "@angular/localize": "^17.0.0", - "@angular/platform-server": "^17.0.0", - "@angular/service-worker": "^17.0.0", - "@web/test-runner": "^0.18.0", - "browser-sync": "^3.0.2", - "jest": "^29.5.0", - "jest-environment-jsdom": "^29.5.0", - "karma": "^6.3.0", - "ng-packagr": "^17.0.0", - "protractor": "^7.0.0", - "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=5.2 <5.5" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, - "peerDependenciesMeta": { - "@angular/localize": { - "optional": true - }, - "@angular/platform-server": { - "optional": true - }, - "@angular/service-worker": { - "optional": true - }, - "@web/test-runner": { - "optional": true - }, - "browser-sync": { - "optional": true - }, - "jest": { - "optional": true - }, - "jest-environment-jsdom": { - "optional": true - }, - "karma": { - "optional": true - }, - "ng-packagr": { - "optional": true - }, - "protractor": { - "optional": true - }, - "tailwindcss": { - "optional": true - } + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/autoprefixer": { - "version": "10.4.18", + "node_modules/@algolia/client-personalization": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.52.0.tgz", + "integrity": "sha512-96s4Uzc3kk+/f4jJXIVVGWP5XlngOGNQ1x6hW9AT59pOixHlOs5tqJg+ZUS/GQ6h/iYP0ceQcmxDQeLyCLTaDQ==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001591", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, - "bin": { - "autoprefixer": "bin/autoprefixer" + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.52.0.tgz", + "integrity": "sha512-lqeycNpSPe5Qa0OUWpejVvYQjQWV5nQuLT0a4aq7XzRAvCxprV/6Lf841EygdD2nrFnuS58ok7Au1uOtXzpnkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.52.0.tgz", + "integrity": "sha512-ly1wETVGRo30cx61O7fetESN+ElL9c9K+bD/AVgnT1ar4c6v+/Yqjrhdtu6Fm4D0s4NZP081Isf6tunH1wUXHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, - "peerDependencies": { - "postcss": "^8.1.0" + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/postcss": { - "version": "8.4.35", + "node_modules/@algolia/ingestion": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.52.0.tgz", + "integrity": "sha512-U4EeTvgmluRjj39ykZSAd5X+a6LD5m7/mcOWDmB7hqm1R6QY0yT8jLxpNVEjYhzgEN5hcDGW6X67EWQY8KiYGQ==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">= 14.0.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/postcss-value-parser": { - "version": "4.2.0", + "node_modules/@algolia/monitoring": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.52.0.tgz", + "integrity": "sha512-FCPnDcILfpTE94u7BVlV4DmnSV5wE3+j25EEF+3dYPrVzkVCSoAHs318oWDGxnxsAgiL4HpL12Jc4XHmw9shpA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" + } }, - "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { - "version": "7.8.1", + "node_modules/@algolia/recommend": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.52.0.tgz", + "integrity": "sha512-br3DO7n4N8CXwTRbZS0MnB4WQ9YHfNjCwkCEzVR/wek/qNTDQKDb0nROmkFaNZ8ucUqUVKZi074dbwMwRDlK8Q==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "tslib": "^2.1.0" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@angular-devkit/build-angular/node_modules/sass": { - "version": "1.71.1", + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.52.0.tgz", + "integrity": "sha512-b0T/Ca2c9KyEslKsVrGZvbe1UrrKKSdfXhBZ2pbpKahFUzJfziRZ0urbOm7V65O0tO/jwU+Lo/+bIiiyhzGt8w==", "dev": true, "license": "MIT", "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" + "@algolia/client-common": "5.52.0" }, - "bin": { - "sass": "sass.js" + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.52.0.tgz", + "integrity": "sha512-ozBT8J/mtD4H4IAojw8QPirlcL2gHrI1BGuZ4/ZXXO/rTE1yQ4VIPJj4mTTbwo4FbkS1MoJsD/DsrqLzhnc4/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.0" }, "engines": { - "node": ">=14.0.0" + "node": ">= 14.0.0" } }, - "node_modules/@angular-devkit/build-webpack": { - "version": "0.1703.7", + "node_modules/@algolia/requester-node-http": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.52.0.tgz", + "integrity": "sha512-gyyWcLD22tnabmoit4iukCXuoRc5HYJuUjPSEa8a0D/f/NlRafpWi52AlAaa4Uu/rsl7saHsJFTNjTptWbu2+A==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1703.7", - "rxjs": "7.8.1" + "@algolia/client-common": "5.52.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "node": ">= 14.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" }, - "peerDependencies": { - "webpack": "^5.30.0", - "webpack-dev-server": "^4.0.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { - "version": "7.8.1", + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "tslib": "^2.1.0" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.2200.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2200.4.tgz", + "integrity": "sha512-X/iEQiZ0pRmpjUt11jCM+mtOLRl4XxI9hnM0IC9aAcsm5AzRBb9WY6QIEqOSficjxC/+MI7MGwrerrcP6QN8UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "22.0.4", + "rxjs": "7.8.2" + }, + "bin": { + "architect": "bin/cli.js" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, "node_modules/@angular-devkit/core": { - "version": "17.3.7", + "version": "22.0.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.0.4.tgz", + "integrity": "sha512-zA2UJSMAU3su5uJTOn5ul/gCLRcw6/uIQ6EC5v/Ju/ePjgDIw9R3y3MAvWQ4Ibi/fXiq0FVxpF8hE7RUclYmJA==", + "dev": true, "license": "MIT", "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.1", - "picomatch": "4.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" + "ajv": "8.20.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.2", + "source-map": "0.7.6" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "peerDependencies": { - "chokidar": "^3.5.2" + "chokidar": "^5.0.0" }, "peerDependenciesMeta": { "chokidar": { @@ -478,4102 +389,4233 @@ } } }, - "node_modules/@angular-devkit/core/node_modules/rxjs": { - "version": "7.8.1", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/@angular-devkit/schematics": { - "version": "17.3.7", + "version": "22.0.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-22.0.4.tgz", + "integrity": "sha512-VRxL1hD/Q3TQglM6EfQ0ksAW4OIvtKyZgtaUpyGsJRfD6tGmLFn7MDnmyyq1ceLX/Clq+3tzH/wN0tF6rcE0jA==", + "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "17.3.7", - "jsonc-parser": "3.2.1", - "magic-string": "0.30.8", - "ora": "5.4.1", - "rxjs": "7.8.1" + "@angular-devkit/core": "22.0.4", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.21", + "ora": "9.4.0", + "rxjs": "7.8.2" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/schematics/node_modules/rxjs": { - "version": "7.8.1", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/@angular-eslint/builder": { - "version": "17.4.0", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-22.0.0.tgz", + "integrity": "sha512-T2vWQYUhJs6iUlgocHV12OgoxbmN63f17a+tgW+3sYrKN0KAB3xuHsPOoYpRYoWqkVVC44HD441Ju4IDvo8vKg==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "^17.2.8 || ^18.0.0", - "nx": "^17.2.8 || ^18.0.0" + "@angular-devkit/architect": ">= 0.2200.0 < 0.2300.0", + "@angular-devkit/core": ">= 22.0.0 < 23.0.0" }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "@angular/cli": ">= 22.0.0 < 23.0.0", + "eslint": "^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "17.4.0", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-22.0.0.tgz", + "integrity": "sha512-rv15vGDpGW8zZFaLdhQ+iIO1f0bZds/xvuxoX277hFisXp5Kt6FumJNNIb4g/qxq3xsY46a7fD6R7KvGY3smHg==", "dev": true, "license": "MIT" }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "17.4.0", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-22.0.0.tgz", + "integrity": "sha512-mKLScPZhqG64ic0KIQoxqSqCdkPwtEZuTOuunvc9lYTw05MJSHRUM2yVFODlCGq97c6BN1F6KBk2I+a+KFnr1g==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.4.0", - "@angular-eslint/utils": "17.4.0", - "@typescript-eslint/utils": "7.8.0" + "@angular-eslint/bundled-angular-compiler": "22.0.0", + "@angular-eslint/utils": "22.0.0", + "ts-api-utils": "^2.1.0" }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "@typescript-eslint/utils": "^8.0.0", + "eslint": "^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "17.4.0", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-22.0.0.tgz", + "integrity": "sha512-y6XL5HJ8C31NpBvkVHpU3bWc+Rk9g1zRtHrs39omhuT29eEUcS3zu47HMFV6tf8rHOI97B2Mstg6qYS5XL9ATg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.4.0", - "@angular-eslint/utils": "17.4.0", - "@typescript-eslint/type-utils": "7.8.0", - "@typescript-eslint/utils": "7.8.0", - "aria-query": "5.3.0", - "axobject-query": "4.0.0" + "@angular-eslint/bundled-angular-compiler": "22.0.0", + "@angular-eslint/utils": "22.0.0", + "aria-query": "5.3.2", + "axobject-query": "4.1.0" }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "@angular-eslint/template-parser": "22.0.0", + "@typescript-eslint/types": "^8.0.0", + "@typescript-eslint/utils": "^8.0.0", + "eslint": "^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/schematics": { - "version": "17.4.0", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-22.0.0.tgz", + "integrity": "sha512-gsJQx6c+WIWC5d+NAqn4rRdUzwhinUCTNmCM9x4wygV9DrbAfVG+6OFPEbaDMryNvf0HYDcnGclbIbXjukGCaw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/eslint-plugin": "17.4.0", - "@angular-eslint/eslint-plugin-template": "17.4.0", - "@nx/devkit": "^17.2.8 || ^18.0.0", - "ignore": "5.3.1", - "nx": "^17.2.8 || ^18.0.0", - "strip-json-comments": "3.1.1", - "tmp": "0.2.3" + "@angular-devkit/core": ">= 22.0.0 < 23.0.0", + "@angular-devkit/schematics": ">= 22.0.0 < 23.0.0", + "@angular-eslint/eslint-plugin": "22.0.0", + "@angular-eslint/eslint-plugin-template": "22.0.0", + "ignore": "7.0.5", + "semver": "7.8.0", + "strip-json-comments": "3.1.1" }, "peerDependencies": { - "@angular/cli": ">= 17.0.0 < 18.0.0" + "@angular/cli": ">= 22.0.0 < 23.0.0" + } + }, + "node_modules/@angular-eslint/schematics/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/@angular-eslint/template-parser": { - "version": "17.4.0", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-22.0.0.tgz", + "integrity": "sha512-jU5MKQ24bBB4J99gSSexmUrLm2LvTJZCuCHhNTQ1LavWX4e1lrIxhm+6pJILOm6Cixf8jyNXnHMty6nljX8J+Q==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.4.0", - "eslint-scope": "^8.0.0" + "@angular-eslint/bundled-angular-compiler": "22.0.0", + "eslint-scope": "9.1.2" }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "eslint": "^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular-eslint/utils": { - "version": "17.4.0", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-22.0.0.tgz", + "integrity": "sha512-VFodMojghnPYm+B3U+HRYrqebPMj8NyobNjVzDdY8V5XIBW+4ivOSEINIz81G48rmm/NZKwj56+bJ88bVX4KIw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "17.4.0", - "@typescript-eslint/utils": "7.8.0" + "@angular-eslint/bundled-angular-compiler": "22.0.0" }, "peerDependencies": { - "eslint": "^7.20.0 || ^8.0.0", + "@typescript-eslint/utils": "^8.0.0", + "eslint": "^9.0.0 || ^10.0.0", "typescript": "*" } }, "node_modules/@angular/animations": { - "version": "17.3.8", + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-22.0.3.tgz", + "integrity": "sha512-j6N7s/tffYoTch++g45eKBp0pI0vsYsT1veyrALsTn732qa1E6rMsaiL2+L2s2PkHzkZosNMW1U3e0gRv6auzA==", + "deprecated": "@angular/animations is deprecated. Use `animate.enter` and `animate.leave` instead. For more information see: https://v22.angular.dev/guide/animations.", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/core": "17.3.8" - } - }, - "node_modules/@angular/cdk": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.10.tgz", - "integrity": "sha512-b1qktT2c1TTTe5nTji/kFAVW92fULK0YhYAvJ+BjZTPKu2FniZNe8o4qqQ0pUuvtMu+ZQxp/QqFYoidIVCjScg==", - "dependencies": { - "tslib": "^2.3.0" - }, - "optionalDependencies": { - "parse5": "^7.1.2" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/common": "^17.0.0 || ^18.0.0", - "@angular/core": "^17.0.0 || ^18.0.0", - "rxjs": "^6.5.3 || ^7.4.0" + "@angular/core": "22.0.3" } }, - "node_modules/@angular/cli": { - "version": "17.3.7", + "node_modules/@angular/build": { + "version": "22.0.4", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-22.0.4.tgz", + "integrity": "sha512-hst/KhP5mMPajY32l7qmyW/5fuqrMHCjHEeNhMMNqP82+j8jPBFfMEL26AH9w2kIIdKGpC3WKN5JgLotyFD7tQ==", + "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1703.7", - "@angular-devkit/core": "17.3.7", - "@angular-devkit/schematics": "17.3.7", - "@schematics/angular": "17.3.7", - "@yarnpkg/lockfile": "1.1.0", - "ansi-colors": "4.1.3", - "ini": "4.1.2", - "inquirer": "9.2.15", - "jsonc-parser": "3.2.1", - "npm-package-arg": "11.0.1", - "npm-pick-manifest": "9.0.0", - "open": "8.4.2", - "ora": "5.4.1", - "pacote": "17.0.6", - "resolve": "1.22.8", - "semver": "7.6.0", - "symbol-observable": "4.0.0", - "yargs": "17.7.2" - }, - "bin": { - "ng": "bin/ng.js" + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.2200.4", + "@babel/core": "7.29.0", + "@babel/helper-annotate-as-pure": "7.27.3", + "@babel/helper-split-export-declaration": "7.24.7", + "@inquirer/confirm": "6.0.12", + "@vitejs/plugin-basic-ssl": "2.3.0", + "beasties": "0.4.2", + "browserslist": "^4.26.0", + "esbuild": "0.28.1", + "https-proxy-agent": "9.0.0", + "jsonc-parser": "3.3.1", + "listr2": "10.2.1", + "magic-string": "0.30.21", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "8.0.1", + "picomatch": "4.0.4", + "piscina": "5.2.0", + "rollup": "4.60.2", + "sass": "1.99.0", + "semver": "7.7.4", + "source-map-support": "0.5.21", + "tinyglobby": "0.2.16", + "vite": "7.3.5", + "watchpack": "2.5.1" }, "engines": { - "node": "^18.13.0 || >=20.9.0", + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/common": { - "version": "17.3.8", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/core": "17.3.8", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/compiler": { - "version": "17.3.8", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0" }, - "peerDependencies": { - "@angular/core": "17.3.8" + "optionalDependencies": { + "lmdb": "3.5.4" + }, + "peerDependencies": { + "@angular/compiler": "^22.0.0", + "@angular/compiler-cli": "^22.0.0", + "@angular/core": "^22.0.0", + "@angular/localize": "^22.0.0", + "@angular/platform-browser": "^22.0.0", + "@angular/platform-server": "^22.0.0", + "@angular/service-worker": "^22.0.0", + "@angular/ssr": "^22.0.4", + "istanbul-lib-instrument": "^6.0.0", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^22.0.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=6.0 <6.1", + "vitest": "^4.0.8" }, "peerDependenciesMeta": { "@angular/core": { "optional": true - } - } - }, - "node_modules/@angular/compiler-cli": { - "version": "17.3.8", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "7.23.9", - "@jridgewell/sourcemap-codec": "^1.4.14", - "chokidar": "^3.0.0", - "convert-source-map": "^1.5.1", - "reflect-metadata": "^0.2.0", - "semver": "^7.0.0", - "tslib": "^2.3.0", - "yargs": "^17.2.1" - }, - "bin": { - "ng-xi18n": "bundles/src/bin/ng_xi18n.js", - "ngc": "bundles/src/bin/ngc.js", - "ngcc": "bundles/ngcc/index.js" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/compiler": "17.3.8", - "typescript": ">=5.2 <5.5" - } - }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core": { - "version": "7.23.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.9", - "@babel/parser": "^7.23.9", - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "istanbul-lib-instrument": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + }, + "vitest": { + "optional": true + } } }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", + "node_modules/@angular/build/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", + "node_modules/@angular/build/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@angular/core": { - "version": "17.3.8", + "node_modules/@angular/build/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.14.0" + "node": ">=18" } }, - "node_modules/@angular/forms": { - "version": "17.3.8", + "node_modules/@angular/build/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/common": "17.3.8", - "@angular/core": "17.3.8", - "@angular/platform-browser": "17.3.8", - "rxjs": "^6.5.3 || ^7.4.0" + "node": ">=18" } }, - "node_modules/@angular/language-service": { - "version": "17.3.8", + "node_modules/@angular/build/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.13.0 || >=20.9.0" + "node": ">=18" } }, - "node_modules/@angular/material": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.10.tgz", - "integrity": "sha512-hHMQES0tQPH5JW33W+mpBPuM8ybsloDTqFPuRV8cboDjosAWfJhzAKF3ozICpNlUrs62La/2Wu/756GcQrxebg==", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/auto-init": "15.0.0-canary.7f224ddd4.0", - "@material/banner": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/button": "15.0.0-canary.7f224ddd4.0", - "@material/card": "15.0.0-canary.7f224ddd4.0", - "@material/checkbox": "15.0.0-canary.7f224ddd4.0", - "@material/chips": "15.0.0-canary.7f224ddd4.0", - "@material/circular-progress": "15.0.0-canary.7f224ddd4.0", - "@material/data-table": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dialog": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/drawer": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/fab": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/floating-label": "15.0.0-canary.7f224ddd4.0", - "@material/form-field": "15.0.0-canary.7f224ddd4.0", - "@material/icon-button": "15.0.0-canary.7f224ddd4.0", - "@material/image-list": "15.0.0-canary.7f224ddd4.0", - "@material/layout-grid": "15.0.0-canary.7f224ddd4.0", - "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", - "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", - "@material/list": "15.0.0-canary.7f224ddd4.0", - "@material/menu": "15.0.0-canary.7f224ddd4.0", - "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", - "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", - "@material/radio": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/segmented-button": "15.0.0-canary.7f224ddd4.0", - "@material/select": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/slider": "15.0.0-canary.7f224ddd4.0", - "@material/snackbar": "15.0.0-canary.7f224ddd4.0", - "@material/switch": "15.0.0-canary.7f224ddd4.0", - "@material/tab": "15.0.0-canary.7f224ddd4.0", - "@material/tab-bar": "15.0.0-canary.7f224ddd4.0", - "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", - "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", - "@material/textfield": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tooltip": "15.0.0-canary.7f224ddd4.0", - "@material/top-app-bar": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/animations": "^17.0.0 || ^18.0.0", - "@angular/cdk": "17.3.10", - "@angular/common": "^17.0.0 || ^18.0.0", - "@angular/core": "^17.0.0 || ^18.0.0", - "@angular/forms": "^17.0.0 || ^18.0.0", - "@angular/platform-browser": "^17.0.0 || ^18.0.0", - "rxjs": "^6.5.3 || ^7.4.0" + "node_modules/@angular/build/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@angular/material-date-fns-adapter": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular/material-date-fns-adapter/-/material-date-fns-adapter-17.3.10.tgz", - "integrity": "sha512-Q4QAPGImZTjKW9ZhLSTkBeQX21I0dtak3JbexYx4CN/pHxKRpen6KaVAEqiORqq6vNUP2Kwb7cZznQyj6L7oQw==", - "dependencies": { - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/core": "^17.0.0 || ^18.0.0", - "@angular/material": "17.3.10", - "date-fns": ">2.20.0 <4.0" + "node_modules/@angular/build/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@angular/platform-browser": { - "version": "17.3.8", + "node_modules/@angular/build/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/animations": "17.3.8", - "@angular/common": "17.3.8", - "@angular/core": "17.3.8" - }, - "peerDependenciesMeta": { - "@angular/animations": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@angular/platform-browser-dynamic": { - "version": "17.3.8", + "node_modules/@angular/build/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/common": "17.3.8", - "@angular/compiler": "17.3.8", - "@angular/core": "17.3.8", - "@angular/platform-browser": "17.3.8" + "node": ">=18" } }, - "node_modules/@angular/router": { - "version": "17.3.8", + "node_modules/@angular/build/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/common": "17.3.8", - "@angular/core": "17.3.8", - "@angular/platform-browser": "17.3.8", - "rxjs": "^6.5.3 || ^7.4.0" + "node": ">=18" } }, - "node_modules/@angular/service-worker": { - "version": "17.3.8", + "node_modules/@angular/build/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "bin": { - "ngsw-config": "ngsw-config.js" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/common": "17.3.8", - "@angular/core": "17.3.8" + "node": ">=18" } }, - "node_modules/@angular/upgrade": { - "version": "17.3.8", + "node_modules/@angular/build/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.13.0 || >=20.9.0" - }, - "peerDependencies": { - "@angular/compiler": "17.3.8", - "@angular/core": "17.3.8", - "@angular/platform-browser": "17.3.8", - "@angular/platform-browser-dynamic": "17.3.8" + "node": ">=18" } }, - "node_modules/@babel/code-frame": { - "version": "7.24.2", + "node_modules/@angular/build/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.24.2", - "picocolors": "^1.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/compat-data": { - "version": "7.24.4", + "node_modules/@angular/build/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/core": { - "version": "7.24.0", + "node_modules/@angular/build/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.0", - "@babel/parser": "^7.24.0", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", - "@babel/types": "^7.24.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node": ">=18" } }, - "node_modules/@babel/generator": { - "version": "7.23.6", + "node_modules/@angular/build/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", + "node_modules/@angular/build/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.15", + "node_modules/@angular/build/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.15" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", + "node_modules/@angular/build/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node": ">=18" } }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.5", + "node_modules/@angular/build/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.24.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "semver": "^6.3.1" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "node": ">=18" } }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", + "node_modules/@angular/build/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.5" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node": ">=18" } }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.15", + "node_modules/@angular/build/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "regexpu-core": "^5.3.1", - "semver": "^6.3.1" - }, + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + "node": ">=18" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", + "node_modules/@angular/build/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", + "node_modules/@angular/build/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", + "node_modules/@angular/build/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.5", + "node_modules/@angular/build/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.5" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.24.3", + "node_modules/@angular/build/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.24.5", + "node_modules/@angular/build/node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.24.3", - "@babel/helper-simple-access": "^7.24.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/helper-validator-identifier": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@babel/helper-module-transforms/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", + "node_modules/@angular/build/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", + "node_modules/@angular/build/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.5", + "node_modules/@angular/build/node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">=6.9.0" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.20", + "node_modules/@angular/build/node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-wrap-function": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.24.1", + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.5", + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@babel/helper-string-parser": { - "version": "7.24.1", + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">=6.9.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">=6.9.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">=6.9.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.24.5", + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-function-name": "^7.23.0", - "@babel/template": "^7.24.0", - "@babel/types": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@babel/helpers": { - "version": "7.24.5", + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@babel/highlight": { - "version": "7.24.5", + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.5", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@babel/parser": { - "version": "7.24.5", + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.24.1", + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.24.1", + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.24.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.24.1", + "node_modules/@angular/build/node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } + "optional": true, + "os": [ + "openbsd" + ] }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", + "node_modules/@angular/build/node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } + "optional": true, + "os": [ + "openharmony" + ] }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", + "node_modules/@angular/build/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", + "node_modules/@angular/build/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", + "node_modules/@angular/build/node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", + "node_modules/@angular/build/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", + "node_modules/@angular/build/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } + "license": "MIT" }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.24.1", + "node_modules/@angular/build/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=6.9.0" + "node": ">=18" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.1", + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/@angular/build/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, "engines": { - "node": ">=6.9.0" + "node": ">= 14.18.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", + "node_modules/@angular/build/node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@types/estree": "1.0.8" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" } }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", + "node_modules/@angular/build/node_modules/sass": { + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz", + "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "chokidar": "^4.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" } }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", + "node_modules/@angular/build/node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "readdirp": "^4.0.1" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "dev": true, + "node_modules/@angular/cdk": { + "version": "22.0.2", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-22.0.2.tgz", + "integrity": "sha512-3AOyLNIpvXkxbiCeUc4R5ubwCBpY83ZPe2I6Q/cTUW53SnFapEBNYZ2spSY+jPVY4IVPnQN1Tvjlzq6R9K4M3w==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "parse5": "^8.0.0", + "tslib": "^2.3.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/common": "^22.0.0 || ^23.0.0", + "@angular/core": "^22.0.0 || ^23.0.0", + "@angular/platform-browser": "^22.0.0 || ^23.0.0", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", + "node_modules/@angular/cli": { + "version": "22.0.4", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-22.0.4.tgz", + "integrity": "sha512-3eJy6VoNlgskKFzvqy3AJsYXFRhSBLLGObF2iTpJymsukuxWUen7hlVVVWrO5++bW1LEgd6PTCCD5fdT2UuRiA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@angular-devkit/architect": "0.2200.4", + "@angular-devkit/core": "22.0.4", + "@angular-devkit/schematics": "22.0.4", + "@inquirer/prompts": "8.4.2", + "@listr2/prompt-adapter-inquirer": "4.2.3", + "@modelcontextprotocol/sdk": "1.29.0", + "@schematics/angular": "22.0.4", + "@yarnpkg/lockfile": "1.1.0", + "algoliasearch": "5.52.0", + "ini": "6.0.0", + "jsonc-parser": "3.3.1", + "listr2": "10.2.1", + "npm-package-arg": "13.0.2", + "pacote": "21.5.1", + "parse5-html-rewriting-stream": "8.0.1", + "semver": "7.7.4", + "yargs": "18.0.0", + "zod": "4.4.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "dev": true, + "node_modules/@angular/common": { + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-22.0.3.tgz", + "integrity": "sha512-LRglsR4Xerw/vrqoEMd489fF5PMJZ3kqGAPwO1332S42lN5KXlYITCB8WJw1iIqvoBqZvlR9R8u85cLUsfDzbg==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/core": "22.0.3", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "dev": true, + "node_modules/@angular/compiler": { + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-22.0.3.tgz", + "integrity": "sha512-tZYq4RYYGCT6enI3wlOZsG81VJkR9wGhf5kexhvffCiMHfcA9IACHu7gjvtZPZlibeKs1PY5TMoOG3/rUqJH6Q==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "tslib": "^2.3.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" } }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", + "node_modules/@angular/compiler-cli": { + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-22.0.3.tgz", + "integrity": "sha512-TMpKn2KefhGXM1FzcLYeyTMsBbxtFJU83bYnlI0pTw7k3jBAmN6H+ydRcNhdEYr3R2Ja3ncOXGV98RWLm9ElcQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/core": "7.29.7", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^5.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^18.0.0" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/compiler": "22.0.3", + "typescript": ">=6.0 <6.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/core": { + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-22.0.3.tgz", + "integrity": "sha512-u6bYkPBB9PfYyyQ29JiytYbTqHO0lhigkkSVFoAT0WXu3R0ohycjYJooxJbHNBivWkQsASwv/k125Wb3SQoL2g==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "tslib": "^2.3.0" }, "engines": { - "node": ">=6.9.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/compiler": "22.0.3", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0 || ~0.16.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } } }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "dev": true, + "node_modules/@angular/forms": { + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-22.0.3.tgz", + "integrity": "sha512-AhZEKvOw+5cqS3j1hjM0jbrWEg4xUg/l0h7yDpWAsgenWFutATGzeeRvl9dJ3HPh2Ga6leIraDi0Q/+YLCDE/A==", "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@standard-schema/spec": "^1.0.0", + "tslib": "^2.3.0", + "zod": "^4.0.10" }, "engines": { - "node": ">=6.9.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@angular/common": "22.0.3", + "@angular/core": "22.0.3", + "@angular/platform-browser": "22.0.3", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.24.1", + "node_modules/@angular/language-service": { + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-22.0.3.tgz", + "integrity": "sha512-2PFFw0WVJ75RjmgT1BzFUOC2HOA6VK/Il0LozxLKxKc2UWIEv+Fw42T2Yo798UaarpZSYh91mcFu5+v7I5YWGg==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, "engines": { - "node": ">=6.9.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@angular/material": { + "version": "22.0.2", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-22.0.2.tgz", + "integrity": "sha512-a2sp9ipozR4THqu5A3ff3VXBpbQHpfTmH+Oqb0+RD47fJ+/kvyBUZQ5JK2Yh6eUXVceAOW4s+sL0ev8tS1EfuQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/cdk": "22.0.2", + "@angular/common": "^22.0.0 || ^23.0.0", + "@angular/core": "^22.0.0 || ^23.0.0", + "@angular/forms": "^22.0.0 || ^23.0.0", + "@angular/platform-browser": "^22.0.0 || ^23.0.0", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.9", - "dev": true, + "node_modules/@angular/material-date-fns-adapter": { + "version": "22.0.2", + "resolved": "https://registry.npmjs.org/@angular/material-date-fns-adapter/-/material-date-fns-adapter-22.0.2.tgz", + "integrity": "sha512-xoxECE2NowCIT3GlKIfxFh8tvuaS4g7wV9wN9rbMH284SRITC1TavvhtFdLcCDrMafUvSxY/du441SBcBmOoKg==", "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" + "tslib": "^2.3.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/core": "^22.0.0 || ^23.0.0", + "@angular/material": "22.0.2", + "date-fns": ">2.20.0 <5.0" } }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "dev": true, + "node_modules/@angular/platform-browser": { + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.0.3.tgz", + "integrity": "sha512-MAOGciS6zrw4CzEH6n/Cry0tl5MtgWWYEs/IH2McMtixpun1EOr5TVn84xIELloOtNBNT4679g5rpAVm6onLAw==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20" + "tslib": "^2.3.0" }, "engines": { - "node": ">=6.9.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/animations": "22.0.3", + "@angular/common": "22.0.3", + "@angular/core": "22.0.3" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } } }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.24.1", - "dev": true, + "node_modules/@angular/platform-browser-dynamic": { + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-22.0.3.tgz", + "integrity": "sha512-3tAPLVfOLXAwoZQR8agPOrs42J9R/zNrE1u9PnHdT+DFu2weQjRqRGGKz0iyAbLa9ljPoxUFFG9WFoEGj2Z0HA==", + "deprecated": "@angular/platform-browser-dynamic is deprecated. Use `@angular/platform-browser` instead.", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "tslib": "^2.3.0" }, "engines": { - "node": ">=6.9.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/common": "22.0.3", + "@angular/compiler": "22.0.3", + "@angular/core": "22.0.3", + "@angular/platform-browser": "22.0.3" } }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.24.5", - "dev": true, + "node_modules/@angular/router": { + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-22.0.3.tgz", + "integrity": "sha512-wkD7axjX43fIbhJKLy83O/XnJI1LO/C6aXdVFE+D6dVWCzwI8wL+cqHk9ovW9TsLOtxWvp6DE7emcc+54vlshw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "tslib": "^2.3.0" }, "engines": { - "node": ">=6.9.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/common": "22.0.3", + "@angular/core": "22.0.3", + "@angular/platform-browser": "22.0.3", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.24.1", - "dev": true, + "node_modules/@angular/service-worker": { + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-22.0.3.tgz", + "integrity": "sha512-Sm8AeJkek0755dMndRxUebPkRh0Lxd0D6qGVyNN2Th1skHcEIax+lqAezj7yKCZaQxFdOe8RiOZMk3HEkA0dXw==", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0" + "tslib": "^2.3.0" + }, + "bin": { + "ngsw-config": "ngsw-config.js" }, "engines": { - "node": ">=6.9.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@angular/core": "22.0.3", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.24.4", + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.4", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-class-static-block": "^7.14.5" + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.24.5", + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/helper-replace-supers": "^7.24.1", - "@babel/helper-split-export-declaration": "^7.24.5", - "globals": "^11.1.0" + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.5" - }, "engines": { - "node": ">=6.9.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.24.1", + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/template": "^7.24.0" + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.5", + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.24.1", + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.24.1", + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.24.1", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.24.1", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.24.1", + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.24.1", + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.24.1", + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.24.1", + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.24.1", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.24.1", + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.24.1", + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.24.1", + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/types": "^7.29.7" }, - "engines": { - "node": ">=6.9.0" + "bin": { + "parser": "bin/babel-parser.js" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.1", + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-simple-access": "^7.22.5" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.24.1", + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.24.1", + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" + "css-tree": "^3.0.0" }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "bin": { + "specificity": "bin/cli.js" } }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.24.1", + "node_modules/@commitlint/cli": { + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-21.1.0.tgz", + "integrity": "sha512-CVwY6TxGv5naEaWxBdgNHko1xgL95Mb4WcIqp9iik33H0ctVqRv6YtekCntayhEP0T/apuiGvHu5HcCwFuVxEA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@commitlint/config-conventional": "^21.1.0", + "@commitlint/format": "^21.1.0", + "@commitlint/lint": "^21.1.0", + "@commitlint/load": "^21.1.0", + "@commitlint/read": "^21.1.0", + "@commitlint/types": "^21.1.0", + "tinyexec": "^1.0.0", + "yargs": "^18.0.0" }, - "engines": { - "node": ">=6.9.0" + "bin": { + "commitlint": "cli.js" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=22.12.0" } }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.24.1", + "node_modules/@commitlint/config-conventional": { + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-21.1.0.tgz", + "integrity": "sha512-BIFl8xM+3SLy3jrblUC3wmQLCVbLty+++6o859BDCmybVrQdXmIWO+dlkGIbv/M2bBoC55wGuh0zGiw3TPjL1g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + "@commitlint/types": "^21.1.0", + "conventional-changelog-conventionalcommits": "^9.2.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=22.12.0" } }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.24.1", + "node_modules/@commitlint/config-validator": { + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-21.1.0.tgz", + "integrity": "sha512-gHczt1xqQSwfNqBmOI3HjejtTljkiBEUneExMmTBLD0WwTC78lAqDvNMyydbySt3DhpH0F9oX7Vvuks6s5XPFw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" + "@commitlint/types": "^21.1.0", + "ajv": "^8.11.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=22.12.0" } }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.24.5", + "node_modules/@commitlint/ensure": { + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-21.1.0.tgz", + "integrity": "sha512-/S8Mo3Q1NtQUYDQjDmyQVPxfIwtnxq+guzMOkuGk8OSdwlzanm1WB9wDPIuuzlbMDDnBNbiAuBEUCcCNlfjrTQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.24.5" + "@commitlint/types": "^21.1.0", + "es-toolkit": "^1.46.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=22.12.0" } }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.24.1", + "node_modules/@commitlint/execute-rule": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-21.0.1.tgz", + "integrity": "sha512-RifH+FmImozKBE6mozhF4K3r2RRKP7SMi/Q/zLCmExtp5e05lhHOUYqGBlFBAGNHaZxU/WYw1XuugYK9jQzqnA==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-replace-supers": "^7.24.1" - }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=22.12.0" } }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.24.1", + "node_modules/@commitlint/format": { + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-21.1.0.tgz", + "integrity": "sha512-ySymqKYBfjNrQ5N4W/l1iF2ISW1W7Eu/Oi/wRxlri31N0yjNyzUyUzQwyuZLDzTXIlMs4IZ7hIOfAZx8lO18gA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + "@commitlint/types": "^21.1.0", + "picocolors": "^1.1.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=22.12.0" } }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.5", + "node_modules/@commitlint/is-ignored": { + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-21.1.0.tgz", + "integrity": "sha512-RoRh1/YI+fYH+aid5lMQ2UD0vZ3p3Vf1KeUWT1ir3H/p/7T/6SFv1OiXLgLwUT8dP72EVWeEIyOfkiSWLZYVvw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "@commitlint/types": "^21.1.0", + "semver": "^7.6.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=22.12.0" } }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.24.5", + "node_modules/@commitlint/lint": { + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-21.1.0.tgz", + "integrity": "sha512-0DbfVVUjAWBfixW6v7CXXWVxMcj6Ukf/oB7O8NAbouP3jxmqUaC4eVQphxl3B3M0ii3cCQiR3sRAYxICwU2gAA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" + "@commitlint/is-ignored": "^21.1.0", + "@commitlint/parse": "^21.1.0", + "@commitlint/rules": "^21.1.0", + "@commitlint/types": "^21.1.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=22.12.0" } }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.24.1", + "node_modules/@commitlint/load": { + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-21.1.0.tgz", + "integrity": "sha512-juiClVEcoreNB0TNVkseO2EmNcpEs/Yhnmgbnm/hQAKBFRynKwIaoNIljXkx/3yvZcMO0EE8I2XOEI7d5KZG8Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.1", - "@babel/helper-plugin-utils": "^7.24.0" + "@commitlint/config-validator": "^21.1.0", + "@commitlint/execute-rule": "^21.0.1", + "@commitlint/resolve-extends": "^21.1.0", + "@commitlint/types": "^21.1.0", + "cosmiconfig": "^9.0.1", + "cosmiconfig-typescript-loader": "^6.1.0", + "es-toolkit": "^1.46.0", + "is-plain-obj": "^4.1.0", + "picocolors": "^1.1.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=22.12.0" } }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.24.5", + "node_modules/@commitlint/message": { + "version": "21.0.2", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-21.0.2.tgz", + "integrity": "sha512-5n4aqHGD/FNnom/D5L8i7cYtV+xjuXcBL832C3w9VglEsZzIsoHpJsvxzJ7cgiOsOdc/2jU4t5+7qMHh7GBX3g==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.24.5", - "@babel/helper-plugin-utils": "^7.24.5", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=22.12.0" } }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.24.1", + "node_modules/@commitlint/parse": { + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-21.1.0.tgz", + "integrity": "sha512-HdAqbbjQS8eEtbR74Ysg2VNmbvAfeWLVYMkip/lHibNrtjRsC/97XAYN3/H5P0pEJtDfyTb3iLs8x6y0eu4OYA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@commitlint/types": "^21.1.0", + "conventional-changelog-angular": "^8.2.0", + "conventional-commits-parser": "^6.3.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=22.12.0" } }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.24.1", + "node_modules/@commitlint/read": { + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-21.1.0.tgz", + "integrity": "sha512-ID7m79aw8d0dMlxuXHD2QGxEX3Fhl/mUPA80WwEW5VgeOpUHNahhwWJefDdoBDVZcDfbHuf429NrcK0gxQsQjA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "regenerator-transform": "^0.15.2" + "@commitlint/top-level": "^21.0.2", + "@commitlint/types": "^21.1.0", + "git-raw-commits": "^5.0.0", + "tinyexec": "^1.0.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=22.12.0" } }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.24.1", + "node_modules/@commitlint/resolve-extends": { + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-21.1.0.tgz", + "integrity": "sha512-SANYkxJDfMl3TvnyALWHEaiF5nc6FFaOnh7VvfxjT4X2vD4i2gVHhmfMm1fsrBwDRX98/XyM1XDo5sAd/KXcyQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@commitlint/config-validator": "^21.1.0", + "@commitlint/types": "^21.1.0", + "es-toolkit": "^1.46.0", + "global-directory": "^5.0.0", + "resolve-from": "^5.0.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=22.12.0" } }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.24.0", + "node_modules/@commitlint/rules": { + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-21.1.0.tgz", + "integrity": "sha512-fOPEYSmKn1ZJptjLmCEjJfYqz0PUYr8ng6VY2ZW26sB7KtENR90CmAXHEmScBbOIZip+d/+OwqK12DFBuHTqsQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0", - "babel-plugin-polyfill-corejs2": "^0.4.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", - "semver": "^6.3.1" + "@commitlint/ensure": "^21.1.0", + "@commitlint/message": "^21.0.2", + "@commitlint/to-lines": "^21.0.1", + "@commitlint/types": "^21.1.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=22.12.0" } }, - "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.1", + "node_modules/@commitlint/to-lines": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-21.0.1.tgz", + "integrity": "sha512-bd1BFII7p1EQZre9Kaj+kKaMFP3cFCdt21K7DItVux9XP5WjLgJ0/Uy1pJJh9aPwVJ6SKg62PxqlZaHI8hQAXw==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "engines": { + "node": ">=22.12.0" } }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.24.1", + "node_modules/@commitlint/top-level": { + "version": "21.0.2", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-21.0.2.tgz", + "integrity": "sha512-s9KKM+e+mXgFeIh4n7KmOGAVT3mkJ3Fp1bBYHIK5pjeUwlEMzp/tZfb5u0Poa680AsQTXMEMRxZi1vQ9m2X5ug==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "escalade": "^3.2.0" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=22.12.0" } }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.24.1", + "node_modules/@commitlint/types": { + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-21.1.0.tgz", + "integrity": "sha512-YodnnnH1Cp+08nP8HGNJAIuB6L3/vdCTHVRTfF8Ik/wRCLOTsU9zwv3yO1cSPQRDa9CLYtE+UJ2K67r7CwMSFw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "conventional-commits-parser": "^6.3.0", + "picocolors": "^1.1.1" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=22.12.0" } }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.24.1", + "node_modules/@conventional-changelog/git-client": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-2.7.0.tgz", + "integrity": "sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@simple-libs/child-process-utils": "^1.0.0", + "@simple-libs/stream-utils": "^1.2.0", + "semver": "^7.5.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=18" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "conventional-commits-filter": "^5.0.0", + "conventional-commits-parser": "^6.4.0" + }, + "peerDependenciesMeta": { + "conventional-commits-filter": { + "optional": true + }, + "conventional-commits-parser": { + "optional": true + } } }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.24.1", + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@jridgewell/trace-mapping": "0.3.9" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=12" } }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.5", + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.24.1", + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" - }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=20.19.0" } }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.24.1", + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" - }, "engines": { - "node": ">=6.9.0" + "node": ">=20.19.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.24.1", + "node_modules/@csstools/css-color-parser": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.7.tgz", + "integrity": "sha512-CmjJFQTFQx/U/xNJhSjCQ0ilpesPmNQ8+eOUeM/+kDOVW33qsIjeOXc27vrQDdWVkf83ZSWwtg7kXSUvKDJ8cQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" }, "engines": { - "node": ">=6.9.0" + "node": ">=20.19.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.24.1", + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0" - }, "engines": { - "node": ">=6.9.0" + "node": ">=20.19.0" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@babel/preset-env": { - "version": "7.24.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.9", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.4", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.4", - "@babel/plugin-transform-classes": "^7.23.8", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.4", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.4", - "@babel/plugin-transform-for-of": "^7.23.6", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.4", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.9", - "@babel/plugin-transform-modules-umd": "^7.23.3", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", - "@babel/plugin-transform-numeric-separator": "^7.23.4", - "@babel/plugin-transform-object-rest-spread": "^7.24.0", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.4", - "@babel/plugin-transform-optional-chaining": "^7.23.4", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.4", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", - "core-js-compat": "^3.31.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", "peerDependencies": { - "@babel/core": "^7.0.0-0" + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } } }, - "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.1", + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" } }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "dev": true, - "license": "MIT", + "node_modules/@ctrl/ngx-emoji-mart": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@ctrl/ngx-emoji-mart/-/ngx-emoji-mart-9.3.0.tgz", + "integrity": "sha512-9uFzAvlFT21OLsTfhL3ZEO5mp51qvL1F4ErIZVBIsvAlji46u6p2KGgVA60oIheFBX4JoZI7HBDGOkGnm9dTUQ==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" + "tslib": "^2.3.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + "@angular/core": ">=15.0.0-0" } }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "dev": true, - "license": "MIT" + "node_modules/@date-fns/tz": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.5.0.tgz", + "integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==", + "license": "MIT", + "peer": true }, - "node_modules/@babel/runtime": { - "version": "7.24.0", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/template": { - "version": "7.24.0", + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/traverse": { - "version": "7.24.5", + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/types": "^7.24.5", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.24.5", + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.5", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/traverse/node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.5", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.5" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@babel/types": { - "version": "7.24.5", + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", - "to-fast-properties": "^2.0.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6.9.0" + "node": ">=18" } }, - "node_modules/@colors/colors": { - "version": "1.5.0", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=0.1.90" + "node": ">=18" } }, - "node_modules/@commitlint/cli": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.5.0.tgz", - "integrity": "sha512-yNkyN/tuKTJS3wdVfsZ2tXDM4G4Gi7z+jW54Cki8N8tZqwKBltbIvUUrSbT4hz1bhW/h0CdR+5sCSpXD+wMKaQ==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@commitlint/format": "^20.5.0", - "@commitlint/lint": "^20.5.0", - "@commitlint/load": "^20.5.0", - "@commitlint/read": "^20.5.0", - "@commitlint/types": "^20.5.0", - "tinyexec": "^1.0.0", - "yargs": "^17.0.0" - }, - "bin": { - "commitlint": "cli.js" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=v18" + "node": ">=18" } }, - "node_modules/@commitlint/config-conventional": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.5.0.tgz", - "integrity": "sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==", + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@commitlint/types": "^20.5.0", - "conventional-changelog-conventionalcommits": "^9.2.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=v18" + "node": ">=18" } }, - "node_modules/@commitlint/config-validator": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.5.0.tgz", - "integrity": "sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@commitlint/types": "^20.5.0", - "ajv": "^8.11.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=v18" + "node": ">=18" } }, - "node_modules/@commitlint/ensure": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.5.0.tgz", - "integrity": "sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "@commitlint/types": "^20.5.0", - "lodash.camelcase": "^4.3.0", - "lodash.kebabcase": "^4.1.1", - "lodash.snakecase": "^4.1.1", - "lodash.startcase": "^4.4.0", - "lodash.upperfirst": "^4.3.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=v18" + "node": ">=18" } }, - "node_modules/@commitlint/execute-rule": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz", - "integrity": "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=v18" + "node": ">=18" } }, - "node_modules/@commitlint/format": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.5.0.tgz", - "integrity": "sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", - "dependencies": { - "@commitlint/types": "^20.5.0", - "picocolors": "^1.1.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=v18" + "node": ">=18" } }, - "node_modules/@commitlint/is-ignored": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.5.0.tgz", - "integrity": "sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@commitlint/types": "^20.5.0", - "semver": "^7.6.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=v18" + "node": ">=18" } }, - "node_modules/@commitlint/lint": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.5.0.tgz", - "integrity": "sha512-jiM3hNUdu04jFBf1VgPdjtIPvbuVfDTBAc6L98AWcoLjF5sYqkulBHBzlVWll4rMF1T5zeQFB6r//a+s+BBKlA==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@commitlint/is-ignored": "^20.5.0", - "@commitlint/parse": "^20.5.0", - "@commitlint/rules": "^20.5.0", - "@commitlint/types": "^20.5.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=v18" + "node": ">=18" } }, - "node_modules/@commitlint/load": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.5.0.tgz", - "integrity": "sha512-sLhhYTL/KxeOTZjjabKDhwidGZan84XKK1+XFkwDYL/4883kIajcz/dZFAhBJmZPtL8+nBx6bnkzA95YxPeDPw==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "dependencies": { - "@commitlint/config-validator": "^20.5.0", - "@commitlint/execute-rule": "^20.0.0", - "@commitlint/resolve-extends": "^20.5.0", - "@commitlint/types": "^20.5.0", - "cosmiconfig": "^9.0.1", - "cosmiconfig-typescript-loader": "^6.1.0", - "is-plain-obj": "^4.1.0", - "lodash.mergewith": "^4.6.2", - "picocolors": "^1.1.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=v18" + "node": ">=18" } }, - "node_modules/@commitlint/load/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/@commitlint/message": { - "version": "20.4.3", - "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-20.4.3.tgz", - "integrity": "sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/parse": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-20.5.0.tgz", - "integrity": "sha512-SeKWHBMk7YOTnnEWUhx+d1a9vHsjjuo6Uo1xRfPNfeY4bdYFasCH1dDpAv13Lyn+dDPOels+jP6D2GRZqzc5fA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^20.5.0", - "conventional-changelog-angular": "^8.2.0", - "conventional-commits-parser": "^6.3.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/read": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-20.5.0.tgz", - "integrity": "sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/top-level": "^20.4.3", - "@commitlint/types": "^20.5.0", - "git-raw-commits": "^5.0.0", - "minimist": "^1.2.8", - "tinyexec": "^1.0.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/resolve-extends": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.5.0.tgz", - "integrity": "sha512-3SHPWUW2v0tyspCTcfSsYml0gses92l6TlogwzvM2cbxDgmhSRc+fldDjvGkCXJrjSM87BBaWYTPWwwyASZRrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/config-validator": "^20.5.0", - "@commitlint/types": "^20.5.0", - "global-directory": "^4.0.1", - "import-meta-resolve": "^4.0.0", - "lodash.mergewith": "^4.6.2", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/rules": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.5.0.tgz", - "integrity": "sha512-5NdQXQEdnDPT5pK8O39ZA7HohzPRHEsDGU23cyVCNPQy4WegAbAwrQk3nIu7p2sl3dutPk8RZd91yKTrMTnRkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/ensure": "^20.5.0", - "@commitlint/message": "^20.4.3", - "@commitlint/to-lines": "^20.0.0", - "@commitlint/types": "^20.5.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/to-lines": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-20.0.0.tgz", - "integrity": "sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/top-level": { - "version": "20.4.3", - "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-20.4.3.tgz", - "integrity": "sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@commitlint/types": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.5.0.tgz", - "integrity": "sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==", - "dev": true, - "license": "MIT", - "dependencies": { - "conventional-commits-parser": "^6.3.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=v18" - } - }, - "node_modules/@conventional-changelog/git-client": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-2.6.0.tgz", - "integrity": "sha512-T+uPDciKf0/ioNNDpMGc8FDsehJClZP0yR3Q5MN6wE/Y/1QZ7F+80OgznnTCOlMEG4AV0LvH2UJi3C/nBnaBUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@simple-libs/child-process-utils": "^1.0.0", - "@simple-libs/stream-utils": "^1.2.0", - "semver": "^7.5.2" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { "node": ">=18" - }, - "peerDependencies": { - "conventional-commits-filter": "^5.0.0", - "conventional-commits-parser": "^6.3.0" - }, - "peerDependenciesMeta": { - "conventional-commits-filter": { - "optional": true - }, - "conventional-commits-parser": { - "optional": true - } - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@ctrl/ngx-emoji-mart": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@ctrl/ngx-emoji-mart/-/ngx-emoji-mart-9.3.0.tgz", - "integrity": "sha512-9uFzAvlFT21OLsTfhL3ZEO5mp51qvL1F4ErIZVBIsvAlji46u6p2KGgVA60oIheFBX4JoZI7HBDGOkGnm9dTUQ==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/core": ">=15.0.0-0" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@es-joy/jsdoccomment": { - "version": "0.31.0", - "dev": true, - "license": "MIT", - "dependencies": { - "comment-parser": "1.3.1", - "esquery": "^1.4.0", - "jsdoc-type-pratt-parser": "~3.1.0" - }, - "engines": { - "node": "^14 || ^16 || ^17 || ^18" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", - "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz", - "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ - "arm" + "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ - "android" + "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz", - "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ - "android" + "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz", - "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ - "android" + "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz", - "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==", + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "openharmony" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz", - "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==", + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" + "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz", - "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==", + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ - "freebsd" + "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz", - "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==", + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ - "x64" + "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ - "freebsd" + "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz", - "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==", + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ - "arm" + "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz", - "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=12" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz", - "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "license": "MIT", "engines": { - "node": ">=12" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz", - "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz", - "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", "engines": { - "node": ">=12" + "node": "18 || 20 || >=22" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz", - "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, "engines": { - "node": ">=12" + "node": "18 || 20 || >=22" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz", - "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, "engines": { - "node": ">=12" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", - "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz", - "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz", - "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "license": "MIT", "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz", - "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz", - "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz", - "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==", - "cpu": [ - "ia32" - ], + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", "dev": true, - "optional": true, - "os": [ - "win32" - ], + "license": "MIT", "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", - "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", - "cpu": [ - "x64" - ], + "node_modules/@gar/promise-retry": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", + "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", "dev": true, - "optional": true, - "os": [ - "win32" - ], + "license": "MIT", "engines": { - "node": ">=12" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", + "node_modules/@harperfast/extended-iterable": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", + "integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", "dev": true, "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=18.14.1" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "hono": "^4" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "dev": true, - "license": "MIT", + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=18.18.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "dev": true, - "license": "MIT", + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "license": "Apache-2.0", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" }, "funding": { - "url": "https://opencollective.com/eslint" + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" }, "funding": { "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "node_modules/@inquirer/ansi": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", + "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", + "node_modules/@inquirer/checkbox": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.1.tgz", + "integrity": "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==", "dev": true, "license": "MIT", "dependencies": { - "type-fest": "^0.20.2" + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=8" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", + "node_modules/@inquirer/confirm": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.12.tgz", + "integrity": "sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==", "dev": true, "license": "MIT", "dependencies": { - "argparse": "^2.0.1" + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", + "node_modules/@inquirer/core": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.1.tgz", + "integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@inquirer/ansi": "^2.0.7", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": "*" - } - }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@eslint/js": { - "version": "8.57.0", + "node_modules/@inquirer/editor": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.2.tgz", + "integrity": "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==", "dev": true, "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "dev": true, - "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@inquirer/core": "^11.2.1", + "@inquirer/external-editor": "^3.0.3", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=10.10.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "node_modules/@inquirer/expand": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.1.tgz", + "integrity": "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", + "node_modules/@inquirer/external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==", "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "license": "ISC", + "license": "MIT", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" }, "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "license": "MIT", - "engines": { - "node": ">=12" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", + "node_modules/@inquirer/figures": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", + "integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { + "node_modules/@inquirer/input": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.2.tgz", + "integrity": "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==", + "dev": true, "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=12" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", + "node_modules/@inquirer/number": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.1.tgz", + "integrity": "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=12" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", + "node_modules/@inquirer/password": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.1.tgz", + "integrity": "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=12" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "peerDependencies": { + "@types/node": ">=18" }, - "engines": { - "node": ">=8" + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", + "node_modules/@inquirer/prompts": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.4.2.tgz", + "integrity": "sha512-XJmn/wY4AX56l1BRU+ZjDrFtg9+2uBEi4JvJQj82kwJDQKiPgSn4CEsbfGGygS4Gw6rkL4W18oATjfVfaqub2Q==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "@inquirer/checkbox": "^5.1.4", + "@inquirer/confirm": "^6.0.12", + "@inquirer/editor": "^5.1.1", + "@inquirer/expand": "^5.0.13", + "@inquirer/input": "^5.0.12", + "@inquirer/number": "^4.0.12", + "@inquirer/password": "^5.0.12", + "@inquirer/rawlist": "^5.2.8", + "@inquirer/search": "^4.1.8", + "@inquirer/select": "^5.1.4" }, "engines": { - "node": ">=8" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", + "node_modules/@inquirer/rawlist": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.1.tgz", + "integrity": "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==", "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=8" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", + "node_modules/@inquirer/search": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.1.tgz", + "integrity": "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==", "dev": true, "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=6" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", + "node_modules/@inquirer/select": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.1.tgz", + "integrity": "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=8" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", + "node_modules/@inquirer/type": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", + "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@sinclair/typebox": "^0.27.8" + "minipass": "^7.0.4" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "dev": true, + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "dev": true, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "license": "MIT", - "engines": { - "node": ">=6.0.0" + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "dev": true, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", "engines": { "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "dev": true, + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-4.2.3.tgz", + "integrity": "sha512-Co9U3AJ3LW0J8XBHjVoNKA79dMAyFt8EZH3OaKTMcDTj8r+6kG3vSUPq/eGLHT7P0iK3uLaFfhdFYd3033P24g==", "dev": true, - "license": "MIT" - }, - "node_modules/@ljharb/through": { - "version": "2.3.13", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7" + "@inquirer/type": "^4.0.5" }, "engines": { - "node": ">= 0.4" - } - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" + "node": ">=22.13.0" }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 9", + "listr2": "10.2.1" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.4.tgz", + "integrity": "sha512-Kk4Kz3iyu1QiLsLZBS9Af1eSKUC8VR2T+/jyE2iAyuGw2VwK08pp5iTbZnXn6sWu0LogO/RFktMxOjiDA2sS3w==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } + "os": [ + "darwin" + ] }, - "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.4.tgz", + "integrity": "sha512-BEe5Rp3trn26oxoXOVL5HVDoiYmjUDwr8NRPkBOdUdCSBEorKI+7JrZLRKAdxO+G6cGQLgseXk0gR7qIQa7aGw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } + "os": [ + "darwin" + ] }, - "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.4.tgz", + "integrity": "sha512-SGbFR7816uBcTHc2ZY4S6WyOkl9bICnzqTQd2Mv4V/j24cfds88xx2nC6cm/y8zGQL7Ds31YF/5NGxjgcdM5Hw==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "os": [ + "linux" + ] }, - "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.4.tgz", + "integrity": "sha512-cUXEengO8o60v1SWerJTH4/RH4U3+9jC0/4njp2Z9NdmvaGzhKsbRM2wpXuRYrN8tytsoJCg0SvWEWwHAwLbCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, - "bin": { - "semver": "bin/semver.js" - } + "os": [ + "linux" + ] }, - "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "license": "ISC", + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.4.tgz", + "integrity": "sha512-Gxq8jpgOWXwd0PUR+c9R2Ik1/uBnGd5GMIIzRRDqABCkvmjtC3KWcyhesV9jSPCz759isl0NlbsstZ2oyvk8lA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } + "os": [ + "linux" + ] }, - "node_modules/@material/animation": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@lmdb/lmdb-win32-arm64": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.4.tgz", + "integrity": "sha512-pKv1DJ1bPZAaHkdFsSz5IDfUG8x9vntgquXF9/Dm2xuupcIe/EkLzylpoBxppFVK5vzbV561Dq26jNY2fIMA7g==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.1.0" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@material/auto-init": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.4.tgz", + "integrity": "sha512-JF1BmLCm9kGEVZgYmJq43zeQVdHVgAJnTi/NURWEsy6L1ZrrlSmdltS+D17QN4LODwf+1LMXAA9auIZVXtWwzw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@material/banner": { - "version": "15.0.0-canary.7f224ddd4.0", - "license": "MIT", + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "optional": true, "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/button": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" } }, - "node_modules/@material/base": { - "version": "15.0.0-canary.7f224ddd4.0", - "license": "MIT", - "dependencies": { - "tslib": "^2.1.0" - } + "node_modules/@mapbox/node-pre-gyp/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true }, - "node_modules/@material/button": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "license": "MIT", + "optional": true, "dependencies": { - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" } }, - "node_modules/@material/card": { - "version": "15.0.0-canary.7f224ddd4.0", - "license": "MIT", - "dependencies": { - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@mapbox/node-pre-gyp/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" } }, - "node_modules/@material/checkbox": { - "version": "15.0.0-canary.7f224ddd4.0", - "license": "MIT", + "node_modules/@mapbox/node-pre-gyp/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "optional": true, "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/@material/chips": { - "version": "15.0.0-canary.7f224ddd4.0", - "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/checkbox": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "safevalues": "^0.3.4", - "tslib": "^2.1.0" + "node_modules/@mapbox/node-pre-gyp/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@material/circular-progress": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "license": "MIT", + "optional": true, "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/progress-indicator": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/data-table": { - "version": "15.0.0-canary.7f224ddd4.0", - "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/checkbox": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/icon-button": "15.0.0-canary.7f224ddd4.0", - "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", - "@material/list": "15.0.0-canary.7f224ddd4.0", - "@material/menu": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/select": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/@material/density": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "license": "MIT", + "optional": true, "dependencies": { - "tslib": "^2.1.0" + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@material/dialog": { - "version": "15.0.0-canary.7f224ddd4.0", - "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/button": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/icon-button": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@material/dom": { - "version": "15.0.0-canary.7f224ddd4.0", - "license": "MIT", - "dependencies": { - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "node_modules/@mapbox/node-pre-gyp/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=8" } }, - "node_modules/@material/drawer": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@mapbox/node-pre-gyp/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "license": "MIT", + "optional": true, "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/list": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/@material/elevation": { - "version": "15.0.0-canary.7f224ddd4.0", - "license": "MIT", + "node_modules/@mapbox/node-pre-gyp/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@material/fab": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@mapbox/node-pre-gyp/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" } }, - "node_modules/@material/feature-targeting": { - "version": "15.0.0-canary.7f224ddd4.0", - "license": "MIT", + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, "dependencies": { - "tslib": "^2.1.0" + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" } }, - "node_modules/@material/floating-label": { - "version": "15.0.0-canary.7f224ddd4.0", - "license": "MIT", + "node_modules/@mapbox/node-pre-gyp/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar 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", + "optional": true, "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" } }, - "node_modules/@material/focus-ring": { - "version": "15.0.0-canary.7f224ddd4.0", - "license": "MIT", - "dependencies": { - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0" - } + "node_modules/@mapbox/node-pre-gyp/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true }, - "node_modules/@material/form-field": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@mattlewis92/dom-autoscroller": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@mattlewis92/dom-autoscroller/-/dom-autoscroller-2.4.2.tgz", + "integrity": "sha512-YbrUWREPGEjE/FU6foXcAT1YbVwqD/jkYnY1dFb0o4AxtP3s4xKBthlELjndZih8uwsDWgQZx1eNskRNe2BgZQ==", "license": "MIT", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "peer": true }, - "node_modules/@material/icon-button": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "dev": true, "license": "MIT", "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } } }, - "node_modules/@material/image-list": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@material/layout-grid": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.1.0" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@material/line-ripple": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@material/linear-progress": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/progress-indicator": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@material/list": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@material/menu": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/list": "15.0.0-canary.7f224ddd4.0", - "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@material/menu-surface": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", + "dev": true, "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/notched-outline": { - "version": "15.0.0-canary.7f224ddd4.0", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/floating-label": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/progress-indicator": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.1.0" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/radio": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/ripple": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/rtl": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/segmented-button": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/touch-target": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/select": { - "version": "15.0.0-canary.7f224ddd4.0", - "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/floating-label": "15.0.0-canary.7f224ddd4.0", - "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", - "@material/list": "15.0.0-canary.7f224ddd4.0", - "@material/menu": "15.0.0-canary.7f224ddd4.0", - "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", - "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/shape": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/slider": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/snackbar": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/button": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/icon-button": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/switch": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "safevalues": "^0.3.4", - "tslib": "^2.1.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/tab": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/tab-bar": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/tab": "15.0.0-canary.7f224ddd4.0", - "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", - "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/tab-indicator": { - "version": "15.0.0-canary.7f224ddd4.0", - "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/tab-scroller": { - "version": "15.0.0-canary.7f224ddd4.0", - "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/tab": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/textfield": { - "version": "15.0.0-canary.7f224ddd4.0", - "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/density": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/floating-label": "15.0.0-canary.7f224ddd4.0", - "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", - "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/theme": { - "version": "15.0.0-canary.7f224ddd4.0", - "license": "MIT", - "dependencies": { - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/tokens": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/elevation": "15.0.0-canary.7f224ddd4.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/tooltip": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/button": "15.0.0-canary.7f224ddd4.0", - "@material/dom": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/tokens": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "safevalues": "^0.3.4", - "tslib": "^2.1.0" + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/top-app-bar": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/animation": "15.0.0-canary.7f224ddd4.0", - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/elevation": "15.0.0-canary.7f224ddd4.0", - "@material/ripple": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/shape": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "@material/typography": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/touch-target": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/base": "15.0.0-canary.7f224ddd4.0", - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/rtl": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@material/typography": { - "version": "15.0.0-canary.7f224ddd4.0", + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", - "@material/theme": "15.0.0-canary.7f224ddd4.0", - "tslib": "^2.1.0" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@mattlewis92/dom-autoscroller": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@mattlewis92/dom-autoscroller/-/dom-autoscroller-2.4.2.tgz", - "integrity": "sha512-YbrUWREPGEjE/FU6foXcAT1YbVwqD/jkYnY1dFb0o4AxtP3s4xKBthlELjndZih8uwsDWgQZx1eNskRNe2BgZQ==", - "license": "MIT" - }, "node_modules/@ngneat/hotkeys": { - "version": "4.0.0", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ngneat/hotkeys/-/hotkeys-4.1.0.tgz", + "integrity": "sha512-bqtmK0wMGQOFtNnxmklnbhVbiUoOIp5rXY4UeWGRoMgf7RGvW6dO5moZSPzenJwp8pgi2EmSyo+xpQ8R512hIw==", "license": "MIT", "dependencies": { "tslib": "^2.0.0" } }, "node_modules/@ngstack/code-editor": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@ngstack/code-editor/-/code-editor-7.3.0.tgz", - "integrity": "sha512-ZqjRynFAA74sVGF5bHecwjXQf5vOM7+bkCd++mNlVGjCq6qVY0TtM7+9pCiCIvzVBn3++9KNDhlpDSq1m5WIYA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@ngstack/code-editor/-/code-editor-9.0.0.tgz", + "integrity": "sha512-sioi0qyeo9Q8PIdhGFmUeuEx6LETqSjC/4A1Fnl9HFVYzCU7mDVdyeUITubu+4THjraH1mIAVw2t0R7rDgdHdA==", "license": "MIT", "dependencies": { "tslib": "^2.5.0" @@ -4583,516 +4625,695 @@ "@angular/core": ">=17.1.1" } }, - "node_modules/@ngtools/webpack": { - "version": "17.3.7", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "@angular/compiler-cli": "^17.0.0", - "typescript": ">=5.2 <5.5", - "webpack": "^5.54.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", + "node_modules/@npmcli/agent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.2.tgz", + "integrity": "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" }, "engines": { - "node": ">= 8" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", + "node_modules/@npmcli/agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 8" + "node": ">= 14" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", + "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@npmcli/agent": { - "version": "2.2.2", - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">= 14" } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.2.2", - "license": "ISC", + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": "14 || >=16.14" + "node": "20 || >=22" } }, "node_modules/@npmcli/fs": { - "version": "3.1.1", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "dev": true, "license": "ISC", "dependencies": { "semver": "^7.3.5" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/git": { - "version": "5.0.7", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", + "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", + "dev": true, "license": "ISC", "dependencies": { - "@npmcli/promise-spawn": "^7.0.0", - "lru-cache": "^10.0.1", - "npm-pick-manifest": "^9.0.0", - "proc-log": "^4.0.0", - "promise-inflight": "^1.0.1", - "promise-retry": "^2.0.1", + "@gar/promise-retry": "^1.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", "semver": "^7.3.5", - "which": "^4.0.0" + "which": "^6.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/git/node_modules/isexe": { - "version": "3.1.1", - "license": "ISC", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=20" } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "10.2.2", - "license": "ISC", - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/@npmcli/git/node_modules/proc-log": { - "version": "4.2.0", - "license": "ISC", + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "20 || >=22" } }, "node_modules/@npmcli/git/node_modules/which": { - "version": "4.0.0", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/installed-package-contents": { - "version": "2.1.0", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", + "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", + "dev": true, "license": "ISC", "dependencies": { - "npm-bundled": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" }, "bin": { "installed-package-contents": "bin/index.js" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/node-gyp": { - "version": "3.0.0", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", + "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", + "dev": true, "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/package-json": { - "version": "5.1.0", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", + "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", + "dev": true, "license": "ISC", "dependencies": { - "@npmcli/git": "^5.0.0", - "glob": "^10.2.2", - "hosted-git-info": "^7.0.0", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^6.0.0", - "proc-log": "^4.0.0", - "semver": "^7.5.3" + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.5.3", + "spdx-expression-parse": "^4.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/package-json/node_modules/glob": { - "version": "10.3.15", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, + "node_modules/@npmcli/package-json/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "18 || 20 || >=22" } }, - "node_modules/@npmcli/package-json/node_modules/hosted-git-info": { - "version": "7.0.2", - "license": "ISC", + "node_modules/@npmcli/package-json/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", "dependencies": { - "lru-cache": "^10.0.1" + "balanced-match": "^4.0.2" }, "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/package-json/node_modules/lru-cache": { - "version": "10.2.2", - "license": "ISC", - "engines": { - "node": "14 || >=16.14" + "node": "18 || 20 || >=22" } }, - "node_modules/@npmcli/package-json/node_modules/normalize-package-data": { - "version": "6.0.1", - "license": "BSD-2-Clause", + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "hosted-git-info": "^7.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@npmcli/package-json/node_modules/proc-log": { - "version": "4.2.0", - "license": "ISC", + "node_modules/@npmcli/package-json/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@npmcli/promise-spawn": { - "version": "7.0.2", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", + "dev": true, "license": "ISC", "dependencies": { - "which": "^4.0.0" + "which": "^6.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/promise-spawn/node_modules/isexe": { - "version": "3.1.1", - "license": "ISC", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=20" } }, "node_modules/@npmcli/promise-spawn/node_modules/which": { - "version": "4.0.0", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/redact": { - "version": "1.1.0", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", + "dev": true, "license": "ISC", "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@npmcli/run-script": { - "version": "7.0.4", - "license": "ISC", - "dependencies": { - "@npmcli/node-gyp": "^3.0.0", - "@npmcli/package-json": "^5.0.0", - "@npmcli/promise-spawn": "^7.0.0", - "node-gyp": "^10.0.0", - "which": "^4.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/run-script/node_modules/isexe": { - "version": "3.1.1", - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/@npmcli/run-script/node_modules/which": { - "version": "4.0.0", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", + "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", + "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0" }, "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@nrwl/devkit": { - "version": "18.3.4", - "dev": true, + "node_modules/@nx/nx-darwin-arm64": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-23.0.1.tgz", + "integrity": "sha512-gQJvgPnbI91DBe23Th2CqD9R/S54cPS3C1f0DhyQ8YEf9rR7EEc+sVGjhgVxlhfOk2W7I1Gy6EkXwpN4aDoW4w==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "@nx/devkit": "18.3.4" - } + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@nx/nx-darwin-x64": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-23.0.1.tgz", + "integrity": "sha512-e/lvzHKN6gpuD7MqEtfH1fOfnR75E55ytYNt8jaRxKI6EvpCq+Q3MunDuh9GQYAkqDrUqE7AhHrHc+eKATVEHw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@nx/nx-linux-arm64-gnu": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-23.0.1.tgz", + "integrity": "sha512-zX2JdHQejZWB3DRgNsh77qOVYaSSjSLuBP2qIqc7EWVlCUnR7Aj3e65PTIps4LxMMmUp4twZA2ezS0rtyK2A4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@nx/nx-linux-x64-gnu": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-23.0.1.tgz", + "integrity": "sha512-kVszY2xRyyrCXgdCdM1qG1WUhDjNPZxtdWq86a0TyIRJjfJTP9NHqpyhmvj9c2RdZxKVWHotx6fBJzY6Vn2ZrA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@nx/nx-win32-x64-msvc": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-23.0.1.tgz", + "integrity": "sha512-TE/wvBa2cpkVXmk/AXUQAneong4JReS2hyNpAUONKG1yXU7TDKe0wvn1xQXxAbyspudT9NuCnVtpVuEkRz8S+Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@nrwl/tao": { - "version": "18.3.4", + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "optional": true, "dependencies": { - "nx": "18.3.4", - "tslib": "^2.3.0" + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" }, - "bin": { - "tao": "index.js" - } - }, - "node_modules/@nx/devkit": { - "version": "18.3.4", + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@nrwl/devkit": "18.3.4", - "ejs": "^3.1.7", - "enquirer": "~2.3.6", - "ignore": "^5.0.4", - "semver": "^7.5.3", - "tmp": "~0.2.1", - "tslib": "^2.3.0", - "yargs-parser": "21.1.1" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" }, - "peerDependencies": { - "nx": ">= 16 <= 19" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-darwin-arm64": { - "version": "18.3.5", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-18.3.5.tgz", - "integrity": "sha512-4I5UpZ/x2WO9OQyETXKjaYhXiZKUTYcLPewruRMODWu6lgTM9hHci0SqMQB+TWe3f80K8VT8J8x3+uJjvllGlg==", + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ "arm64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-darwin-x64": { - "version": "18.3.5", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-18.3.5.tgz", - "integrity": "sha512-Drn6jOG237AD/s6OWPt06bsMj0coGKA5Ce1y5gfLhptOGk4S4UPE/Ay5YCjq+/yhTo1gDHzCHxH0uW2X9MN9Fg==", + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", "cpu": [ "x64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-freebsd-x64": { - "version": "18.3.4", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-18.3.4.tgz", - "integrity": "sha512-bjSPak/d+bcR95/pxHMRhnnpHc6MnrQcG6f5AjX15Esm4JdrdQKPBmG1RybuK0WKSyD5wgVhkAGc/QQUom9l8g==", + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "18.3.4", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-18.3.4.tgz", - "integrity": "sha512-/1HnUL7jhH0S7PxJqf6R1pk3QlAU22GY89EQV9fd+RDUtp7IyzaTlkebijTIqfxlSjC4OO3bPizaxEaxdd3uKQ==", + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-linux-arm64-gnu": { - "version": "18.3.4", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-18.3.4.tgz", - "integrity": "sha512-g/2IaB2bZTKaBNPEf9LxtIXb1XHdhh3VO9PnePIrwkkixPMLN0dTxT5Sttt75lvLP3EU1AUR5w3Aaz2Q1mYtWA==", + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", "cpu": [ "arm64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-linux-arm64-musl": { - "version": "18.3.4", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-18.3.4.tgz", - "integrity": "sha512-MgfKLoEF6I1cCS+0ooFLEjJSSVdCYyCT9Q96IHRJntAEL8u/0GR2OUoBoLC+q1lnbIkJr/uqTJxA2Jh+sJTIbA==", + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-linux-x64-gnu": { - "version": "18.3.5", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-18.3.5.tgz", - "integrity": "sha512-vYrikG6ff4I9cvr3Ysk3y3gjQ9cDcvr3iAr+4qqcQ4qVE+OLL2++JDS6xfPvG/TbS3GTQpyy2STRBwiHgxTeJw==", + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", "cpu": [ "x64" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-linux-x64-musl": { - "version": "18.3.4", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-18.3.4.tgz", - "integrity": "sha512-qIJKJCYFRLVSALsvg3avjReOjuYk91Q0hFXMJ2KaEM1Y3tdzcFN0fKBiaHexgbFIUk8zJuS4dJObTqSYMXowbg==", + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-win32-arm64-msvc": { - "version": "18.3.4", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-18.3.4.tgz", - "integrity": "sha512-UxC8mRkFTPdZbKFprZkiBqVw8624xU38kI0xyooxKlFpt5lccTBwJ0B7+R8p1RoWyvh2DSyFI9VvfD7lczg1lA==", + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@nx/nx-win32-x64-msvc": { - "version": "18.3.5", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-18.3.5.tgz", - "integrity": "sha512-xFwKVTIXSgjdfxkpriqHv5NpmmFILTrWLEkUGSoimuRaAm1u15YWx/VmaUQ+UWuJnmgqvB/so4SMHSfNkq3ijA==", + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ - "x64" + "ia32" ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/@pkgr/core": { - "version": "0.1.1", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", "dev": true, "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/pkgr" } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", - "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", "cpu": [ "arm" ], @@ -5104,9 +5325,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", - "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", "cpu": [ "arm64" ], @@ -5118,9 +5339,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", - "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", "cpu": [ "arm64" ], @@ -5132,9 +5353,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", - "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", "cpu": [ "x64" ], @@ -5146,9 +5367,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", - "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", "cpu": [ "arm64" ], @@ -5160,9 +5381,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", - "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", "cpu": [ "x64" ], @@ -5174,9 +5395,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", - "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", "cpu": [ "arm" ], @@ -5188,9 +5409,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", - "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", "cpu": [ "arm" ], @@ -5202,9 +5423,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", - "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", "cpu": [ "arm64" ], @@ -5215,9 +5436,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", - "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", "cpu": [ "arm64" ], @@ -5229,9 +5450,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", - "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", "cpu": [ "loong64" ], @@ -5243,9 +5464,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", - "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", "cpu": [ "loong64" ], @@ -5257,9 +5478,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", - "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", "cpu": [ "ppc64" ], @@ -5271,9 +5492,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", - "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", "cpu": [ "ppc64" ], @@ -5285,9 +5506,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", - "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", "cpu": [ "riscv64" ], @@ -5299,9 +5520,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", - "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", "cpu": [ "riscv64" ], @@ -5313,9 +5534,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", - "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", "cpu": [ "s390x" ], @@ -5327,9 +5548,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", - "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", "cpu": [ "x64" ], @@ -5340,9 +5561,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", - "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", "cpu": [ "x64" ], @@ -5354,9 +5575,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", - "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", "cpu": [ "x64" ], @@ -5368,9 +5589,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", - "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", "cpu": [ "arm64" ], @@ -5381,15783 +5602,7216 @@ "openharmony" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", - "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", - "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", - "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", - "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@scarf/scarf": { - "version": "1.3.0", - "hasInstallScript": true, - "license": "Apache-2.0" - }, - "node_modules/@schematics/angular": { - "version": "17.3.7", - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "17.3.7", - "@angular-devkit/schematics": "17.3.7", - "jsonc-parser": "3.2.1" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@sigstore/bundle": { - "version": "2.3.1", - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.3.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@sigstore/core": { - "version": "1.1.0", - "license": "Apache-2.0", - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@sigstore/protobuf-specs": { - "version": "0.3.2", - "license": "Apache-2.0", - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@sigstore/sign": { - "version": "2.3.1", - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^2.3.0", - "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.1", - "make-fetch-happen": "^13.0.1", - "proc-log": "^4.2.0", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@sigstore/sign/node_modules/proc-log": { - "version": "4.2.0", - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@sigstore/tuf": { - "version": "2.3.3", - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.3.0", - "tuf-js": "^2.2.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@sigstore/verify": { - "version": "1.2.0", - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^2.3.1", - "@sigstore/core": "^1.1.0", - "@sigstore/protobuf-specs": "^0.3.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@simple-libs/child-process-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", - "integrity": "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@simple-libs/stream-utils": "^1.2.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://ko-fi.com/dangreen" - } - }, - "node_modules/@simple-libs/stream-utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", - "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://ko-fi.com/dangreen" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "dev": true, - "license": "MIT" - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@tufjs/canonical-json": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@tufjs/models": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@types/angular": { - "version": "1.5.11", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/jquery": "1.10.*" - } - }, - "node_modules/@types/babel-types": { - "version": "7.0.15", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@types/babylon": { - "version": "6.16.9", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/babel-types": "*" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bonjour": { - "version": "3.5.13", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/canvas-confetti": { - "version": "1.6.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "node_modules/@types/cookie": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/cors": { - "version": "2.8.17", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/d3": { - "version": "3.5.53", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/eslint": { - "version": "8.56.10", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/express": { - "version": "4.17.21", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/file-saver": { - "version": "2.0.7", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/http-proxy": { - "version": "1.17.14", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/jasmine": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-6.0.0.tgz", - "integrity": "sha512-18lgGsLmEh3VJk9eZ5wAjTISxdqzl6YOwu8UdMpolajN57QOCNbl+AbHUd+Yu9ItrsFdB+c8LSZSGNg8nHaguw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/jasminewd2": { - "version": "2.0.13", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/jasmine": "*" - } - }, - "node_modules/@types/jquery": { - "version": "1.10.45", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/lodash": { - "version": "4.17.1", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/minimatch": { - "version": "3.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.12.11", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/node-forge": { - "version": "1.3.11", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/q": { - "version": "0.0.32", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/qs": { - "version": "6.9.15", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/retry": { - "version": "0.12.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/selenium-webdriver": { - "version": "3.0.26", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/semver": { - "version": "7.5.8", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "0.17.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-index": { - "version": "1.9.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/sockjs": { - "version": "0.3.36", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ws": { - "version": "8.5.10", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.8.0", - "@typescript-eslint/type-utils": "7.8.0", - "@typescript-eslint/utils": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "7.8.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "7.8.0", - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/typescript-estree": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "7.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "7.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "7.8.0", - "@typescript-eslint/utils": "7.8.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "7.8.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.8.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "7.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.15", - "@types/semver": "^7.5.8", - "@typescript-eslint/scope-manager": "7.8.0", - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/typescript-estree": "7.8.0", - "semver": "^7.6.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.8.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@uirouter/angular": { - "version": "13.0.0", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": ">=6.0.0" - }, - "peerDependencies": { - "@angular/common": "^17.0.0", - "@angular/core": "^17.0.0", - "@uirouter/core": "^6.0.8", - "@uirouter/rx": "^1.0.0" - } - }, - "node_modules/@uirouter/angular-hybrid": { - "version": "17.1.0", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/core": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", - "@angular/upgrade": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", - "@uirouter/angular": "^13.0.0", - "@uirouter/angularjs": "^1.0.30", - "@uirouter/core": "^6.1.0", - "angular": "^1.5.0" - } - }, - "node_modules/@uirouter/angularjs": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - }, - "peerDependencies": { - "@uirouter/core": "^6.0.8", - "angular": ">=1.2.0" - } - }, - "node_modules/@uirouter/core": { - "version": "6.1.0", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/@uirouter/rx": { - "version": "1.0.0", - "license": "MIT", - "peerDependencies": { - "@uirouter/core": ">=6.0.1", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "dev": true, - "license": "ISC" - }, - "node_modules/@vitejs/plugin-basic-ssl": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.6.0" - }, - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@worktile/gantt": { - "version": "18.0.5", - "resolved": "https://registry.npmjs.org/@worktile/gantt/-/gantt-18.0.5.tgz", - "integrity": "sha512-LCcWaFBmeg5u9cVDEmREHdR+qJJHE3Ld4VxdoJpTXfhDkx2f19tp0wMR9MkwOLRwwTCx/5gGJ1kTbz4P0Zfc1Q==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/cdk": ">=17.0.0", - "@angular/common": ">=17.0.0", - "@angular/core": ">=17.0.0", - "date-fns": ">=2.0.0", - "rxjs": "^6.5.0 || ^7.0.0" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@yarnpkg/lockfile": { - "version": "1.1.0", - "license": "BSD-2-Clause" - }, - "node_modules/@yarnpkg/parsers": { - "version": "3.0.0-rc.46", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "js-yaml": "^3.10.0", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=14.15.0" - } - }, - "node_modules/@zkochan/js-yaml": { - "version": "0.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@zkochan/js-yaml/node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "devOptional": true, - "license": "ISC" - }, - "node_modules/accepts": { - "version": "1.3.8", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.11.3", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-globals": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "acorn": "^4.0.4" - } - }, - "node_modules/acorn-globals/node_modules/acorn": { - "version": "4.0.13", - "dev": true, - "license": "MIT", - "optional": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/adjust-sourcemap-loader": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "regex-parser": "^2.2.11" - }, - "engines": { - "node": ">=8.9" - } - }, - "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/adm-zip": { - "version": "0.5.12", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.1", - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ajv": { - "version": "8.12.0", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/align-text": { - "version": "0.1.4", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/align-text/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/alter": { - "version": "0.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "stable": "~0.1.3" - } - }, - "node_modules/angular": { - "version": "1.5.11", - "license": "MIT" - }, - "node_modules/angular-calendar": { - "version": "0.31.1", - "resolved": "https://registry.npmjs.org/angular-calendar/-/angular-calendar-0.31.1.tgz", - "integrity": "sha512-pjSIpoAaUzS/gx+14eOr4hPZhlQ8HxpiZypCSGqJNptq5PD+vOdVQ3h/Aaqnk86GraVcAQPXqfu64MtdKwTVNw==", - "license": "MIT", - "dependencies": { - "@scarf/scarf": "^1.1.1", - "angular-draggable-droppable": "^8.0.0", - "angular-resizable-element": "^7.0.0", - "calendar-utils": "^0.10.4", - "positioning": "^2.0.1", - "tslib": "^2.4.1" - }, - "funding": { - "url": "https://github.com/sponsors/mattlewis92" - }, - "peerDependencies": { - "@angular/core": ">=15.0.0" - } - }, - "node_modules/angular-draggable-droppable": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/angular-draggable-droppable/-/angular-draggable-droppable-8.0.0.tgz", - "integrity": "sha512-+gpSNBbygjV1pxTxsM3UPJKcXHXJabYoTtKcgQe74rGnb1umKc07XCBD1qDzvlG/kocthvhQ12qfYOYzHnE3ZA==", - "license": "MIT", - "dependencies": { - "@mattlewis92/dom-autoscroller": "^2.4.2", - "tslib": "^2.4.1" - }, - "peerDependencies": { - "@angular/core": ">=15.0.0" - } - }, - "node_modules/angular-filter": { - "version": "0.5.17", - "license": "MIT", - "dependencies": { - "angular": "*" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/angular-markdown-filter": { - "version": "1.3.2", - "license": "MIT", - "dependencies": { - "showdown": "^1.2.3" - } - }, - "node_modules/angular-md5": { - "version": "0.1.10" - }, - "node_modules/angular-mocks": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/angular-mocks/-/angular-mocks-1.8.3.tgz", - "integrity": "sha512-vqsT6zwu80cZ8RY7qRQBZuy6Fq5X7/N5hkV9LzNT0c8b546rw4ErGK6muW1u2JnDKYa7+jJuaGM702bWir4HGw==", - "license": "MIT" - }, - "node_modules/angular-nvd3": { - "version": "1.0.9", - "license": "MIT", - "dependencies": { - "angular": "^1.x", - "d3": "^3.3", - "nvd3": "^1.7.1" - } - }, - "node_modules/angular-resizable-element": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/angular-resizable-element/-/angular-resizable-element-7.0.2.tgz", - "integrity": "sha512-/BGuNiA38n9klexHO1xgnsA3VYigj9v+jUGjKtBRgfB26bCxZKsNWParSu2k3EqbATrfAJC4Nl8f7cORpJFf4w==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/core": ">=15.0.0" - } - }, - "node_modules/angular-resource": { - "version": "1.5.11", - "license": "MIT" - }, - "node_modules/angular-sanitize": { - "version": "1.5.11", - "resolved": "https://registry.npmjs.org/angular-sanitize/-/angular-sanitize-1.5.11.tgz", - "integrity": "sha512-9yVOr8YOefo0/4q+ImqNdGcbfGzelQIoHW0OoaoU/U5wpRZNn5IqlkdLW9udieSiprYzuXeqiS1V7ZiHurYisw==", - "deprecated": "For the actively supported Angular, see https://www.npmjs.com/package/@angular/core. AngularJS support has officially ended. For extended AngularJS support options, see https://goo.gle/angularjs-path-forward.", - "license": "MIT" - }, - "node_modules/angular-ui-bootstrap": { - "version": "0.13.4", - "license": "MIT", - "peerDependencies": { - "angular": "^1.3.x || >= 1.4.0-beta.0 || >= 1.5.0-beta.0", - "bootstrap": "^3.x" - } - }, - "node_modules/angular-ui-codemirror": { - "version": "0.3.0", - "license": "MIT" - }, - "node_modules/angular-xeditable": { - "version": "0.9.0", - "license": "MIT", - "dependencies": { - "angular": "~1.x" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/angulartics": { - "version": "1.0.3" - }, - "node_modules/angulartics-google-analytics": { - "version": "0.1.4", - "license": "MIT", - "peerDependencies": { - "angulartics": "~1.0.0" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "license": "Apache-2.0", - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "devOptional": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/aproba": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", - "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "license": "ISC", - "optional": true - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "1.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/aria-query": { - "version": "5.3.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/arr-diff": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-flatten": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-union": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-differ": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/array-each": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/array-ify": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/array-includes": { - "version": "3.1.8", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-slice": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/array-uniq": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-unique": { - "version": "0.3.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arrify": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/asn1": { - "version": "0.2.6", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/async": { - "version": "1.5.2", - "dev": true, - "license": "MIT" - }, - "node_modules/async-each": { - "version": "1.0.6", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/atob": { - "version": "2.1.2", - "dev": true, - "license": "(MIT OR Apache-2.0)", - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, - "node_modules/autoprefixer": { - "version": "6.7.7", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^1.7.6", - "caniuse-db": "^1.0.30000634", - "normalize-range": "^0.1.2", - "num2fraction": "^1.2.2", - "postcss": "^5.2.16", - "postcss-value-parser": "^3.2.3" - } - }, - "node_modules/autoprefixer/node_modules/ansi-regex": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/autoprefixer/node_modules/ansi-styles": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/autoprefixer/node_modules/browserslist": { - "version": "1.7.7", - "dev": true, - "license": "MIT", - "dependencies": { - "caniuse-db": "^1.0.30000639", - "electron-to-chromium": "^1.2.7" - }, - "bin": { - "browserslist": "cli.js" - } - }, - "node_modules/autoprefixer/node_modules/chalk": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/autoprefixer/node_modules/chalk/node_modules/supports-color": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/autoprefixer/node_modules/has-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/autoprefixer/node_modules/postcss": { - "version": "5.2.18", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/autoprefixer/node_modules/source-map": { - "version": "0.5.7", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/autoprefixer/node_modules/strip-ansi": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/autoprefixer/node_modules/supports-color": { - "version": "3.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^1.0.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.12.0", - "dev": true, - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^2.1.0" - } - }, - "node_modules/axobject-query": { - "version": "4.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/babel-loader": { - "version": "9.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "find-cache-dir": "^4.0.0", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0", - "webpack": ">=5" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.11", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.2", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.9.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.5.0", - "core-js-compat": "^3.34.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.5.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-runtime": { - "version": "6.26.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "node_modules/babel-runtime/node_modules/core-js": { - "version": "2.6.12", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true - }, - "node_modules/babel-runtime/node_modules/regenerator-runtime": { - "version": "0.11.1", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/babel-types": { - "version": "6.26.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - } - }, - "node_modules/babel-types/node_modules/to-fast-properties": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babylon": { - "version": "6.18.0", - "dev": true, - "license": "MIT", - "optional": true, - "bin": { - "babylon": "bin/babylon.js" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/base": { - "version": "0.11.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/define-property": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/is-descriptor": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/base64-arraybuffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/base64id": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, - "node_modules/basic-auth": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/batch": { - "version": "0.6.1", - "dev": true, - "license": "MIT" - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/big.js": { - "version": "5.2.2", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/blocking-proxy": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "blocking-proxy": "built/lib/bin.js" - }, - "engines": { - "node": ">=6.9.x" - } - }, - "node_modules/body": { - "version": "5.1.0", - "dev": true, - "dependencies": { - "continuable-cache": "^0.3.1", - "error": "^7.0.0", - "raw-body": "~1.1.0", - "safe-json-parse": "~1.0.1" - } - }, - "node_modules/body-parser": { - "version": "1.20.2", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.4.24", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/body-parser/node_modules/on-finished": { - "version": "2.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body/node_modules/bytes": { - "version": "1.0.0", - "dev": true - }, - "node_modules/body/node_modules/raw-body": { - "version": "1.1.7", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "1", - "string_decoder": "0.10" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/body/node_modules/string_decoder": { - "version": "0.10.31", - "dev": true, - "license": "MIT" - }, - "node_modules/bonjour-service": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/bootstrap": { - "version": "3.4.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/bootstrap-sass": { - "version": "3.4.3", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "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" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "devOptional": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.23.0", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/browserstack": { - "version": "1.6.1", - "dev": true, - "license": "MIT", - "dependencies": { - "https-proxy-agent": "^2.2.1" - } - }, - "node_modules/browserstack/node_modules/agent-base": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "es6-promisify": "^5.0.0" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/browserstack/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/browserstack/node_modules/https-proxy-agent": { - "version": "2.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - }, - "engines": { - "node": ">= 4.5.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cacache": { - "version": "18.0.3", - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^3.1.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "10.3.15", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "10.2.2", - "license": "ISC", - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/cache-base": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/calendar-utils": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/calendar-utils/-/calendar-utils-0.10.4.tgz", - "integrity": "sha512-gBK4xCJ42yjaUKwuUha6cZOfxAmGzvSgbdAaX3xLRioeKbYoOK1x1qeD6dch72rsMZlTgATPbBBx42bnkStqgQ==", - "license": "MIT" - }, - "node_modules/call-bind": { - "version": "1.0.7", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camel-case": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^2.2.0", - "upper-case": "^1.1.1" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-db": { - "version": "1.0.30001617", - "dev": true, - "license": "CC-BY-4.0" - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001617", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/canonical-path": { - "version": "0.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/canvas": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", - "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.17.0", - "simple-get": "^3.0.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/canvas-confetti": { - "version": "1.9.3", - "license": "ISC", - "funding": { - "type": "donate", - "url": "https://www.paypal.me/kirilvatev" - } - }, - "node_modules/caseless": { - "version": "0.12.0", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/center-align": { - "version": "0.1.3", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/character-parser": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-regex": "^1.0.3" - } - }, - "node_modules/chardet": { - "version": "0.7.0", - "license": "MIT" - }, - "node_modules/chokidar": { - "version": "3.6.0", - "devOptional": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/class-utils": { - "version": "0.3.6", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clean-css": { - "version": "4.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/clean-css/node_modules/source-map": { - "version": "0.6.1", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cli": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "exit": "0.1.2", - "glob": "^7.1.1" - }, - "engines": { - "node": ">=0.2.5" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.6.1", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-width": { - "version": "4.1.0", - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/cliui/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/clone": { - "version": "1.0.4", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/codemirror": { - "version": "5.65.0", - "license": "MIT" - }, - "node_modules/coffeelint": { - "version": "1.16.2", - "dev": true, - "license": "MIT", - "dependencies": { - "coffee-script": "~1.11.0", - "glob": "^7.0.6", - "ignore": "^3.0.9", - "optimist": "^0.6.1", - "resolve": "^0.6.3", - "strip-json-comments": "^1.0.2" - }, - "bin": { - "coffeelint": "bin/coffeelint" - }, - "engines": { - "node": ">=0.8.0", - "npm": ">=1.3.7" - } - }, - "node_modules/coffeelint-stylish": { - "version": "0.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^1.0.0", - "text-table": "^0.2.0" - }, - "engines": { - "node": ">=0.10.0", - "npm": ">=1.3.7" - } - }, - "node_modules/coffeelint-stylish/node_modules/ansi-regex": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/coffeelint-stylish/node_modules/ansi-styles": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/coffeelint-stylish/node_modules/chalk": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/coffeelint-stylish/node_modules/strip-ansi": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/coffeelint-stylish/node_modules/supports-color": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/coffeelint/node_modules/coffee-script": { - "version": "1.11.1", - "dev": true, - "license": "MIT", - "bin": { - "cake": "bin/cake", - "coffee": "bin/coffee" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/coffeelint/node_modules/ignore": { - "version": "3.3.10", - "dev": true, - "license": "MIT" - }, - "node_modules/coffeelint/node_modules/resolve": { - "version": "0.6.3", - "dev": true, - "license": "MIT" - }, - "node_modules/coffeelint/node_modules/strip-json-comments": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "bin": { - "strip-json-comments": "cli.js" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/collection-visit": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "license": "MIT" - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "optional": true, - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/colorette": { - "version": "2.0.20", - "dev": true, - "license": "MIT" - }, - "node_modules/colors": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "2.6.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.x" - } - }, - "node_modules/comment-parser": { - "version": "1.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/common-path-prefix": { - "version": "3.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/compare-func": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-ify": "^1.0.0", - "dot-prop": "^5.1.0" - } - }, - "node_modules/component-emitter": { - "version": "1.3.1", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/compressible": { - "version": "2.0.18", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.4", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/bytes": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "devOptional": true, - "license": "MIT" - }, - "node_modules/concurrently": { - "version": "3.6.1", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^2.4.1", - "commander": "2.6.0", - "date-fns": "^1.23.0", - "lodash": "^4.5.1", - "read-pkg": "^3.0.0", - "rx": "2.3.24", - "spawn-command": "^0.0.2-1", - "supports-color": "^3.2.3", - "tree-kill": "^1.1.0" - }, - "bin": { - "concurrent": "src/main.js", - "concurrently": "src/main.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/concurrently/node_modules/date-fns": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", - "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", - "dev": true - }, - "node_modules/concurrently/node_modules/has-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/concurrently/node_modules/supports-color": { - "version": "3.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^1.0.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/connect": { - "version": "3.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "finalhandler": "1.1.2", - "parseurl": "~1.3.3", - "utils-merge": "1.0.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/connect-livereload": { - "version": "0.5.4", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/connect/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/connect/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/console-browserify": { - "version": "1.1.0", - "dev": true, - "dependencies": { - "date-now": "^0.1.4" - } - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC", - "optional": true - }, - "node_modules/constantinople": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/babel-types": "^7.0.0", - "@types/babylon": "^6.16.2", - "babel-types": "^6.26.0", - "babylon": "^6.18.0" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.2.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/content-type": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/continuable-cache": { - "version": "0.3.1", - "dev": true - }, - "node_modules/conventional-changelog-angular": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.3.0.tgz", - "integrity": "sha512-DOuBwYSqWzfwuRByY9O4oOIvDlkUCTDzfbOgcSbkY+imXXj+4tmrEFao3K+FxemClYfYnZzsvudbwrhje9VHDA==", - "dev": true, - "license": "ISC", - "dependencies": { - "compare-func": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-changelog-conventionalcommits": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.0.tgz", - "integrity": "sha512-kYFx6gAyjSIMwNtASkI3ZE99U1fuVDJr0yTYgVy+I2QG46zNZfl2her+0+eoviG82c5WQvW1jMt1eOQTeJLodA==", - "dev": true, - "license": "ISC", - "dependencies": { - "compare-func": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/conventional-commits-parser": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.3.0.tgz", - "integrity": "sha512-RfOq/Cqy9xV9bOA8N+ZH6DlrDR+5S3Mi0B5kACEjESpE+AviIpAptx9a9cFpWCCvgRtWT+0BbUw+e1BZfts9jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@simple-libs/stream-utils": "^1.2.0", - "meow": "^13.0.0" - }, - "bin": { - "conventional-commits-parser": "dist/cli/index.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.4.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "dev": true, - "license": "MIT" - }, - "node_modules/copy-anything": { - "version": "2.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "is-what": "^3.14.1" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, - "node_modules/copy-descriptor": { - "version": "0.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/copy-webpack-plugin": { - "version": "11.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-glob": "^3.2.11", - "glob-parent": "^6.0.1", - "globby": "^13.1.1", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/globby": { - "version": "13.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.3.0", - "ignore": "^5.2.4", - "merge2": "^1.4.1", - "slash": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/copy-webpack-plugin/node_modules/slash": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/core-js": { - "version": "3.37.0", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.37.0", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cosmiconfig": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", - "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cosmiconfig-typescript-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.2.0.tgz", - "integrity": "sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "jiti": "^2.6.1" - }, - "engines": { - "node": ">=v18" - }, - "peerDependencies": { - "@types/node": "*", - "cosmiconfig": ">=9", - "typescript": ">=5" - } - }, - "node_modules/cosmiconfig-typescript-loader/node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/cosmiconfig/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/critters": { - "version": "0.0.22", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "chalk": "^4.1.0", - "css-select": "^5.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.2", - "htmlparser2": "^8.0.2", - "postcss": "^8.4.23", - "postcss-media-query-parser": "^0.2.3" - } - }, - "node_modules/critters/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/critters/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/critters/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/critters/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/critters/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/critters/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-line-break": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", - "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", - "license": "MIT", - "dependencies": { - "utrie": "^1.0.2" - } - }, - "node_modules/css-loader": { - "version": "6.10.0", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.4", - "postcss-modules-scope": "^3.1.1", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/css-loader/node_modules/postcss-value-parser": { - "version": "4.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/css-select": { - "version": "5.1.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/custom-event": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/d3": { - "version": "3.5.17", - "license": "BSD-3-Clause" - }, - "node_modules/dashdash": { - "version": "1.14.1", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/date-fns": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", - "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, - "node_modules/date-format": { - "version": "4.0.14", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/date-now": { - "version": "0.1.4", - "dev": true - }, - "node_modules/dateformat": { - "version": "4.6.3", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "license": "MIT", - "optional": true, - "dependencies": { - "mimic-response": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/default-gateway": { - "version": "6.0.3", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/defaults": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-property": { - "version": "0.2.5", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/del": { - "version": "2.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "globby": "^5.0.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "rimraf": "^2.2.8" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/del/node_modules/array-union": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "array-uniq": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/del/node_modules/arrify": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/del/node_modules/globby": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/del/node_modules/pify": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/del/node_modules/rimraf": { - "version": "2.7.1", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT", - "optional": true - }, - "node_modules/depd": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-file": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/di": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/diff": { - "version": "2.2.3", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", - "license": "MIT" - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dlv": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/dns-packet": { - "version": "5.6.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/doctypes": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/dom-serialize": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "custom-event": "~1.0.0", - "ent": "~2.2.0", - "extend": "^3.0.0", - "void-elements": "^2.0.0" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.1.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-prop": { - "version": "5.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "16.3.2", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" - } - }, - "node_modules/dotenv-expand": { - "version": "10.0.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/duplexer": { - "version": "0.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "license": "MIT" - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/ecc-jsbn/node_modules/jsbn": { - "version": "0.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/ejs": { - "version": "3.1.10", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.763", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "license": "MIT" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/encoding": { - "version": "0.1.13", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/engine.io": { - "version": "6.5.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/cookie": "^0.4.1", - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.4.1", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.11.0" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.16.1", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/enquirer": { - "version": "2.3.6", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/ent": { - "version": "2.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/entities": { - "version": "4.5.0", - "devOptional": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "license": "MIT" - }, - "node_modules/errno": { - "version": "0.1.8", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "prr": "~1.0.1" - }, - "bin": { - "errno": "cli.js" - } - }, - "node_modules/error": { - "version": "7.2.1", - "dev": true, - "dependencies": { - "string-template": "~0.2.1" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.23.3", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.5.2", - "dev": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es5-shim": { - "version": "4.6.7", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/es6-promise": { - "version": "4.2.8", - "dev": true, - "license": "MIT" - }, - "node_modules/es6-promisify": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "es6-promise": "^4.0.3" - } - }, - "node_modules/esbuild": { - "version": "0.20.1", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.1", - "@esbuild/android-arm": "0.20.1", - "@esbuild/android-arm64": "0.20.1", - "@esbuild/android-x64": "0.20.1", - "@esbuild/darwin-arm64": "0.20.1", - "@esbuild/darwin-x64": "0.20.1", - "@esbuild/freebsd-arm64": "0.20.1", - "@esbuild/freebsd-x64": "0.20.1", - "@esbuild/linux-arm": "0.20.1", - "@esbuild/linux-arm64": "0.20.1", - "@esbuild/linux-ia32": "0.20.1", - "@esbuild/linux-loong64": "0.20.1", - "@esbuild/linux-mips64el": "0.20.1", - "@esbuild/linux-ppc64": "0.20.1", - "@esbuild/linux-riscv64": "0.20.1", - "@esbuild/linux-s390x": "0.20.1", - "@esbuild/linux-x64": "0.20.1", - "@esbuild/netbsd-x64": "0.20.1", - "@esbuild/openbsd-x64": "0.20.1", - "@esbuild/sunos-x64": "0.20.1", - "@esbuild/win32-arm64": "0.20.1", - "@esbuild/win32-ia32": "0.20.1", - "@esbuild/win32-x64": "0.20.1" - } - }, - "node_modules/esbuild-wasm": { - "version": "0.20.1", - "dev": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-arm64": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz", - "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/eslint": { - "version": "8.57.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-prettier": { - "version": "8.10.0", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.8.1", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.26.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.3", - "has": "^1.0.3", - "is-core-module": "^2.8.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.values": "^1.1.5", - "resolve": "^1.22.0", - "tsconfig-paths": "^3.14.1" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-import/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-plugin-jsdoc": { - "version": "39.3.6", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@es-joy/jsdoccomment": "~0.31.0", - "comment-parser": "1.3.1", - "debug": "^4.3.4", - "escape-string-regexp": "^4.0.0", - "esquery": "^1.4.0", - "semver": "^7.3.7", - "spdx-expression-parse": "^3.0.1" - }, - "engines": { - "node": "^14 || ^16 || ^17 || ^18" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-prefer-arrow": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=2.0.0" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": "*", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "8.0.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter2": { - "version": "0.4.14", - "dev": true, - "license": "MIT" - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "dev": true, - "license": "MIT" - }, - "node_modules/events": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expand-brackets": { - "version": "2.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/expand-brackets/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/expand-tilde": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.1", - "license": "Apache-2.0" - }, - "node_modules/express": { - "version": "4.19.2", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/cookie": { - "version": "0.6.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/finalhandler": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/express/node_modules/on-finished": { - "version": "2.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/safe-buffer": { - "version": "5.2.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/express/node_modules/statuses": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/external-editor": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/external-editor/node_modules/iconv-lite": { - "version": "0.4.24", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/external-editor/node_modules/tmp": { - "version": "0.0.33", - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/extglob": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/define-property": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-descriptor": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/fast-diff": { - "version": "1.3.0", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.17.1", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/faye-websocket": { - "version": "0.10.0", - "dev": true, - "license": "MIT", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/figures": { - "version": "3.2.0", - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/file-saver": { - "version": "2.0.5", - "license": "MIT" - }, - "node_modules/file-sync-cmp": { - "version": "0.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "optional": true - }, - "node_modules/filelist": { - "version": "1.0.4", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "devOptional": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/find-cache-dir": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "common-path-prefix": "^3.0.0", - "pkg-dir": "^7.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/findup-sync": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.3", - "micromatch": "^4.0.4", - "resolve-dir": "^1.0.1" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/fined": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/flagged-respawn": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/font-awesome": { - "version": "4.7.0", - "license": "(OFL-1.1 AND MIT)", - "engines": { - "node": ">=0.10.3" - } - }, - "node_modules/for-each": { - "version": "0.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/for-in": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/for-own": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "for-in": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/foreground-child": { - "version": "3.1.1", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/forever-agent": { - "version": "0.6.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fragment-cache": { - "version": "0.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "map-cache": "^0.2.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/fs-minipass": { - "version": "3.0.3", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/fs-monkey": { - "version": "1.0.6", - "dev": true, - "license": "Unlicense" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "devOptional": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gaze": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "globule": "^1.0.0" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stdin": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-value": { - "version": "2.0.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/getobject": { - "version": "1.0.2", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/getpass": { - "version": "0.1.7", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, - "node_modules/git-raw-commits": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-5.0.1.tgz", - "integrity": "sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@conventional-changelog/git-client": "^2.6.0", - "meow": "^13.0.0" - }, - "bin": { - "git-raw-commits": "src/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/glob": { - "version": "7.1.7", - "devOptional": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "devOptional": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "devOptional": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/global-directory": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "4.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/global-directory/node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/global-modules": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/global-prefix": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/global-prefix/node_modules/ini": { - "version": "1.3.8", - "dev": true, - "license": "ISC" - }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globule": { - "version": "1.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "~7.1.1", - "lodash": "^4.17.21", - "minimatch": "~3.0.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/globule/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/globule/node_modules/minimatch": { - "version": "3.0.8", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/grunt": { - "version": "1.6.1", - "dev": true, - "license": "MIT", - "dependencies": { - "dateformat": "~4.6.2", - "eventemitter2": "~0.4.13", - "exit": "~0.1.2", - "findup-sync": "~5.0.0", - "glob": "~7.1.6", - "grunt-cli": "~1.4.3", - "grunt-known-options": "~2.0.0", - "grunt-legacy-log": "~3.0.0", - "grunt-legacy-util": "~2.0.1", - "iconv-lite": "~0.6.3", - "js-yaml": "~3.14.0", - "minimatch": "~3.0.4", - "nopt": "~3.0.6" - }, - "bin": { - "grunt": "bin/grunt" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/grunt-bump": { - "version": "0.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^5.1.0" - }, - "engines": { - "node": ">= 0.8.0" - }, - "peerDependencies": { - "grunt": ">=1.0.1" - } - }, - "node_modules/grunt-bump/node_modules/semver": { - "version": "5.7.2", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/grunt-cli": { - "version": "1.4.3", - "dev": true, - "license": "MIT", - "dependencies": { - "grunt-known-options": "~2.0.0", - "interpret": "~1.1.0", - "liftup": "~3.0.1", - "nopt": "~4.0.1", - "v8flags": "~3.2.0" - }, - "bin": { - "grunt": "bin/grunt" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/grunt-cli/node_modules/nopt": { - "version": "4.0.3", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "1", - "osenv": "^0.1.4" - }, - "bin": { - "nopt": "bin/nopt.js" - } - }, - "node_modules/grunt-coffeelint": { - "version": "0.0.16", - "dev": true, - "dependencies": { - "coffeelint": "^1", - "coffeelint-stylish": "~0.1.0" - }, - "engines": { - "node": "*" - }, - "peerDependencies": { - "grunt": ">=0.4.0" - } - }, - "node_modules/grunt-contrib-clean": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "async": "^1.5.2", - "rimraf": "^2.5.1" - }, - "engines": { - "node": ">= 0.10.0" - }, - "peerDependencies": { - "grunt": ">= 0.4.5" - } - }, - "node_modules/grunt-contrib-clean/node_modules/rimraf": { - "version": "2.7.1", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/grunt-contrib-coffee": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "~1.0.0", - "coffee-script": "~1.10.0", - "lodash": "~4.3.0", - "uri-path": "~1.0.0" - }, - "engines": { - "node": ">= 0.10.0" - }, - "peerDependencies": { - "grunt": ">= 0.4.5" - } - }, - "node_modules/grunt-contrib-coffee/node_modules/ansi-regex": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-coffee/node_modules/ansi-styles": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-coffee/node_modules/chalk": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^2.0.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^1.0.3", - "strip-ansi": "^2.0.1", - "supports-color": "^1.3.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-coffee/node_modules/coffee-script": { - "version": "1.10.0", - "dev": true, - "license": "MIT", - "bin": { - "cake": "bin/cake", - "coffee": "bin/coffee" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/grunt-contrib-coffee/node_modules/has-ansi": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^1.1.0", - "get-stdin": "^4.0.1" - }, - "bin": { - "has-ansi": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-coffee/node_modules/lodash": { - "version": "4.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/grunt-contrib-coffee/node_modules/strip-ansi": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^1.0.0" - }, - "bin": { - "strip-ansi": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-coffee/node_modules/supports-color": { - "version": "1.3.1", - "dev": true, - "license": "MIT", - "bin": { - "supports-color": "cli.js" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/grunt-contrib-concat": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^1.0.0", - "source-map": "^0.5.3" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "grunt": ">=0.4.0" - } - }, - "node_modules/grunt-contrib-concat/node_modules/ansi-regex": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-concat/node_modules/ansi-styles": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-concat/node_modules/chalk": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-concat/node_modules/source-map": { - "version": "0.5.7", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-concat/node_modules/strip-ansi": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-concat/node_modules/supports-color": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/grunt-contrib-connect": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "async": "^1.5.2", - "connect": "^3.4.0", - "connect-livereload": "^0.5.0", - "http2": "^3.3.4", - "morgan": "^1.6.1", - "opn": "^4.0.0", - "portscanner": "^1.0.0", - "serve-index": "^1.7.1", - "serve-static": "^1.10.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "grunt": ">=0.4.0" - } - }, - "node_modules/grunt-contrib-copy": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^1.1.1", - "file-sync-cmp": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-copy/node_modules/ansi-regex": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-copy/node_modules/ansi-styles": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-copy/node_modules/chalk": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-copy/node_modules/strip-ansi": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-copy/node_modules/supports-color": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/grunt-contrib-jshint": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^1.1.1", - "hooker": "^0.2.3", - "jshint": "~2.9.1" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "grunt": ">=0.4.0" - } - }, - "node_modules/grunt-contrib-jshint/node_modules/ansi-regex": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-jshint/node_modules/ansi-styles": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-jshint/node_modules/chalk": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-jshint/node_modules/strip-ansi": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-jshint/node_modules/supports-color": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/grunt-contrib-watch": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "async": "^2.6.0", - "gaze": "^1.1.0", - "lodash": "^4.17.10", - "tiny-lr": "^1.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-contrib-watch/node_modules/async": { - "version": "2.6.4", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.14" - } - }, - "node_modules/grunt-env": { - "version": "0.4.4", - "dev": true, - "dependencies": { - "ini": "~1.3.0", - "lodash": "~2.4.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/grunt-env/node_modules/ini": { - "version": "1.3.8", - "dev": true, - "license": "ISC" - }, - "node_modules/grunt-env/node_modules/lodash": { - "version": "2.4.2", - "dev": true, - "engines": [ - "node", - "rhino" - ], - "license": "MIT" - }, - "node_modules/grunt-html2js": { - "version": "0.6.0", - "dev": true, - "license": "SEE LICENSE IN LICENSE-MIT", - "dependencies": { - "chokidar": "^2", - "html-minifier": "^3" - }, - "engines": { - "node": ">= 0.8.0" - }, - "optionalDependencies": { - "pug": "^2" - }, - "peerDependencies": { - "grunt": ">=0.4.0" - } - }, - "node_modules/grunt-html2js/node_modules/anymatch": { - "version": "2.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "node_modules/grunt-html2js/node_modules/anymatch/node_modules/normalize-path": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-html2js/node_modules/binary-extensions": { - "version": "1.13.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-html2js/node_modules/braces": { - "version": "2.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-html2js/node_modules/chokidar": { - "version": "2.1.8", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - }, - "optionalDependencies": { - "fsevents": "^1.2.7" - } - }, - "node_modules/grunt-html2js/node_modules/define-property": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-html2js/node_modules/fill-range": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-html2js/node_modules/fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "deprecated": "Upgrade to fsevents v2 to mitigate potential security issues", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/grunt-html2js/node_modules/glob-parent": { - "version": "3.1.0", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "node_modules/grunt-html2js/node_modules/glob-parent/node_modules/is-glob": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-html2js/node_modules/is-binary-path": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-html2js/node_modules/is-descriptor": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/grunt-html2js/node_modules/is-extendable": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-html2js/node_modules/is-number": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-html2js/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-html2js/node_modules/isarray": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/grunt-html2js/node_modules/micromatch": { - "version": "3.1.10", - "dev": true, - "license": "MIT", - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-html2js/node_modules/micromatch/node_modules/extend-shallow": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-html2js/node_modules/readable-stream": { - "version": "2.3.8", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/grunt-html2js/node_modules/readdirp": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/grunt-html2js/node_modules/string_decoder": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/grunt-html2js/node_modules/to-regex-range": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-karma": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash": "^3.10.1" - }, - "peerDependencies": { - "grunt": ">=0.4.x", - "karma": "^0.13.0 || >= 0.14.0-rc.0" - } - }, - "node_modules/grunt-karma/node_modules/lodash": { - "version": "3.10.1", - "dev": true, - "license": "MIT" - }, - "node_modules/grunt-known-options": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-legacy-log": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "colors": "~1.1.2", - "grunt-legacy-log-utils": "~2.1.0", - "hooker": "~0.2.3", - "lodash": "~4.17.19" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/grunt-legacy-log-utils": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "~4.1.0", - "lodash": "~4.17.19" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/grunt-legacy-log-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/grunt-legacy-log-utils/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/grunt-legacy-log-utils/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/grunt-legacy-log-utils/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/grunt-legacy-log-utils/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/grunt-legacy-log-utils/node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, - "license": "MIT" - }, - "node_modules/grunt-legacy-log-utils/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/grunt-legacy-log/node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, - "license": "MIT" - }, - "node_modules/grunt-legacy-util": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "async": "~3.2.0", - "exit": "~0.1.2", - "getobject": "~1.0.0", - "hooker": "~0.2.3", - "lodash": "~4.17.21", - "underscore.string": "~3.3.5", - "which": "~2.0.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/grunt-legacy-util/node_modules/async": { - "version": "3.2.5", - "dev": true, - "license": "MIT" - }, - "node_modules/grunt-legacy-util/node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, - "license": "MIT" - }, - "node_modules/grunt-legacy-util/node_modules/sprintf-js": { - "version": "1.1.3", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/grunt-legacy-util/node_modules/underscore.string": { - "version": "3.3.6", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "^1.1.1", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/grunt-newer": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "async": "^1.5.2", - "rimraf": "^2.5.2" - }, - "engines": { - "node": ">= 0.8.0" - }, - "peerDependencies": { - "grunt": ">=0.4.1" - } - }, - "node_modules/grunt-newer/node_modules/rimraf": { - "version": "2.7.1", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/grunt-ng-annotate": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.clonedeep": "^4.5.0", - "ng-annotate": "^1.2.1" - }, - "engines": { - "node": ">=4.4 <5 || >=6.9" - }, - "peerDependencies": { - "grunt": ">=0.4.5" - } - }, - "node_modules/grunt-postcss": { - "version": "0.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^1.0.0", - "diff": "^2.0.2", - "postcss": "^5.0.0" - }, - "engines": { - "node": ">= 0.12.0" - }, - "peerDependencies": { - "grunt": ">=0.4.5" - } - }, - "node_modules/grunt-postcss/node_modules/ansi-regex": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-postcss/node_modules/ansi-styles": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-postcss/node_modules/chalk": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-postcss/node_modules/has-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-postcss/node_modules/postcss": { - "version": "5.2.18", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/grunt-postcss/node_modules/postcss/node_modules/supports-color": { - "version": "3.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^1.0.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/grunt-postcss/node_modules/source-map": { - "version": "0.5.7", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-postcss/node_modules/strip-ansi": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-postcss/node_modules/supports-color": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/grunt-preprocess": { - "version": "5.1.0", - "dev": true, - "dependencies": { - "lodash": "^4.5.0", - "preprocess": "^3.0.2" - }, - "bin": { - "grunt-preprocess": "bin/grunt-preprocess" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "grunt": ">=0.4.0" - } - }, - "node_modules/grunt-sass": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "grunt": ">=1" - } - }, - "node_modules/grunt-sass-globbing": { - "version": "1.5.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10.0" - }, - "peerDependencies": { - "grunt": ">=0.4.0" - } - }, - "node_modules/grunt/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/grunt/node_modules/minimatch": { - "version": "3.0.8", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/har-schema": { - "version": "2.0.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/har-validator/node_modules/ajv": { - "version": "6.12.6", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/har-validator/node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "node_modules/has": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-ansi": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC", - "optional": true - }, - "node_modules/has-value": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/is-number": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/he": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/homedir-polyfill": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "parse-passwd": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/hooker": { - "version": "0.2.3", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/hpack.js": { - "version": "2.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/hpack.js/node_modules/isarray": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.8", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/html-entities": { - "version": "2.5.2", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/html-minifier": { - "version": "3.5.21", - "dev": true, - "license": "MIT", - "dependencies": { - "camel-case": "3.0.x", - "clean-css": "4.2.x", - "commander": "2.17.x", - "he": "1.2.x", - "param-case": "2.1.x", - "relateurl": "0.2.x", - "uglify-js": "3.4.x" - }, - "bin": { - "html-minifier": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/html-minifier/node_modules/commander": { - "version": "2.17.1", - "dev": true, - "license": "MIT" - }, - "node_modules/html2canvas": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", - "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", - "license": "MIT", - "dependencies": { - "css-line-break": "^2.1.0", - "text-segmentation": "^1.0.3" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/html5-qrcode": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz", - "integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==", - "license": "Apache-2.0" - }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "license": "BSD-2-Clause" - }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.8", - "dev": true, - "license": "MIT" - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "dev": true, - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, - "node_modules/http-signature": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, - "node_modules/http2": { - "version": "3.3.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0 <9.0.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.4", - "license": "MIT", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/husky": { - "version": "8.0.3", - "dev": true, - "license": "MIT", - "bin": { - "husky": "lib/bin.js" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "devOptional": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/ignore-walk": { - "version": "6.0.5", - "license": "ISC", - "dependencies": { - "minimatch": "^9.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/image-size": { - "version": "0.5.5", - "dev": true, - "license": "MIT", - "optional": true, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "license": "MIT" - }, - "node_modules/immutable": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.8.tgz", - "integrity": "sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/import-meta-resolve": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", - "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "devOptional": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "license": "ISC" - }, - "node_modules/ini": { - "version": "4.1.2", - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/inquirer": { - "version": "9.2.15", - "license": "MIT", - "dependencies": { - "@ljharb/through": "^2.3.12", - "ansi-escapes": "^4.3.2", - "chalk": "^5.3.0", - "cli-cursor": "^3.1.0", - "cli-width": "^4.1.0", - "external-editor": "^3.1.0", - "figures": "^3.2.0", - "lodash": "^4.17.21", - "mute-stream": "1.0.0", - "ora": "^5.4.1", - "run-async": "^3.0.0", - "rxjs": "^7.8.1", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/inquirer/node_modules/chalk": { - "version": "5.3.0", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/internal-slot": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/interpret": { - "version": "1.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/ip": { - "version": "1.1.9", - "dev": true, - "license": "MIT" - }, - "node_modules/ip-address": { - "version": "9.0.5", - "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/ip-address/node_modules/sprintf-js": { - "version": "1.1.3", - "license": "BSD-3-Clause" - }, - "node_modules/ipaddr.js": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/is-absolute": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-accessor-descriptor": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "dev": true, - "license": "MIT" - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "devOptional": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-buffer": { - "version": "1.1.6", - "dev": true, - "license": "MIT" - }, - "node_modules/is-callable": { - "version": "1.2.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-descriptor": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-data-view": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-descriptor": { - "version": "0.1.7", - "dev": true, - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-expression": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "acorn": "~4.0.2", - "object-assign": "^4.0.1" - } - }, - "node_modules/is-expression/node_modules/acorn": { - "version": "4.0.13", - "dev": true, - "license": "MIT", - "optional": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "devOptional": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-interactive": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-lambda": { - "version": "1.0.1", - "license": "MIT" - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-path-cwd": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-path-in-cwd": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-path-inside": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-path-in-cwd/node_modules/is-path-inside": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "path-is-inside": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-promise": { - "version": "2.2.2", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/is-regex": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-relative": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-unc-path": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/is-unc-path": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "unc-path-regex": "^0.1.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-what": { - "version": "3.14.1", - "dev": true, - "license": "MIT" - }, - "node_modules/is-windows": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "dev": true, - "license": "MIT" - }, - "node_modules/isbinaryfile": { - "version": "4.0.10", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "license": "ISC" - }, - "node_modules/isobject": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isstream": { - "version": "0.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "3.0.6", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "rimraf": "^2.6.3", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/istanbul-lib-coverage": { - "version": "2.0.5", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=6" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/make-dir": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/pify": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/rimraf": { - "version": "2.7.1", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/semver": { - "version": "5.7.2", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.6.1", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/istanbul-reports": { - "version": "3.1.7", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "2.3.6", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jake": { - "version": "10.9.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jake/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jake/node_modules/async": { - "version": "3.2.5", - "dev": true, - "license": "MIT" - }, - "node_modules/jake/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/jake/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jake/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jake/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/jake/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jake/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/jake/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jasmine": { - "version": "2.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "exit": "^0.1.2", - "glob": "^7.0.6", - "jasmine-core": "~2.8.0" - }, - "bin": { - "jasmine": "bin/jasmine.js" - } - }, - "node_modules/jasmine-core": { - "version": "4.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/jasmine-spec-reporter": { - "version": "5.0.2", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "colors": "1.4.0" - } - }, - "node_modules/jasmine-spec-reporter/node_modules/colors": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/jasmine/node_modules/jasmine-core": { - "version": "2.8.0", - "dev": true, - "license": "MIT" - }, - "node_modules/jasminewd2": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.9.x" - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-diff/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-diff/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-diff/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jiti": { - "version": "1.21.0", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/jquery": { - "version": "2.1.4" - }, - "node_modules/js-base64": { - "version": "2.6.4", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/js-stringify": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsbn": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/jsdoc-type-pratt-parser": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/jsesc": { - "version": "2.5.2", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/jshint": { - "version": "2.9.7", - "dev": true, - "license": "(MIT AND JSON)", - "dependencies": { - "cli": "~1.0.0", - "console-browserify": "1.1.x", - "exit": "0.1.x", - "htmlparser2": "3.8.x", - "lodash": "~4.17.10", - "minimatch": "~3.0.2", - "shelljs": "0.3.x", - "strip-json-comments": "1.0.x" - }, - "bin": { - "jshint": "bin/jshint" - } - }, - "node_modules/jshint/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/jshint/node_modules/dom-serializer": { - "version": "0.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - } - }, - "node_modules/jshint/node_modules/dom-serializer/node_modules/domelementtype": { - "version": "2.3.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/jshint/node_modules/dom-serializer/node_modules/entities": { - "version": "2.2.0", - "dev": true, - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/jshint/node_modules/domelementtype": { - "version": "1.3.1", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/jshint/node_modules/domhandler": { - "version": "2.3.0", - "dev": true, - "dependencies": { - "domelementtype": "1" - } - }, - "node_modules/jshint/node_modules/domutils": { - "version": "1.5.1", - "dev": true, - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "node_modules/jshint/node_modules/entities": { - "version": "1.0.0", - "dev": true, - "license": "BSD-like" - }, - "node_modules/jshint/node_modules/htmlparser2": { - "version": "3.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "1", - "domhandler": "2.3", - "domutils": "1.5", - "entities": "1.0", - "readable-stream": "1.1" - } - }, - "node_modules/jshint/node_modules/isarray": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/jshint/node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, - "license": "MIT" - }, - "node_modules/jshint/node_modules/minimatch": { - "version": "3.0.8", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/jshint/node_modules/readable-stream": { - "version": "1.1.14", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/jshint/node_modules/string_decoder": { - "version": "0.10.31", - "dev": true, - "license": "MIT" - }, - "node_modules/jshint/node_modules/strip-json-comments": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "bin": { - "strip-json-comments": "cli.js" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "3.0.2", - "license": "MIT", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/json-schema": { - "version": "0.4.0", - "dev": true, - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "dev": true, - "license": "ISC" - }, - "node_modules/json5": { - "version": "2.2.3", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonc-parser": { - "version": "3.2.1", - "license": "MIT" - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonparse": { - "version": "1.3.1", - "engines": [ - "node >= 0.2.0" - ], - "license": "MIT" - }, - "node_modules/jsprim": { - "version": "1.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/jstransformer": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-promise": "^2.0.0", - "promise": "^7.0.1" - } - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/jszip/node_modules/isarray": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/jszip/node_modules/readable-stream": { - "version": "2.3.8", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/jszip/node_modules/string_decoder": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/karma": { - "version": "6.4.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@colors/colors": "1.5.0", - "body-parser": "^1.19.0", - "braces": "^3.0.2", - "chokidar": "^3.5.1", - "connect": "^3.7.0", - "di": "^0.0.1", - "dom-serialize": "^2.2.1", - "glob": "^7.1.7", - "graceful-fs": "^4.2.6", - "http-proxy": "^1.18.1", - "isbinaryfile": "^4.0.8", - "lodash": "^4.17.21", - "log4js": "^6.4.1", - "mime": "^2.5.2", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.5", - "qjobs": "^1.2.0", - "range-parser": "^1.2.1", - "rimraf": "^3.0.2", - "socket.io": "^4.7.2", - "source-map": "^0.6.1", - "tmp": "^0.2.1", - "ua-parser-js": "^0.7.30", - "yargs": "^16.1.1" - }, - "bin": { - "karma": "bin/karma" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/karma-chrome-launcher": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "which": "^1.2.1" - } - }, - "node_modules/karma-chrome-launcher/node_modules/which": { - "version": "1.3.1", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/karma-coverage-istanbul-reporter": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^3.0.6", - "istanbul-reports": "^3.0.2", - "minimatch": "^3.0.4" - }, - "funding": { - "url": "https://github.com/sponsors/mattlewis92" - } - }, - "node_modules/karma-coverage-istanbul-reporter/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/karma-coverage-istanbul-reporter/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/karma-jasmine": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "jasmine-core": "^3.6.0" - }, - "engines": { - "node": ">= 10" - }, - "peerDependencies": { - "karma": "*" - } - }, - "node_modules/karma-jasmine-html-reporter": { - "version": "1.7.0", - "dev": true, - "license": "MIT", - "peerDependencies": { - "jasmine-core": ">=3.8", - "karma": ">=0.9", - "karma-jasmine": ">=1.1" - } - }, - "node_modules/karma-jasmine/node_modules/jasmine-core": { - "version": "3.99.1", - "dev": true, - "license": "MIT" - }, - "node_modules/karma-source-map-support": { - "version": "1.4.0", - "dev": true, - "license": "MIT", - "dependencies": { - "source-map-support": "^0.5.5" - } - }, - "node_modules/karma/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/karma/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/karma/node_modules/cliui": { - "version": "7.0.4", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/karma/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/karma/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/karma/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/karma/node_modules/source-map": { - "version": "0.6.1", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/karma/node_modules/wrap-ansi": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/karma/node_modules/yargs": { - "version": "16.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/karma/node_modules/yargs-parser": { - "version": "20.2.9", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/klona": { - "version": "2.0.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/launch-editor": { - "version": "2.6.1", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^1.0.0", - "shell-quote": "^1.8.1" - } - }, - "node_modules/lazy-cache": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/less": { - "version": "4.2.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "copy-anything": "^2.0.1", - "parse-node-version": "^1.0.1", - "tslib": "^2.3.0" - }, - "bin": { - "lessc": "bin/lessc" - }, - "engines": { - "node": ">=6" - }, - "optionalDependencies": { - "errno": "^0.1.1", - "graceful-fs": "^4.1.2", - "image-size": "~0.5.0", - "make-dir": "^2.1.0", - "mime": "^1.4.1", - "needle": "^3.1.0", - "source-map": "~0.6.0" - } - }, - "node_modules/less-loader": { - "version": "11.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "klona": "^2.0.4" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "less": "^3.5.0 || ^4.0.0", - "webpack": "^5.0.0" - } - }, - "node_modules/less/node_modules/make-dir": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/less/node_modules/mime": { - "version": "1.6.0", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", "optional": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } + "os": [ + "win32" + ] }, - "node_modules/less/node_modules/pify": { - "version": "4.0.1", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", "optional": true, - "engines": { - "node": ">=6" - } + "os": [ + "win32" + ] }, - "node_modules/less/node_modules/semver": { - "version": "5.7.2", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC", + "license": "MIT", "optional": true, - "bin": { - "semver": "bin/semver" - } + "os": [ + "win32" + ] }, - "node_modules/less/node_modules/source-map": { - "version": "0.6.1", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "optional": true, - "engines": { - "node": ">=0.10.0" - } + "os": [ + "win32" + ] }, - "node_modules/levn": { - "version": "0.4.1", + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@schematics/angular": { + "version": "22.0.4", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-22.0.4.tgz", + "integrity": "sha512-P3V3tkqIR+n0GJSv0ibf34/zMtKbFp6kaTjBe5cm/RyXuHbmdaPYjk8PNphkGMypDtWCof1RtNnW/hl832Wnew==", "dev": true, "license": "MIT", "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "@angular-devkit/core": "22.0.4", + "@angular-devkit/schematics": "22.0.4", + "jsonc-parser": "3.3.1", + "typescript": "6.0.3" }, "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/license-webpack-plugin": { - "version": "4.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "webpack-sources": "^3.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-sources": { - "optional": true - } - } - }, - "node_modules/lie": { - "version": "3.3.0", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, - "node_modules/liftup": { - "version": "3.0.1", - "dev": true, + "node_modules/@sentry/angular": { + "version": "10.61.0", + "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-10.61.0.tgz", + "integrity": "sha512-fmGmFKLPpJeaY2oCsZAM+p5Xzz0u+ex3puayRItiUSFMEuOT9fxkwd5gyJjJlEqPuQAqMo7Kh4y3lPQFQWoxeg==", "license": "MIT", "dependencies": { - "extend": "^3.0.2", - "findup-sync": "^4.0.0", - "fined": "^1.2.0", - "flagged-respawn": "^1.0.1", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.1", - "rechoir": "^0.7.0", - "resolve": "^1.19.0" + "@sentry/browser": "10.61.0", + "@sentry/core": "10.61.0", + "tslib": "^2.4.1" }, "engines": { - "node": ">=10" + "node": ">=18" + }, + "peerDependencies": { + "@angular/common": ">= 14.x <= 22.x", + "@angular/core": ">= 14.x <= 22.x", + "@angular/router": ">= 14.x <= 22.x", + "rxjs": "^6.5.5 || ^7.x" } }, - "node_modules/liftup/node_modules/findup-sync": { - "version": "4.0.0", - "dev": true, + "node_modules/@sentry/browser": { + "version": "10.61.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.61.0.tgz", + "integrity": "sha512-I02k3/tpCbQ+Dm3d1eA+JHVS452gY4fCTh+fT3FcfZkovB/WaeJcwc+ywQN1jpTYBRKZ1HbXXZQhEhXYvb8MkA==", "license": "MIT", "dependencies": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^4.0.2", - "resolve-dir": "^1.0.1" + "@sentry/browser-utils": "10.61.0", + "@sentry/core": "10.61.0", + "@sentry/feedback": "10.61.0", + "@sentry/replay": "10.61.0", + "@sentry/replay-canvas": "10.61.0" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/lines-and-columns": { - "version": "2.0.4", - "dev": true, + "node_modules/@sentry/browser-utils": { + "version": "10.61.0", + "resolved": "https://registry.npmjs.org/@sentry/browser-utils/-/browser-utils-10.61.0.tgz", + "integrity": "sha512-kj/Qs5hz/VQPOLesgv9wq8O1z3aKSHSkyFiCSzaOthb8ARISchk8Eiukbvw9GxX2gmgsCd0L35gD6gxnhlE9ag==", "license": "MIT", + "dependencies": { + "@sentry/core": "10.61.0" + }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" } }, - "node_modules/livereload-js": { - "version": "2.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/load-grunt-tasks": { - "version": "5.1.0", - "dev": true, - "license": "MIT", + "node_modules/@sentry/cli": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-3.5.1.tgz", + "integrity": "sha512-h710aEXT8At4lg7GbXDZkatDDq0uA5QKZmSiF/8Hy6jJZ/9dh6EBDZZpTYnDpNrwRvE8tqhnwEjTZDlHjOhPNQ==", + "hasInstallScript": true, + "license": "FSL-1.1-MIT", "dependencies": { - "arrify": "^2.0.1", - "multimatch": "^4.0.0", - "pkg-up": "^3.1.0", - "resolve-pkg": "^2.0.0" + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0", + "undici": "^6.22.0", + "which": "^2.0.2" + }, + "bin": { + "sentry-cli": "bin/sentry-cli" }, "engines": { - "node": ">=8" + "node": ">= 18" }, - "peerDependencies": { - "grunt": ">=1" + "optionalDependencies": { + "@sentry/cli-darwin": "3.5.1", + "@sentry/cli-linux-arm": "3.5.1", + "@sentry/cli-linux-arm64": "3.5.1", + "@sentry/cli-linux-i686": "3.5.1", + "@sentry/cli-linux-x64": "3.5.1", + "@sentry/cli-win32-arm64": "3.5.1", + "@sentry/cli-win32-i686": "3.5.1", + "@sentry/cli-win32-x64": "3.5.1" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-3.5.1.tgz", + "integrity": "sha512-GxHAtZaXRA650egcepQXU0pR0dZ8ZNvQS8eq3wBxhCK8os5VQpLyBABIaN6AdrE/b8M7UvqGjsuVm0giI1GZ7w==", + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" } }, - "node_modules/load-json-file": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, + "node_modules/@sentry/cli-linux-arm": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-3.5.1.tgz", + "integrity": "sha512-JOfgXCDZKbxQpw8Z4Kbkt8Hl+ASW5pYVUYw8Hc2msPN3HtfTmsc1z0y6jq3TCUWDWbWpWbI1zfXa0v02vQf3gw==", + "cpu": [ + "arm" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, + "node_modules/@sentry/cli-linux-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-3.5.1.tgz", + "integrity": "sha512-Qa0NLXG/FSYWGhKjdm2mxp/GgpluFFkj/J+CpmVzwvezNC/Uy1omquv7J+VficqskNYuptjsoB4dNIPPcXpbxg==", + "cpu": [ + "arm64" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/loader-runner": { - "version": "4.3.0", - "dev": true, - "license": "MIT", + "node_modules/@sentry/cli-linux-i686": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-3.5.1.tgz", + "integrity": "sha512-/Bqcl8EyS6T8RIjBeeMYUPgjMJ8kb4plF0w3QOG6TY+bUEqHEnZyfzKJWZY/OqhmrSgj+ZYynaSHIIR6dWluMA==", + "cpu": [ + "x86", + "ia32" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], "engines": { - "node": ">=6.11.5" + "node": ">=18" } }, - "node_modules/loader-utils": { - "version": "3.2.1", - "dev": true, - "license": "MIT", + "node_modules/@sentry/cli-linux-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-3.5.1.tgz", + "integrity": "sha512-iUJT2GI/soc0myi1sSnXdu71oBkUER3fBeXn10bcEZX+UK9GomsqL40OLlygDipZZ6FqhODORqLeTxHdHbRuOw==", + "cpu": [ + "x64" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], "engines": { - "node": ">= 12.13.0" + "node": ">=18" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, + "node_modules/@sentry/cli-win32-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-3.5.1.tgz", + "integrity": "sha512-Hs4jHOsTKrrI2W8c4wEIvQzSuHqvrjsDHT7iwujHjp4oeAOnYjBUm+/BgDH2Eg1/jL5ilEnJbpnAn2AE2KQUXQ==", + "cpu": [ + "arm64" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.kebabcase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", - "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.startcase": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", - "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.upperfirst": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", - "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", - "dev": true, - "license": "MIT" + "node_modules/@sentry/cli-win32-i686": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-3.5.1.tgz", + "integrity": "sha512-Op+9MYAg0RbVWOQ9/NtFunWcknS8MlqhEa03ENFUrRDQE0m+iHioknGOagKrNXYXj1UbGEf0G9UzIMcRwB/UCQ==", + "cpu": [ + "x86", + "ia32" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/log-symbols": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, + "node_modules/@sentry/cli-win32-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-3.5.1.tgz", + "integrity": "sha512-Vf+CtFPkuGzjddD6dJoAVKszw5QB7P1ffc++yAbU54ditqJdgswo45oyTFOiQFO2isCmhgICzimqe8SkU+p2Mw==", + "cpu": [ + "x64" + ], + "license": "FSL-1.1-MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/log-symbols/node_modules/ansi-styles": { - "version": "4.3.0", + "node_modules/@sentry/core": { + "version": "10.61.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.61.0.tgz", + "integrity": "sha512-Edg8t2w45qEKiFnjeA6zRmU47R6la5FEMG+maZEOB2oTyJ+ujmh8LGaZ3G8aC0VdkKn8CXhHtDesze6oDc1oTA==", "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=18" } }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", + "node_modules/@sentry/feedback": { + "version": "10.61.0", + "resolved": "https://registry.npmjs.org/@sentry/feedback/-/feedback-10.61.0.tgz", + "integrity": "sha512-DvG5pc2BibQjdvFC75u1S5DzghcFZ/juCDb2UnRKjiWnNhiUUAweAkUv4mMednrbuOeGOgO2R3CaiqIvDrAUAw==", "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@sentry/core": "10.61.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=18" } }, - "node_modules/log-symbols/node_modules/color-convert": { - "version": "2.0.1", + "node_modules/@sentry/replay": { + "version": "10.61.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-10.61.0.tgz", + "integrity": "sha512-EkLaPR7A89mRGucZY9WxKLGeiRKMuNeGfIthLrs9cumUP8Smc80qxSAsF3NggRHSQEeYHDAifY/1q1qW16yvJg==", "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "@sentry/browser-utils": "10.61.0", + "@sentry/core": "10.61.0" }, "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/log-symbols/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/log-symbols/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/log-symbols/node_modules/supports-color": { - "version": "7.2.0", + "node_modules/@sentry/replay-canvas": { + "version": "10.61.0", + "resolved": "https://registry.npmjs.org/@sentry/replay-canvas/-/replay-canvas-10.61.0.tgz", + "integrity": "sha512-n6QUN+qylEdKLcijpJsJ58ekY58kg0nG0dKxkD2wecKubl7B1mj/gwP35NmJOZ35vJTk/KbJeWB//finspJqYg==", "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "@sentry/core": "10.61.0", + "@sentry/replay": "10.61.0" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/log4js": { - "version": "6.9.1", + "node_modules/@sigstore/bundle": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "flatted": "^3.2.7", - "rfdc": "^1.3.0", - "streamroller": "^3.1.5" + "@sigstore/protobuf-specs": "^0.5.0" }, "engines": { - "node": ">=8.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/longest": { - "version": "1.0.1", + "node_modules/@sigstore/core": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.1.tgz", + "integrity": "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==", "dev": true, - "license": "MIT", - "optional": true, + "license": "Apache-2.0", "engines": { - "node": ">=0.10.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/lottie-web": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.13.0.tgz", - "integrity": "sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==", - "license": "MIT" - }, - "node_modules/lower-case": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.1.tgz", + "integrity": "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==", "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/magic-string": { - "version": "0.30.8", - "license": "MIT", + "node_modules/@sigstore/sign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.1.tgz", + "integrity": "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@gar/promise-retry": "^1.0.2", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.4", + "proc-log": "^6.1.0" }, "engines": { - "node": ">=12" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/make-dir": { - "version": "4.0.0", + "node_modules/@sigstore/tuf": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.2.tgz", + "integrity": "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "semver": "^7.5.3" + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/make-error": { - "version": "1.3.6", + "node_modules/@sigstore/verify": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.1.tgz", + "integrity": "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==", "dev": true, - "license": "ISC" - }, - "node_modules/make-fetch-happen": { - "version": "13.0.1", - "license": "ISC", + "license": "Apache-2.0", "dependencies": { - "@npmcli/agent": "^2.0.0", - "cacache": "^18.0.0", - "http-cache-semantics": "^4.1.1", - "is-lambda": "^1.0.1", - "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "proc-log": "^4.2.0", - "promise-retry": "^2.0.1", - "ssri": "^10.0.0" + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/proc-log": { - "version": "4.2.0", - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/make-iterator": { - "version": "1.0.1", + "node_modules/@simple-libs/child-process-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", + "integrity": "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==", "dev": true, "license": "MIT", "dependencies": { - "kind-of": "^6.0.2" + "@simple-libs/stream-utils": "^1.2.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=18" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" } }, - "node_modules/map-cache": { - "version": "0.2.2", + "node_modules/@simple-libs/stream-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", + "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-visit": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "object-visit": "^1.0.0" + "node": ">=18" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://ko-fi.com/dangreen" } }, - "node_modules/marked": { - "version": "11.2.0", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@swimlane/ngx-charts": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@swimlane/ngx-charts/-/ngx-charts-20.5.0.tgz", + "integrity": "sha512-PNBIHdu/R3ceD7jnw1uCBVOj4k3T6IxfdW6xsDsglGkZyoWMEEq4tLoEurjLEKzmDtRv9c35kVNOXy0lkOuXeA==", + "license": "MIT", + "dependencies": { + "d3-array": "^3.1.1", + "d3-brush": "^3.0.0", + "d3-color": "^3.1.0", + "d3-ease": "^3.0.1", + "d3-format": "^3.1.0", + "d3-hierarchy": "^3.1.0", + "d3-interpolate": "^3.0.1", + "d3-sankey": "^0.12.3", + "d3-scale": "^4.0.2", + "d3-selection": "^3.0.0", + "d3-shape": "^3.2.0", + "d3-time-format": "^3.0.0", + "d3-transition": "^3.0.1", + "rfdc": "^1.3.0", + "tslib": "^2.0.0" }, - "engines": { - "node": ">= 18" + "peerDependencies": { + "@angular/animations": ">=12.0.0", + "@angular/cdk": ">=12.0.0", + "@angular/common": ">=12.0.0", + "@angular/core": ">=12.0.0", + "@angular/forms": ">=12.0.0", + "@angular/platform-browser": ">=12.0.0", + "@angular/platform-browser-dynamic": ">=12.0.0", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "node_modules/@tailwindcss/node": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", + "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.2" } }, - "node_modules/media-typer": { - "version": "0.3.0", - "dev": true, + "node_modules/@tailwindcss/node/node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memfs": { - "version": "3.5.3", - "dev": true, - "license": "Unlicense", "dependencies": { - "fs-monkey": "^1.0.4" + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" }, "engines": { - "node": ">= 4.0.0" + "node": ">=10.13.0" } }, - "node_modules/memorystream": { - "version": "0.3.1", - "dev": true, - "engines": { - "node": ">= 0.10.0" + "node_modules/@tailwindcss/node/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/meow": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", - "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", - "dev": true, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", + "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", "license": "MIT", "engines": { - "node": ">=18" + "node": ">= 20" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "dev": true, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-x64": "4.3.2", + "@tailwindcss/oxide-freebsd-x64": "4.3.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-x64-musl": "4.3.2", + "@tailwindcss/oxide-wasm32-wasi": "4.3.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", + "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "cpu": [ + "arm64" + ], "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 8" + "node": ">= 20" } }, - "node_modules/methods": { - "version": "1.1.2", - "dev": true, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", + "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "cpu": [ + "arm64" + ], "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 0.6" + "node": ">= 20" } }, - "node_modules/micromatch": { - "version": "4.0.5", - "dev": true, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", + "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=8.6" + "node": ">= 20" } }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "dev": true, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", + "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">= 20" } }, - "node_modules/mime": { - "version": "2.6.0", - "dev": true, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", + "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "cpu": [ + "arm" + ], "license": "MIT", - "bin": { - "mime": "cli.js" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4.0.0" + "node": ">= 20" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "dev": true, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", + "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "cpu": [ + "arm64" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.6" + "node": ">= 20" } }, - "node_modules/mime-types": { - "version": "2.1.35", - "dev": true, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", + "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.6" + "node": ">= 20" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", + "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">= 20" } }, - "node_modules/mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", + "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "cpu": [ + "x64" + ], "license": "MIT", "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 20" } }, - "node_modules/mini-css-extract-plugin": { - "version": "2.8.1", - "dev": true, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", + "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], "license": "MIT", + "optional": true, "dependencies": { - "schema-utils": "^4.0.0", - "tapable": "^2.2.1" + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" }, "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" + "node": ">=14.0.0" } }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "dev": true, - "license": "ISC" - }, - "node_modules/minimatch": { - "version": "9.0.4", - "license": "ISC", + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "inBundle": true, + "license": "MIT", + "optional": true, "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" } }, - "node_modules/minimist": { - "version": "1.2.8", - "dev": true, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "inBundle": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.1", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/minipass-collect": { - "version": "2.0.1", - "license": "ISC", + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "inBundle": true, + "license": "MIT", + "optional": true, "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" + "tslib": "^2.4.0" } }, - "node_modules/minipass-fetch": { - "version": "3.0.5", + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" + "@tybys/wasm-util": "^0.10.1" }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" }, - "optionalDependencies": { - "encoding": "^0.1.13" + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "license": "ISC", + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "inBundle": true, + "license": "MIT", + "optional": true, "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" + "tslib": "^2.4.0" } }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "inBundle": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", + "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=8" + "node": ">= 20" } }, - "node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/minipass-json-stream": { - "version": "1.0.1", + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", + "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "jsonparse": "^1.3.1", - "minipass": "^3.0.0" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" } }, - "node_modules/minipass-json-stream/node_modules/minipass": { - "version": "3.3.6", - "license": "ISC", + "node_modules/@tailwindcss/postcss": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.2.tgz", + "integrity": "sha512-rjVWYCa7Ngbi5AarT6k8TkxUG3Wl1QKzHdIZVsjZSzf36Jmo2IKZt/NHRAwly8oDkbBOH0YTu+CHuf9jPxMc+g==", + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.2", + "@tailwindcss/oxide": "4.3.2", + "postcss": "^8.5.15", + "tailwindcss": "4.3.2" } }, - "node_modules/minipass-json-stream/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "license": "ISC", + "node_modules/@tailwindcss/postcss/node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "dependencies": { - "minipass": "^3.0.0" + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >=14" } }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "license": "ISC", + "node_modules/@trivago/prettier-plugin-sort-imports": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-6.0.2.tgz", + "integrity": "sha512-3DgfkukFyC/sE/VuYjaUUWoFfuVjPK55vOFDsxD56XXynFMCZDYFogH2l/hDfOsQAm1myoU/1xByJ3tWqtulXA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "yallist": "^4.0.0" + "@babel/generator": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "javascript-natural-sort": "^0.7.1", + "lodash-es": "^4.17.21", + "minimatch": "^9.0.0", + "parse-imports-exports": "^0.2.4" }, "engines": { - "node": ">=8" + "node": ">= 20" + }, + "peerDependencies": { + "@vue/compiler-sfc": "3.x", + "prettier": "2.x - 3.x", + "prettier-plugin-ember-template-tag": ">= 2.0.0", + "prettier-plugin-svelte": "3.x", + "svelte": "4.x || 5.x" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + }, + "prettier-plugin-ember-template-tag": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "svelte": { + "optional": true + } } }, - "node_modules/minipass-pipeline/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" }, - "node_modules/minipass-sized": { + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { "version": "1.0.3", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "license": "ISC", + "node_modules/@tufjs/models": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", + "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", + "dev": true, + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" }, "engines": { - "node": ">=8" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" + "node_modules/@tufjs/models/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, - "node_modules/minizlib": { - "version": "2.1.2", + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, "license": "MIT", "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">= 8" + "node": "18 || 20 || >=22" } }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "license": "ISC", + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "yallist": "^4.0.0" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" + "node_modules/@types/canvas-confetti": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz", + "integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==", + "dev": true, + "license": "MIT" }, - "node_modules/mixin-deep": { - "version": "1.3.2", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/mixin-deep/node_modules/is-extendable": { - "version": "1.0.1", + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", "dev": true, "license": "MIT", "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" + "@types/d3-selection": "*" } }, - "node_modules/mkdirp": { - "version": "0.5.6", + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", "dev": true, "license": "MIT", "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" + "@types/d3-selection": "*" } }, - "node_modules/moment": { - "version": "2.30.1", + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, "license": "MIT", - "engines": { - "node": "*" + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" } }, - "node_modules/monaco-editor": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.44.0.tgz", - "integrity": "sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==", + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, "license": "MIT" }, - "node_modules/morgan": { - "version": "1.10.0", + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", "dev": true, "license": "MIT", "dependencies": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.0.2" - }, - "engines": { - "node": ">= 0.8.0" + "@types/d3-selection": "*" } }, - "node_modules/morgan/node_modules/debug": { - "version": "2.6.9", + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", "dev": true, "license": "MIT", "dependencies": { - "ms": "2.0.0" + "@types/d3-dsv": "*" } }, - "node_modules/morgan/node_modules/ms": { - "version": "2.0.0", + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", "dev": true, "license": "MIT" }, - "node_modules/mrmime": { - "version": "2.0.0", + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "@types/geojson": "*" } }, - "node_modules/ms": { - "version": "2.1.2", + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, "license": "MIT" }, - "node_modules/multicast-dns": { - "version": "7.2.5", + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "dev": true, "license": "MIT", "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" + "@types/d3-color": "*" } }, - "node_modules/multimatch": { - "version": "4.0.0", + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "dev": true, "license": "MIT", "dependencies": { - "@types/minimatch": "^3.0.3", - "array-differ": "^3.0.0", - "array-union": "^2.1.0", - "arrify": "^2.0.1", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" + "@types/d3-time": "*" } }, - "node_modules/multimatch/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@types/d3-path": "*" } }, - "node_modules/multimatch/node_modules/minimatch": { - "version": "3.1.2", + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "dev": true, - "license": "ISC", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "@types/d3-selection": "*" } }, - "node_modules/mute-stream": { - "version": "1.0.0", - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" } }, - "node_modules/mz": { - "version": "2.7.0", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", "dev": true, "license": "MIT", "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" + "@types/trusted-types": "*" } }, - "node_modules/nan": { - "version": "2.22.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", - "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", - "optional": true + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "license": "MIT" }, - "node_modules/nanoid": { - "version": "3.3.7", + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", + "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "dependencies": { + "undici-types": "~8.3.0" } }, - "node_modules/nanomatch": { - "version": "1.2.13", - "dev": true, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.0.tgz", + "integrity": "sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw==", "license": "MIT", "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/type-utils": "8.62.0", + "@typescript-eslint/utils": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.62.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/nanomatch/node_modules/define-property": { - "version": "2.0.2", - "dev": true, + "node_modules/@typescript-eslint/parser": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz", + "integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==", "license": "MIT", "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", + "debug": "^4.4.3" }, "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/nanomatch/node_modules/extend-shallow": { - "version": "3.0.2", - "dev": true, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz", + "integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==", "license": "MIT", "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" + "@typescript-eslint/tsconfig-utils": "^8.62.1", + "@typescript-eslint/types": "^8.62.1", + "debug": "^4.4.3" }, "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/nanomatch/node_modules/is-descriptor": { - "version": "1.0.3", - "dev": true, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz", + "integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==", "license": "MIT", "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/nanomatch/node_modules/is-extendable": { - "version": "1.0.1", - "dev": true, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz", + "integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==", "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz", + "integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "dev": true, - "license": "MIT" + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz", + "integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==", + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.62.1", + "@typescript-eslint/tsconfig-utils": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } }, - "node_modules/needle": { - "version": "3.3.1", - "dev": true, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz", + "integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==", "license": "MIT", - "optional": true, "dependencies": { - "iconv-lite": "^0.6.3", - "sax": "^1.2.4" - }, - "bin": { - "needle": "bin/needle" + "@typescript-eslint/types": "8.62.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { - "node": ">= 4.4.x" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/negotiator": { - "version": "0.6.3", + "node_modules/@typescript-eslint/parser/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": "18 || 20 || >=22" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "dev": true, - "license": "MIT" - }, - "node_modules/ng-annotate": { - "version": "1.2.2", - "dev": true, + "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "license": "MIT", "dependencies": { - "acorn": "~2.6.4", - "alter": "~0.2.0", - "convert-source-map": "~1.1.2", - "optimist": "~0.6.1", - "ordered-ast-traverse": "~1.1.1", - "simple-fmt": "~0.1.0", - "simple-is": "~0.2.0", - "source-map": "~0.5.3", - "stable": "~0.1.5", - "stringmap": "~0.2.2", - "stringset": "~0.2.1", - "tryor": "~0.1.2" - }, - "bin": { - "ng-annotate": "build/es5/ng-annotate" - } - }, - "node_modules/ng-annotate/node_modules/acorn": { - "version": "2.6.4", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=0.4.0" + "node": "18 || 20 || >=22" } }, - "node_modules/ng-annotate/node_modules/convert-source-map": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/ng-annotate/node_modules/source-map": { - "version": "0.5.7", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "license": "Apache-2.0", "engines": { - "node": ">=0.10.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/ng-csv": { - "version": "0.2.3", + "node_modules/@typescript-eslint/parser/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, "engines": { - "node": ">=0.8.0" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/ng-file-upload": { - "version": "5.0.9", - "license": "MIT" - }, - "node_modules/ng-flex-layout": { - "version": "17.3.7-beta.1", + "node_modules/@typescript-eslint/project-service": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.0.tgz", + "integrity": "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ==", "license": "MIT", "dependencies": { - "tslib": "^2.3.0" + "@typescript-eslint/tsconfig-utils": "^8.62.0", + "@typescript-eslint/types": "^8.62.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@angular/cdk": ">=17.0.0", - "@angular/common": ">=17.0.0", - "@angular/core": ">=17.0.0", - "@angular/platform-browser": ">=17.0.0", - "rxjs": "^6.5.3 || ^7.4.0" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/ng2-pdf-viewer": { - "version": "10.2.2", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.0.tgz", + "integrity": "sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA==", "license": "MIT", "dependencies": { - "pdfjs-dist": "^3.11.174", - "tslib": "^2.3.0" + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/ngx-bootstrap": { - "version": "6.2.0", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz", + "integrity": "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==", "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, "peerDependencies": { - "@angular/common": ">=7.0.0", - "@angular/core": ">=7.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/ngx-entity-service": { - "version": "0.0.41", - "resolved": "https://registry.npmjs.org/ngx-entity-service/-/ngx-entity-service-0.0.41.tgz", - "integrity": "sha512-rf3gZQr4CthXV34WKySg200c06ZuXm2Z7EclUR6bAuiuvUDwRAPMEAJmtSmDyYbnpQqLMMb+5rfaQQakkTNotA==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.0.tgz", + "integrity": "sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w==", + "license": "MIT", "dependencies": { - "tslib": "^2.3.0" + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/utils": "8.62.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@angular/common": "^13.0 || ^14.0 || ^15.0 || ^16.0 || ^17 || ^18", - "@angular/core": "^13.0 || ^14.0 || ^15.0 || ^16.0 || ^17 || ^18" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/ngx-lottie": { - "version": "11.0.2", + "node_modules/@typescript-eslint/types": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz", + "integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==", "license": "MIT", - "dependencies": { - "@scarf/scarf": "^1.1.1", - "tslib": "^2.3.0" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "@angular/core": ">=17", - "lottie-web": ">=5.9.2" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/ngx-monaco-editor-v2": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/ngx-monaco-editor-v2/-/ngx-monaco-editor-v2-17.0.1.tgz", - "integrity": "sha512-GP+Ni6zKFQjF/ve5ZQtfE9eRLKL4GxMvdmDTrla1x6F5pSIcYGCcjZ4gQ1/AHMa5dgarfs+Et+1bBtAOJtI6KA==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.0.tgz", + "integrity": "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A==", "license": "MIT", "dependencies": { - "tslib": "^2.1.0" + "@typescript-eslint/project-service": "8.62.0", + "@typescript-eslint/tsconfig-utils": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@angular/common": "^17.0.3", - "@angular/core": "^17.0.3", - "monaco-editor": "^0.44.0" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/nice-napi": { - "version": "1.0.2", - "dev": true, - "hasInstallScript": true, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", - "optional": true, - "os": [ - "!win32" - ], - "dependencies": { - "node-addon-api": "^3.0.0", - "node-gyp-build": "^4.2.2" + "engines": { + "node": "18 || 20 || >=22" } }, - "node_modules/no-case": { - "version": "2.3.2", - "dev": true, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { - "lower-case": "^1.1.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, - "node_modules/node-addon-api": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "optional": true, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", "dependencies": { - "whatwg-url": "^5.0.0" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" + "node": "18 || 20 || >=22" }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-forge": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", - "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", - "dev": true, - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/node-gyp": { - "version": "10.1.0", + "node_modules/@typescript-eslint/utils": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.0.tgz", + "integrity": "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g==", "license": "MIT", "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "glob": "^10.3.10", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^13.0.0", - "nopt": "^7.0.0", - "proc-log": "^3.0.0", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^4.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/node-gyp-build": { - "version": "4.8.1", - "dev": true, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.0.tgz", + "integrity": "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ==", "license": "MIT", - "optional": true, - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-gyp/node_modules/abbrev": { - "version": "2.0.0", - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/node-gyp/node_modules/glob": { - "version": "10.3.15", - "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" + "@typescript-eslint/types": "8.62.0", + "eslint-visitor-keys": "^5.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "license": "Apache-2.0", "engines": { - "node": ">=16 || 14 >=14.18" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://opencollective.com/eslint" } }, - "node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.1", - "license": "ISC", + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.3.0.tgz", + "integrity": "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=16" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/node-gyp/node_modules/nopt": { - "version": "7.2.1", - "license": "ISC", + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", "dependencies": { - "abbrev": "^2.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/node-gyp/node_modules/which": { - "version": "4.0.0", - "license": "ISC", + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", "dependencies": { - "isexe": "^3.1.1" + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, - "bin": { - "node-which": "bin/which.js" + "funding": { + "url": "https://opencollective.com/vitest" }, - "engines": { - "node": "^16.13.0 || >=18.0.0" + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/node-machine-id": { - "version": "1.1.12", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.14", + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/nopt": { - "version": "3.0.6", + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "abbrev": "1" + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" }, - "bin": { - "nopt": "bin/nopt.js" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "devOptional": true, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/normalize-range": { - "version": "0.1.2", + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/npm-bundled": { - "version": "3.0.1", - "license": "ISC", + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", "dependencies": { - "npm-normalize-package-bin": "^3.0.0" + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/npm-install-checks": { - "version": "6.3.0", - "license": "BSD-2-Clause", + "node_modules/@vitest/utils/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@worktile/gantt": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@worktile/gantt/-/gantt-21.0.0.tgz", + "integrity": "sha512-btoj+li91y2F7VoR0TDgB64kHvlpJmz4DhD2jFsGtCgqk3+9HKHmVLKDj9pG6XZTmNe/IxsDfFDHblX233rXug==", + "license": "MIT", "dependencies": { - "semver": "^7.1.1" + "tslib": "^2.3.0" }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "peerDependencies": { + "@angular/cdk": ">=21.0.0", + "@angular/common": ">=21.0.0", + "@angular/core": ">=21.0.0", + "@date-fns/tz": ">=1.0.0", + "date-fns": ">=4.0.0", + "rxjs": "^6.5.0 || ^7.0.0" } }, - "node_modules/npm-normalize-package-bin": { - "version": "3.0.1", + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/npm-package-arg": { - "version": "11.0.1", - "license": "ISC", + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", "dependencies": { - "hosted-git-info": "^7.0.0", - "proc-log": "^3.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^5.0.0" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">= 0.6" } }, - "node_modules/npm-package-arg/node_modules/hosted-git-info": { - "version": "7.0.2", - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">=0.4.0" } }, - "node_modules/npm-package-arg/node_modules/lru-cache": { - "version": "10.2.2", - "license": "ISC", - "engines": { - "node": "14 || >=16.14" + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/npm-packlist": { - "version": "8.0.2", - "license": "ISC", + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", "dependencies": { - "ignore-walk": "^6.0.4" + "acorn": "^8.11.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=0.4.0" } }, - "node_modules/npm-pick-manifest": { + "node_modules/agent-base": { "version": "9.0.0", - "license": "ISC", - "dependencies": { - "npm-install-checks": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0", - "npm-package-arg": "^11.0.0", - "semver": "^7.3.5" - }, + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "dev": true, + "license": "MIT", "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">= 20" } }, - "node_modules/npm-registry-fetch": { - "version": "16.2.1", - "license": "ISC", + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", "dependencies": { - "@npmcli/redact": "^1.1.0", - "make-fetch-happen": "^13.0.0", - "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minipass-json-stream": "^1.0.1", - "minizlib": "^2.1.2", - "npm-package-arg": "^11.0.0", - "proc-log": "^4.0.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm-registry-fetch/node_modules/proc-log": { - "version": "4.2.0", - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/npm-run-all2": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-7.0.2.tgz", - "integrity": "sha512-7tXR+r9hzRNOPNTvXegM+QzCuMjzUIIq66VDunL6j60O4RrExx32XUhlrS7UK4VcdGw5/Wxzb3kfNcFix9JKDA==", + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "cross-spawn": "^7.0.6", - "memorystream": "^0.3.1", - "minimatch": "^9.0.0", - "pidtree": "^0.6.0", - "read-package-json-fast": "^4.0.0", - "shell-quote": "^1.7.3", - "which": "^5.0.0" + "ajv": "^8.0.0" }, - "bin": { - "npm-run-all": "bin/npm-run-all/index.js", - "npm-run-all2": "bin/npm-run-all/index.js", - "run-p": "bin/run-p/index.js", - "run-s": "bin/run-s/index.js" + "peerDependencies": { + "ajv": "^8.0.0" }, - "engines": { - "node": "^18.17.0 || >=20.5.0", - "npm": ">= 9" + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/npm-run-all2/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "node_modules/algoliasearch": { + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.52.0.tgz", + "integrity": "sha512-0ZzY9mjqV7gop/AH8pIBiAS8giXP7WcSiUfoFYIzYAK9QC5c37E4SIVtJVBMwlURc0/uNt2o4RcNRvdHa4CJ5w==", "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.18.0", + "@algolia/client-abtesting": "5.52.0", + "@algolia/client-analytics": "5.52.0", + "@algolia/client-common": "5.52.0", + "@algolia/client-insights": "5.52.0", + "@algolia/client-personalization": "5.52.0", + "@algolia/client-query-suggestions": "5.52.0", + "@algolia/client-search": "5.52.0", + "@algolia/ingestion": "1.52.0", + "@algolia/monitoring": "1.52.0", + "@algolia/recommend": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" + }, "engines": { - "node": ">=12" + "node": ">= 14.0.0" + } + }, + "node_modules/angular-calendar": { + "version": "0.32.2", + "resolved": "https://registry.npmjs.org/angular-calendar/-/angular-calendar-0.32.2.tgz", + "integrity": "sha512-eVPim7FOBjx/e58bvuHUpN5IuiFz8k97+RjatKUJ2Lj8Anro5c34NTOgKgZ7uYRHhgL++yUOcXV4ar8D/bMcFA==", + "license": "MIT", + "dependencies": { + "@scarf/scarf": "^1.1.1", + "calendar-utils": "^0.12.5", + "positioning": "^3.0.1", + "tslib": "^2.4.1" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/mattlewis92" + }, + "peerDependencies": { + "@angular/core": ">=20.2.0", + "angular-draggable-droppable": "^9.0.1", + "angular-resizable-element": "^8.0.0", + "date-fns": "^4.0.0", + "moment": "^2.0.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "moment": { + "optional": true + } } }, - "node_modules/npm-run-all2/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "engines": { - "node": ">=16" + "node_modules/angular-draggable-droppable": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/angular-draggable-droppable/-/angular-draggable-droppable-9.0.1.tgz", + "integrity": "sha512-nxxFzBMEzB6RsRUqnHWelt9G7QXG2wc18czYL75YhJ9IkBJOBkxXBd0ZOTeDgV9C3mmkkw+PMLJOayx0GH6gXA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@mattlewis92/dom-autoscroller": "^2.4.2", + "tslib": "^2.4.1" + }, + "peerDependencies": { + "@angular/core": ">=20.0.0" } }, - "node_modules/npm-run-all2/node_modules/json-parse-even-better-errors": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", - "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", + "node_modules/angular-eslint": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/angular-eslint/-/angular-eslint-22.0.0.tgz", + "integrity": "sha512-6tHLndzM6rU+2iuICakJS/hD1scK5sWLkcD7828zStT1ViA9zX8z9g/V1IlBiKEdZeMsl+m7K2DlNc34AkYyoQ==", "dev": true, - "engines": { - "node": "^18.17.0 || >=20.5.0" + "license": "MIT", + "dependencies": { + "@angular-devkit/core": ">= 22.0.0 < 23.0.0", + "@angular-devkit/schematics": ">= 22.0.0 < 23.0.0", + "@angular-eslint/builder": "22.0.0", + "@angular-eslint/eslint-plugin": "22.0.0", + "@angular-eslint/eslint-plugin-template": "22.0.0", + "@angular-eslint/schematics": "22.0.0", + "@angular-eslint/template-parser": "22.0.0", + "@typescript-eslint/types": "^8.0.0", + "@typescript-eslint/utils": "^8.0.0" + }, + "peerDependencies": { + "@angular/cli": ">= 22.0.0 < 23.0.0", + "eslint": "^9.0.0 || ^10.0.0", + "typescript": "*", + "typescript-eslint": "^8.0.0" } }, - "node_modules/npm-run-all2/node_modules/npm-normalize-package-bin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", - "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", - "dev": true, - "engines": { - "node": "^18.17.0 || >=20.5.0" + "node_modules/angular-resizable-element": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/angular-resizable-element/-/angular-resizable-element-8.0.3.tgz", + "integrity": "sha512-Jf/iVj9BNOTjune/cpdRj+c2Rd0WFrVf9yXMILU2INmAS5GUqs7pBrsbJkGo4NrXMfOsb/vS+sXWbv60UAheHg==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=20.0.0" } }, - "node_modules/npm-run-all2/node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "dev": true, - "bin": { - "pidtree": "bin/pidtree.js" + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" }, "engines": { - "node": ">=0.10" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npm-run-all2/node_modules/read-package-json-fast": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz", - "integrity": "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==", + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, - "dependencies": { - "json-parse-even-better-errors": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" - }, + "license": "MIT", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/npm-run-all2/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", - "dev": true, + "node_modules/ansi-to-html": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", + "integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==", + "license": "MIT", "dependencies": { - "isexe": "^3.1.1" + "entities": "^2.2.0" }, "bin": { - "node-which": "bin/which.js" + "ansi-to-html": "bin/ansi-to-html" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=8.0.0" } }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "dev": true, - "license": "MIT", + "node_modules/ansi-to-html/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, "dependencies": { - "path-key": "^3.0.0" + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", "optional": true, "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/nth-check": { - "version": "2.1.1", + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, - "node_modules/num2fraction": { - "version": "1.2.2", + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", "dev": true, "license": "MIT" }, - "node_modules/nvd3": { - "version": "1.8.6", - "license": "Apache-2.0", - "peerDependencies": { - "d3": "^3.4.4" + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" } }, - "node_modules/nx": { - "version": "18.3.4", + "node_modules/autoprefixer": { + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz", + "integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==", "dev": true, - "hasInstallScript": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "@nrwl/tao": "18.3.4", - "@yarnpkg/lockfile": "^1.1.0", - "@yarnpkg/parsers": "3.0.0-rc.46", - "@zkochan/js-yaml": "0.0.6", - "axios": "^1.6.0", - "chalk": "^4.1.0", - "cli-cursor": "3.1.0", - "cli-spinners": "2.6.1", - "cliui": "^8.0.1", - "dotenv": "~16.3.1", - "dotenv-expand": "~10.0.0", - "enquirer": "~2.3.6", - "figures": "3.2.0", - "flat": "^5.0.2", - "fs-extra": "^11.1.0", - "ignore": "^5.0.4", - "jest-diff": "^29.4.1", - "js-yaml": "4.1.0", - "jsonc-parser": "3.2.0", - "lines-and-columns": "~2.0.3", - "minimatch": "9.0.3", - "node-machine-id": "1.1.12", - "npm-run-path": "^4.0.1", - "open": "^8.4.0", - "ora": "5.3.0", - "semver": "^7.5.3", - "string-width": "^4.2.3", - "strong-log-transformer": "^2.1.0", - "tar-stream": "~2.2.0", - "tmp": "~0.2.1", - "tsconfig-paths": "^4.1.2", - "tslib": "^2.3.0", - "yargs": "^17.6.2", - "yargs-parser": "21.1.1" + "browserslist": "^4.28.4", + "caniuse-lite": "^1.0.30001799", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" }, "bin": { - "nx": "bin/nx.js", - "nx-cloud": "bin/nx-cloud.js" + "autoprefixer": "bin/autoprefixer" }, - "optionalDependencies": { - "@nx/nx-darwin-arm64": "18.3.4", - "@nx/nx-darwin-x64": "18.3.4", - "@nx/nx-freebsd-x64": "18.3.4", - "@nx/nx-linux-arm-gnueabihf": "18.3.4", - "@nx/nx-linux-arm64-gnu": "18.3.4", - "@nx/nx-linux-arm64-musl": "18.3.4", - "@nx/nx-linux-x64-gnu": "18.3.4", - "@nx/nx-linux-x64-musl": "18.3.4", - "@nx/nx-win32-arm64-msvc": "18.3.4", - "@nx/nx-win32-x64-msvc": "18.3.4" + "engines": { + "node": "^10 || ^12 || >=14" }, "peerDependencies": { - "@swc-node/register": "^1.8.0", - "@swc/core": "^1.3.85" - }, - "peerDependenciesMeta": { - "@swc-node/register": { - "optional": true - }, - "@swc/core": { - "optional": true - } + "postcss": "^8.1.0" } }, - "node_modules/nx/node_modules/@nx/nx-darwin-arm64": { - "version": "18.3.4", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-18.3.4.tgz", - "integrity": "sha512-MOGk9z4fIoOkJB68diH3bwoWrC8X9IzMNsz1mu0cbVfgCRAfIV3b+lMsiwQYzWal3UWW5DE5Rkss4F8whiV5Uw==", - "cpu": [ - "arm64" - ], + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", "engines": { - "node": ">= 10" + "node": ">= 0.4" } }, - "node_modules/nx/node_modules/@nx/nx-darwin-x64": { - "version": "18.3.4", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-18.3.4.tgz", - "integrity": "sha512-tSzPRnNB3QdPM+KYiIuRCUtyCwcuIRC95FfP0ZB3WvfDeNxJChEAChNqmCMDE4iFvZhGuze8WqkJuIVdte+lyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/base64-arraybuffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz", + "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==", "engines": { - "node": ">= 10" + "node": ">= 0.6.0" } }, - "node_modules/nx/node_modules/@nx/nx-linux-x64-gnu": { - "version": "18.3.4", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-18.3.4.tgz", - "integrity": "sha512-vbHxv7m3gjthBvw50EYCtgyY0Zg5nVTaQtX+wRsmKybV2i7wHbw5zIe1aL4zHUm6TcPGbIQK+utVM+hyCqKHVA==", - "cpu": [ - "x64" - ], + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, "engines": { - "node": ">= 10" + "node": ">=6.0.0" } }, - "node_modules/nx/node_modules/@nx/nx-win32-x64-msvc": { - "version": "18.3.4", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-18.3.4.tgz", - "integrity": "sha512-/RqEjNU9hxIBxRLafCNKoH3SaB2FShf+1ZnIYCdAoCZBxLJebDpnhiyrVs0lPnMj9248JbizEMdJj1+bs/bXig==", - "cpu": [ - "x64" - ], + "node_modules/beasties": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.2.tgz", + "integrity": "sha512-NvcGjG/7AVUAfRbvrJmHunDQS9uHnE6Q/7AkaPr8oKE8HjOlpjRG5075z/th2Tmlezk3VlaaS8+X9I1RwHJMQw==", "dev": true, - "optional": true, - "os": [ - "win32" - ], + "license": "Apache-2.0", + "dependencies": { + "css-select": "^6.0.0", + "css-what": "^7.0.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^10.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3", + "postcss-safe-parser": "^7.0.1" + }, "engines": { - "node": ">= 10" + "node": ">=18.0.0" } }, - "node_modules/nx/node_modules/ansi-styles": { - "version": "4.3.0", + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "require-from-string": "^2.0.2" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/nx/node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/nx/node_modules/chalk": { - "version": "4.1.2", + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/nx/node_modules/color-convert": { - "version": "2.0.1", + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" }, "engines": { - "node": ">=7.0.0" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/nx/node_modules/color-name": { - "version": "1.1.4", + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, "license": "MIT" }, - "node_modules/nx/node_modules/fs-extra": { - "version": "11.2.0", + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", + "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", + "dev": true, + "license": "ISC", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0" }, "engines": { - "node": ">=14.14" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/nx/node_modules/has-flag": { - "version": "4.0.0", + "node_modules/cacache/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" } }, - "node_modules/nx/node_modules/js-yaml": { - "version": "4.1.0", + "node_modules/cacache/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { - "argparse": "^2.0.1" + "balanced-match": "^4.0.2" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": "18 || 20 || >=22" } }, - "node_modules/nx/node_modules/jsonc-parser": { - "version": "3.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/nx/node_modules/minimatch": { - "version": "9.0.3", + "node_modules/cacache/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/nx/node_modules/ora": { - "version": "5.3.0", + "node_modules/cacache/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "log-symbols": "^4.0.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "20 || >=22" } }, - "node_modules/nx/node_modules/supports-color": { - "version": "7.2.0", + "node_modules/cacache/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "has-flag": "^4.0.0" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/nx/node_modules/tsconfig-paths": { - "version": "4.2.0", - "dev": true, + "node_modules/calendar-utils": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/calendar-utils/-/calendar-utils-0.12.5.tgz", + "integrity": "sha512-resk9x4GGwzsea55oUmW4Gs52v97YitHsA4gfTbIfygGyBMYrQV6H0HyHJyI95FCK5mu7xKVDikMrywSY1hTlA==", "license": "MIT", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" + "peerDependencies": { + "date-fns": "^4.0.0", + "luxon": "^3.0.0", + "moment": "^2.0.0" }, - "engines": { - "node": ">=6" - } - }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "*" + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } } }, - "node_modules/object-assign": { - "version": "4.1.1", - "devOptional": true, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/object-copy": { - "version": "0.1.0", + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", "dependencies": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-copy/node_modules/kind-of": { - "version": "3.2.2", + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "dev": true, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=6" } }, - "node_modules/object-inspect": { - "version": "1.13.1", + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", "dev": true, - "license": "MIT", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvas-confetti": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz", + "integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==", + "license": "ISC", "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" } }, - "node_modules/object-keys": { - "version": "1.1.1", + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/object-visit": { - "version": "1.0.1", + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", - "dependencies": { - "isobject": "^3.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/object.assign": { - "version": "4.1.5", + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" + "readdirp": "^5.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">= 20.19.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://paulmillr.com/funding/" } }, - "node_modules/object.defaults": { - "version": "1.1.0", + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, - "license": "MIT", - "dependencies": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/object.map": { - "version": "1.0.1", + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "license": "MIT", "dependencies": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" + "restore-cursor": "^5.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/object.pick": { - "version": "1.3.0", + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", "dev": true, "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, "engines": { - "node": ">=0.10.0" + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/object.values": { - "version": "1.2.0", + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" }, "engines": { - "node": ">= 0.4" + "node": ">=20" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/obuf": { - "version": "1.1.2", + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, - "license": "MIT" + "license": "ISC", + "engines": { + "node": ">= 12" + } }, - "node_modules/on-finished": { - "version": "2.3.0", + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "ee-first": "1.1.1" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">=20" } }, - "node_modules/on-headers": { - "version": "1.0.2", + "node_modules/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/once": { - "version": "1.4.0", - "devOptional": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } + "node_modules/cliui/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" }, - "node_modules/onetime": { - "version": "5.1.2", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, "license": "MIT", "dependencies": { - "mimic-fn": "^2.1.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=6" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/open": { - "version": "8.4.2", + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, "license": "MIT", "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/opn": { - "version": "4.0.2", - "dev": true, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "license": "MIT", - "dependencies": { - "object-assign": "^4.0.1", - "pinkie-promise": "^2.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">= 10" } }, - "node_modules/optimist": { - "version": "0.6.1", + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", "dev": true, - "license": "MIT/X11", + "license": "MIT", "dependencies": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" } }, - "node_modules/optimist/node_modules/minimist": { - "version": "0.0.10", - "dev": true, - "license": "MIT" + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "optional": true }, - "node_modules/optionator": { - "version": "0.9.4", + "node_modules/concurrently": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz", + "integrity": "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==", "dev": true, "license": "MIT", "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" + "chalk": "5.6.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.4", + "supports-color": "10.2.2", + "tree-kill": "1.2.2", + "yargs": "18.0.0" }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ora": { - "version": "5.4.1", - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" + "bin": { + "conc": "dist/bin/index.js", + "concurrently": "dist/bin/index.js" }, "engines": { - "node": ">=10" + "node": ">=22" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, - "node_modules/ora/node_modules/ansi-styles": { - "version": "4.3.0", + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/ora/node_modules/chalk": { - "version": "4.1.2", + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">= 0.6" } }, - "node_modules/ora/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", + "node_modules/conventional-changelog-angular": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.3.1.tgz", + "integrity": "sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==", + "dev": true, + "license": "ISC", "dependencies": { - "color-name": "~1.1.4" + "compare-func": "^2.0.0" }, "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ora/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/ora/node_modules/has-flag": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/ora/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", + "node_modules/conventional-changelog-conventionalcommits": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz", + "integrity": "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==", + "dev": true, + "license": "ISC", "dependencies": { - "has-flag": "^4.0.0" + "compare-func": "^2.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/ordered-ast-traverse": { - "version": "1.1.1", + "node_modules/conventional-commits-parser": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.4.0.tgz", + "integrity": "sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==", "dev": true, "license": "MIT", "dependencies": { - "ordered-esprima-props": "~1.1.0" + "@simple-libs/stream-utils": "^1.2.0", + "meow": "^13.0.0" + }, + "bin": { + "conventional-commits-parser": "dist/cli/index.js" + }, + "engines": { + "node": ">=18" } }, - "node_modules/ordered-esprima-props": { - "version": "1.1.0", + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true, "license": "MIT" }, - "node_modules/os-homedir": { - "version": "1.0.2", + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.6" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=6.6.0" } }, - "node_modules/osenv": { - "version": "0.1.5", - "dev": true, - "license": "ISC", - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" }, - "node_modules/p-limit": { - "version": "3.1.0", + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "dev": true, "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "object-assign": "^4", + "vary": "^1" }, "engines": { - "node": ">=10" + "node": ">= 0.10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/p-locate": { - "version": "5.0.0", + "node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^3.0.2" + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" }, "engines": { - "node": ">=10" + "node": ">=14" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" + "url": "https://github.com/sponsors/d-fischer" }, - "engines": { - "node": ">=10" + "peerDependencies": { + "typescript": ">=4.9.5" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/p-retry": { - "version": "4.6.2", + "node_modules/cosmiconfig-typescript-loader": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.3.0.tgz", + "integrity": "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==", "dev": true, "license": "MIT", "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" + "jiti": "2.6.1" }, "engines": { - "node": ">=8" + "node": ">=v18" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=9", + "typescript": ">=5" } }, - "node_modules/p-retry/node_modules/retry": { - "version": "0.13.1", + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } + "license": "MIT" }, - "node_modules/p-try": { - "version": "2.2.0", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pacote": { - "version": "17.0.6", - "license": "ISC", "dependencies": { - "@npmcli/git": "^5.0.0", - "@npmcli/installed-package-contents": "^2.0.1", - "@npmcli/promise-spawn": "^7.0.0", - "@npmcli/run-script": "^7.0.0", - "cacache": "^18.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^7.0.2", - "npm-package-arg": "^11.0.0", - "npm-packlist": "^8.0.0", - "npm-pick-manifest": "^9.0.0", - "npm-registry-fetch": "^16.0.0", - "proc-log": "^3.0.0", - "promise-retry": "^2.0.1", - "read-package-json": "^7.0.0", - "read-package-json-fast": "^3.0.0", - "sigstore": "^2.2.0", - "ssri": "^10.0.0", - "tar": "^6.1.11" - }, - "bin": { - "pacote": "lib/bin.js" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">= 8" } }, - "node_modules/pako": { - "version": "1.0.11", - "license": "(MIT AND Zlib)" - }, - "node_modules/param-case": { - "version": "2.1.1", - "dev": true, + "node_modules/css-line-break": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz", + "integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==", "license": "MIT", "dependencies": { - "no-case": "^2.2.0" + "base64-arraybuffer": "^0.2.0" } }, - "node_modules/parent-module": { - "version": "1.0.1", + "node_modules/css-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "callsites": "^3.0.0" + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" }, - "engines": { - "node": ">=6" + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/parse-filepath": { - "version": "1.0.2", + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", "dependencies": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=0.8" + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "node_modules/parse-json": { - "version": "5.2.0", + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">=8" + "node": ">= 6" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/parse-json/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/parse-json/node_modules/lines-and-columns": { - "version": "1.2.4", - "dev": true, - "license": "MIT" - }, - "node_modules/parse-node-version": { - "version": "1.0.1", + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, "engines": { - "node": ">= 0.10" + "node": ">=4" } }, - "node_modules/parse-passwd": { - "version": "1.0.0", - "dev": true, - "license": "MIT", + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/parse5": { - "version": "7.1.2", - "devOptional": true, - "license": "MIT", + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", "dependencies": { - "entities": "^4.4.0" + "internmap": "1 - 2" }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "engines": { + "node": ">=12" } }, - "node_modules/parse5-html-rewriting-stream": { - "version": "7.0.0", - "dev": true, - "license": "MIT", + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", "dependencies": { - "entities": "^4.3.0", - "parse5": "^7.0.0", - "parse5-sax-parser": "^7.0.0" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "engines": { + "node": ">=12" } }, - "node_modules/parse5-sax-parser": { - "version": "7.0.0", - "dev": true, - "license": "MIT", + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", "dependencies": { - "parse5": "^7.0.0" + "d3-path": "1 - 3" }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "engines": { + "node": ">=12" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "dev": true, - "license": "MIT", + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", "engines": { - "node": ">= 0.8" + "node": ">=12" } }, - "node_modules/pascalcase": { - "version": "0.1.1", - "dev": true, - "license": "MIT", + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/path-dirname": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/path-exists": { - "version": "4.0.0", - "license": "MIT", + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "devOptional": true, - "license": "MIT", + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/path-key": { - "version": "3.1.1", - "license": "MIT", + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "license": "MIT" + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/path-root": { - "version": "0.1.1", - "dev": true, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { - "path-root-regex": "^0.1.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/path-root-regex": { - "version": "0.1.2", - "dev": true, - "license": "MIT", + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "license": "BlueOak-1.0.0", + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "d3-dsv": "1 - 3" }, "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=12" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.2", + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, "engines": { - "node": "14 || >=16.14" + "node": ">=12" } }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "dev": true, - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "dev": true, - "license": "MIT", + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/path2d-polyfill": { - "version": "2.0.1", - "license": "MIT", - "optional": true, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/pdfjs-dist": { - "version": "3.11.174", - "license": "Apache-2.0", + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "canvas": "^2.11.2", - "path2d-polyfill": "^2.0.1" + "node": ">=12" } }, - "node_modules/performance-now": { - "version": "2.1.0", - "dev": true, - "license": "MIT" + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } }, - "node_modules/picomatch": { - "version": "4.0.1", - "license": "MIT", + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", "engines": { "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "3.0.0", - "dev": true, - "license": "MIT", + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", "engines": { - "node": ">=4" + "node": ">=12" } }, - "node_modules/pinkie": { - "version": "2.0.4", - "dev": true, - "license": "MIT", + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "dev": true, - "license": "MIT", + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" } }, - "node_modules/pirates": { - "version": "4.0.6", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" } }, - "node_modules/piscina": { - "version": "4.4.0", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "nice-napi": "^1.0.2" + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" } }, - "node_modules/pkg-dir": { - "version": "7.0.0", - "dev": true, - "license": "MIT", + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", "dependencies": { - "find-up": "^6.3.0" + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" }, "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "6.3.0", - "dev": true, - "license": "MIT", + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^6.0.0" - }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "4.0.0", - "dev": true, - "license": "MIT", + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", "dependencies": { - "yocto-queue": "^1.0.0" + "d3-path": "^3.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "6.0.0", - "dev": true, - "license": "MIT", + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", "dependencies": { - "p-limit": "^4.0.0" + "d3-array": "2 - 3" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/pkg-dir/node_modules/path-exists": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node_modules/d3-time-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", + "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-time": "1 - 2" } }, - "node_modules/pkg-dir/node_modules/yocto-queue": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node_modules/d3-time-format/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" } }, - "node_modules/pkg-up": { - "version": "3.1.0", - "dev": true, - "license": "MIT", + "node_modules/d3-time-format/node_modules/d3-time": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz", + "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==", + "license": "BSD-3-Clause", "dependencies": { - "find-up": "^3.0.0" - }, + "d3-array": "2" + } + }, + "node_modules/d3-time-format/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/pkg-up/node_modules/find-up": { - "version": "3.0.0", - "dev": true, - "license": "MIT", + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", "dependencies": { - "locate-path": "^3.0.0" + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" }, "engines": { - "node": ">=6" + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" } }, - "node_modules/pkg-up/node_modules/locate-path": { + "node_modules/d3-zoom": { "version": "3.0.0", - "dev": true, - "license": "MIT", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" }, "engines": { - "node": ">=6" + "node": ">=12" } }, - "node_modules/pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "dev": true, - "license": "MIT", + "node_modules/d3/node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", "dependencies": { - "p-try": "^2.0.0" + "d3-time": "1 - 3" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/pkg-up/node_modules/p-locate": { - "version": "3.0.0", + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^2.0.0" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" }, "engines": { - "node": ">=6" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "dev": true, + "node_modules/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, "engines": { - "node": ">=4" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "license": "MIT", "engines": { - "node": ">=10.13.0" + "node": ">=0.10.0" } }, - "node_modules/portscanner": { - "version": "1.2.0", + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true, - "dependencies": { - "async": "1.5.2" - }, - "engines": { - "node": ">=0.4", - "npm": ">=1.0.0" - } + "license": "MIT" }, - "node_modules/positioning": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/positioning/-/positioning-2.0.1.tgz", - "integrity": "sha512-DsAgM42kV/ObuwlRpAzDTjH9E8fGKkMDJHWFX+kfNXSxh7UCCQxEmdjv/Ws5Ft1XDnt3JT8fIDYeKNSE2TbttA==", + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "license": "MIT" }, - "node_modules/posix-character-classes": { - "version": "0.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" } }, - "node_modules/possible-typed-array-names": { + "node_modules/delegates": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">= 0.8" } }, - "node_modules/postcss": { - "version": "8.4.38", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" - }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=8" } }, - "node_modules/postcss-import": { - "version": "15.1.0", + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, + "license": "BSD-3-Clause", "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" + "node": ">=0.3.1" } }, - "node_modules/postcss-import/node_modules/postcss-value-parser": { - "version": "4.2.0", - "dev": true, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, - "node_modules/postcss-js": { - "version": "4.0.1", + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "license": "MIT", "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/postcss-load-config": { - "version": "4.0.2", + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, { "type": "github", - "url": "https://github.com/sponsors/ai" + "url": "https://github.com/sponsors/fb55" } ], - "license": "MIT", + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" + "domelementtype": "^2.3.0" }, "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" + "node": ">= 4" }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.1", + "node_modules/dompurify": { + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/postcss-load-config/node_modules/yaml": { - "version": "2.4.2", + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" }, "engines": { - "node": ">= 14" + "node": ">=8" } }, - "node_modules/postcss-loader": { - "version": "8.1.1", + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", "dependencies": { - "cosmiconfig": "^9.0.0", - "jiti": "^1.20.0", - "semver": "^7.5.4" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } + "node": ">= 0.4" } }, - "node_modules/postcss-media-query-parser": { - "version": "0.2.3", + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true, "license": "MIT" }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", + "node_modules/electron-to-chromium": { + "version": "1.5.378", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.378.tgz", + "integrity": "sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ==", "dev": true, - "license": "ISC", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">= 0.8" } }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.5", + "node_modules/enhanced-resolve": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.23.0.tgz", + "integrity": "sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==", "dev": true, "license": "MIT", "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" }, "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">=10.13.0" } }, - "node_modules/postcss-modules-local-by-default/node_modules/postcss-value-parser": { - "version": "4.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.0", + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, + "license": "BSD-2-Clause", "engines": { - "node": "^10 || ^12 || >= 14" + "node": ">=0.12" }, - "peerDependencies": { - "postcss": "^8.1.0" + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, - "license": "ISC", - "dependencies": { - "icss-utils": "^5.0.0" - }, + "license": "MIT", "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">=6" } }, - "node_modules/postcss-nested": { - "version": "6.0.1", + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "dev": true, "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.11" - }, "engines": { - "node": ">=12.0" + "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/postcss-scss": { - "version": "0.1.9", + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", "dependencies": { - "postcss": "^5.1.0" + "is-arrayish": "^0.2.1" } }, - "node_modules/postcss-scss/node_modules/ansi-regex": { - "version": "2.1.1", + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/postcss-scss/node_modules/ansi-styles": { - "version": "2.2.1", + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/postcss-scss/node_modules/chalk": { - "version": "1.1.3", + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "es-errors": "^1.3.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/postcss-scss/node_modules/chalk/node_modules/supports-color": { - "version": "2.0.0", + "node_modules/es-toolkit": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.48.1.tgz", + "integrity": "sha512-wfnXlwd5I75eXRtdD2vuEs50xHHESECDsGD7yiQnfFVNoa5522NwXEbmgo98LfiukSQHs+mBM7/YG3qKJB9/mQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.8.0" - } + "workspaces": [ + "docs", + "benchmarks" + ] }, - "node_modules/postcss-scss/node_modules/has-flag": { - "version": "1.0.0", + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { - "node": ">=0.10.0" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, - "node_modules/postcss-scss/node_modules/postcss": { - "version": "5.2.18", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", - "dependencies": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - }, "engines": { - "node": ">=0.12" + "node": ">=6" } }, - "node_modules/postcss-scss/node_modules/source-map": { - "version": "0.5.7", + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, - "node_modules/postcss-scss/node_modules/strip-ansi": { - "version": "3.0.1", - "dev": true, + "node_modules/eslint": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", + "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { - "ansi-regex": "^2.0.0" + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" }, "engines": { - "node": ">=0.10.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, - "node_modules/postcss-scss/node_modules/supports-color": { - "version": "3.2.3", + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^1.0.0" + "bin": { + "eslint-config-prettier": "bin/cli.js" }, - "engines": { - "node": ">=0.8.0" + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" } }, - "node_modules/postcss-selector-parser": { - "version": "6.0.16", + "node_modules/eslint-plugin-prettier": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", + "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", "dev": true, "license": "MIT", "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.13" }, "engines": { - "node": ">=4" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } } }, - "node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", + "node_modules/eslint-plugin-tailwindcss": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-4.0.4.tgz", + "integrity": "sha512-cN43gHInx32ZwymgBPz6wQcH/QWEn/1fXvu51KfwFZMTGimviXcn61CPR8NY4mpstgNkbCqN9UVCspr+YqpV9w==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.37.0", + "postcss": "^8.4.4", + "postcss-nested": "^7.0.2", + "synckit": "^0.11.11", + "tailwind-api-utils": "^1.0.3" + }, "engines": { - "node": ">= 0.8.0" + "node": ">=20.19.0" + }, + "peerDependencies": { + "tailwindcss": "^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/preprocess": { - "version": "3.2.0", - "dev": true, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "license": "BSD-2-Clause", "dependencies": { - "xregexp": "3.1.0" + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">= 0.10.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/prettier": { - "version": "3.2.5", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "license": "Apache-2.0", "engines": { - "node": ">=14" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "url": "https://opencollective.com/eslint" } }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "dev": true, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "license": "MIT", "dependencies": { - "fast-diff": "^1.1.2" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": "18 || 20 || >=22" } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "dev": true, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "balanced-match": "^4.0.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "18 || 20 || >=22" } }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "dev": true, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "license": "MIT", "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/proc-log": { - "version": "3.0.0", - "license": "ISC", + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "license": "Apache-2.0", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "license": "MIT" - }, - "node_modules/promise": { - "version": "7.3.1", - "dev": true, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "license": "MIT", - "optional": true, - "dependencies": { - "asap": "~2.0.3" + "engines": { + "node": ">= 4" } }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "license": "ISC" + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" }, - "node_modules/promise-retry": { - "version": "2.0.1", - "license": "MIT", + "node_modules/eslint/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=10" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/protractor": { - "version": "7.0.0", - "dev": true, - "license": "MIT", + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "license": "BSD-2-Clause", "dependencies": { - "@types/q": "^0.0.32", - "@types/selenium-webdriver": "^3.0.0", - "blocking-proxy": "^1.0.0", - "browserstack": "^1.5.1", - "chalk": "^1.1.3", - "glob": "^7.0.3", - "jasmine": "2.8.0", - "jasminewd2": "^2.1.0", - "q": "1.4.1", - "saucelabs": "^1.5.0", - "selenium-webdriver": "3.6.0", - "source-map-support": "~0.4.0", - "webdriver-js-extender": "2.1.0", - "webdriver-manager": "^12.1.7", - "yargs": "^15.3.1" - }, - "bin": { - "protractor": "bin/protractor", - "webdriver-manager": "bin/webdriver-manager" + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": ">=10.13.x" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/protractor/node_modules/ansi-regex": { - "version": "2.1.1", - "dev": true, - "license": "MIT", + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "license": "Apache-2.0", "engines": { - "node": ">=0.10.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/protractor/node_modules/ansi-styles": { - "version": "2.2.1", - "dev": true, - "license": "MIT", + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=0.10" } }, - "node_modules/protractor/node_modules/chalk": { - "version": "1.1.3", - "dev": true, - "license": "MIT", + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "estraverse": "^5.2.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=4.0" } }, - "node_modules/protractor/node_modules/cliui": { - "version": "6.0.0", + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" + "@types/estree": "^1.0.0" } }, - "node_modules/protractor/node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.6" } }, - "node_modules/protractor/node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "eventsource-parser": "^3.0.1" }, "engines": { - "node": ">=8" + "node": ">=18.0.0" } }, - "node_modules/protractor/node_modules/find-up": { - "version": "4.1.0", + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", "dev": true, "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": ">=18.0.0" } }, - "node_modules/protractor/node_modules/locate-path": { - "version": "5.0.0", + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">=8" + "node": ">=12.0.0" } }, - "node_modules/protractor/node_modules/p-limit": { - "version": "2.3.0", + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">=6" + "node": ">= 18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/protractor/node_modules/p-locate": { - "version": "4.1.0", + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "ip-address": "^10.2.0" }, "engines": { - "node": ">=8" + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" } }, - "node_modules/protractor/node_modules/q": { - "version": "1.4.1", + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" }, - "node_modules/protractor/node_modules/source-map": { - "version": "0.5.7", + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" }, - "node_modules/protractor/node_modules/source-map-support": { - "version": "0.4.18", + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", "dev": true, "license": "MIT", "dependencies": { - "source-map": "^0.5.6" + "fast-string-truncated-width": "^3.0.2" } }, - "node_modules/protractor/node_modules/strip-ansi": { - "version": "3.0.1", + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" + "fast-string-width": "^3.0.2" } }, - "node_modules/protractor/node_modules/supports-color": { - "version": "2.0.0", - "dev": true, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", "engines": { - "node": ">=0.8.0" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/protractor/node_modules/y18n": { - "version": "4.0.3", - "dev": true, - "license": "ISC" - }, - "node_modules/protractor/node_modules/yargs": { - "version": "15.4.1", - "dev": true, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "license": "MIT", "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" + "flat-cache": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=16.0.0" } }, - "node_modules/protractor/node_modules/yargs-parser": { - "version": "18.1.3", + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": ">=6" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "dev": true, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "license": "MIT", "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "dev": true, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, "engines": { - "node": ">= 0.10" + "node": ">=16" } }, - "node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "license": "ISC" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">= 0.6" } }, - "node_modules/prr": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/psl": { - "version": "1.9.0", - "dev": true, - "license": "MIT" - }, - "node_modules/pug": { - "version": "2.0.4", + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "pug-code-gen": "^2.0.2", - "pug-filters": "^3.1.1", - "pug-lexer": "^4.1.0", - "pug-linker": "^3.0.6", - "pug-load": "^2.0.12", - "pug-parser": "^5.0.1", - "pug-runtime": "^2.0.5", - "pug-strip-comments": "^1.0.4" + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" } }, - "node_modules/pug-attrs": { - "version": "2.0.4", + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "constantinople": "^3.0.1", - "js-stringify": "^1.0.1", - "pug-runtime": "^2.0.5" + "engines": { + "node": ">= 0.8" } }, - "node_modules/pug-code-gen": { - "version": "2.0.3", + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", "dev": true, - "license": "MIT", - "optional": true, + "license": "ISC", "dependencies": { - "constantinople": "^3.1.2", - "doctypes": "^1.1.0", - "js-stringify": "^1.0.1", - "pug-attrs": "^2.0.4", - "pug-error": "^1.3.3", - "pug-runtime": "^2.0.5", - "void-elements": "^2.0.1", - "with": "^5.0.0" + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/pug-error": { - "version": "1.3.3", - "dev": true, - "license": "MIT", + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", "optional": true }, - "node_modules/pug-filters": { - "version": "3.1.1", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", "optional": true, - "dependencies": { - "clean-css": "^4.1.11", - "constantinople": "^3.0.1", - "jstransformer": "1.0.0", - "pug-error": "^1.3.3", - "pug-walk": "^1.1.8", - "resolve": "^1.1.6", - "uglify-js": "^2.6.1" + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/pug-filters/node_modules/camelcase": { - "version": "1.2.1", + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/pug-filters/node_modules/cliui": { - "version": "2.1.0", - "dev": true, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", "license": "ISC", "optional": true, "dependencies": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - } - }, - "node_modules/pug-filters/node_modules/source-map": { - "version": "0.5.7", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pug-filters/node_modules/uglify-js": { - "version": "2.8.29", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "source-map": "~0.5.1", - "yargs": "~3.10.0" - }, - "bin": { - "uglifyjs": "bin/uglifyjs" + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" }, "engines": { - "node": ">=0.8.0" - }, - "optionalDependencies": { - "uglify-to-browserify": "~1.0.0" + "node": ">=10" } }, - "node_modules/pug-filters/node_modules/wordwrap": { - "version": "0.0.2", - "dev": true, - "license": "MIT/X11", + "node_modules/gauge/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", "optional": true, "engines": { - "node": ">=0.4.0" + "node": ">=8" } }, - "node_modules/pug-filters/node_modules/yargs": { - "version": "3.10.0", - "dev": true, + "node_modules/gauge/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "optional": true, - "dependencies": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" + "engines": { + "node": ">=8" } }, - "node_modules/pug-lexer": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "character-parser": "^2.1.1", - "is-expression": "^3.0.0", - "pug-error": "^1.3.3" - } + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true }, - "node_modules/pug-linker": { - "version": "3.0.6", - "dev": true, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "optional": true, "dependencies": { - "pug-error": "^1.3.3", - "pug-walk": "^1.1.8" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/pug-load": { - "version": "2.0.12", - "dev": true, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "optional": true, "dependencies": { - "object-assign": "^4.1.0", - "pug-walk": "^1.1.8" + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/pug-parser": { - "version": "5.0.1", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "pug-error": "^1.3.3", - "token-stream": "0.0.1" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/pug-runtime": { + "node_modules/get-caller-file": { "version": "2.0.5", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/pug-strip-comments": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "pug-error": "^1.3.3" + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/pug-walk": { - "version": "1.1.8", + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "dev": true, "license": "MIT", - "optional": true - }, - "node_modules/punycode": { - "version": "2.3.1", - "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/q": { - "version": "1.5.1", + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/qjobs": { - "version": "1.2.0", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, "engines": { - "node": ">=0.9" + "node": ">= 0.4" } }, - "node_modules/qrcode": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "node_modules/git-raw-commits": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-5.0.1.tgz", + "integrity": "sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==", + "dev": true, "license": "MIT", "dependencies": { - "dijkstrajs": "^1.0.1", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" + "@conventional-changelog/git-client": "^2.6.0", + "meow": "^13.0.0" }, "bin": { - "qrcode": "bin/qrcode" + "git-raw-commits": "src/cli.js" }, "engines": { - "node": ">=10.13.0" + "node": ">=18" } }, - "node_modules/qrcode/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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", + "optional": true, "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/qrcode/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "is-glob": "^4.0.3" }, "engines": { - "node": ">=8" + "node": ">=10.13.0" } }, - "node_modules/qrcode/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "license": "MIT", + "optional": true, "dependencies": { - "p-locate": "^4.1.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=8" + "node": "*" } }, - "node_modules/qrcode/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/global-directory": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-5.0.0.tgz", + "integrity": "sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==", + "dev": true, "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "ini": "6.0.0" }, "engines": { - "node": ">=6" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/qrcode/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/qrcode/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, - "node_modules/qrcode/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/qrcode/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } + "optional": true }, - "node_modules/qs": { - "version": "6.11.0", + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "side-channel": "^1.0.4" + "function-bind": "^1.1.2" }, "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.4" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", + "node_modules/hono": { + "version": "4.12.26", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz", + "integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } }, - "node_modules/randombytes": { - "version": "2.1.0", + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "safe-buffer": "^5.1.0" + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/range-parser": { - "version": "1.2.1", + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "engines": { - "node": ">= 0.6" + "node": "20 || >=22" } }, - "node_modules/raw-body": { - "version": "2.5.2", + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "@exodus/bytes": "^1.6.0" }, "engines": { - "node": ">= 0.8" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.4.24", - "dev": true, + "node_modules/html2canvas": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.7.tgz", + "integrity": "sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "css-line-break": "1.1.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8.0.0" } }, - "node_modules/react-is": { - "version": "18.3.1", - "dev": true, - "license": "MIT" + "node_modules/html5-qrcode": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/html5-qrcode/-/html5-qrcode-2.3.8.tgz", + "integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==", + "license": "Apache-2.0" }, - "node_modules/read-cache": { - "version": "1.0.0", + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], "license": "MIT", "dependencies": { - "pify": "^2.3.0" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" } }, - "node_modules/read-cache/node_modules/pify": { - "version": "2.3.0", + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-package-json": { - "version": "7.0.1", - "license": "ISC", - "dependencies": { - "glob": "^10.2.2", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0" + "node": ">=0.12" }, - "engines": { - "node": "^16.14.0 || >=18.0.0" + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/read-package-json-fast": { - "version": "3.0.2", - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" }, - "node_modules/read-package-json/node_modules/glob": { - "version": "10.3.15", - "license": "ISC", + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": ">= 0.8" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/read-package-json/node_modules/hosted-git-info": { + "node_modules/http-proxy-agent": { "version": "7.0.2", - "license": "ISC", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", "dependencies": { - "lru-cache": "^10.0.1" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">= 14" } }, - "node_modules/read-package-json/node_modules/lru-cache": { - "version": "10.2.2", - "license": "ISC", + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", "engines": { - "node": "14 || >=16.14" + "node": ">= 14" } }, - "node_modules/read-package-json/node_modules/normalize-package-data": { - "version": "6.0.1", - "license": "BSD-2-Clause", + "node_modules/https-proxy-agent": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz", + "integrity": "sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==", + "dev": true, + "license": "MIT", "dependencies": { - "hosted-git-info": "^7.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" + "agent-base": "9.0.0", + "debug": "^4.3.4" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">= 20" } }, - "node_modules/read-pkg": { - "version": "3.0.0", + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, "license": "MIT", - "dependencies": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" + "bin": { + "husky": "bin.js" }, "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg/node_modules/hosted-git-info": { - "version": "2.8.9", - "dev": true, - "license": "ISC" - }, - "node_modules/read-pkg/node_modules/normalize-package-data": { - "version": "2.5.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" } }, - "node_modules/read-pkg/node_modules/path-type": { - "version": "3.0.0", + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dev": true, "license": "MIT", "dependencies": { - "pify": "^3.0.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.2", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/readable-stream": { - "version": "3.6.2", + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, "engines": { - "node": ">= 6" + "node": ">= 4" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "devOptional": true, - "license": "MIT", + "node_modules/ignore-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", + "dev": true, + "license": "ISC", "dependencies": { - "picomatch": "^2.2.1" + "minimatch": "^10.0.3" }, "engines": { - "node": ">=8.10.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "devOptional": true, + "node_modules/ignore-walk/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": "18 || 20 || >=22" } }, - "node_modules/rechoir": { - "version": "0.7.1", + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { - "resolve": "^1.9.0" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">= 0.10" + "node": "18 || 20 || >=22" } }, - "node_modules/reflect-metadata": { - "version": "0.2.2", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/regenerate": { - "version": "1.4.2", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.1", + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "regenerate": "^1.4.2" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=4" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "dev": true, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, - "node_modules/regenerator-transform": { - "version": "0.15.2", + "node_modules/immutable": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.6.tgz", + "integrity": "sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.4" - } + "license": "MIT" }, - "node_modules/regex-not": { - "version": "1.0.2", + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/regex-not/node_modules/extend-shallow": { - "version": "3.0.2", + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/regex-not/node_modules/is-extendable": { - "version": "1.0.1", - "dev": true, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, "engines": { - "node": ">=0.10.0" + "node": ">=0.8.19" } }, - "node_modules/regex-parser": { - "version": "2.3.0", - "dev": true, - "license": "MIT" + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.2", + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.6", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" - }, + "license": "ISC", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/regexpu-core": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", "engines": { - "node": ">=4" + "node": ">=12" } }, - "node_modules/regjsparser": { - "version": "0.9.1", + "node_modules/ip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } + "license": "MIT" }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "dev": true, - "bin": { - "jsesc": "bin/jsesc" + "license": "MIT", + "engines": { + "node": ">= 12" } }, - "node_modules/relateurl": { - "version": "0.2.7", + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" } }, - "node_modules/remove-trailing-separator": { - "version": "1.1.0", + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, - "license": "ISC" + "license": "MIT" }, - "node_modules/repeat-element": { - "version": "1.1.4", - "dev": true, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/repeat-string": { - "version": "1.6.1", + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/request": { - "version": "2.88.2", - "dev": true, - "license": "Apache-2.0", "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" + "get-east-asian-width": "^1.3.1" }, "engines": { - "node": ">= 6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/request/node_modules/form-data": { - "version": "2.3.3", - "dev": true, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "license": "MIT", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">= 0.12" + "node": ">=0.10.0" } }, - "node_modules/request/node_modules/qs": { - "version": "6.5.3", + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "engines": { - "node": ">=0.6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/require-directory": { - "version": "2.1.1", + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/require-from-string": { - "version": "2.0.2", + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "license": "ISC" + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" }, - "node_modules/requires-port": { - "version": "1.0.0", + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "dev": true, "license": "MIT" }, - "node_modules/resolve": { - "version": "1.22.8", + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" + "engines": { + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/resolve-dir": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" }, - "node_modules/resolve-from": { - "version": "5.0.0", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", "dev": true, + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, "license": "MIT", - "engines": { - "node": ">=8" + "bin": { + "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/resolve-pkg": { - "version": "2.0.0", + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", "dev": true, "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/panva" } }, - "node_modules/resolve-url": { - "version": "0.2.1", + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT" }, - "node_modules/resolve-url-loader": { - "version": "5.0.0", + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { - "adjust-sourcemap-loader": "^4.0.0", - "convert-source-map": "^1.7.0", - "loader-utils": "^2.0.0", - "postcss": "^8.2.14", - "source-map": "0.6.1" + "argparse": "^2.0.1" }, - "engines": { - "node": ">=12" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/resolve-url-loader/node_modules/loader-utils": { - "version": "2.0.4", + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", "dev": true, "license": "MIT", "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=8.9.0" + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, - "node_modules/resolve-url-loader/node_modules/source-map": { - "version": "0.6.1", + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "dev": true, - "license": "BSD-3-Clause", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=0.10.0" + "node": "20 || >=22" } }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "license": "MIT", + "node_modules/jsdom/node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "tldts": "^7.0.5" }, "engines": { - "node": ">=8" - } - }, - "node_modules/ret": { - "version": "0.1.15", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "license": "MIT", - "engines": { - "node": ">= 4" + "node": ">=16" } }, - "node_modules/reusify": { - "version": "1.0.4", + "node_modules/jsdom/node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "dev": true, "license": "MIT", "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" + "node": ">=20.18.1" } }, - "node_modules/rfdc": { - "version": "1.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/right-align": { - "version": "0.1.3", + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "align-text": "^0.1.1" + "bin": { + "jsesc": "bin/jsesc" }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "devOptional": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" }, - "node_modules/rollup": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", - "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "node_modules/json-parse-even-better-errors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.0", - "@rollup/rollup-android-arm64": "4.60.0", - "@rollup/rollup-darwin-arm64": "4.60.0", - "@rollup/rollup-darwin-x64": "4.60.0", - "@rollup/rollup-freebsd-arm64": "4.60.0", - "@rollup/rollup-freebsd-x64": "4.60.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", - "@rollup/rollup-linux-arm-musleabihf": "4.60.0", - "@rollup/rollup-linux-arm64-gnu": "4.60.0", - "@rollup/rollup-linux-arm64-musl": "4.60.0", - "@rollup/rollup-linux-loong64-gnu": "4.60.0", - "@rollup/rollup-linux-loong64-musl": "4.60.0", - "@rollup/rollup-linux-ppc64-gnu": "4.60.0", - "@rollup/rollup-linux-ppc64-musl": "4.60.0", - "@rollup/rollup-linux-riscv64-gnu": "4.60.0", - "@rollup/rollup-linux-riscv64-musl": "4.60.0", - "@rollup/rollup-linux-s390x-gnu": "4.60.0", - "@rollup/rollup-linux-x64-gnu": "4.60.0", - "@rollup/rollup-linux-x64-musl": "4.60.0", - "@rollup/rollup-openbsd-x64": "4.60.0", - "@rollup/rollup-openharmony-arm64": "4.60.0", - "@rollup/rollup-win32-arm64-msvc": "4.60.0", - "@rollup/rollup-win32-ia32-msvc": "4.60.0", - "@rollup/rollup-win32-x64-gnu": "4.60.0", - "@rollup/rollup-win32-x64-msvc": "4.60.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-async": { - "version": "3.0.0", - "license": "MIT", "engines": { - "node": ">=0.12.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/run-parallel": { - "version": "1.2.0", + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } + "license": "MIT" }, - "node_modules/rx": { - "version": "2.3.24", - "dev": true + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "license": "MIT" }, - "node_modules/safe-array-concat": { - "version": "1.1.2", + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" + "bin": { + "json5": "lib/cli.js" }, "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6" } }, - "node_modules/safe-buffer": { - "version": "5.1.2", + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, "license": "MIT" }, - "node_modules/safe-json-parse": { - "version": "1.0.1", - "dev": true - }, - "node_modules/safe-regex": { - "version": "1.1.0", + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", "dev": true, - "license": "MIT", + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", "dependencies": { - "ret": "~0.1.10" + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" } }, - "node_modules/safe-regex-test": { - "version": "1.0.3", - "dev": true, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-regex": "^1.1.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "json-buffer": "3.0.1" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "license": "MIT" - }, - "node_modules/safevalues": { - "version": "0.3.4", - "license": "Apache-2.0" - }, - "node_modules/sass": { - "version": "1.77.1", - "dev": true, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "license": "MIT", "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, "engines": { - "node": ">=14.0.0" + "node": ">= 0.8.0" } }, - "node_modules/sass-loader": { - "version": "14.1.1", - "dev": true, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "license": "MIT", "dependencies": { - "neo-async": "^2.6.2" + "immediate": "~3.0.5" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" }, "engines": { - "node": ">= 18.12.0" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://opencollective.com/parcel" }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "webpack": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/saucelabs": { - "version": "1.5.0", - "dev": true, - "dependencies": { - "https-proxy-agent": "^2.2.1" + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "*" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/saucelabs/node_modules/agent-base": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "es6-promisify": "^5.0.0" + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 4.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/saucelabs/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/saucelabs/node_modules/https-proxy-agent": { - "version": "2.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 4.5.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/sax": { - "version": "1.3.0", - "dev": true, - "license": "ISC" + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/schema-utils": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 12.13.0" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://opencollective.com/parcel" } }, - "node_modules/select-hose": { - "version": "2.0.0", + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true, "license": "MIT" }, - "node_modules/selenium-webdriver": { - "version": "3.6.0", + "node_modules/listr2": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", + "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "jszip": "^3.1.3", - "rimraf": "^2.5.4", - "tmp": "0.0.30", - "xml2js": "^0.4.17" + "cli-truncate": "^5.2.0", + "eventemitter3": "^5.0.4", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^10.0.0" }, "engines": { - "node": ">= 6.9.0" + "node": ">=22.13.0" } }, - "node_modules/selenium-webdriver/node_modules/rimraf": { - "version": "2.7.1", + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" + "license": "MIT", + "engines": { + "node": ">=12" }, - "bin": { - "rimraf": "bin.js" + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/selenium-webdriver/node_modules/tmp": { - "version": "0.0.30", + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "dev": true, - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.1" - }, - "engines": { - "node": ">=0.4.0" - } + "license": "MIT" }, - "node_modules/selfsigned": { - "version": "2.4.1", + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", + "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=10" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/semver": { - "version": "7.6.0", - "license": "ISC", + "node_modules/lmdb": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.4.tgz", + "integrity": "sha512-9FKQA6G1MMtqNxfxvSBNXD/axeG2QRjYbNh0/ykRL5xYcRbCm2vXq7B9bhc7nSuKdHzr8/BHIwfPuYYH1UsXXw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, "dependencies": { - "lru-cache": "^6.0.0" + "@harperfast/extended-iterable": "^1.0.3", + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" }, "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" + "download-lmdb-prebuilds": "bin/download-prebuilds.js" }, - "engines": { - "node": ">=10" + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.5.4", + "@lmdb/lmdb-darwin-x64": "3.5.4", + "@lmdb/lmdb-linux-arm": "3.5.4", + "@lmdb/lmdb-linux-arm64": "3.5.4", + "@lmdb/lmdb-linux-x64": "3.5.4", + "@lmdb/lmdb-win32-arm64": "3.5.4", + "@lmdb/lmdb-win32-x64": "3.5.4" } }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/send": { - "version": "0.18.0", + "node_modules/local-pkg": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", + "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", "dev": true, "license": "MIT", "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" }, "engines": { - "node": ">= 0.8.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "dev": true, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "license": "MIT", "dependencies": { - "ms": "2.0.0" + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "dev": true, "license": "MIT" }, - "node_modules/send/node_modules/mime": { - "version": "1.6.0", + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", "dev": true, "license": "MIT", - "bin": { - "mime": "cli.js" + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" }, "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/send/node_modules/on-finished": { - "version": "2.4.1", + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, "license": "MIT", "dependencies": { - "ee-first": "1.1.1" + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/send/node_modules/statuses": { - "version": "2.0.1", + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } + "license": "MIT" }, - "node_modules/serve-index": { - "version": "1.9.1", + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dev": true, "license": "MIT", "dependencies": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/serve-index/node_modules/debug": { - "version": "2.6.9", + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "dev": true, - "license": "MIT", + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "dev": true, - "license": "ISC" - }, - "node_modules/serve-index/node_modules/ms": { - "version": "2.0.0", - "dev": true, + "node_modules/lottie-web": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.13.0.tgz", + "integrity": "sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==", "license": "MIT" }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, - "license": "ISC" + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } }, - "node_modules/serve-static": { - "version": "1.15.0", - "dev": true, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/set-blocking": { - "version": "2.0.0", + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, "license": "ISC" }, - "node_modules/set-function-length": { - "version": "1.2.2", - "license": "MIT", + "node_modules/make-fetch-happen": { + "version": "15.0.6", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.6.tgz", + "integrity": "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==", + "dev": true, + "license": "ISC", "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" }, "engines": { - "node": ">= 0.4" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/set-function-name": { - "version": "2.0.2", - "dev": true, + "node_modules/marked": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" + "bin": { + "marked": "bin/marked.js" }, "engines": { - "node": ">= 0.4" + "node": ">= 20" } }, - "node_modules/set-value": { - "version": "2.0.1", + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "license": "MIT" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true, - "license": "ISC" + "license": "CC0-1.0" }, - "node_modules/shallow-clone": { - "version": "3.0.1", + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "dev": true, "license": "MIT", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, "engines": { - "node": ">=8" + "node": ">= 0.8" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "license": "MIT", + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, "engines": { - "node": ">=8" + "node": ">= 0.10.0" } }, - "node_modules/shell-quote": { - "version": "1.8.1", + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/shelljs": { - "version": "0.3.0", - "dev": true, - "license": "BSD*", - "bin": { - "shjs": "bin/shjs" - }, "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/showdown": { - "version": "1.9.1", - "license": "BSD-3-Clause", - "dependencies": { - "yargs": "^14.2" + "node": ">=18" }, - "bin": { - "showdown": "bin/showdown.js" - } - }, - "node_modules/showdown/node_modules/ansi-regex": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/showdown/node_modules/cliui": { - "version": "5.0.0", - "license": "ISC", - "dependencies": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/showdown/node_modules/emoji-regex": { - "version": "7.0.3", - "license": "MIT" - }, - "node_modules/showdown/node_modules/find-up": { - "version": "3.0.0", + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, "engines": { - "node": ">=6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/showdown/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 0.6" } }, - "node_modules/showdown/node_modules/locate-path": { - "version": "3.0.0", + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">=6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/showdown/node_modules/p-limit": { - "version": "2.3.0", + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, "engines": { - "node": ">=6" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/showdown/node_modules/p-locate": { - "version": "3.0.0", - "license": "MIT", + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", "dependencies": { - "p-limit": "^2.0.0" + "brace-expansion": "^2.0.2" }, "engines": { - "node": ">=6" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/showdown/node_modules/path-exists": { - "version": "3.0.0", - "license": "MIT", + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=4" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/showdown/node_modules/string-width": { - "version": "3.1.0", - "license": "MIT", + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "minipass": "^7.0.3" }, "engines": { - "node": ">=6" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/showdown/node_modules/strip-ansi": { - "version": "5.2.0", + "node_modules/minipass-fetch": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^4.1.0" + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" }, "engines": { - "node": ">=6" + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" } }, - "node_modules/showdown/node_modules/wrap-ansi": { - "version": "5.1.0", - "license": "MIT", + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" + "minipass": "^3.0.0" }, "engines": { - "node": ">=6" + "node": ">= 8" } }, - "node_modules/showdown/node_modules/y18n": { - "version": "4.0.3", - "license": "ISC" - }, - "node_modules/showdown/node_modules/yargs": { - "version": "14.2.3", - "license": "MIT", + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", "dependencies": { - "cliui": "^5.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^15.0.1" + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/showdown/node_modules/yargs-parser": { - "version": "15.0.3", + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, "license": "ISC", "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.0.6", + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "yallist": "^4.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/signal-exit": { - "version": "3.0.7", + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, "license": "ISC" }, - "node_modules/sigstore": { - "version": "2.3.0", - "license": "Apache-2.0", + "node_modules/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "dev": true, + "license": "ISC", "dependencies": { - "@sigstore/bundle": "^2.3.1", - "@sigstore/core": "^1.0.0", - "@sigstore/protobuf-specs": "^0.3.1", - "@sigstore/sign": "^2.3.0", - "@sigstore/tuf": "^2.3.1", - "@sigstore/verify": "^1.2.0" + "minipass": "^7.1.2" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">=8" } }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, "license": "MIT", - "optional": true + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } }, - "node_modules/simple-fmt": { - "version": "0.1.0", + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", "dev": true, - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", "license": "MIT", - "optional": true, "dependencies": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" } }, - "node_modules/simple-is": { - "version": "0.2.0", + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "dev": true, "license": "MIT" }, - "node_modules/slash": { - "version": "3.0.0", + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "license": "MIT", "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" + "node": "*" } }, - "node_modules/snapdragon": { - "version": "0.8.2", - "dev": true, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "license": "MIT", "dependencies": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "engines": { - "node": ">=0.10.0" + "dompurify": "3.2.7", + "marked": "14.0.0" } }, - "node_modules/snapdragon-node": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" + "node_modules/monaco-editor/node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" } }, - "node_modules/snapdragon-node/node_modules/define-property": { - "version": "1.0.0", - "dev": true, + "node_modules/monaco-editor/node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" + "bin": { + "marked": "bin/marked.js" }, "engines": { - "node": ">=0.10.0" + "node": ">= 18" } }, - "node_modules/snapdragon-node/node_modules/is-descriptor": { - "version": "1.0.3", + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "dev": true, "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - }, "engines": { - "node": ">= 0.4" + "node": ">=10" } }, - "node_modules/snapdragon-util": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^3.2.0" - }, - "engines": { - "node": ">=0.10.0" - } + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, - "node_modules/snapdragon-util/node_modules/kind-of": { - "version": "3.2.2", + "node_modules/msgpackr": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.12.1.tgz", + "integrity": "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ==", "dev": true, "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" } }, - "node_modules/snapdragon/node_modules/debug": { - "version": "2.6.9", + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "optional": true, "dependencies": { - "ms": "2.0.0" + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" } }, - "node_modules/snapdragon/node_modules/ms": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/snapdragon/node_modules/source-map": { - "version": "0.5.7", + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", "dev": true, - "license": "BSD-3-Clause", + "license": "ISC", "engines": { - "node": ">=0.10.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/socket.io": { - "version": "4.7.5", - "dev": true, + "node_modules/nan": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz", + "integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==", + "license": "MIT", + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.5.2", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">=10.2.0" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/socket.io-adapter": { - "version": "2.5.4", + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, "license": "MIT", - "dependencies": { - "debug": "~4.3.4", - "ws": "~8.11.0" + "engines": { + "node": ">= 0.6" } }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "dev": true, + "node_modules/ng-flex-layout": { + "version": "17.3.7-beta.1", + "resolved": "https://registry.npmjs.org/ng-flex-layout/-/ng-flex-layout-17.3.7-beta.1.tgz", + "integrity": "sha512-MTjlQUldB/hEsn0DY/RoY2SKBQoyaKltlOOFjCjH2OfcYA3COHhkJS3PZe9NAjR7TBDb9Ve989m18bSucRzACQ==", "license": "MIT", "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" + "tslib": "^2.3.0" }, - "engines": { - "node": ">=10.0.0" + "peerDependencies": { + "@angular/cdk": ">=17.0.0", + "@angular/common": ">=17.0.0", + "@angular/core": ">=17.0.0", + "@angular/platform-browser": ">=17.0.0", + "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/sockjs": { - "version": "0.3.24", - "dev": true, + "node_modules/ng2-pdf-viewer": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/ng2-pdf-viewer/-/ng2-pdf-viewer-10.2.2.tgz", + "integrity": "sha512-GaKAvF0nXAiR9U4LFWuT54MM9nzp0ie8GGscp34W+lFsSOXdlwS0iFx5UPuVlODRm3YEUKx6xcK5oaJeBq0SAw==", "license": "MIT", "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" + "pdfjs-dist": "^3.11.174", + "tslib": "^2.3.0" } }, - "node_modules/sockjs/node_modules/faye-websocket": { - "version": "0.11.4", - "dev": true, - "license": "Apache-2.0", + "node_modules/ngx-entity-service": { + "version": "0.0.44", + "resolved": "https://registry.npmjs.org/ngx-entity-service/-/ngx-entity-service-0.0.44.tgz", + "integrity": "sha512-FmSYKulHJxILKzLkBhXjNJsMdd55Slu11/TLg89GJZq3guBBD8RDsxHtbN1mKku5+Dp8i8H6vNSqJ3bwsyYwVw==", "dependencies": { - "websocket-driver": ">=0.5.1" + "tslib": "^2.3.0" }, - "engines": { - "node": ">=0.8.0" + "peerDependencies": { + "@angular/common": "^13.0 || ^14.0 || ^15.0 || ^16.0 || ^17 || ^18 || ^19 || ^20 || ^21 || ^22", + "@angular/core": "^13.0 || ^14.0 || ^15.0 || ^16.0 || ^17 || ^18 || ^19 || ^20 || ^21 || ^22" } }, - "node_modules/sockjs/node_modules/uuid": { - "version": "8.3.2", - "dev": true, + "node_modules/ngx-lottie": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/ngx-lottie/-/ngx-lottie-22.0.0.tgz", + "integrity": "sha512-nsKk2fJifG2H4AWNYanF/wfJ3FsWzClkxkcD7KnzD6qpIwDrDU+89Tguv8h9i3Q9rn4r4awxvewYTJkrrIsj1g==", "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" + "dependencies": { + "@scarf/scarf": "^1.1.1", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=22", + "lottie-web": ">=5.9.2" } }, - "node_modules/socks": { - "version": "2.8.3", + "node_modules/ngx-monaco-editor-v2-alternative": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/ngx-monaco-editor-v2-alternative/-/ngx-monaco-editor-v2-alternative-22.0.0.tgz", + "integrity": "sha512-JB5eSWdtDhJF5hhuDUQQyABfx+1JHHWFwvhdn/R74twxE2NcCODWqgPFNKYLZhWKiPIG/w6TlpsyfS68wRNw0g==", "license": "MIT", "dependencies": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" + "tslib": "^2.4.0" }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" + "peerDependencies": { + "@angular/common": "^22.0.0", + "@angular/core": "^22.0.0", + "monaco-editor": "^0.55.1" } }, - "node_modules/socks-proxy-agent": { - "version": "8.0.3", + "node_modules/ngx-skeleton-loader": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/ngx-skeleton-loader/-/ngx-skeleton-loader-13.0.0.tgz", + "integrity": "sha512-IEJh0RbrQRA1MHWdFTpLtbibk6KulRno+YqibsCCankr3FVEl3JwspcbHhxQuWahC8EuahJLrcBPLYxFyF/Swg==", "license": "MIT", "dependencies": { - "agent-base": "^7.1.1", - "debug": "^4.3.4", - "socks": "^2.7.1" + "tslib": "^2.0.0" }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/source-map": { - "version": "0.7.4", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 8" + "peerDependencies": { + "@angular/common": ">=19.0.0", + "@angular/core": ">=19.0.0" } }, - "node_modules/source-map-js": { - "version": "1.2.0", + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } + "license": "MIT", + "optional": true }, - "node_modules/source-map-loader": { - "version": "5.0.0", - "dev": true, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", + "optional": true, "dependencies": { - "iconv-lite": "^0.6.3", - "source-map-js": "^1.0.2" + "whatwg-url": "^5.0.0" }, "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": "4.x || >=6.0.0" }, "peerDependencies": { - "webpack": "^5.72.1" + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "node_modules/source-map-resolve": { - "version": "0.5.3", - "dev": true, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT", - "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } + "optional": true }, - "node_modules/source-map-support": { - "version": "0.5.21", - "dev": true, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "license": "MIT", + "optional": true, "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, "engines": { - "node": ">=0.10.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/source-map-url": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "node_modules/spawn-command": { - "version": "0.0.2-1", + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", "dev": true, - "license": "MIT" - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", "license": "MIT", + "optional": true, "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" } }, - "node_modules/spdx-license-ids": { - "version": "3.0.17", - "license": "CC0-1.0" + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } }, - "node_modules/spdy": { - "version": "4.0.2", + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" }, "engines": { - "node": ">=6.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/spdy-transport": { - "version": "3.0.0", + "node_modules/node-releases": { + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", + "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" + "engines": { + "node": ">=18" } }, - "node_modules/split-string": { - "version": "3.1.0", + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "extend-shallow": "^3.0.0" + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" }, "engines": { - "node": ">=0.10.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/split-string/node_modules/extend-shallow": { - "version": "3.0.2", + "node_modules/npm-bundled": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" + "npm-normalize-package-bin": "^5.0.0" }, "engines": { - "node": ">=0.10.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/split-string/node_modules/is-extendable": { - "version": "1.0.1", + "node_modules/npm-install-checks": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", + "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "is-plain-object": "^2.0.4" + "semver": "^7.1.1" }, "engines": { - "node": ">=0.10.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", + "node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", "dev": true, - "license": "BSD-3-Clause" + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } }, - "node_modules/sshpk": { - "version": "1.18.0", + "node_modules/npm-package-arg": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", + "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" + "hosted-git-info": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^7.0.0" }, "engines": { - "node": ">=0.10.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/sshpk/node_modules/jsbn": { - "version": "0.1.1", + "node_modules/npm-packlist": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", "dev": true, - "license": "MIT" - }, - "node_modules/ssri": { - "version": "10.0.6", "license": "ISC", "dependencies": { - "minipass": "^7.0.3" + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/stable": { - "version": "0.1.8", - "dev": true, - "license": "MIT" - }, - "node_modules/static-extend": { - "version": "0.1.2", + "node_modules/npm-pick-manifest": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", + "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" }, "engines": { - "node": ">=0.10.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/statuses": { - "version": "1.5.0", + "node_modules/npm-registry-fetch": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", "dev": true, - "license": "MIT", + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^4.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" + }, "engines": { - "node": ">= 0.6" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/streamroller": { - "version": "3.1.5", + "node_modules/npm-run-all2": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-9.0.2.tgz", + "integrity": "sha512-+dd4SO2jAlLE06OzmJKzIe6QvvjXezcbmobnh8usR0a8BzQCABTdqTXqVPji0ICOhSQpIIrkGd7IzNl5iDaRSA==", "dev": true, "license": "MIT", "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "fs-extra": "^8.1.0" + "ansi-styles": "^6.2.1", + "cross-spawn": "^7.0.6", + "memorystream": "^0.3.1", + "picomatch": "^4.0.2", + "pidtree": "^1.0.0", + "read-package-json-fast": "^6.0.0", + "shell-quote": "^1.8.4", + "which": "^7.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "npm-run-all2": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" }, "engines": { - "node": ">=8.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0", + "npm": ">= 10" } }, - "node_modules/streamroller/node_modules/fs-extra": { - "version": "8.1.0", + "node_modules/npm-run-all2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, "engines": { - "node": ">=6 <7 || >=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/streamroller/node_modules/jsonfile": { + "node_modules/npm-run-all2/node_modules/isexe": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" } }, - "node_modules/streamroller/node_modules/universalify": { - "version": "0.1.2", + "node_modules/npm-run-all2/node_modules/which": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-7.0.0.tgz", + "integrity": "sha512-RancgH2dmbLdHl6LRhEqvklWMgl/Hdnun0Y90KhBOLkMefg8Qa7/Zel8Sm+8HEcP6DEjzsWzpkuBQEZok58isA==", "dev": true, - "license": "MIT", + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, "engines": { - "node": ">= 4.0.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "license": "MIT", + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, "dependencies": { - "safe-buffer": "~5.2.0" + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/string-template": { - "version": "0.2.1", - "dev": true - }, - "node_modules/string-width": { - "version": "4.2.3", - "license": "MIT", + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "boolbase": "^1.0.0" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "devOptional": true, "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/string.prototype.trim": { - "version": "1.2.9", + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" - }, "engines": { "node": ">= 0.4" }, @@ -21165,2267 +12819,2552 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/string.prototype.trimend": { - "version": "1.0.8", + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=12.20.0" } }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "ee-first": "1.1.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.8" } }, - "node_modules/stringmap": { - "version": "0.2.2", - "dev": true, - "license": "MIT" + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } }, - "node_modules/stringset": { - "version": "0.2.1", + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, - "license": "MIT" - }, - "node_modules/strip-ansi": { - "version": "6.0.1", "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "mimic-function": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { - "node": ">=8" + "node": ">= 0.8.0" } }, - "node_modules/strip-bom": { - "version": "3.0.0", + "node_modules/ora": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.0.tgz", + "integrity": "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==", "dev": true, "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.2", + "string-width": "^8.1.0" + }, "engines": { - "node": ">=4" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-final-newline": { - "version": "2.0.0", + "node_modules/ordered-binary": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", + "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" - } + "optional": true }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "dev": true, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strong-log-transformer": { - "version": "2.1.0", - "dev": true, - "license": "Apache-2.0", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", "dependencies": { - "duplexer": "^0.1.1", - "minimist": "^1.2.0", - "through": "^2.3.4" - }, - "bin": { - "sl-log-transformer": "bin/sl-log-transformer.js" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/sucrase": { - "version": "3.35.0", + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "dev": true, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=6" } }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.3.15", + "node_modules/pacote": { + "version": "21.5.1", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.5.1.tgz", + "integrity": "sha512-KvcJ9iy3crysCsgqc4+PknH/w6jkrp8JN36mpZBPwNaDRwTfMZD37YzRazNstiZUOhuF5pno9f78n9mEJBavwg==", "dev": true, "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.11.0" + "@gar/promise-retry": "^1.0.0", + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" }, "bin": { - "glob": "dist/esm/bin.mjs" + "pacote": "bin/index.js" }, "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/sucrase/node_modules/lines-and-columns": { - "version": "1.2.4", - "dev": true, - "license": "MIT" + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" }, - "node_modules/supports-color": { - "version": "5.5.0", + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "callsites": "^3.0.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6" } }, - "node_modules/symbol-observable": { - "version": "4.0.0", + "node_modules/parse-imports-exports": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", + "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10" + "dependencies": { + "parse-statements": "1.0.11" } }, - "node_modules/synckit": { - "version": "0.8.8", + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": ">=8" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tailwindcss": { - "version": "3.3.7", + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-statements": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", + "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", "license": "MIT", "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" + "entities": "^8.0.0" }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/tailwindcss/node_modules/glob-parent": { - "version": "6.0.2", + "node_modules/parse5-html-rewriting-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.1.tgz", + "integrity": "sha512-NaRku2aMpUN1Sh1Gyk1KWUh2A7EJx2c6qYzvwsPtqhoHoaURshdrceYK3LunVCm3WHhm6FS7Vcczbvdh3/UIVw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.3" + "entities": "^8.0.0", + "parse5": "^8.0.0", + "parse5-sax-parser": "^8.0.0" }, - "engines": { - "node": ">=10.13.0" + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/tapable": { - "version": "2.2.1", + "node_modules/parse5-html-rewriting-stream/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "6.2.1", - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "node": ">=20.19.0" }, - "engines": { - "node": ">=10" + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/tar-stream": { - "version": "2.2.0", + "node_modules/parse5-sax-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", + "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", "dev": true, "license": "MIT", "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" + "parse5": "^8.0.0" }, - "engines": { - "node": ">=6" + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/tar/node_modules/fs-minipass": { - "version": "2.1.0", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, + "node_modules/parse5/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "license": "BSD-2-Clause", "engines": { - "node": ">= 8" + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.8" } }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "license": "ISC", + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/tar/node_modules/mkdirp": { - "version": "1.0.4", + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, + "optional": true, "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, - "node_modules/terser": { - "version": "5.29.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=8" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.10", + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">= 10.13.0" + "node": "18 || 20 || >=22" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, - "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "dev": true, "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "dev": true, + "node_modules/path2d-polyfill": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", + "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, + "optional": true, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=8" } }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, - "node_modules/test-exclude": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, + "node_modules/pdfjs-dist": { + "version": "3.11.174", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", + "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", + "license": "Apache-2.0", "engines": { - "node": ">=8" + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d-polyfill": "^2.0.1" } }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, + "node_modules/pdfjs-dist/node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, "license": "MIT", + "optional": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" } }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", + "node_modules/pdfjs-dist/node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "license": "MIT", + "optional": true, "dependencies": { - "brace-expansion": "^1.1.7" + "mimic-response": "^2.0.0" }, "engines": { - "node": "*" + "node": ">=8" } }, - "node_modules/text-segmentation": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", - "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "node_modules/pdfjs-dist/node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", "license": "MIT", - "dependencies": { - "utrie": "^1.0.2" + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/text-table": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/thenify": { - "version": "3.3.1", - "dev": true, + "node_modules/pdfjs-dist/node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", "license": "MIT", + "optional": true, "dependencies": { - "any-promise": "^1.0.0" + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" } }, - "node_modules/thenify-all": { - "version": "1.6.0", - "dev": true, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, "engines": { - "node": ">=0.8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/through": { - "version": "2.3.8", - "dev": true, - "license": "MIT" - }, - "node_modules/thunky": { - "version": "1.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/tiny-lr": { - "version": "1.1.1", + "node_modules/pidtree": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-1.0.0.tgz", + "integrity": "sha512-avfAvjB9Dd0wdj3rjJX//yS+G79OO0KrS5pJHFJENjYGX6N4SMgEDBBI/yFy0lloOYSaC6XQxzpOAMPfSYFV/Q==", "dev": true, "license": "MIT", - "dependencies": { - "body": "^5.1.0", - "debug": "^3.1.0", - "faye-websocket": "~0.10.0", - "livereload-js": "^2.3.0", - "object-assign": "^4.1.0", - "qs": "^6.4.0" + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=18" } }, - "node_modules/tiny-lr/node_modules/debug": { - "version": "3.2.7", + "node_modules/piscina": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.2.0.tgz", + "integrity": "sha512-DszUCKeVN/5G5QKo6jAVHL8fmKnkJvQ0ACiVgY7YGCq3TUB2oznAOayvZPIAdEThvhczkXR+qm3IHsNXpFCYfA==", "dev": true, "license": "MIT", - "dependencies": { - "ms": "^2.1.1" + "engines": { + "node": ">=20.x" + }, + "optionalDependencies": { + "@napi-rs/nice": "^1.0.4" } }, - "node_modules/tinyexec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", - "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=16.20.0" } }, - "node_modules/tmp": { - "version": "0.2.3", + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=14.14" + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "dev": true, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">=10.13.0" } }, - "node_modules/to-object-path": { - "version": "0.3.0", + "node_modules/positioning": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/positioning/-/positioning-3.0.1.tgz", + "integrity": "sha512-cqg00fwFtEu14YwlLUuvFih5ztTd9RYUguJA55lWjeIGFQTpik7ca+TMU87YhAgwWjyjcGIG6l80eUh7X6uHog==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "kind-of": "^3.0.2" + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=0.10.0" + "node": "^10 || ^12 || >=14" } }, - "node_modules/to-object-path/node_modules/kind-of": { - "version": "3.2.2", + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", "dev": true, + "license": "MIT" + }, + "node_modules/postcss-nested": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-7.0.2.tgz", + "integrity": "sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "is-buffer": "^1.1.5" + "postcss-selector-parser": "^7.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" } }, - "node_modules/to-regex": { - "version": "3.0.2", + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "dependencies": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "devOptional": true, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, "engines": { - "node": ">=8.0" + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" } }, - "node_modules/to-regex/node_modules/define-property": { - "version": "2.0.2", + "node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", "dev": true, "license": "MIT", "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/to-regex/node_modules/extend-shallow": { - "version": "3.0.2", + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.8.0" } }, - "node_modules/to-regex/node_modules/is-descriptor": { - "version": "1.0.3", + "node_modules/prettier": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", "dev": true, "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" + "bin": { + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">= 0.4" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/to-regex/node_modules/is-extendable": { + "node_modules/prettier-linter-helpers": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", "dev": true, "license": "MIT", "dependencies": { - "is-plain-object": "^2.0.4" + "fast-diff": "^1.1.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=6.0.0" } }, - "node_modules/toidentifier": { - "version": "1.0.1", + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, - "license": "MIT", + "license": "ISC", "engines": { - "node": ">=0.6" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/token-stream": { - "version": "0.0.1", - "dev": true, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "license": "MIT", - "optional": true + "engines": { + "node": ">=0.4.0" + } }, - "node_modules/tough-cookie": { - "version": "2.5.0", + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" }, "engines": { - "node": ">=0.8" + "node": ">= 0.10" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT", - "optional": true + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" }, - "node_modules/tree-kill": { - "version": "1.2.2", - "dev": true, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, "bin": { - "tree-kill": "cli.js" + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" } }, - "node_modules/tryor": { - "version": "0.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/ts-api-utils": { - "version": "1.3.0", - "dev": true, + "node_modules/qrcode/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" + "node": ">=8" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/ts-md5": { - "version": "1.3.1", - "license": "MIT", - "engines": { - "node": ">=12" + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" } }, - "node_modules/ts-node": { - "version": "10.9.2", - "dev": true, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "license": "MIT", "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/ts-node/node_modules/arg": { - "version": "4.1.3", - "dev": true, - "license": "MIT" + "engines": { + "node": ">=8" + } }, - "node_modules/ts-node/node_modules/diff": { - "version": "4.0.2", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/qrcode/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", "engines": { - "node": ">=0.3.1" + "node": ">=8" } }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "dev": true, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "license": "MIT", "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.2", - "dev": true, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "license": "MIT", "dependencies": { - "minimist": "^1.2.0" + "p-try": "^2.0.0" }, - "bin": { - "json5": "lib/cli.js" + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/tslib": { - "version": "2.6.2", - "license": "0BSD" + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/tuf-js": { - "version": "2.2.1", + "node_modules/qrcode/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "@tufjs/models": "2.0.1", - "debug": "^4.3.4", - "make-fetch-happen": "^13.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">=8" } }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "dev": true, - "license": "Apache-2.0", + "node_modules/qrcode/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", "dependencies": { - "safe-buffer": "^5.0.1" + "ansi-regex": "^5.0.1" }, "engines": { - "node": "*" + "node": ">=8" } }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "dev": true, - "license": "Unlicense" + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" }, - "node_modules/type-check": { - "version": "0.4.0", - "dev": true, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "license": "MIT", "dependencies": { - "prelude-ls": "^1.2.1" + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" }, "engines": { - "node": ">= 0.8.0" + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" } }, - "node_modules/type-fest": { - "version": "0.21.3", - "license": "(MIT OR CC0-1.0)", + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, "engines": { - "node": ">=10" + "node": ">=0.6" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/type-is": { - "version": "1.6.18", + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true, "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, "engines": { "node": ">= 0.6" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.2", + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.10" } }, - "node_modules/typed-array-byte-length": { - "version": "1.0.1", + "node_modules/read-package-json-fast": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-6.0.0.tgz", + "integrity": "sha512-PNaGjoCnw9DBA2Kl8D+8po957z778q/HOPuY2u3Bkw/JO3eC8MDx7jn/PgMtSgpcBbs+6UOjDbwReGpXmRvs0g==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "json-parse-even-better-errors": "^6.0.0", + "npm-normalize-package-bin": "^6.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.2", + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-6.0.0.tgz", + "integrity": "sha512-2/8adwnK1/+Fdjyts4r6wSpfANWw8zdNhU9U/Llk59c6O+DjSisPWPykwoL8gZmocP9Dy64S7oie2g+Mia123A==", "dev": true, "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, - "node_modules/typed-array-length": { - "version": "1.0.6", + "node_modules/read-package-json-fast/node_modules/npm-normalize-package-bin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-6.0.0.tgz", + "integrity": "sha512-tdt4aFn9QamlhdN3HV2D2ccpBwO5/fyjjbXUxYA6uBjyekMZcZvDq0aSj9t5Jo+tih6AYFnt/cuIRn9013e0Uw==", "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" - }, + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">= 20.19.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/typed-assert": { - "version": "1.0.9", + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "dev": true, - "license": "MIT" + "license": "Apache-2.0" }, - "node_modules/typescript": { - "version": "5.2.2", + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, + "license": "MIT", "engines": { - "node": ">=14.17" + "node": ">=0.10.0" } }, - "node_modules/ua-parser-js": { - "version": "0.7.37", + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], "license": "MIT", "engines": { - "node": "*" + "node": ">=8" } }, - "node_modules/uglify-js": { - "version": "3.4.10", + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", "dependencies": { - "commander": "~2.19.0", - "source-map": "~0.6.1" + "@types/estree": "1.0.9" }, "bin": { - "uglifyjs": "bin/uglifyjs" + "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=0.8.0" + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" } }, - "node_modules/uglify-js/node_modules/commander": { - "version": "2.19.0", - "dev": true, - "license": "MIT" - }, - "node_modules/uglify-js/node_modules/source-map": { - "version": "0.6.1", + "node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/uglify-to-browserify": { - "version": "1.0.2", + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "optional": true + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/unbox-primitive": { - "version": "1.0.2", + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/unc-path-regex": { - "version": "0.1.2", - "dev": true, - "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 18" } }, - "node_modules/underscore": { - "version": "1.13.6", - "dev": true, - "license": "MIT" + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" }, - "node_modules/underscore.string": { - "version": "2.3.3", - "engines": { - "node": "*" + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" } }, - "node_modules/undici": { - "version": "6.11.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0" - } + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" }, - "node_modules/undici-types": { - "version": "5.26.5", - "dev": true, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", + "node_modules/sass": { + "version": "1.101.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.101.0.tgz", + "integrity": "sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==", "dev": true, "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, "engines": { - "node": ">=4" + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" } }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" + "xmlchars": "^2.2.0" }, "engines": { - "node": ">=4" + "node": ">=v12.22.7" } }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "dev": true, - "license": "MIT", + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, "engines": { - "node": ">=4" + "node": ">=10" } }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dev": true, "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, "engines": { - "node": ">=4" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/union-value": { - "version": "1.0.1", + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dev": true, "license": "MIT", "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unique-filename": { - "version": "3.0.0", - "license": "ISC", - "dependencies": { - "unique-slug": "^4.0.0" + "node": ">= 18" }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/unique-slug": { - "version": "4.0.0", - "license": "ISC", + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", "dependencies": { - "imurmurhash": "^0.1.4" + "shebang-regex": "^3.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=8" } }, - "node_modules/universalify": { - "version": "2.0.1", - "dev": true, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "license": "MIT", "engines": { - "node": ">= 10.0.0" + "node": ">=8" } }, - "node_modules/unpipe": { - "version": "1.0.0", + "node_modules/shell-quote": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/unset-value": { - "version": "1.0.0", + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", "dependencies": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/unset-value/node_modules/has-value": { - "version": "0.3.1", + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { - "version": "2.1.0", + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", "dependencies": { - "isarray": "1.0.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/unset-value/node_modules/has-values": { - "version": "0.1.4", + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/unset-value/node_modules/isarray": { - "version": "1.0.0", + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/upath": { - "version": "1.2.0", + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "license": "MIT", + "license": "ISC", "engines": { - "node": ">=4", - "yarn": "*" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/update-browserslist-db": { - "version": "1.0.15", + "node_modules/sigstore": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.1.tgz", + "integrity": "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.1", + "@sigstore/tuf": "^4.0.2", + "@sigstore/verify": "^3.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", "funding": [ { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" + "type": "github", + "url": "https://github.com/sponsors/feross" }, { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" + "type": "patreon", + "url": "https://www.patreon.com/feross" }, { - "type": "github", - "url": "https://github.com/sponsors/ai" + "type": "consulting", + "url": "https://feross.org/support" } ], - "license": "MIT", - "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/upper-case": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/uri-js": { - "version": "4.4.1", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/uri-path": { - "version": "1.0.0", - "dev": true, - "license": "WTFPL OR MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/urix": { - "version": "0.1.0", - "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, - "node_modules/use": { - "version": "3.1.1", + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/utrie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", - "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, "license": "MIT", - "dependencies": { - "base64-arraybuffer": "^1.0.2" + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" } }, - "node_modules/uuid": { - "version": "3.4.0", + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", "dev": true, "license": "MIT", - "bin": { - "uuid": "bin/uuid" + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" } }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/v8flags": { - "version": "3.2.0", + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, "license": "MIT", "dependencies": { - "homedir-polyfill": "^1.0.1" + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" }, "engines": { - "node": ">= 0.10" + "node": ">= 14" } }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" } }, - "node_modules/validate-npm-package-name": { - "version": "5.0.1", - "license": "ISC", + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">= 12" } }, - "node_modules/vary": { - "version": "1.1.2", - "dev": true, - "license": "MIT", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { - "node": ">= 0.8" + "node": ">=0.10.0" } }, - "node_modules/verror": { - "version": "1.10.0", + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, - "engines": [ - "node >=0.6.0" - ], "license": "MIT", "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, - "node_modules/vite": { - "version": "5.1.7", + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.35", - "rollup": "^4.2.0" - }, - "bin": { - "vite": "bin/vite.js" - }, + "license": "BSD-3-Clause", "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } + "node": ">=0.10.0" } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", - "cpu": [ - "ppc64" - ], + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } + "license": "CC-BY-3.0" }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", - "cpu": [ - "arm" - ], + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" } }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", - "cpu": [ - "arm64" - ], + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } + "license": "CC0-1.0" }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "cpu": [ - "x64" - ], + "node_modules/ssri": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", "dev": true, - "optional": true, - "os": [ - "android" - ], + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, "engines": { - "node": ">=12" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", - "cpu": [ - "arm64" - ], + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", - "cpu": [ - "x64" - ], + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 0.8" } }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", - "cpu": [ - "arm64" - ], + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } + "license": "MIT" }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", - "cpu": [ - "x64" - ], + "node_modules/stdin-discarder": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", - "cpu": [ - "arm" - ], + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, "engines": { - "node": ">=12" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "cpu": [ - "arm64" - ], + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "cpu": [ - "ia32" - ], + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "cpu": [ - "loong64" - ], + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "cpu": [ - "mips64el" - ], + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, "engines": { - "node": ">=12" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" } }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "cpu": [ - "ppc64" - ], + "node_modules/tailwind-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tailwind-api-utils/-/tailwind-api-utils-1.0.3.tgz", + "integrity": "sha512-KpzUHkH1ug1sq4394SLJX38ZtpeTiqQ1RVyFTTSY2XuHsNSTWUkRo108KmyyrMWdDbQrLYkSHaNKj/a3bmA4sQ==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "local-pkg": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/hyoban" + }, + "peerDependencies": { + "tailwindcss": "^3.3.0 || ^4.0.0 || ^4.0.0-beta" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", + "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "cpu": [ - "riscv64" - ], + "node_modules/tar": { + "version": "7.5.16", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", + "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "cpu": [ - "s390x" - ], + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "BlueOak-1.0.0", "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", - "cpu": [ - "x64" - ], + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, - "optional": true, - "os": [ - "linux" - ], + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, "engines": { - "node": ">=12" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, - "optional": true, - "os": [ - "openbsd" - ], + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=14.0.0" } }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "cpu": [ - "x64" - ], + "node_modules/tldts": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz", + "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==", "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.2" + }, + "bin": { + "tldts": "bin/cli.js" } }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "cpu": [ - "arm64" - ], + "node_modules/tldts-core": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz", + "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==", "dev": true, - "optional": true, - "os": [ - "win32" - ], + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=0.6" } }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "cpu": [ - "ia32" - ], + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", "dev": true, - "optional": true, - "os": [ - "win32" - ], + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, "engines": { - "node": ">=12" + "node": ">=20" } }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "cpu": [ - "x64" - ], + "node_modules/tr46/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, - "optional": true, - "os": [ - "win32" - ], + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=6" } }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.19.12", + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, - "hasInstallScript": true, "license": "MIT", "bin": { - "esbuild": "bin/esbuild" - }, + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18.12" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" - } - }, - "node_modules/void-elements": { - "version": "2.0.1", + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } } }, - "node_modules/watchpack": { - "version": "2.4.0", + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true, - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, - "node_modules/wbuf": { - "version": "1.7.3", + "node_modules/tuf-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", + "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", "dev": true, "license": "MIT", "dependencies": { - "minimalistic-assert": "^1.0.0" + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/wcwidth": { - "version": "1.0.1", + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "license": "MIT", "dependencies": { - "defaults": "^1.0.3" + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/webdriver-js-extender": { + "node_modules/type-is": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "dev": true, "license": "MIT", "dependencies": { - "@types/selenium-webdriver": "^3.0.0", - "selenium-webdriver": "^3.0.1" + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, "engines": { - "node": ">=6.9.x" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/webdriver-manager": { - "version": "12.1.9", + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "dev": true, "license": "MIT", - "dependencies": { - "adm-zip": "^0.5.2", - "chalk": "^1.1.1", - "del": "^2.2.0", - "glob": "^7.0.3", - "ini": "^1.3.4", - "minimist": "^1.2.0", - "q": "^1.4.1", - "request": "^2.87.0", - "rimraf": "^2.5.2", - "semver": "^5.3.0", - "xml2js": "^0.4.17" + "engines": { + "node": ">=18" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", "bin": { - "webdriver-manager": "bin/webdriver-manager" + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=6.9.x" + "node": ">=14.17" } }, - "node_modules/webdriver-manager/node_modules/ansi-regex": { - "version": "2.1.1", - "dev": true, + "node_modules/typescript-eslint": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.0.tgz", + "integrity": "sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q==", "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.62.0", + "@typescript-eslint/parser": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/utils": "8.62.0" + }, "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/webdriver-manager/node_modules/ansi-styles": { - "version": "2.2.1", - "dev": true, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": { + "version": "8.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.0.tgz", + "integrity": "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA==", "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.62.0", + "@typescript-eslint/types": "8.62.0", + "@typescript-eslint/typescript-estree": "8.62.0", + "@typescript-eslint/visitor-keys": "8.62.0", + "debug": "^4.4.3" + }, "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/webdriver-manager/node_modules/chalk": { - "version": "1.1.3", + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", "license": "MIT", - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=18.17" } }, - "node_modules/webdriver-manager/node_modules/ini": { - "version": "1.3.8", + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true, - "license": "ISC" + "license": "MIT" }, - "node_modules/webdriver-manager/node_modules/rimraf": { - "version": "2.7.1", + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, - "node_modules/webdriver-manager/node_modules/semver": { - "version": "5.7.2", + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, - "license": "ISC", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, "bin": { - "semver": "bin/semver" + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "node_modules/webdriver-manager/node_modules/strip-ansi": { - "version": "3.0.1", - "dev": true, - "license": "MIT", + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" + "punycode": "^2.1.0" } }, - "node_modules/webdriver-manager/node_modules/supports-color": { - "version": "2.0.0", - "dev": true, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "license": "MIT", "engines": { - "node": ">=0.8.0" + "node": ">=6" } }, - "node_modules/webidl-conversions": { + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause", - "optional": true + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" }, - "node_modules/webpack": { - "version": "5.90.3", + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.21.10", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, + "license": "ISC", "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/webpack-dev-middleware": { - "version": "6.1.2", + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, "license": "MIT", - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.12", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - } + "node": ">= 0.8" } }, - "node_modules/webpack-dev-server": { - "version": "4.15.1", + "node_modules/vite": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", "dev": true, "license": "MIT", "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.5", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", - "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.13.0" + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" + "vite": "bin/vite.js" }, "engines": { - "node": ">= 12.13.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" }, "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { - "webpack": { + "@types/node": { + "optional": true + }, + "jiti": { "optional": true }, - "webpack-cli": { + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { "optional": true } } }, - "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "5.3.4", + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", "dev": true, "license": "MIT", "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.3", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" }, "engines": { - "node": ">= 12.13.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.17.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { - "bufferutil": { + "@edge-runtime/vm": { "optional": true }, - "utf-8-validate": { + "@opentelemetry/api": { "optional": true - } - } - }, - "node_modules/webpack-merge": { - "version": "5.10.0", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } } }, - "node_modules/webpack-subresource-integrity": { - "version": "5.1.0", + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", "dependencies": { - "typed-assert": "^1.0.8" + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", - "webpack": "^5.12.0" - }, - "peerDependenciesMeta": { - "html-webpack-plugin": { - "optional": true - } + "node": ">=18" } }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "3.5.2", + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", "dev": true, "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } + "optional": true }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=4.0" + "node": ">=20" } }, - "node_modules/webpack/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", "dev": true, "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=20" } }, - "node_modules/websocket-driver": { - "version": "0.7.4", + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" }, "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -23437,41 +15376,27 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/which-module": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "license": "ISC" }, - "node_modules/which-typed-array": { - "version": "1.1.15", + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" + "siginfo": "^2.0.0", + "stackback": "0.0.2" }, - "engines": { - "node": ">= 0.4" + "bin": { + "why-is-node-running": "cli.js" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=8" } }, "node_modules/wide-align": { @@ -23484,59 +15409,67 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, - "node_modules/wildcard": { - "version": "2.0.1", - "dev": true, - "license": "MIT" + "node_modules/wide-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } }, - "node_modules/window-size": { - "version": "0.1.0", - "dev": true, + "node_modules/wide-align/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", "optional": true, "engines": { - "node": ">= 0.8.0" + "node": ">=8" } }, - "node_modules/with": { - "version": "5.1.1", - "dev": true, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "optional": true, "dependencies": { - "acorn": "^3.1.0", - "acorn-globals": "^3.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/with/node_modules/acorn": { - "version": "3.3.0", - "dev": true, + "node_modules/wide-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "optional": true, - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=0.4.0" + "node": ">=8" } }, "node_modules/word-wrap": { "version": "1.2.5", - "dev": true, + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/wordwrap": { - "version": "0.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/wrap-ansi": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -23547,24 +15480,19 @@ "node": ">=8" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=8" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -23576,8 +15504,10 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "node_modules/wrap-ansi/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -23586,89 +15516,76 @@ "node": ">=7.0.0" } }, - "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "node_modules/wrap-ansi/node_modules/color-name": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=7.0.0" + "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, "node_modules/wrappy": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "devOptional": true, "license": "ISC" }, - "node_modules/ws": { - "version": "8.11.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml2js": { - "version": "0.4.23", - "dev": true, - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=4.0" + "node": ">=18" } }, - "node_modules/xregexp": { - "version": "3.1.0", + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true, "license": "MIT" }, "node_modules/y18n": { "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -23676,34 +15593,68 @@ }, "node_modules/yallist": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" }, "node_modules/yargs": { - "version": "17.7.2", + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^7.2.0", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, "license": "ISC", "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/yn": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, "license": "MIT", "engines": { @@ -23712,7 +15663,8 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", - "dev": true, + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "license": "MIT", "engines": { "node": ">=10" @@ -23721,12 +15673,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zone.js": { - "version": "0.14.5", + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.2.tgz", + "integrity": "sha512-IynmDyxsEsb9RKzO3J9+4SxXnl2FTFSzNBaKKaMV6tsSk0rw9gYw9gs+JFCq/qk2LCZ78KDwyj+Z289TijSkUw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "node_modules/zone.js": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.2.tgz", + "integrity": "sha512-Eky7p2Z1Ig3NnbfodSPoARCjKBSTFMnE/ACsP1L/XJEfY4SdOFce19BsUCWVwL6K5ABZFy5J3bjcMWffX+YM3Q==", + "license": "MIT" } } } diff --git a/package.json b/package.json index 3b58d7c3ef..7013576f44 100644 --- a/package.json +++ b/package.json @@ -1,182 +1,125 @@ { "name": "doubtfire", - "version": "10.0.1-29", + "version": "11.0.0-45", "homepage": "http://github.com/doubtfire-lms/", "description": "Learning and feedback tool.", "license": "AGPL-3.0", "repository": {}, "engines": { - "node": ">=20.9.0" + "node": ">=22.22.3" }, "scripts": { - "build": "run-s -l build:angular1 build:angular17", - "build:angular1": "grunt build", - "build:angular17": "ng build", + "build": "ng build", + "build:angular18": "ng build", "lint:fix": "ng lint --fix", - "lint": "ng lint", - "serve:angular17": "export NODE_OPTIONS=--max_old_space_size=4096 && ng serve --poll=2000 --configuration $NODE_ENV --proxy-config proxy.conf.json", - "serve:angular17-compose": "export NODE_OPTIONS=--max_old_space_size=4096 && ng serve --configuration $NODE_ENV --proxy-config proxy-compose.conf.json", - "start": "npm-run-all -l -s build:angular1 -p watch:angular1 serve:angular17", - "start-compose": "npm-run-all -l -s build:angular1 -p watch:angular1 serve:angular17-compose", - "watch:angular1": "grunt delta", + "lint": "ng lint --max-warnings 0", + "serve:angular18": "export NG_FORCE_TTY=false && export NODE_OPTIONS=--max_old_space_size=4096 && ng serve --poll=2000 --configuration $NODE_ENV --proxy-config proxy.conf.json", + "format": "prettier --write .", + "start": "npm-run-all serve:angular18", "deploy:build2api": "ng build --delete-output-path=true --optimization=true --configuration production --output-path dist", - "deploy": "run-s -l build:angular1 deploy:build2api", + "deploy:build2api:sourcemaps": "ng build --delete-output-path=true --optimization=true --configuration production --source-map=true --output-path dist", + "sentry:sourcemaps": "node scripts/upload-sentry-sourcemaps.js", + "deploy": "run-s -l deploy:build2api", "prepare": "husky install", "test": "ng test", - "test:ci": "ng test --karma-config=src/karma-ci.conf.js --no-progress" + "test:ci": "ng test --no-watch --no-progress", + "typecheck": "ngc -p src/tsconfig.app.json --noEmit" }, "keywords": [], "author": "", "dependencies": { - "@angular/animations": "^17.3.6", - "@angular/cdk": "^17.3.6", - "@angular/cli": "^17.3.6", - "@angular/common": "^17.3.6", - "@angular/compiler": "^17.3.6", - "@angular/core": "^17.3.6", - "@angular/forms": "^17.3.6", - "@angular/material": "^17.3.10", - "@angular/material-date-fns-adapter": "^17.3.10", - "@angular/platform-browser": "^17.3.6", - "@angular/platform-browser-dynamic": "^17.3.6", - "@angular/router": "^17.3.6", - "@angular/service-worker": "^17.3.6", - "@angular/upgrade": "^17.3.6", + "@angular/animations": "^22.0.3", + "@angular/cdk": "^22.0.2", + "@angular/common": "^22.0.3", + "@angular/compiler": "^22.0.3", + "@angular/core": "^22.0.3", + "@angular/forms": "^22.0.3", + "@angular/material": "^22.0.2", + "@angular/material-date-fns-adapter": "^22.0.2", + "@angular/platform-browser": "^22.0.3", + "@angular/platform-browser-dynamic": "^22.0.3", + "@angular/router": "^22.0.3", + "@angular/service-worker": "^22.0.3", "@ctrl/ngx-emoji-mart": "^9.3.0", + "@eslint/js": "^10.0.1", "@ngneat/hotkeys": "^4.0.0", - "@ngstack/code-editor": "7.3.0", - "@uirouter/angular": "^13.0", - "@uirouter/angular-hybrid": "^17.1.0", - "@uirouter/angularjs": "^1.0.30", - "@uirouter/core": "^6.1.0", - "@uirouter/rx": "^1.0.0", - "@worktile/gantt": "^18.0.5", - "angular": "1.5.11", - "angular-calendar": "^0.31.1", - "angular-filter": "0.5.17", - "angular-markdown-filter": "1.3.2", - "angular-md5": "0.1.10", - "angular-mocks": "1.8.3", - "angular-nvd3": "1.0.9", - "angular-resource": "1.5.11", - "angular-sanitize": "1.5.11", - "angular-ui-bootstrap": "0.13.4", - "angular-ui-codemirror": "0.3.0", - "angular-xeditable": "0.9.0", - "angulartics": "~1.0.3", - "angulartics-google-analytics": "0.1.4", - "bootstrap": "~3.4", - "bootstrap-sass": "~3.4", - "canvas-confetti": "^1.6.0", - "codemirror": "5.65.0", - "core-js": "^3.21.1", - "d3": "3.5.17", - "date-fns": "^3.6.0", - "es5-shim": "^4.5.12", - "file-saver": "^2.0.5", - "font-awesome": "~4.7.0", - "html2canvas": "^1.4.1", + "@ngstack/code-editor": "^9.0.0", + "@sentry/angular": "^10.61.0", + "@sentry/cli": "^3.5.1", + "@swimlane/ngx-charts": "^20.5.0", + "@tailwindcss/postcss": "^4.3.2", + "@worktile/gantt": "^21.0.0", + "angular-calendar": "^0.32.2", + "ansi-to-html": "^0.7.2", + "canvas-confetti": "^1.9", + "d3": "^7.9.0", + "date-fns": "^4.4.0", + "dompurify": "^3.4.11", + "html2canvas": "^1.0.0-rc.7", "html5-qrcode": "^2.3.8", - "jquery": "2.1.4", "jszip": "^3.10.1", - "lodash": "~4.18", "lottie-web": "^5.13.0", - "marked": "^11.1.0", - "moment": "^2.29.4", - "monaco-editor": "^0.44.0", - "ng-csv": "0.2.3", - "ng-file-upload": "~5.0.9", + "marked": "^18.0.5", + "moment": "^2.30", + "monaco-editor": "^0.55.1", "ng-flex-layout": "^17.3.7-beta.1", "ng2-pdf-viewer": "10.2.2", - "ngx-bootstrap": "^6.1.0", - "ngx-entity-service": "^0.0.41", - "ngx-lottie": "^11.0.2", - "ngx-monaco-editor-v2": "^17.0.1", - "nvd3": "1.8.6", + "ngx-entity-service": "^0.0.44", + "ngx-lottie": "^22.0.0", + "ngx-monaco-editor-v2-alternative": "^22.0.0", + "ngx-skeleton-loader": "^13.0.0", "qrcode": "^1.5.4", "rxjs": "~7.8.2", - "ts-md5": "^1.3.1", - "tslib": "^2.6.2", - "underscore.string": "2.3.3", - "zone.js": "~0.14" + "tslib": "^2.8.1", + "typescript-eslint": "^8.62.0", + "zone.js": "~0.16.2" }, "devDependencies": { - "@angular-devkit/build-angular": "^17.3.6", - "@angular-eslint/builder": "^17.3.0", - "@angular-eslint/eslint-plugin": "^17.3.0", - "@angular-eslint/eslint-plugin-template": "^17.3.0", - "@angular-eslint/schematics": "^17.3.0", - "@angular-eslint/template-parser": "^17.3.0", - "@angular/compiler-cli": "^17.3.6", - "@angular/language-service": "^17.3.6", - "@commitlint/cli": "^20.5.0", - "@commitlint/config-conventional": "^20", - "@types/angular": "1.5.11", + "@angular-eslint/builder": "^22.0.0", + "@angular-eslint/eslint-plugin": "^22.0.0", + "@angular-eslint/eslint-plugin-template": "^22.0.0", + "@angular-eslint/schematics": "^22.0.0", + "@angular-eslint/template-parser": "^22.0.0", + "@angular/build": "^22.0.4", + "@angular/cli": "^22.0.4", + "@angular/compiler-cli": "^22.0.3", + "@angular/language-service": "^22.0.3", + "@commitlint/cli": "^21.1.0", + "@commitlint/config-conventional": "^21.1.0", + "@trivago/prettier-plugin-sort-imports": "^6.0.2", "@types/canvas-confetti": "^1.6.0", - "@types/d3": "^3.5.17", - "@types/file-saver": "^2.0.1", - "@types/jasmine": "~6.0.0", - "@types/jasminewd2": "~2.0.3", - "@types/lodash": "^4.14.115", - "@types/node": "^20.9.0", - "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.2.0", - "autoprefixer": "~6", - "canonical-path": "0.0.2", - "concurrently": "^3.2.0", - "eslint": "^8.57.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-import": "2.26.0", - "eslint-plugin-jsdoc": "39.3.6", - "eslint-plugin-prefer-arrow": "1.2.3", - "eslint-plugin-prettier": "^5.0.1", - "grunt": "^1.0.4", - "grunt-bump": "0.8.0", - "grunt-coffeelint": "0.0.16", - "grunt-contrib-clean": "~1.0.0", - "grunt-contrib-coffee": "^1.0.0", - "grunt-contrib-concat": "~1.0.1", - "grunt-contrib-connect": "^1.0.2", - "grunt-contrib-copy": "~1.0.0", - "grunt-contrib-jshint": "~1.0.0", - "grunt-contrib-watch": "^1.1.0", - "grunt-env": "0.4.4", - "grunt-html2js": "^0.6.0", - "grunt-karma": "~2.0.0", - "grunt-newer": "^1.1.2", - "grunt-ng-annotate": "^3.0.0", - "grunt-postcss": "~0.8", - "grunt-preprocess": "5.1.0", - "grunt-sass": "^3.0.2", - "grunt-sass-globbing": "^1.4.0", - "husky": "~8", - "ip": "^1.1.2", - "jasmine-core": "~4.1.0", - "jasmine-spec-reporter": "~5.0.0", - "karma": "^6.3.4", - "karma-chrome-launcher": "~3.1.0", - "karma-coverage-istanbul-reporter": "~3.0.2", - "karma-jasmine": "~4.0.0", - "karma-jasmine-html-reporter": "^1.5.0", - "load-grunt-tasks": "^5.0.0", - "npm-run-all2": "^7.0", - "postcss": "^8.4.27", - "postcss-scss": "^0.1.7", - "prettier": "^3.1.0", - "protractor": "~7.0.0", - "sass": "^1.48.0", - "tailwindcss": "~3.3", + "@types/d3": "^7.4.3", + "@types/dompurify": "^3.0.5", + "@types/node": "^26.0.1", + "@typescript-eslint/eslint-plugin": "^8.60.1", + "@typescript-eslint/parser": "^8.62.1", + "angular-eslint": "^22.0.0", + "autoprefixer": "^10.5.2", + "concurrently": "^10.0.3", + "eslint": "^10.6.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.6", + "eslint-plugin-tailwindcss": "^4.0.4", + "husky": "^9.1.7", + "ip": "^2.0.1", + "jsdom": "^29.1.1", + "npm-run-all2": "^9.0.2", + "postcss": "^8.5.16", + "postcss-scss": "^4.0.9", + "prettier": "^3.8.4", + "sass": "^1.101.0", + "tailwindcss": "^4.3.1", "ts-node": "~10.9", - "typescript": "~5.2", - "underscore": "^1.8.3" + "typescript": "~6.0.3", + "vitest": "^4.1.9" }, "optionalDependencies": { - "@nx/nx-darwin-arm64": "^18.0", - "@nx/nx-darwin-x64": "^18.0", - "@nx/nx-linux-arm64-gnu": "^18.0", - "@nx/nx-linux-x64-gnu": "^18.0", - "@nx/nx-win32-x64-msvc": "^18.0", - "@rollup/rollup-linux-arm64-gnu": "*", - "@rollup/rollup-linux-x64-gnu": "*" + "@nx/nx-darwin-arm64": "^23.0.1", + "@nx/nx-darwin-x64": "^23.0.1", + "@nx/nx-linux-arm64-gnu": "^23.0.1", + "@nx/nx-linux-x64-gnu": "^23.0.1", + "@nx/nx-win32-x64-msvc": "^23.0.1", + "@rollup/rollup-linux-arm64-gnu": "^4.62.2", + "@rollup/rollup-linux-x64-gnu": "^4.62.2" } } diff --git a/scripts/upload-sentry-sourcemaps.js b/scripts/upload-sentry-sourcemaps.js new file mode 100644 index 0000000000..c415e1a246 --- /dev/null +++ b/scripts/upload-sentry-sourcemaps.js @@ -0,0 +1,36 @@ +const {execFileSync} = require('node:child_process'); + +const requiredEnvironment = [ + 'SENTRY_DSN', + 'SENTRY_AUTH_TOKEN', + 'SENTRY_ORG', + 'SENTRY_PROJECT', + 'SENTRY_RELEASE', + 'SENTRY_DIST', +]; +const missingEnvironment = requiredEnvironment.filter((name) => !process.env[name]); + +if (missingEnvironment.length > 0) { + console.log( + `Skipping Sentry sourcemap upload because ${missingEnvironment.join(', ')} ${ + missingEnvironment.length === 1 ? 'is' : 'are' + } not set.`, + ); + process.exit(0); +} + +const sentryArgs = ['--org', process.env.SENTRY_ORG, '--project', process.env.SENTRY_PROJECT]; +const releaseArgs = ['--release', process.env.SENTRY_RELEASE]; +const distArgs = ['--dist', process.env.SENTRY_DIST]; +const distPath = './dist/browser'; + +execFileSync('sentry-cli', ['sourcemaps', 'inject', ...sentryArgs, ...releaseArgs, distPath], { + stdio: 'inherit', +}); +execFileSync( + 'sentry-cli', + ['sourcemaps', 'upload', ...sentryArgs, ...releaseArgs, ...distArgs, distPath], + { + stdio: 'inherit', + }, +); diff --git a/src/app/account/edit-profile/edit-profile.component.html b/src/app/account/edit-profile/edit-profile.component.html index a7b9c7b6e9..43c2d6788c 100644 --- a/src/app/account/edit-profile/edit-profile.component.html +++ b/src/app/account/edit-profile/edit-profile.component.html @@ -1,6 +1,6 @@
-
+
@if (!loading) { } diff --git a/src/app/account/edit-profile/edit-profile.component.scss b/src/app/account/edit-profile/edit-profile.component.scss index d344d62e6a..627d96752e 100644 --- a/src/app/account/edit-profile/edit-profile.component.scss +++ b/src/app/account/edit-profile/edit-profile.component.scss @@ -1,7 +1,7 @@ #parent { - margin-top: 30px; + margin-top: 30px; } .form-container { - max-width: 800px; -} \ No newline at end of file + max-width: 800px; +} diff --git a/src/app/account/edit-profile/edit-profile.component.spec.ts b/src/app/account/edit-profile/edit-profile.component.spec.ts index 189015c5a9..b15e0bfc85 100644 --- a/src/app/account/edit-profile/edit-profile.component.spec.ts +++ b/src/app/account/edit-profile/edit-profile.component.spec.ts @@ -1,5 +1,11 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { EditProfileComponent } from './edit-profile.component'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Router} from '@angular/router'; +import {AuthenticationService} from 'src/app/api/services/authentication.service'; +import {EditProfileComponent} from './edit-profile.component'; + +const emptyProvider = {}; describe('EditProfileComponent', () => { let component: EditProfileComponent; @@ -7,12 +13,20 @@ describe('EditProfileComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [EditProfileComponent] - }).compileComponents(); + declarations: [EditProfileComponent], + providers: [ + {provide: AuthenticationService, useValue: emptyProvider}, + {provide: Router, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(EditProfileComponent, {set: {template: ''}}) + .compileComponents(); + }); + beforeEach(() => { fixture = TestBed.createComponent(EditProfileComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/account/edit-profile/edit-profile.component.ts b/src/app/account/edit-profile/edit-profile.component.ts index 55c6143ca4..91e2c95b78 100644 --- a/src/app/account/edit-profile/edit-profile.component.ts +++ b/src/app/account/edit-profile/edit-profile.component.ts @@ -1,25 +1,27 @@ -import {Component, OnInit} from '@angular/core'; -import {StateService} from '@uirouter/core'; +import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core'; +import {Router} from '@angular/router'; import {AuthenticationService} from 'src/app/api/services/authentication.service'; @Component({ selector: 'f-edit-profile', templateUrl: './edit-profile.component.html', styleUrls: ['./edit-profile.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class EditProfileComponent implements OnInit { public loading: boolean = true; constructor( private authenticationService: AuthenticationService, - private state: StateService, + private router: Router, ) {} public ngOnInit(): void { this.loading = true; this.authenticationService.afterAuthCall((result) => { if (!result) { - return this.state.go('sign_in'); + return this.router.navigateByUrl('/sign_in'); } this.loading = false; }); diff --git a/src/app/admin/institution-settings/activity-type-list/activity-type-list.component.html b/src/app/admin/institution-settings/activity-type-list/activity-type-list.component.html index 9c91b329f7..7d53f9eb83 100644 --- a/src/app/admin/institution-settings/activity-type-list/activity-type-list.component.html +++ b/src/app/admin/institution-settings/activity-type-list/activity-type-list.component.html @@ -1,80 +1,129 @@
-
-

Activities

+
+

Activities

Add new activities or modify existing ones

- + @if (loadingActivities) { +
+
+ @for (width of ['30%', '22%', '4%']; track $index) { + + } +
+ + @for (row of skeletonRows; track row) { +
+ + + +
+ } +
+ } + +
- - - + - - - - + - - - + - - - - + + +
Name + + Name @if (!editing(activityType)) { -
- {{ activityType.name }} -
+
+ {{ activityType.name }} +
} @else { - + }
+ - + Abbreviation + + Abbreviation @if (!editing(activityType)) { -
- {{ activityType.abbreviation }} -
+
+ {{ activityType.abbreviation }} +
} @else { - + }
+ - + + @if (!editing(activityType)) { -
- -
+
+ +
} @else {
- - -
}
+
diff --git a/src/app/admin/institution-settings/activity-type-list/activity-type-list.component.ts b/src/app/admin/institution-settings/activity-type-list/activity-type-list.component.ts index 39c4c83d07..87f9b7e71c 100644 --- a/src/app/admin/institution-settings/activity-type-list/activity-type-list.component.ts +++ b/src/app/admin/institution-settings/activity-type-list/activity-type-list.component.ts @@ -1,24 +1,32 @@ -import {Component, ViewChild} from '@angular/core'; -import {MatTableDataSource, MatTable} from '@angular/material/table'; -import {ActivityType, ActivityTypeService} from 'src/app/api/models/doubtfire-model'; -import {EntityFormComponent} from 'src/app/common/entity-form/entity-form.component'; +import {AfterViewInit, ChangeDetectionStrategy, Component, ViewChild} from '@angular/core'; import {UntypedFormControl, Validators} from '@angular/forms'; import {MatSort, Sort} from '@angular/material/sort'; +import {MatTable, MatTableDataSource} from '@angular/material/table'; +import {finalize} from 'rxjs'; +import {ActivityType, ActivityTypeService} from 'src/app/api/models/doubtfire-model'; +import {EntityFormComponent} from 'src/app/common/entity-form/entity-form.component'; import {AlertService} from 'src/app/common/services/alert.service'; @Component({ selector: 'activity-type-list', templateUrl: 'activity-type-list.component.html', styleUrls: ['activity-type-list.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class ActivityTypeListComponent extends EntityFormComponent { - @ViewChild(MatTable, {static: true}) table: MatTable; +export class ActivityTypeListComponent + extends EntityFormComponent + implements AfterViewInit +{ + @ViewChild(MatTable, {static: true}) table: MatTable; @ViewChild(MatSort, {static: true}) sort: MatSort; // Set up the table columns: string[] = ['name', 'abbreviation', 'options']; activityTypes: ActivityType[] = new Array(); dataSource = new MatTableDataSource(this.activityTypes); + loadingActivities = true; + skeletonRows = Array.from({length: 3}, (_, index) => index); // Calls the parent's constructor, passing in an object // that maps all of the form controls that this form consists of. @@ -37,9 +45,13 @@ export class ActivityTypeListComponent extends EntityFormComponent ngAfterViewInit() { // Get all the activity types and add them to the table - this.activityTypeService.query().subscribe((activityTypes) => { - this.pushToTable(activityTypes); - }); + this.loadingActivities = true; + this.activityTypeService + .query() + .pipe(finalize(() => (this.loadingActivities = false))) + .subscribe((activityTypes) => { + this.pushToTable(activityTypes); + }); } // This method is passed to the submit method on the parent @@ -53,8 +65,14 @@ export class ActivityTypeListComponent extends EntityFormComponent // Push the values that will be displayed in the table // to the datasource private pushToTable(value: ActivityType | ActivityType[]) { - if (!value) return; - value instanceof Array ? this.activityTypes.push(...value) : this.activityTypes.push(value); + if (!value) { + return; + } + if (value instanceof Array) { + this.activityTypes.push(...value); + } else { + this.activityTypes.push(value); + } this.dataSource.sort = this.sort; this.table.renderRows(); } diff --git a/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.html b/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.html index 6fed39bc24..16bcd8b53b 100644 --- a/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.html +++ b/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.html @@ -1,57 +1,64 @@
-
-

Campuses

+
+

Campuses

Add new campuses or modify existing ones

- +
- - - + - - - - + - - - - + - - + - - - - + - - - + - - - - + + +
Name + + Name @if (!editing(campus)) {
{{ campus.name }}
} @else { - + }
+ - + Abbreviation + + Abbreviation @if (!editing(campus)) {
{{ campus.abbreviation }}
} @else { - + }
+ - + Default Sync Mode + + Default Sync Mode @if (!editing(campus)) {
{{ campus.mode | titlecase }} @@ -59,24 +66,24 @@

Campuses

} @else { Default Sync Mode - + @for (mode of syncModes; track mode) { - - {{ mode | titlecase }} - + + {{ mode | titlecase }} + } }
+ Default Sync Mode - + @for (mode of syncModes; track mode) { - - {{ mode | titlecase }} - + + {{ mode | titlecase }} + } @@ -84,9 +91,9 @@

Campuses

- -
Timezone + + Timezone @if (!editing(campus)) {
{{ campus.timezone }} @@ -95,67 +102,77 @@

Campuses

Timezone - + }
+ Timezone - + Active + + Active @if (!editing(campus)) {
- +
} @else { - + }
+ + @if (!editing(campus)) { -
- - - - -
+ + + + } @else {
- - -
}
+
diff --git a/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.ts b/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.ts index 1fa8871cc3..b953bc3f16 100644 --- a/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.ts +++ b/src/app/admin/institution-settings/campuses/campus-list/campus-list.component.ts @@ -1,7 +1,7 @@ -import {Component, ViewChild} from '@angular/core'; -import {MatSort, Sort} from '@angular/material/sort'; -import {MatTableDataSource, MatTable} from '@angular/material/table'; +import {AfterViewInit, ChangeDetectionStrategy, Component, ViewChild} from '@angular/core'; import {UntypedFormControl, Validators} from '@angular/forms'; +import {MatSort, Sort} from '@angular/material/sort'; +import {MatTable, MatTableDataSource} from '@angular/material/table'; import {Campus, CampusService} from 'src/app/api/models/doubtfire-model'; import {EntityFormComponent} from 'src/app/common/entity-form/entity-form.component'; import {AlertService} from 'src/app/common/services/alert.service'; @@ -10,8 +10,10 @@ import {AlertService} from 'src/app/common/services/alert.service'; selector: 'campus-list', templateUrl: 'campus-list.component.html', styleUrls: ['campus-list.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class CampusListComponent extends EntityFormComponent { +export class CampusListComponent extends EntityFormComponent implements AfterViewInit { @ViewChild(MatTable, {static: true}) table: MatTable; @ViewChild(MatSort, {static: true}) sort: MatSort; @@ -58,9 +60,15 @@ export class CampusListComponent extends EntityFormComponent { // Push the values that will be displayed in the table // to the datasource private pushToTable(value: Campus | Campus[]) { - if (!value) return; + if (!value) { + return; + } - value instanceof Array ? this.campuses.push(...value) : this.campuses.push(value); + if (value instanceof Array) { + this.campuses.push(...value); + } else { + this.campuses.push(value); + } this.dataSource.sort = this.sort; } diff --git a/src/app/admin/institution-settings/institution-settings.component.html b/src/app/admin/institution-settings/institution-settings.component.html index 590a56f54f..e57317aca4 100644 --- a/src/app/admin/institution-settings/institution-settings.component.html +++ b/src/app/admin/institution-settings/institution-settings.component.html @@ -1,33 +1,44 @@
- - - - - - - - - - - -
-
-
-

Learning Outcomes

-

Manage global learning outcomes for the institution

+ + + @for (tab of tabs; track tab.routeSegment) { + + } + + +
+ @switch (currentTab.routeSegment) { + @case ('campuses') { + + } + @case ('activities') { + + } + @case ('teaching-periods') { + + } + @case ('learning-outcomes') { +
+
+
+

Learning Outcomes

+

Manage global learning outcomes for the institution

+
+
- -
- - @if (overseerEnabled) { - - - - } @if (tiiEnabled) { - - - + } + @case ('overseer-images') { + + } + @case ('turnitin') { + + } } - +
diff --git a/src/app/admin/institution-settings/institution-settings.component.ts b/src/app/admin/institution-settings/institution-settings.component.ts index c4fd2d78af..49035ea4ca 100644 --- a/src/app/admin/institution-settings/institution-settings.component.ts +++ b/src/app/admin/institution-settings/institution-settings.component.ts @@ -1,22 +1,93 @@ -import { Component } from '@angular/core'; -import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; +import {ChangeDetectionStrategy, Component, OnDestroy, OnInit} from '@angular/core'; +import {MatTabChangeEvent} from '@angular/material/tabs'; +import {ActivatedRoute, Router} from '@angular/router'; +import {Subscription} from 'rxjs'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; + +type InstitutionSettingsTabKey = + | 'campuses' + | 'activities' + | 'teaching-periods' + | 'learning-outcomes' + | 'overseer-images' + | 'turnitin'; + +interface InstitutionSettingsTab { + label: string; + routeSegment: InstitutionSettingsTabKey; + enabled: boolean; +} @Component({ selector: 'institution-settings', templateUrl: 'institution-settings.component.html', - styleUrls: ['institution-settings.component.scss'] + styleUrls: ['institution-settings.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class InstitutionSettingsComponent { +export class InstitutionSettingsComponent implements OnInit, OnDestroy { + public currentTab: InstitutionSettingsTab = { + label: 'Campuses', + routeSegment: 'campuses', + enabled: true, + }; + + private subscriptions: Subscription[] = []; constructor( private constants: DoubtfireConstants, - ) { } + private route: ActivatedRoute, + private router: Router, + ) {} + + public ngOnInit(): void { + this.updateCurrentTabFromState(this.route.snapshot.paramMap.get('tab')); + + this.subscriptions.push( + this.route.paramMap.subscribe((params) => this.updateCurrentTabFromState(params.get('tab'))), + ); + } + + public ngOnDestroy(): void { + this.subscriptions.forEach((subscription) => subscription.unsubscribe()); + } - public get overseerEnabled() : boolean { + public get tabs(): InstitutionSettingsTab[] { + const tabs: InstitutionSettingsTab[] = [ + {label: 'Campuses', routeSegment: 'campuses', enabled: true}, + {label: 'Activities', routeSegment: 'activities', enabled: true}, + {label: 'Teaching Periods', routeSegment: 'teaching-periods', enabled: true}, + {label: 'Learning Outcomes', routeSegment: 'learning-outcomes', enabled: true}, + {label: 'Overseer Images', routeSegment: 'overseer-images', enabled: this.overseerEnabled}, + {label: 'TurnItIn', routeSegment: 'turnitin', enabled: this.tiiEnabled}, + ]; + + return tabs.filter((tab) => tab.enabled); + } + + public get currentIndex(): number { + const index = this.tabs.findIndex((tab) => tab.routeSegment === this.currentTab.routeSegment); + return index >= 0 ? index : 0; + } + + public get overseerEnabled(): boolean { return this.constants.IsOverseerEnabled.value; } - public get tiiEnabled() : boolean { + public get tiiEnabled(): boolean { return this.constants.IsTiiEnabled.value; } + + public onTabChange(event: MatTabChangeEvent): void { + const nextTab = this.tabs[event.index] ?? this.tabs[0]; + this.currentTab = nextTab; + this.router.navigate(['/admin/institution-settings', nextTab.routeSegment], {replaceUrl: true}); + } + + private updateCurrentTabFromState(tabParam?: string | null): void { + this.currentTab = + this.tabs.find((tab) => tab.routeSegment === tabParam) ?? + this.tabs.find((tab) => tab.routeSegment === 'campuses') ?? + this.tabs[0]; + } } diff --git a/src/app/admin/institution-settings/overseer-images/overseer-image-list.component.html b/src/app/admin/institution-settings/overseer-images/overseer-image-list.component.html index 9ca1c6a6ba..c9b0d15df4 100644 --- a/src/app/admin/institution-settings/overseer-images/overseer-image-list.component.html +++ b/src/app/admin/institution-settings/overseer-images/overseer-image-list.component.html @@ -1,5 +1,5 @@ - +
{{ data.text }}
@@ -16,154 +16,203 @@

Add new image or modify existing ones used for automated task analysis

+ @if (loadingImages) { +
+
+ @for (width of ['22%', '18%', '8%', '16%', '10%', '4%']; track $index) { + + } +
+ + @for (row of skeletonRows; track row) { +
+ + + + + +
+ } +
+ } + - - - + - - - - + - - - + - + - - + - + - - - + - - + - - - - + + +
Name -
- {{ overseerImage.name }} -
- - - + +
Name + @if (!editing(overseerImage)) { +
+ {{ overseerImage.name }} +
+ } @else { + + Name + - + }
- - + + + Name + Tag -
- {{ overseerImage.tag }} -
- - - + +
Tag + @if (!editing(overseerImage)) { +
+ {{ overseerImage.tag }} +
+ } @else { + + Tag + - + }
- - + + + Tag + -
- -
+
+ @if (!editing(overseerImage)) { +
+ +
+ }
Last Pulled -
- {{ overseerImage.lastPulledDate | humanizedDate }} -
+
Last Pulled + @if (!editing(overseerImage)) { +
+ {{ overseerImage.lastPulledDate | humanizedDate }} +
+ }
Status -
-
+ @if (!editing(overseerImage)) { +
- check_circle_outline - - - -
+ @if (overseerImage.pulledImageStatus === 'success') { + + } + @if (overseerImage.pulledImageStatus === 'loading') { + + } + @if (overseerImage.pulledImageStatus === 'failed') { + + } + + }
-
- - -
+ @if (!editing(overseerImage)) { +
+ - -
- -
- - - - - +
-
+ } @else { +
+ + + +
+ }
+
diff --git a/src/app/admin/institution-settings/overseer-images/overseer-image-list.component.ts b/src/app/admin/institution-settings/overseer-images/overseer-image-list.component.ts index 05173d587d..9f7ab13091 100644 --- a/src/app/admin/institution-settings/overseer-images/overseer-image-list.component.ts +++ b/src/app/admin/institution-settings/overseer-images/overseer-image-list.component.ts @@ -1,26 +1,36 @@ import {HttpClient} from '@angular/common/http'; -import {AfterViewInit, Component, TemplateRef, ViewChild} from '@angular/core'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + TemplateRef, + ViewChild, +} from '@angular/core'; import {UntypedFormControl, Validators} from '@angular/forms'; import {MatDialog} from '@angular/material/dialog'; import {MatSort, Sort} from '@angular/material/sort'; import {MatTable, MatTableDataSource} from '@angular/material/table'; +import {finalize} from 'rxjs'; import {OverseerImage, OverseerImageService} from 'src/app/api/models/doubtfire-model'; import {EntityFormComponent} from 'src/app/common/entity-form/entity-form.component'; import {SidekiqProgressModalService} from 'src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.service'; import {AlertService} from 'src/app/common/services/alert.service'; +import API_URL from 'src/app/config/constants/apiUrl'; @Component({ selector: 'overseer-image-list', templateUrl: 'overseer-image-list.component.html', styleUrls: ['overseer-image-list.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class OverseerImageListComponent extends EntityFormComponent implements AfterViewInit { - @ViewChild('textDialog') textDialog!: TemplateRef; + @ViewChild('textDialog') textDialog!: TemplateRef; - @ViewChild(MatTable, {static: true}) table: MatTable; + @ViewChild(MatTable, {static: true}) table: MatTable; @ViewChild(MatSort, {static: true}) sort: MatSort; // Set up the table @@ -28,6 +38,8 @@ export class OverseerImageListComponent overseerImages: OverseerImage[] = new Array(); dataSource = new MatTableDataSource(this.overseerImages); loading = false; + loadingImages = true; + skeletonRows = Array.from({length: 2}, (_, index) => index); public diskSpace: number | null = null; @@ -51,11 +63,15 @@ export class OverseerImageListComponent ngAfterViewInit() { // Get all the overseer images and add them to the table - this.overseerImageService.fetchAll().subscribe((response) => { - this.pushToTable(response); - }); + this.loadingImages = true; + this.overseerImageService + .fetchAll() + .pipe(finalize(() => (this.loadingImages = false))) + .subscribe((response) => { + this.pushToTable(response); + }); - this.httpClient.get('/api/admin/disk_space').subscribe({ + this.httpClient.get(`${API_URL}/admin/disk_space`).subscribe({ next: (diskSpace) => { this.diskSpace = diskSpace; }, @@ -73,8 +89,14 @@ export class OverseerImageListComponent // Push the values that will be displayed in the table // to the datasource private pushToTable(value: OverseerImage | OverseerImage[]) { - if (!value) return; - value instanceof Array ? this.overseerImages.push(...value) : this.overseerImages.push(value); + if (!value) { + return; + } + if (value instanceof Array) { + this.overseerImages.push(...value); + } else { + this.overseerImages.push(value); + } this.dataSource.sort = this.sort; } @@ -102,7 +124,7 @@ export class OverseerImageListComponent deleteOverseerImage(image: OverseerImage) { this.overseerImageService.delete(image).subscribe( - ((response) => { + ((_response) => { this.cancelEdit(); this.overseerImages.splice(this.overseerImages.indexOf(image), 1); this.dataSource.data = this.overseerImages; diff --git a/src/app/admin/modals/create-new-unit-modal/create-new-unit-modal-content.component.html b/src/app/admin/modals/create-new-unit-modal/create-new-unit-modal-content.component.html index 2cb803dfae..466d9ab2e3 100644 --- a/src/app/admin/modals/create-new-unit-modal/create-new-unit-modal-content.component.html +++ b/src/app/admin/modals/create-new-unit-modal/create-new-unit-modal-content.component.html @@ -1,42 +1,59 @@

Create Unit

-
+
Unit Code - + Unit Name - + Teaching Period - + Custom teaching period - - {{ tp.name }} - + @for (tp of teachingPeriods; track tp; let i = $index) { + + {{ tp.name }} + + } - @if(showDates) { - - Enter a date range - - - - - DD/MM/YYYY - DD/MM/YYYY - - - + @if (showDates) { + + Enter a date range + + + + + DD/MM/YYYY - DD/MM/YYYY + + + } - +
diff --git a/src/app/admin/modals/create-new-unit-modal/create-new-unit-modal-content.component.ts b/src/app/admin/modals/create-new-unit-modal/create-new-unit-modal-content.component.ts index e3b5fb6e0a..e844877f73 100644 --- a/src/app/admin/modals/create-new-unit-modal/create-new-unit-modal-content.component.ts +++ b/src/app/admin/modals/create-new-unit-modal/create-new-unit-modal-content.component.ts @@ -1,12 +1,15 @@ -import { Component, OnInit } from '@angular/core'; -import { MatDialogRef } from '@angular/material/dialog'; -import { TeachingPeriod } from 'src/app/api/models/teaching-period'; -import { TeachingPeriodService } from 'src/app/api/services/teaching-period.service'; -import { UnitService } from 'src/app/api/services/unit.service'; -import { AlertService } from 'src/app/common/services/alert.service'; +import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core'; +import {MatDialogRef} from '@angular/material/dialog'; +import {TeachingPeriod} from 'src/app/api/models/teaching-period'; +import {TeachingPeriodService} from 'src/app/api/services/teaching-period.service'; +import {UnitService} from 'src/app/api/services/unit.service'; +import {AlertService} from 'src/app/common/services/alert.service'; + @Component({ selector: 'create-new-unit-modal-content', templateUrl: 'create-new-unit-modal-content.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class CreateNewUnitModalContentComponent implements OnInit { constructor( @@ -27,7 +30,11 @@ export class CreateNewUnitModalContentComponent implements OnInit { }); } - public createUnit(unit: { unitName: string; unitCode: string; selectedTeachingPeriod: number }): void { + public createUnit(unit: { + unitName: string; + unitCode: string; + selectedTeachingPeriod: number; + }): void { let newUnit; if (this.selectedTeachingPeriod === null) { @@ -61,7 +68,6 @@ export class CreateNewUnitModalContentComponent implements OnInit { } public handleChangeTeachingPeriod(teachingPeriod: number | string): void { if (typeof teachingPeriod === 'string') { - teachingPeriod = null; this.showDates = true; } else { this.showDates = false; diff --git a/src/app/admin/states/f-units/f-units.component.spec.ts b/src/app/admin/modals/create-new-unit-modal/create-new-unit-modal.component.html similarity index 100% rename from src/app/admin/states/f-units/f-units.component.spec.ts rename to src/app/admin/modals/create-new-unit-modal/create-new-unit-modal.component.html diff --git a/src/app/admin/modals/create-new-unit-modal/create-new-unit-modal.component.ts b/src/app/admin/modals/create-new-unit-modal/create-new-unit-modal.component.ts index e2e7fe5768..d3e7dde8f5 100644 --- a/src/app/admin/modals/create-new-unit-modal/create-new-unit-modal.component.ts +++ b/src/app/admin/modals/create-new-unit-modal/create-new-unit-modal.component.ts @@ -1,10 +1,12 @@ -import { Component } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; -import { CreateNewUnitModalContentComponent } from './create-new-unit-modal-content.component'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {MatDialog} from '@angular/material/dialog'; +import {CreateNewUnitModalContentComponent} from './create-new-unit-modal-content.component'; @Component({ selector: 'create-new-unit-modal', - template: '', + templateUrl: './create-new-unit-modal.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class CreateNewUnitModal { constructor(public dialog: MatDialog) {} diff --git a/src/app/admin/modals/create-unit-modal/create-unit-modal.coffee b/src/app/admin/modals/create-unit-modal/create-unit-modal.coffee deleted file mode 100644 index 22bb0e6c71..0000000000 --- a/src/app/admin/modals/create-unit-modal/create-unit-modal.coffee +++ /dev/null @@ -1,30 +0,0 @@ -angular.module('doubtfire.admin.modals.create-unit-modal', []) - -# -# This modal allows administrators to quickly create new units -# -.factory('CreateUnitModal', ($modal) -> - CreateUnitModal = {} - CreateUnitModal.show = (units) -> - $modal.open - controller: 'CreateUnitModalCtrl' - templateUrl: 'admin/modals/create-unit-modal/create-unit-modal.tpl.html' - resolve: - units: -> units - CreateUnitModal -) -.controller('CreateUnitModalCtrl', ($scope, $modalInstance, DoubtfireConstants, alertService, units, newUnitService, analyticsService) -> - analyticsService.event 'Unit Admin', 'Started to Create Unit' - $scope.units = units - $scope.unit = { code: null, name: null } - $scope.saveUnit = -> - newUnitService.create( {unit: $scope.unit} ).subscribe( - next: (response) -> - alertService.success( "Unit created.", 2000) - $modalInstance.close() - error: (response) -> - alertService.error response, 6000 - ) - # Get the configurable, external name of Doubtfire - $scope.externalName = DoubtfireConstants.ExternalName -) diff --git a/src/app/admin/modals/create-unit-modal/create-unit-modal.tpl.html b/src/app/admin/modals/create-unit-modal/create-unit-modal.tpl.html deleted file mode 100644 index a73b646929..0000000000 --- a/src/app/admin/modals/create-unit-modal/create-unit-modal.tpl.html +++ /dev/null @@ -1,24 +0,0 @@ -
-
- - - -
-
diff --git a/src/app/admin/modals/modals.coffee b/src/app/admin/modals/modals.coffee deleted file mode 100644 index 0cc9ff7c5b..0000000000 --- a/src/app/admin/modals/modals.coffee +++ /dev/null @@ -1,3 +0,0 @@ -angular.module('doubtfire.admin.modals', [ - 'doubtfire.admin.modals.create-unit-modal' -]) diff --git a/src/app/admin/states/teaching-periods/teaching-period-list/new-teaching-period-dialog.component.html b/src/app/admin/states/teaching-periods/teaching-period-list/new-teaching-period-dialog.component.html index b7c9cf66d7..cce85cf326 100644 --- a/src/app/admin/states/teaching-periods/teaching-period-list/new-teaching-period-dialog.component.html +++ b/src/app/admin/states/teaching-periods/teaching-period-list/new-teaching-period-dialog.component.html @@ -1,7 +1,9 @@ -
- - - +
+ + + + @@ -9,23 +11,35 @@ --> -
- - +
-
+ Teaching Period Name - + Teaching Period Year - + @@ -33,17 +47,17 @@ @@ -55,10 +69,10 @@ Active Until DD/MM/YYYY @@ -68,34 +82,34 @@ -

Teaching Breaks for {{ newOrSelectedTeachingPeriod.name }}

+

Teaching Breaks for {{ newOrSelectedTeachingPeriod.name }}

- @for (break of newOrSelectedTeachingPeriod.breaksCache.values | async; track break) { - - - - {{ break.startDate | date }} - {{ break.numberOfWeeks }} week(s) + @for (break of teachingBreaks$ | async; track break) { + + + + {{ break.startDate | date }} + {{ break.numberOfWeeks }} week(s) + + + - - - - + } - - + + Break Start Date MM/DD/YYYY @@ -105,13 +119,19 @@

Teaching Breaks for {{ newOrSelectedTeachingPeriod.name }}

Number of weeks - +
- - + - - + + - - + + - - + + - - + + - - + - - - - + +
Active - + Active + Name{{ element.name }}Name{{ element.name }} Start Date{{ element.startDate | date }}Start Date{{ element.startDate | date }} End date{{ element.endDate | date }}End date{{ element.endDate | date }} Active until{{ element.activeUntil | date }}Active until{{ element.activeUntil | date }} Actions + Actions
- @@ -63,19 +63,11 @@

Teaching periods

-
- -
-
diff --git a/src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.spec.ts b/src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.spec.ts index 8d7bb7452e..6b38a84b69 100644 --- a/src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.spec.ts +++ b/src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.spec.ts @@ -1,6 +1,12 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatDialog} from '@angular/material/dialog'; +import {TeachingPeriodService} from 'src/app/api/services/teaching-period.service'; +import {TeachingPeriodUnitImportService} from '../teaching-period-unit-import/teaching-period-unit-import.dialog'; +import {TeachingPeriodListComponent} from './teaching-period-list.component'; -import { TeachingPeriodListComponent } from './teaching-period-list.component'; +const emptyProvider = {}; describe('TeachingPeriodListComponent', () => { let component: TeachingPeriodListComponent; @@ -8,13 +14,21 @@ describe('TeachingPeriodListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ TeachingPeriodListComponent ] + declarations: [TeachingPeriodListComponent], + providers: [ + {provide: TeachingPeriodService, useValue: emptyProvider}, + {provide: MatDialog, useValue: emptyProvider}, + {provide: TeachingPeriodUnitImportService, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], }) - .compileComponents(); + .overrideComponent(TeachingPeriodListComponent, {set: {template: ''}}) + .compileComponents(); + }); + beforeEach(() => { fixture = TestBed.createComponent(TeachingPeriodListComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.ts b/src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.ts index f4099456cf..430dcd9dfd 100644 --- a/src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.ts +++ b/src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.ts @@ -1,25 +1,28 @@ -import {Component, Inject, OnInit, ViewChild} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Inject, OnInit, ViewChild} from '@angular/core'; import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog'; import {MatPaginator} from '@angular/material/paginator'; import {MatSort, Sort} from '@angular/material/sort'; import {MatTableDataSource} from '@angular/material/table'; +import {Observable} from 'rxjs'; import {TeachingPeriodBreak} from 'src/app/api/models/teaching-period'; import {TeachingPeriod} from 'src/app/api/models/teaching-period'; import {TeachingPeriodBreakService} from 'src/app/api/services/teaching-period-break.service'; import {TeachingPeriodService} from 'src/app/api/services/teaching-period.service'; -import {TeachingPeriodUnitImportService} from '../teaching-period-unit-import/teaching-period-unit-import.dialog'; import {AlertService} from 'src/app/common/services/alert.service'; +import {TeachingPeriodUnitImportService} from '../teaching-period-unit-import/teaching-period-unit-import.dialog'; @Component({ selector: 'f-teaching-period-list', templateUrl: './teaching-period-list.component.html', styleUrls: ['./teaching-period-list.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TeachingPeriodListComponent implements OnInit { @ViewChild(MatSort) sort = new MatSort(); @ViewChild(MatPaginator) paginator: MatPaginator; - public dataSource = new MatTableDataSource(); + public dataSource: MatTableDataSource = new MatTableDataSource(); displayedColumns: string[] = ['active', 'name', 'startDate', 'endDate', 'activeUntil', 'actions']; @@ -31,7 +34,7 @@ export class TeachingPeriodListComponent implements OnInit { ngOnInit(): void { // update the Teaching Periods - this.teachingPeriodsService.query().subscribe((_) => {}); + this.teachingPeriodsService.query().subscribe(); // Bind to the Teaching Periods this.teachingPeriodsService.cache.values.subscribe((teachingPeriods) => { @@ -94,6 +97,8 @@ export class TeachingPeriodListComponent implements OnInit { @Component({ selector: 'f-new-teaching-period-dialog', templateUrl: 'new-teaching-period-dialog.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class NewTeachingPeriodDialogComponent { constructor( @@ -103,7 +108,10 @@ export class NewTeachingPeriodDialogComponent { public teachingPeriodBreakService: TeachingPeriodBreakService, public alertService: AlertService, ) {} - public newOrSelectedTeachingPeriod = this.data.teachingPeriod || new TeachingPeriod(); + public newOrSelectedTeachingPeriod: TeachingPeriod = + this.data.teachingPeriod || new TeachingPeriod(); + public teachingBreaks$: Observable = this.newOrSelectedTeachingPeriod + .breaksCache.values as Observable; public tempBreak = new TeachingPeriodBreak(); @@ -142,6 +150,7 @@ export class NewTeachingPeriodDialogComponent { observer.subscribe({ next: (teachingPeriod) => { this.alertService.success(`${teachingPeriod.name} saved`); + this.dialogRef.close(teachingPeriod); }, error: (response) => { this.alertService.error(`Error saving teaching period. ${response}`); diff --git a/src/app/admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog.html b/src/app/admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog.html index 290fcbb3ba..4c03aa36e7 100644 --- a/src/app/admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog.html +++ b/src/app/admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog.html @@ -2,17 +2,21 @@

Import Units Into {{ data.teachingPeriod.name }}

- - + - - + - + - + - + - - + - - + +
Unit Code + Unit Code - + Source Unit + Source Unit Import Units Into {{ data.teachingPeriod.name }} - Unit Name + Unit Name @if (unitToImport.sourceUnit) { {{ unitToImport.sourceUnit.name }} } @if (!unitToImport.sourceUnit) { @@ -37,18 +41,18 @@

Import Units Into {{ data.teachingPeriod.name }}

-
Main Convenor + Main Convenor - @for (staff of unitToImport.filteredStaff | async; track staff) { + @for (staff of filteredStaffFor(unitToImport) | async; track staff) { {{staff.name}} } @@ -57,32 +61,32 @@

Import Units Into {{ data.teachingPeriod.name }}

-
Status + Status {{ statusForUnit(unitToImport) }} +
-
+
Unit Code(s) - + - +
diff --git a/src/app/admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog.ts b/src/app/admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog.ts index 53416abcc8..bf46e53458 100644 --- a/src/app/admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog.ts +++ b/src/app/admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog.ts @@ -1,10 +1,23 @@ -import { Component, Inject, Injectable, OnInit, ViewChild } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef, MatDialog } from '@angular/material/dialog'; -import { FormControl } from '@angular/forms'; -import { Unit, TeachingPeriod, User, UserService, UnitService } from 'src/app/api/models/doubtfire-model'; -import { MatTable, MatTableDataSource } from '@angular/material/table'; -import { GlobalStateService } from 'src/app/projects/states/index/global-state.service'; -import { Observable, map, startWith } from 'rxjs'; +import { + ChangeDetectionStrategy, + Component, + Inject, + Injectable, + OnInit, + ViewChild, +} from '@angular/core'; +import {FormControl} from '@angular/forms'; +import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog'; +import {MatTable, MatTableDataSource} from '@angular/material/table'; +import {Observable, map, startWith} from 'rxjs'; +import { + TeachingPeriod, + Unit, + UnitService, + User, + UserService, +} from 'src/app/api/models/doubtfire-model'; +import {GlobalStateService} from 'src/app/projects/states/index/global-state.service'; export interface TeachingPeriodUnitImportData { teachingPeriod: TeachingPeriod; @@ -15,7 +28,7 @@ interface UnitImportData { unitName?: string; sourceUnit: Unit; convenor: User; - relatedUnits?: { value: Unit; text: string }[]; + relatedUnits?: {value: Unit; text: string}[]; done?: boolean; convenorFormControl: FormControl; filteredStaff: Observable; @@ -27,7 +40,7 @@ export class TeachingPeriodUnitImportService { openImportUnitsDialog(teachingPeriod: TeachingPeriod): void { const dialogRef = this.dialog.open(TeachingPeriodUnitImportDialogComponent, { - data: { teachingPeriod: teachingPeriod }, + data: {teachingPeriod: teachingPeriod}, }); dialogRef.afterClosed().subscribe(() => { @@ -44,9 +57,11 @@ export class TeachingPeriodUnitImportService { selector: 'f-teaching-period-unit-import', templateUrl: 'teaching-period-unit-import.dialog.html', styleUrls: ['teaching-period-unit-import.dialog.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TeachingPeriodUnitImportDialogComponent implements OnInit { - @ViewChild(MatTable, { static: true }) table: MatTable; + @ViewChild(MatTable, {static: true}) table: MatTable; /** * The list of unit related data for the import. @@ -65,7 +80,18 @@ export class TeachingPeriodUnitImportDialogComponent implements OnInit { */ public codesToAdd: string = ''; - public displayedColumns: string[] = ['unitCode', 'sourceUnit', 'unitName', 'convenor', 'status', 'actions']; + public displayedColumns: string[] = [ + 'unitCode', + 'sourceUnit', + 'unitName', + 'convenor', + 'status', + 'actions', + ]; + + public filteredStaffFor(unitToImport: UnitImportData): Observable { + return unitToImport.filteredStaff; + } constructor( public dialogRef: MatDialogRef, @@ -104,8 +130,8 @@ export class TeachingPeriodUnitImportDialogComponent implements OnInit { private loadAllUnits() { // Load all units - this.unitService.query(undefined, { params: { include_in_active: true } }).subscribe({ - next: (success) => { + this.unitService.query(undefined, {params: {include_in_active: true}}).subscribe({ + next: () => { return; }, error: (failure) => { @@ -133,12 +159,12 @@ export class TeachingPeriodUnitImportDialogComponent implements OnInit { value.sourceUnit = value.relatedUnits.length > 0 ? value.relatedUnits[0].value : null; } - public relatedUnits(code: string): { value: Unit; text: string }[] { + public relatedUnits(code: string): {value: Unit; text: string}[] { return this.allUnits .filter((u) => u.code.includes(code) || code.includes(u.code)) .sort((a, b) => b.startDate.valueOf() - a.startDate.valueOf()) .map((u) => { - return { value: u, text: u.codeAndPeriod }; + return {value: u, text: u.codeAndPeriod}; }); } @@ -147,10 +173,18 @@ export class TeachingPeriodUnitImportDialogComponent implements OnInit { } public statusForUnit(value: UnitImportData): string { - if (value.done) return 'Done!'; - if (value.done !== undefined && !value.done) return 'Error! - check log'; - if (!value.sourceUnit) return 'Create new unit'; - if (this.teachigPeriod.hasUnitLike(value.sourceUnit)) return 'Skip - Already in teaching period'; + if (value.done) { + return 'Done!'; + } + if (value.done !== undefined && !value.done) { + return 'Error! - check log'; + } + if (!value.sourceUnit) { + return 'Create new unit'; + } + if (this.teachigPeriod.hasUnitLike(value.sourceUnit)) { + return 'Skip - Already in teaching period'; + } if (this.unitsToImport.filter((u) => u.unitCode === value.sourceUnit.code).length > 1) { return 'Duplicate - Source unit appears twice'; } @@ -173,12 +207,18 @@ export class TeachingPeriodUnitImportDialogComponent implements OnInit { public addUnitsByCode() { const codes = this.codesToAdd.split(',').map((code) => code.trim()); for (const code of codes) { - if (code.length == 0) continue; - if (this.unitsToImport.find((u) => u.unitCode === code)) continue; + if (code.length == 0) { + continue; + } + if (this.unitsToImport.find((u) => u.unitCode === code)) { + continue; + } const relatedUnits = this.relatedUnits(code); const sourceUnit = relatedUnits.length > 0 ? relatedUnits[0].value : null; - const formControl = new FormControl(sourceUnit?.mainConvenor?.user || sourceUnit?.mainConvenorUser); + const formControl: FormControl = new FormControl( + sourceUnit?.mainConvenor?.user || sourceUnit?.mainConvenorUser, + ); this.unitsToImport.push({ unitCode: code, @@ -251,7 +291,7 @@ export class TeachingPeriodUnitImportDialogComponent implements OnInit { }, }) .subscribe({ - next: (newUnit: Unit) => { + next: () => { unitToImport.done = true; this.importUnit(idx + 1); }, @@ -265,7 +305,9 @@ export class TeachingPeriodUnitImportDialogComponent implements OnInit { private importUnit(idx: number) { // Stop when past last unit to import - if (idx >= this.unitsToImport.length) return; + if (idx >= this.unitsToImport.length) { + return; + } const unitToImport = this.unitsToImport[idx]; const code = unitToImport.sourceUnit ? unitToImport.sourceUnit.code : unitToImport.unitCode; diff --git a/src/app/admin/states/f-units/f-units.component.html b/src/app/admin/states/units/units.component.html similarity index 71% rename from src/app/admin/states/f-units/f-units.component.html rename to src/app/admin/states/units/units.component.html index 3fedd9d088..b7ef11e74c 100644 --- a/src/app/admin/states/f-units/f-units.component.html +++ b/src/app/admin/states/units/units.component.html @@ -1,5 +1,5 @@ -
-
+
+

{{ title }}

@@ -13,68 +13,68 @@

{{ title }}

- - + - - + + - - - + - - + + - - + + - - + - + @if (mode === 'tutor') { } @if (mode === 'admin') { } @if (mode === 'student') { }
Unit Code + Unit Code Name{{ element.name }}Name{{ element.name }} Unit Role + {{ element.unit_role }} Teaching Period + Teaching Period {{ element.teaching_period }} Start Date{{ element.start_date | date: 'EEE d MMM y' }}Start Date{{ element.start_date | date: 'EEE d MMM y' }} End Date{{ element.end_date | date: 'EEE d MMM y' }}End Date{{ element.end_date | date: 'EEE d MMM y' }} Active + Active @if (element.teachingPeriod) { @if (element.teachingPeriod.active && element.active) { @@ -96,29 +96,26 @@

{{ title }}

@@ -126,7 +123,7 @@

{{ title }}

@if (mode === 'admin') { - diff --git a/src/app/admin/states/f-units/f-units.component.scss b/src/app/admin/states/units/units.component.scss similarity index 100% rename from src/app/admin/states/f-units/f-units.component.scss rename to src/app/admin/states/units/units.component.scss diff --git a/src/app/admin/states/units/units.component.spec.ts b/src/app/admin/states/units/units.component.spec.ts new file mode 100644 index 0000000000..b5ca421a3a --- /dev/null +++ b/src/app/admin/states/units/units.component.spec.ts @@ -0,0 +1,39 @@ +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {ActivatedRoute} from '@angular/router'; +import {UnitService} from 'src/app/api/services/unit.service'; +import {GlobalStateService} from 'src/app/projects/states/index/global-state.service'; +import {CreateNewUnitModal} from '../../modals/create-new-unit-modal/create-new-unit-modal.component'; +import {FUnitsComponent} from './units.component'; + +const emptyProvider = {}; + +describe('FUnitsComponent', () => { + let component: FUnitsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [FUnitsComponent], + providers: [ + {provide: CreateNewUnitModal, useValue: emptyProvider}, + {provide: GlobalStateService, useValue: emptyProvider}, + {provide: UnitService, useValue: emptyProvider}, + {provide: ActivatedRoute, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(FUnitsComponent, {set: {template: ''}}) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FUnitsComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/admin/states/f-units/f-units.component.ts b/src/app/admin/states/units/units.component.ts similarity index 94% rename from src/app/admin/states/f-units/f-units.component.ts rename to src/app/admin/states/units/units.component.ts index 03ce4ddad6..86f03ab90d 100644 --- a/src/app/admin/states/f-units/f-units.component.ts +++ b/src/app/admin/states/units/units.component.ts @@ -1,16 +1,24 @@ -import {Component, AfterViewInit, ViewChild, Input, OnInit} from '@angular/core'; -import {Unit} from 'src/app/api/models/unit'; -import {UnitRole} from 'src/app/api/models/unit-role'; -import {MatTable, MatTableDataSource} from '@angular/material/table'; -import {MatSort, Sort} from '@angular/material/sort'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + Input, + OnInit, + ViewChild, +} from '@angular/core'; import {MatPaginator} from '@angular/material/paginator'; -import {CreateNewUnitModal} from '../../modals/create-new-unit-modal/create-new-unit-modal.component'; +import {MatSort, Sort} from '@angular/material/sort'; +import {MatTable, MatTableDataSource} from '@angular/material/table'; +import {ActivatedRoute} from '@angular/router'; import {Project} from 'src/app/api/models/project'; -import {GlobalStateService} from 'src/app/projects/states/index/global-state.service'; +import {Unit} from 'src/app/api/models/unit'; +import {UnitRole} from 'src/app/api/models/unit-role'; import {User} from 'src/app/api/models/user/user'; import {UnitService} from 'src/app/api/services/unit.service'; +import {GlobalStateService} from 'src/app/projects/states/index/global-state.service'; +import {CreateNewUnitModal} from '../../modals/create-new-unit-modal/create-new-unit-modal.component'; -type IUnitOrProject = { +interface IUnitOrProject { id: number; unit_code: string; code: string; @@ -26,12 +34,14 @@ type IUnitOrProject = { matchesTutorialEnrolments?: (filter: string) => boolean; matchesGroup?: (filter: string) => boolean; matches: (filter: string) => boolean; -}; +} @Component({ selector: 'f-units', - templateUrl: './f-units.component.html', - styleUrls: ['./f-units.component.scss'], + templateUrl: './units.component.html', + styleUrls: ['./units.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class FUnitsComponent implements OnInit, AfterViewInit { @ViewChild(MatTable, {static: false}) table: MatTable; @@ -63,11 +73,13 @@ export class FUnitsComponent implements OnInit, AfterViewInit { private createUnitDialog: CreateNewUnitModal, private globalStateService: GlobalStateService, private unitService: UnitService, + private route: ActivatedRoute, ) {} units: IUnitOrProject[] = []; ngOnInit(): void { + this.mode = this.mode ?? this.route.snapshot.data.mode; if (this.mode === 'tutor') { this.title = 'View all units you teach'; @@ -84,7 +96,7 @@ export class FUnitsComponent implements OnInit, AfterViewInit { this.globalStateService.onLoad(() => { this.unitService.query(undefined, {params: {include_in_active: true}}).subscribe({ - next: (units) => { + next: () => { this.globalStateService.loadedUnits.values.subscribe( (loadedUnits) => (this.dataSource.data = this.mapUnitOrProjectsToColumns(loadedUnits)), diff --git a/src/app/admin/states/f-users/f-users.component.html b/src/app/admin/states/users/users.component.html similarity index 65% rename from src/app/admin/states/f-users/f-users.component.html rename to src/app/admin/states/users/users.component.html index 4adb14a1a6..7557ff8dd7 100644 --- a/src/app/admin/states/f-users/f-users.component.html +++ b/src/app/admin/states/users/users.component.html @@ -1,6 +1,6 @@
-
+

{{ externalName }} Users

Users Administration View

@@ -9,9 +9,9 @@

Users Administration View

@@ -20,76 +20,76 @@

Users Administration View

- - + - - + + - - + + - - + + - - + - - + + - - + +
- + + First Name{{ user.firstName }}First Name{{ user.firstName }} Last Name{{ user.lastName }}Last Name{{ user.lastName }} Username{{ user.username }}Username{{ user.username }} Email + Email {{ user.email }} System Role{{ user.systemRole }}System Role{{ user.systemRole }}
- + - + Bulk users operations
diff --git a/src/app/admin/states/f-users/f-users.component.scss b/src/app/admin/states/users/users.component.scss similarity index 100% rename from src/app/admin/states/f-users/f-users.component.scss rename to src/app/admin/states/users/users.component.scss diff --git a/src/app/admin/states/f-users/f-users.component.ts b/src/app/admin/states/users/users.component.ts similarity index 68% rename from src/app/admin/states/f-users/f-users.component.ts rename to src/app/admin/states/users/users.component.ts index 7865e6801f..63a39166bd 100644 --- a/src/app/admin/states/f-users/f-users.component.ts +++ b/src/app/admin/states/users/users.component.ts @@ -1,26 +1,42 @@ -import { Component, AfterViewInit, ViewChild, OnDestroy, OnInit } from '@angular/core'; -import { MatTable, MatTableDataSource } from '@angular/material/table'; -import { MatSort, Sort } from '@angular/material/sort'; -import { User } from 'src/app/api/models/doubtfire-model'; -import { MatPaginator } from '@angular/material/paginator'; -import { UserService } from 'src/app/api/models/doubtfire-model'; -import { EditProfileDialogService } from 'src/app/common/modals/edit-profile-dialog/edit-profile-dialog.service'; -import { Subscription } from 'rxjs'; -import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; -import { FileDownloaderService } from 'src/app/common/file-downloader/file-downloader.service'; -import { AlertService } from 'src/app/common/services/alert.service'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + OnDestroy, + OnInit, + ViewChild, +} from '@angular/core'; +import {MatPaginator} from '@angular/material/paginator'; +import {MatSort, Sort} from '@angular/material/sort'; +import {MatTable, MatTableDataSource} from '@angular/material/table'; +import {Subscription} from 'rxjs'; +import {User} from 'src/app/api/models/doubtfire-model'; +import {UserService} from 'src/app/api/models/doubtfire-model'; +import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; +import {EditProfileDialogService} from 'src/app/common/modals/edit-profile-dialog/edit-profile-dialog.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; @Component({ selector: 'f-users', - templateUrl: './f-users.component.html', - styleUrls: ['./f-users.component.scss'], + templateUrl: './users.component.html', + styleUrls: ['./users.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class FUsersComponent implements OnInit, AfterViewInit, OnDestroy { - @ViewChild(MatTable, { static: false }) table: MatTable; - @ViewChild(MatSort, { static: false }) sort: MatSort; - @ViewChild(MatPaginator, { static: false }) paginator: MatPaginator; - - displayedColumns: string[] = ['avatar', 'firstName', 'lastName', 'username', 'email', 'systemRole']; + @ViewChild(MatTable, {static: false}) table: MatTable; + @ViewChild(MatSort, {static: false}) sort: MatSort; + @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; + + displayedColumns: string[] = [ + 'avatar', + 'firstName', + 'lastName', + 'username', + 'email', + 'systemRole', + ]; public dataSource: MatTableDataSource; public filter: string; dataload: boolean; @@ -37,6 +53,7 @@ export class FUsersComponent implements OnInit, AfterViewInit, OnDestroy { private alerts: AlertService, ) { this.dataload = false; + this.dataSource = new MatTableDataSource([]); } ngOnInit(): void { @@ -49,7 +66,7 @@ export class FUsersComponent implements OnInit, AfterViewInit, OnDestroy { } ngAfterViewInit(): void { - this.dataSource = new MatTableDataSource(this.userService.cache.currentValuesClone()); + this.dataSource.data = this.userService.cache.currentValuesClone(); this.dataSource.paginator = this.paginator; this.dataSource.sort = this.sort; this.dataSource.filterPredicate = (data, filter: string) => data.matches(filter); @@ -86,13 +103,15 @@ export class FUsersComponent implements OnInit, AfterViewInit, OnDestroy { error_string += error.message + '\n'; }); - max_full_errors > num_errors ? (error_string += `... and ${max_full_errors - num_errors} more`) : null; + if (num_errors > max_full_errors) { + error_string += `... and ${num_errors - max_full_errors} more`; + } this.alerts.error(error_string); this.userService.query(); } - public showUserModal(user: User) { + public showUserModal(user?: User) { const userToShow = user ? user : this.userService.createInstanceFrom({}); this.editProfileDialogService.openDialog(userToShow, 'edit'); } diff --git a/src/app/admin/tii-action-log/tii-action-log.component.html b/src/app/admin/tii-action-log/tii-action-log.component.html index e61877520d..4590ee9977 100644 --- a/src/app/admin/tii-action-log/tii-action-log.component.html +++ b/src/app/admin/tii-action-log/tii-action-log.component.html @@ -1,84 +1,89 @@
-
+

Turnitin Actions

- +
- - + - - + - - + - - + - - + - - + - - + - - + - + - +
Action Type + Action Type {{ action.description }} Last Run + Last Run {{ action.lastRun ? (action.lastRun | date: 'd LLL y') : '' }} Retries + Retries {{ action.retries }} Retry? + Retry? {{ action.retry }} Error Code + Error Code {{ action.errorCode }} Complete? + Complete? {{ action.complete }} Error Message + Error Message {{ action.errorMessage }} + @if (!(action.complete || action.retry)) { - + }
diff --git a/src/app/admin/tii-action-log/tii-action-log.component.spec.ts b/src/app/admin/tii-action-log/tii-action-log.component.spec.ts index a3caf79062..d9cd6e6310 100644 --- a/src/app/admin/tii-action-log/tii-action-log.component.spec.ts +++ b/src/app/admin/tii-action-log/tii-action-log.component.spec.ts @@ -1,18 +1,32 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TiiActionService} from 'src/app/api/services/tii-action.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {TiiActionLogComponent} from './tii-action-log.component'; -import { TiiActionLogComponent } from './tii-action-log.component'; +const emptyProvider = {}; describe('TiiActionLogComponent', () => { let component: TiiActionLogComponent; let fixture: ComponentFixture; + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [TiiActionLogComponent], + providers: [ + {provide: TiiActionService, useValue: emptyProvider}, + {provide: AlertService, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(TiiActionLogComponent, {set: {template: ''}}) + .compileComponents(); + }); + beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [TiiActionLogComponent] - }); fixture = TestBed.createComponent(TiiActionLogComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/admin/tii-action-log/tii-action-log.component.ts b/src/app/admin/tii-action-log/tii-action-log.component.ts index ba2f496d00..1f7559a9f9 100644 --- a/src/app/admin/tii-action-log/tii-action-log.component.ts +++ b/src/app/admin/tii-action-log/tii-action-log.component.ts @@ -1,28 +1,39 @@ -import { AfterViewInit, Component, ViewChild } from '@angular/core'; -import { MatPaginator } from '@angular/material/paginator'; -import { MatSort, Sort } from '@angular/material/sort'; -import { MatTable, MatTableDataSource } from '@angular/material/table'; -import { TiiAction } from 'src/app/api/models/doubtfire-model'; -import { TiiActionService } from 'src/app/api/services/tii-action.service'; -import { AlertService } from 'src/app/common/services/alert.service'; +import {AfterViewInit, ChangeDetectionStrategy, Component, ViewChild} from '@angular/core'; +import {MatPaginator} from '@angular/material/paginator'; +import {MatSort, Sort} from '@angular/material/sort'; +import {MatTable, MatTableDataSource} from '@angular/material/table'; +import {TiiAction} from 'src/app/api/models/doubtfire-model'; +import {TiiActionService} from 'src/app/api/services/tii-action.service'; +import {AlertService} from 'src/app/common/services/alert.service'; @Component({ selector: 'f-tii-action-log', templateUrl: './tii-action-log.component.html', - styleUrls: ['./tii-action-log.component.scss'] + styleUrls: ['./tii-action-log.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TiiActionLogComponent implements AfterViewInit { - @ViewChild(MatTable, { static: false }) table: MatTable; - @ViewChild(MatSort, { static: false }) sort: MatSort; - @ViewChild(MatPaginator, { static: false }) paginator: MatPaginator; + @ViewChild(MatTable, {static: false}) table: MatTable; + @ViewChild(MatSort, {static: false}) sort: MatSort; + @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; public tiiActionsSource: MatTableDataSource; - public columns: string[] = ['type', 'lastRun', 'retries', 'retry', 'errorMessage', 'tiiActionTools']; //, 'complete', 'retries', 'lastRun', 'errorCode', 'log', 'tiiActionAction']; + public selectedTaskDefinition: TiiAction | null = null; + public columns: string[] = [ + 'type', + 'lastRun', + 'retries', + 'retry', + 'errorMessage', + 'tiiActionTools', + ]; //, 'complete', 'retries', 'lastRun', 'errorCode', 'log', 'tiiActionAction']; public filter: string; - constructor(private tiiActionService: TiiActionService, private alertService: AlertService) { - - } + constructor( + private tiiActionService: TiiActionService, + private alertService: AlertService, + ) {} ngAfterViewInit(): void { this.tiiActionService.query().subscribe((actions) => { @@ -30,8 +41,10 @@ export class TiiActionLogComponent implements AfterViewInit { this.tiiActionsSource = new MatTableDataSource(actions); this.tiiActionsSource.paginator = this.paginator; this.tiiActionsSource.sort = this.sort; - this.tiiActionsSource.filterPredicate = (data: any, filter: string) => data.matches(filter); - + this.tiiActionsSource.filterPredicate = ( + data: TiiAction & {matches(filter: string): boolean}, + filter: string, + ) => data.matches(filter); }); } @@ -67,20 +80,20 @@ export class TiiActionLogComponent implements AfterViewInit { } public retryAction(action: TiiAction) { - this.tiiActionService.put(action, { - body: { - action: 'retry' - } - }).subscribe({ - next: (updatedAction) => { - action.retry = true; - this.alertService.success('Action has been queued for retry'); - }, - error: (error) => { - this.alertService.error('Failed to queue action for retry'); - } - }); + this.tiiActionService + .put(action, { + body: { + action: 'retry', + }, + }) + .subscribe({ + next: () => { + action.retry = true; + this.alertService.success('Action has been queued for retry'); + }, + error: (error) => { + this.alertService.error(`Failed to queue action for retry: ${error}`); + }, + }); } - - } diff --git a/src/app/ajs-upgraded-providers.ts b/src/app/ajs-upgraded-providers.ts deleted file mode 100644 index 795869225d..0000000000 --- a/src/app/ajs-upgraded-providers.ts +++ /dev/null @@ -1,118 +0,0 @@ -import {InjectionToken} from '@angular/core'; - -// Define an injection token for injecting globally into components. -// Use the name of the angularjs service as the injection token string -export const uploadSubmissionModal = new InjectionToken('uploadSubmissionModal'); -export const gradeTaskModal = new InjectionToken('gradeTaskModal'); -export const analyticsService = new InjectionToken('analyticsService'); -export const dateService = new InjectionToken('dateService'); -export const audioRecorder = new InjectionToken('audioRecorder'); -export const audioRecorderService = new InjectionToken('recorderService'); -export const csvUploadModalService = new InjectionToken('CsvUploadModalAngular'); -export const csvResultModalService = new InjectionToken('CsvResultModalAngular'); -export const confirmationModal = new InjectionToken('ConfirmationModal'); -export const unitStudentEnrolmentModal = new InjectionToken('UnitStudentEnrolmentModalAngular'); -export const commentsModal = new InjectionToken('CommentsModal'); -export const visualisations = new InjectionToken('Visualisation'); -export const rootScope = new InjectionToken('$rootScope'); -export const calendarModal = new InjectionToken('CalendarModal'); -export const aboutDoubtfireModal = new InjectionToken('AboutDoubtfireModal'); -export const plagiarismReportModal = new InjectionToken('PlagiarismReportModal'); - -// Define a provider for the above injection token... -// It will get the service from AngularJS via the factory -export const visualisationsProvider = { - provide: visualisations, - useFactory: (i) => i.get('Visualisation'), - deps: ['$injector'], -}; - -export const calendarModalProvider = { - provide: calendarModal, - useFactory: (i) => i.get('CalendarModal'), - deps: ['$injector'], -}; - -export const rootScopeProvider = { - provide: rootScope, - useFactory: (i) => i.get('$rootScope'), - deps: ['$injector'], -}; - -export const aboutDoubtfireModalProvider = { - provide: aboutDoubtfireModal, - useFactory: (i) => i.get('AboutDoubtfireModal'), - deps: ['$injector'], -}; - -export const plagiarismReportModalProvider = { - provide: plagiarismReportModal, - useFactory: (i) => i.get('PlagiarismReportModal'), - deps: ['$injector'], -}; - -export const commentsModalProvider = { - provide: commentsModal, - useFactory: (i) => i.get('CommentsModal'), - deps: ['$injector'], -}; - -export const uploadSubmissionModalProvider = { - provide: uploadSubmissionModal, - useFactory: (i) => i.get('UploadSubmissionModal'), - deps: ['$injector'], -}; - -export const gradeTaskModalProvider = { - provide: gradeTaskModal, - useFactory: (i) => i.get('GradeTaskModal'), - deps: ['$injector'], -}; - -export const analyticsServiceProvider = { - provide: analyticsService, - useFactory: (i) => i.get('analyticsService'), - deps: ['$injector'], -}; - -export const dateServiceProvider = { - provide: dateService, - useFactory: (i) => i.get('dateService'), - deps: ['$injector'], -}; - -export const AudioRecorderProvider = { - provide: audioRecorder, - useFactory: (i) => i.get('audioRecorder'), - deps: ['$injector'], -}; - -export const AudioRecorderServiceProvider = { - provide: audioRecorderService, - useFactory: (i) => i.get('recorderService'), - deps: ['$injector'], -}; - -export const CsvUploadModalProvider = { - provide: csvUploadModalService, - useFactory: (i) => i.get('CsvUploadModal'), - deps: ['$injector'], -}; - -export const CsvResultModalProvider = { - provide: csvResultModalService, - useFactory: (i) => i.get('CsvResultModal'), - deps: ['$injector'], -}; - -export const ConfirmationModalProvider = { - provide: confirmationModal, - useFactory: (i) => i.get('ConfirmationModal'), - deps: ['$injector'], -}; - -export const UnitStudentEnrolmentModalProvider = { - provide: unitStudentEnrolmentModal, - useFactory: (i) => i.get('UnitStudentEnrolmentModal'), - deps: ['$injector'], -}; diff --git a/src/app/api/models/activity-type/activity-type.ts b/src/app/api/models/activity-type/activity-type.ts index e75d09677d..d084a1901e 100644 --- a/src/app/api/models/activity-type/activity-type.ts +++ b/src/app/api/models/activity-type/activity-type.ts @@ -1,13 +1,16 @@ -import { Entity, EntityMapping } from 'ngx-entity-service'; +import {Entity, EntityMapping} from 'ngx-entity-service'; export class ActivityType extends Entity { id: number; name: string; abbreviation: string; - public override toJson(mappingData: EntityMapping, ignoreKeys?: string[]): object { + public override toJson( + mappingData: EntityMapping, + ignoreKeys?: string[], + ): object { return { - activity_type: super.toJson(mappingData, ignoreKeys) + activity_type: super.toJson(mappingData, ignoreKeys), }; } } diff --git a/src/app/api/models/campus/campus.ts b/src/app/api/models/campus/campus.ts index 9491e29e53..9df9ef23e6 100644 --- a/src/app/api/models/campus/campus.ts +++ b/src/app/api/models/campus/campus.ts @@ -1,4 +1,4 @@ -import { Entity, EntityMapping } from "ngx-entity-service"; +import {Entity, EntityMapping} from 'ngx-entity-service'; type campusModes = 'timetable' | 'automatic' | 'manual'; @@ -9,9 +9,12 @@ export class Campus extends Entity { abbreviation: string; timezone: string; - public override toJson(mappingData: EntityMapping, ignoreKeys?: string[]): object { + public override toJson( + mappingData: EntityMapping, + ignoreKeys?: string[], + ): object { return { - campus: super.toJson(mappingData, ignoreKeys) + campus: super.toJson(mappingData, ignoreKeys), }; } @@ -21,6 +24,9 @@ export class Campus extends Entity { * @param matchText the text to match */ public matches(matchText: string): boolean { - return this.name.toLowerCase().indexOf(matchText) >= 0 || this.abbreviation.toLowerCase().indexOf(matchText) >= 0; + return ( + this.name.toLowerCase().indexOf(matchText) >= 0 || + this.abbreviation.toLowerCase().indexOf(matchText) >= 0 + ); } } diff --git a/src/app/api/models/communication.ts b/src/app/api/models/communication.ts new file mode 100644 index 0000000000..cbc5990a96 --- /dev/null +++ b/src/app/api/models/communication.ts @@ -0,0 +1,142 @@ +import {Entity} from 'ngx-entity-service'; + +export type CommunicationScheduleRecurrence = 'none' | 'daily' | 'weekly' | 'monthly'; + +export class CommunicationSetSchedule extends Entity { + id?: number; + client_key?: string; + communication_set_id?: number; + name?: string; + active = true; + anchor_week = 1; + anchor_day = 'Monday'; + hour = 8; + minute = 0; + timezone = 'UTC'; + recurrence: CommunicationScheduleRecurrence = 'none'; + interval = 1; + repeat_count?: number; + until_at?: string; + ice_cube_schedule?: Record; + next_run_at?: string; + last_run_at?: string; + last_enqueued_at?: string; + + constructor(json?: Partial) { + super(); + Object.assign(this, json); + } +} + +export class CommunicationCondition extends Entity { + id: number; + type: string; + communication_rule_id: number; + operator: string; + target_grade?: number; + task_definition_id?: number; + task_statuses?: string[]; + task_status_count?: number; + task_target_grade?: number; + last_sign_in_at?: string; + spec_con_days?: number; + tutorial_id?: number; + tutorial_stream_id?: number; + campus_id?: number; + + constructor(json?: Partial) { + super(); + Object.assign(this, json); + } +} + +export interface CommunicationRulePreviewStudent { + first_name?: string; + last_name?: string; + preferred_name?: string; + full_name?: string; + username?: string; + student_id?: string; + campus?: string; + target_grade?: number; + spec_con_days?: number; + last_sign_in_at?: string; +} + +export interface CommunicationRulePreviewAllocation { + rule_id: number; + rule_name: string; + position: number; + students: CommunicationRulePreviewStudent[]; +} + +export interface CommunicationRulePreviewResponse { + target_rule_id: number; + allocations: CommunicationRulePreviewAllocation[]; +} + +export interface CommunicationSetPreviewResponse { + id: number; + unit_id: number; + name: string; + active: boolean; + schedules?: Partial[]; + rules: Partial[]; + previews: CommunicationRulePreviewResponse[]; +} + +export class CommunicationAction extends Entity { + id: number; + type: string; + communication_rule_id: number; + task_definition_id?: number; + subject?: string; + body?: string; + email_tutors?: boolean; + email_convenors?: boolean; + target_grade?: number; + + constructor(json?: Partial) { + super(); + Object.assign(this, json); + } +} + +export class CommunicationRule extends Entity { + id: number; + communication_set_id: number; + name: string; + operator: 'and' | 'or'; + position: number; + active: boolean; + send_log_to_convenors: boolean; + conditions: CommunicationCondition[] = []; + actions: CommunicationAction[] = []; + + constructor(json?: Partial) { + super(); + Object.assign(this, json); + + this.conditions = + json?.conditions?.map((condition) => new CommunicationCondition(condition)) ?? []; + this.actions = json?.actions?.map((action) => new CommunicationAction(action)) ?? []; + } +} + +export class CommunicationSet extends Entity { + id: number; + unit_id: number; + name: string; + active: boolean; + schedules: CommunicationSetSchedule[] = []; + rules: CommunicationRule[] = []; + + constructor(json?: Partial) { + super(); + Object.assign(this, json); + + this.schedules = + json?.schedules?.map((schedule) => new CommunicationSetSchedule(schedule)) ?? []; + this.rules = json?.rules?.map((rule) => new CommunicationRule(rule)) ?? []; + } +} diff --git a/src/app/api/models/d2l/d2l_assessment_mapping.service.ts b/src/app/api/models/d2l/d2l_assessment_mapping.service.ts index 3f2fe47087..4b9d22fc7d 100644 --- a/src/app/api/models/d2l/d2l_assessment_mapping.service.ts +++ b/src/app/api/models/d2l/d2l_assessment_mapping.service.ts @@ -1,9 +1,9 @@ -import {Injectable} from '@angular/core'; import {EntityService} from 'ngx-entity-service'; -import API_URL from 'src/app/config/constants/apiUrl'; import {HttpClient} from '@angular/common/http'; -import {D2lAssessmentMapping} from './d2l_assessment_mapping'; +import {Injectable} from '@angular/core'; +import API_URL from 'src/app/config/constants/apiUrl'; import {Unit} from '../doubtfire-model'; +import {D2lAssessmentMapping} from './d2l_assessment_mapping'; @Injectable() export class D2lAssessmentMappingService extends EntityService { diff --git a/src/app/api/models/discussion-prompt.ts b/src/app/api/models/discussion-prompt.ts index e984847084..2f9d7605c5 100644 --- a/src/app/api/models/discussion-prompt.ts +++ b/src/app/api/models/discussion-prompt.ts @@ -51,8 +51,9 @@ export class DiscussionPrompt extends Entity { next: (_response: object) => { AppInjector.get(AlertService).success('Successfully deleted discussion note', 4000); }, - error: (error: any) => { - AppInjector.get(AlertService).error(error?.message || error || 'Unknown error', 2000); + error: (error: Error) => { + const message = error.message || 'Unknown error'; + AppInjector.get(AlertService).error(message, 2000); }, }); } diff --git a/src/app/api/models/doubtfire-model.ts b/src/app/api/models/doubtfire-model.ts index ed485c939a..60b3bfb8b9 100644 --- a/src/app/api/models/doubtfire-model.ts +++ b/src/app/api/models/doubtfire-model.ts @@ -18,6 +18,7 @@ export * from './unit'; export * from './project'; export * from './task'; export * from './task-definition'; +export * from './submission-history'; export * from './learning-outcome'; export * from './tutorial-enrolment'; export * from './unit-role'; @@ -38,6 +39,8 @@ export * from './test-attempt'; export * from './task-comment/scorm-comment'; export * from './task-comment/scorm-extension-comment'; export * from './feedback-template'; +export * from './communication'; +export * from './engagement'; // Users -- are students or staff export * from './user/user'; @@ -52,6 +55,7 @@ export * from '../services/task.service'; export * from '../services/tutorial.service'; export * from '../services/tutorial-stream.service'; export * from '../services/overseer-assessment.service'; +export * from '../services/submission-history.service'; export * from '../services/campus.service'; export * from '../services/user.service'; export * from '../services/unit-role.service'; @@ -61,6 +65,13 @@ export * from '../services/teaching-period-break.service'; export * from '../services/learning-outcome.service'; export * from '../services/group-set.service'; export * from '../services/task-similarity.service'; +export * from '../../common/services/grade.service'; export * from '../services/test-attempt.service'; export * from '../models/d2l/d2l_assessment_mapping.service'; export * from '../services/feedback-template.service'; +export * from '../services/communication-set.service'; +export * from '../services/communication-rule.service'; +export * from '../services/communication-condition.service'; +export * from '../services/communication-action.service'; +export * from '../services/engagement.service'; +export * from '../services/engagement-comment.service'; diff --git a/src/app/api/models/engagement.ts b/src/app/api/models/engagement.ts new file mode 100644 index 0000000000..369fe42745 --- /dev/null +++ b/src/app/api/models/engagement.ts @@ -0,0 +1,69 @@ +import {Entity, EntityCache} from 'ngx-entity-service'; +import {AppInjector} from 'src/app/app-injector'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {Project, User, UserService} from './doubtfire-model'; + +export class Engagement extends Entity { + id: number; + project: Project; + user: User; + engagementType: string; + note: string; + occurredAt: Date; + evidenceUrl?: string; + contentType?: 'image' | 'pdf'; + hasAttachment: boolean; + attachmentFileName?: string; + commentCount: number; + createdAt: Date; + updatedAt: Date; + + readonly commentCache: EntityCache = new EntityCache(); + + constructor(project?: Project) { + super(); + this.project = project; + } + + get comments(): readonly EngagementComment[] { + return this.commentCache.currentValues; + } + + get attachmentUrl(): string { + return `${AppInjector.get(DoubtfireConstants).API_URL}/projects/${this.project.id}/engagements/${this.id}/attachment`; + } +} + +export class EngagementComment extends Entity { + private static readonly EDIT_WINDOW_MS = 10 * 60 * 1000; + + id: number; + engagement: Engagement; + user: User; + comment: string; + replyToId?: number; + replyTo?: EngagementComment; + createdAt: Date; + updatedAt: Date; + + constructor(engagement?: Engagement) { + super(); + this.engagement = engagement; + } + + get authorIsMe(): boolean { + return this.user.id === AppInjector.get(UserService).currentUser.id; + } + + get currentUserCanEdit(): boolean { + return ( + this.authorIsMe && + this.createdAt instanceof Date && + Date.now() - this.createdAt.getTime() <= EngagementComment.EDIT_WINDOW_MS + ); + } + + get currentUserCanDelete(): boolean { + return this.authorIsMe || this.engagement.project.unit.myRole === 'Convenor'; + } +} diff --git a/src/app/api/models/feedback-template.ts b/src/app/api/models/feedback-template.ts index 1634cb70d6..0b33c06453 100644 --- a/src/app/api/models/feedback-template.ts +++ b/src/app/api/models/feedback-template.ts @@ -10,7 +10,13 @@ export class FeedbackTemplate extends Entity { description: string; commentText: string; summaryText: string; - taskStatus: 'fix_and_resubmit' | 'discuss' | 'redo' | 'complete' | 'feedback_exceeded'; + taskStatus: + | 'fix_and_resubmit' + | 'discuss' + | 'rediscuss' + | 'redo' + | 'complete' + | 'feedback_exceeded'; parentChipId: number; learningOutcomeId: number; @@ -61,7 +67,7 @@ export class FeedbackTemplate extends Entity { return !this.id; } - public delete(): Observable { + public delete(): Observable { const svc = AppInjector.get(FeedbackTemplateService); return svc.delete( diff --git a/src/app/api/models/grade.ts b/src/app/api/models/grade.ts index e139dee8fb..7506f647cd 100644 --- a/src/app/api/models/grade.ts +++ b/src/app/api/models/grade.ts @@ -2,7 +2,10 @@ export class Grade { public static readonly PASS_RANGE: number[] = [0, 1, 2, 3]; public static readonly FULL_RANGE: number[] = [-1, 0, 1, 2, 3]; - public static readonly GRADE_ACRONYMS: Map = new Map([ + public static readonly GRADE_ACRONYMS: Map = new Map< + string | number, + string + >([ ['Fail', 'F'], ['Pass', 'P'], ['Credit', 'C'], diff --git a/src/app/api/models/groups/group-membership.ts b/src/app/api/models/groups/group-membership.ts index b7952d8b5e..0a0dca1539 100644 --- a/src/app/api/models/groups/group-membership.ts +++ b/src/app/api/models/groups/group-membership.ts @@ -1,9 +1,8 @@ -import { Entity } from 'ngx-entity-service'; +import {Entity} from 'ngx-entity-service'; export class GroupMembership extends Entity { - public get student_name(): string { - console.log("implement student_name"); - return "TODO NAME"; + console.log('implement student_name'); + return 'TODO NAME'; } } diff --git a/src/app/api/models/groups/group-set.ts b/src/app/api/models/groups/group-set.ts index 42882017d4..5e15231ee1 100644 --- a/src/app/api/models/groups/group-set.ts +++ b/src/app/api/models/groups/group-set.ts @@ -1,11 +1,9 @@ -import { Entity, EntityCache, EntityMapping } from 'ngx-entity-service'; -import { AppInjector } from 'src/app/app-injector'; -import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; -import { Group, Unit, User } from '../doubtfire-model'; - +import {Entity, EntityCache, EntityMapping} from 'ngx-entity-service'; +import {AppInjector} from 'src/app/app-injector'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {Group, Unit} from '../doubtfire-model'; export class GroupSet extends Entity { - public id: number; public name: string; public allowStudentsToCreateGroups: boolean = true; @@ -34,7 +32,7 @@ export class GroupSet extends Entity { } public findGroupById(id: number): Group { - return this.groups.find(grp => grp.id === id); + return this.groups.find((grp) => grp.id === id); } public groupCSVUploadUrl(): string { diff --git a/src/app/api/models/groups/group.ts b/src/app/api/models/groups/group.ts index a730c67430..bcc9f4f5b6 100644 --- a/src/app/api/models/groups/group.ts +++ b/src/app/api/models/groups/group.ts @@ -1,10 +1,17 @@ -import {HttpClient} from '@angular/common/http'; import {Entity, EntityCache, EntityMapping} from 'ngx-entity-service'; +import {HttpClient} from '@angular/common/http'; import {Observable, tap} from 'rxjs'; import {AppInjector} from 'src/app/app-injector'; -import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; -import {Unit, GroupSet, Project, Tutorial, ProjectService} from '../doubtfire-model'; import {AlertService} from 'src/app/common/services/alert.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {GroupSet, Project, ProjectService, Tutorial, Unit} from '../doubtfire-model'; + +export interface MemberContribution { + project: Project; + rating: number; + percent: number; + overStar?: number | null; +} export class Group extends Entity { public id: number; @@ -79,7 +86,7 @@ export class Group extends Entity { httpClient .post(`${AppInjector.get(DoubtfireConstants).API_URL}/${this.memberUri(member)}`, {}) .subscribe({ - next: (success) => { + next: () => { // Get old group.. const grp = member.groupForGroupSet(this.groupSet); if (grp) { @@ -95,7 +102,9 @@ export class Group extends Entity { // Has members so add this member this.projectsCache.add(member); alerts.success(`${member.student.name} was added to '${this.name}'`, 3000); - if (onSuccess) onSuccess(); + if (onSuccess) { + onSuccess(); + } }, error: (message) => alerts.error(message || 'Unknown Error', 6000), }); @@ -112,7 +121,7 @@ export class Group extends Entity { httpClient .delete(`${AppInjector.get(DoubtfireConstants).API_URL}/${this.memberUri(member)}`, {}) .subscribe({ - next: (success) => { + next: () => { // Get old group.. this.projectsCache.delete(member); member.groupCache.delete(this); @@ -145,30 +154,20 @@ export class Group extends Entity { } public hasSpace(): boolean { - if (!this.groupSet.capacity) { - return false; + if (this.groupSet.capacity == null) { + return true; } else { return this.memberCount < this.groupSet.capacity + this.capacityAdjustment; } } - public contributionSum( - contrib: {project: Project; rating: number; confRating: number; percent: number}[], - member?: Project, - value?: number, - ): number { - return contrib.reduce( - ( - prevValue: number, - current: {project: Project; rating: number; confRating: number; percent: number}, - ) => { - if (current.project === member) { - return prevValue + value; - } else { - return prevValue + current.rating; - } - }, - 0, - ); + public contributionSum(contrib: MemberContribution[], member?: Project, value?: number): number { + return contrib.reduce((prevValue: number, current) => { + if (current.project === member) { + return prevValue + value; + } else { + return prevValue + current.rating; + } + }, 0); } } diff --git a/src/app/api/models/learning-outcome.ts b/src/app/api/models/learning-outcome.ts index dfdc55fa19..211c3931d1 100644 --- a/src/app/api/models/learning-outcome.ts +++ b/src/app/api/models/learning-outcome.ts @@ -1,8 +1,8 @@ import {Entity, EntityMapping} from 'ngx-entity-service'; import {Observable, tap} from 'rxjs'; import {AppInjector} from 'src/app/app-injector'; -import {LearningOutcomeService, TaskDefinition, Unit} from './doubtfire-model'; import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {LearningOutcomeService, TaskDefinition, Unit} from './doubtfire-model'; export class LearningOutcome extends Entity { id: number; @@ -121,7 +121,7 @@ export class LearningOutcome extends Entity { return !this.id; } - public delete(): Observable { + public delete(): Observable { const svc = AppInjector.get(LearningOutcomeService); if (this.context) { diff --git a/src/app/api/models/overseer/overseer-assessment.ts b/src/app/api/models/overseer/overseer-assessment.ts index a41bcbb5d2..477d01af7f 100644 --- a/src/app/api/models/overseer/overseer-assessment.ts +++ b/src/app/api/models/overseer/overseer-assessment.ts @@ -2,20 +2,23 @@ import {Entity, EntityCache, EntityMapping} from 'ngx-entity-service'; import {AppInjector} from 'src/app/app-injector'; import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; import {Task} from '../doubtfire-model'; +import {SubmissionArchive} from '../submission-history'; +import {TaskStatusEnum} from '../task-status'; import {OverseerStepResult} from './overseer-step-result'; -export class OverseerAssessment extends Entity { +export class OverseerAssessment extends Entity implements SubmissionArchive { id: number; // overseerStepId: number; timestamp: Date; timestampString: string; - content?: [{label: string; result: string}]; + content?: {label: string; result: string}[]; task?: Task; - taskStatus?: string; + taskStatus?: TaskStatusEnum; submissionStatus?: 'queued' | 'executing' | 'passed' | 'failed' | 'error'; createdAt?: Date; updatedAt?: Date; taskId?: number; + submissionHistoryId?: number; totalSteps: number; passedSteps: number; diff --git a/src/app/api/models/overseer/overseer-image.ts b/src/app/api/models/overseer/overseer-image.ts index 277c3df426..aef5b416bb 100644 --- a/src/app/api/models/overseer/overseer-image.ts +++ b/src/app/api/models/overseer/overseer-image.ts @@ -1,5 +1,4 @@ -import { StringNullableChain } from 'lodash'; -import { Entity, EntityMapping } from 'ngx-entity-service'; +import {Entity, EntityMapping} from 'ngx-entity-service'; export class OverseerImage extends Entity { id: number; @@ -9,7 +8,10 @@ export class OverseerImage extends Entity { pulledImageStatus: string; lastPulledDate: string; - public override toJson(mappingData: EntityMapping, ignoreKeys?: string[]): object { + public override toJson( + mappingData: EntityMapping, + ignoreKeys?: string[], + ): object { return { overseer_image: super.toJson(mappingData, ignoreKeys), }; diff --git a/src/app/api/models/overseer/overseer-step-result.ts b/src/app/api/models/overseer/overseer-step-result.ts index 2a44371825..ebba556c69 100644 --- a/src/app/api/models/overseer/overseer-step-result.ts +++ b/src/app/api/models/overseer/overseer-step-result.ts @@ -18,7 +18,7 @@ export class OverseerStepResult extends Entity { expectedOutputSha256: string; feedbackMessage: string; - constructor(oa?: OverseerAssessment, os?: OverseerStep) { + constructor(oa?: OverseerAssessment, _os?: OverseerStep) { super(); this.overseerAssessment = oa; // this.overseerStep = os; diff --git a/src/app/api/models/overseer/overseer-step.ts b/src/app/api/models/overseer/overseer-step.ts index 6fa14bf1bf..7ed380fee7 100644 --- a/src/app/api/models/overseer/overseer-step.ts +++ b/src/app/api/models/overseer/overseer-step.ts @@ -1,9 +1,9 @@ import {Entity, EntityMapping} from 'ngx-entity-service'; -import {TaskDefinition} from '../task-definition'; -import {TaskStatus, TaskStatusEnum} from '../task-status'; -import {OverseerStepService} from '../../services/overseer-step.service'; import {AppInjector} from 'src/app/app-injector'; import {AlertService} from 'src/app/common/services/alert.service'; +import {OverseerStepService} from '../../services/overseer-step.service'; +import {TaskDefinition} from '../task-definition'; +import {TaskStatusEnum} from '../task-status'; export class OverseerStep extends Entity { id: number; diff --git a/src/app/api/models/project.ts b/src/app/api/models/project.ts index b799ed01d6..48c2720701 100644 --- a/src/app/api/models/project.ts +++ b/src/app/api/models/project.ts @@ -1,13 +1,12 @@ -import { HttpClient } from '@angular/common/http'; -import { Entity, EntityCache, RequestOptions } from 'ngx-entity-service'; -import { Observable, tap } from 'rxjs'; -import { visualisations } from 'src/app/ajs-upgraded-providers'; -import { AppInjector } from 'src/app/app-injector'; -import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; -import { MappingFunctions } from '../services/mapping-fn'; +import {Entity, EntityCache, RequestOptions} from 'ngx-entity-service'; +import {HttpClient} from '@angular/common/http'; +import {Observable, tap} from 'rxjs'; +import {AppInjector} from 'src/app/app-injector'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {MappingFunctions} from '../services/mapping-fn'; import { Campus, - Grade, Group, GroupSet, ProjectService, @@ -21,9 +20,9 @@ import { Unit, User, } from './doubtfire-model'; -import { TaskOutcomeAlignment } from './task-outcome-alignment'; -import { AlertService } from 'src/app/common/services/alert.service'; -import { StaffNote } from './staff-note'; +import {Engagement} from './engagement'; +import {StaffNote} from './staff-note'; +import {TaskOutcomeAlignment} from './task-outcome-alignment'; export class Project extends Entity { public id: number; @@ -53,9 +52,10 @@ export class Project extends Entity { }[]; public orderScale: number; - public burndownChartData: { key: string; values: number[] }[]; + public burndownChartData: {key: string; values: number[]}[]; public readonly taskCache: EntityCache = new EntityCache(); public readonly staffNoteCache: EntityCache = new EntityCache(); + public readonly engagementCache: EntityCache = new EntityCache(); public readonly tutorialEnrolmentsCache: EntityCache = new EntityCache(); public readonly groupCache: EntityCache = new EntityCache(); public readonly taskOutcomeAlignmentsCache: EntityCache = @@ -172,15 +172,17 @@ export class Project extends Entity { } public get targetGradeWord(): string { - return Grade.GRADES[this.targetGrade]; + return this.unit.gradeLabel(this.targetGrade); } public get targetGradeAcronym(): string { - return Grade.GRADE_ACRONYMS.get(this.targetGrade); + return this.unit.gradeAbbreviation(this.targetGrade); } public activeTasks(): Task[] { - return this.taskCache.currentValues.filter((task) => task.definition.targetGrade <= this.targetGrade); + return this.taskCache.currentValues.filter( + (task) => task.definition.targetGrade <= this.targetGrade, + ); } public calcTopTasks() { @@ -207,7 +209,7 @@ export class Project extends Entity { const overdueTasks: Task[] = sortedTasks.filter((task) => task.daysUntilDueDate() <= 7); // Step 2: select tasks not complete that are overdue. Pass tasks are done first. - Grade.PASS_RANGE.forEach((grade) => { + this.unit.gradeValues.forEach((grade) => { // Sorting needs to be done here according to the days past the target date. const closeGradeTasks: Task[] = overdueTasks .filter((task) => task.definition.targetGrade === grade) @@ -262,10 +264,14 @@ export class Project extends Entity { } //# Get the status of the portfolio - public portfolioTaskStatus(): string { - if (this.portfolioAvailable) return 'complete'; - else if (this.compilePortfolio) return 'working_on_it'; - else return 'not_started'; + public portfolioTaskStatus(): TaskStatusEnum { + if (this.portfolioAvailable) { + return 'complete'; + } else if (this.compilePortfolio) { + return 'working_on_it'; + } else { + return 'not_started'; + } } public portfolioTaskStatusClass(): string { @@ -283,16 +289,19 @@ export class Project extends Entity { return httpClient.delete(this.portfolioUrl(false)); } - public deleteFileFromPortfolio(file: { idx: any; kind: any; name: any }) { + public deleteFileFromPortfolio(file: {idx: number; kind: string; name: string}) { const httpClient = AppInjector.get(HttpClient); return httpClient - .delete(`${AppInjector.get(DoubtfireConstants).API_URL}/submission/project/${this.id}/portfolio`, { - body: { - idx: file.idx, - kind: file.kind, - name: file.name, + .delete( + `${AppInjector.get(DoubtfireConstants).API_URL}/submission/project/${this.id}/portfolio`, + { + body: { + idx: file.idx, + kind: file.kind, + name: file.name, + }, }, - }) + ) .pipe( tap(() => { this.portfolioFiles = this.portfolioFiles.filter((value) => value != file); @@ -327,8 +336,9 @@ export class Project extends Entity { cache: this.unit.studentCache, }; - projectService.get(this, options).subscribe((response) => { - (AppInjector.get(visualisations) as any).refreshAll(); + projectService.get(this, options).subscribe(() => { + // Legacy AngularJS visualisation refresh hook removed with upgraded providers. + // (AppInjector.get(visualisations) as any).refreshAll(); }); } @@ -350,7 +360,7 @@ export class Project extends Entity { } public isEnrolledIn(tutorial: Tutorial): boolean { - return this.tutorials.includes(tutorial); + return this.tutorials.some((t) => t.id === tutorial.id); } public updateUnitEnrolment(): void { @@ -377,16 +387,26 @@ export class Project extends Entity { tutorialService.switchTutorial(this, tutorial, !this.isEnrolledIn(tutorial)); } + public get progressStats() { + const stats = {}; + + this.taskStats.forEach((stat) => { + stats[stat.key] = stat.value; + }); + + return stats; + } + public refreshBurndownChartData(): void { - const result: { key: string; values: number[] }[] = []; + const result: {key: string; values: number[]}[] = []; // Setup the dictionaries to contain the keys and values // key = series name // values = array of [ x, y ] values - const projectedResults = { key: 'Projected', values: [] }; - const targetTaskResults = { key: 'Target', values: [] }; - const doneTaskResults = { key: 'To Submit', values: [] }; - const completeTaskResults = { key: 'To Complete', values: [] }; + const projectedResults = {key: 'Projected', values: []}; + const targetTaskResults = {key: 'Target', values: []}; + const doneTaskResults = {key: 'To Submit', values: []}; + const completeTaskResults = {key: 'To Complete', values: []}; result.push(targetTaskResults); result.push(projectedResults); @@ -396,15 +416,19 @@ export class Project extends Entity { // Get the weeks between start and end date as an array // dates = unit.start_date.to_date.step(unit.end_date.to_date + 1.week, step=7).to_a const endDateValue = this.unit.endDate.getTime() + MappingFunctions.weeksMs(3); - const dates = MappingFunctions.step(this.unit.startDate.getTime(), endDateValue, MappingFunctions.weeksMs(1)).map( - (val) => new Date(val), - ); + const dates = MappingFunctions.step( + this.unit.startDate.getTime(), + endDateValue, + MappingFunctions.weeksMs(1), + ).map((val) => new Date(val)); // Get the target task from the unit's task definitions const targetTasks = this.unit.taskDefinitionsForGrade(this.targetGrade); // get total value of all tasks assigned to this project - const total = targetTasks.map((td) => td.weighting).reduce((prev, current, idx, array) => prev + current, 0); + const total = targetTasks + .map((td) => td.weighting) + .reduce((prev, current, _idx, _array) => prev + current, 0); // exit if no tasks or no weights if (targetTasks.length === 0 || total === 0) { @@ -415,28 +439,39 @@ export class Project extends Entity { const tasks = this.tasks; const readyOrCompleteTasks = tasks.filter((task) => - ['ready_for_feedback', 'discuss', 'demonstrate', 'complete', 'assess_in_portfolio'].includes(task.status), + [ + 'ready_for_feedback', + 'discuss', + 'rediscuss', + 'demonstrate', + 'complete', + 'assess_in_portfolio', + ].includes(task.status), ); - let lastTargetDate: Date; + // let lastTargetDate: Date; const completedTasks = tasks.filter((task) => task.status === 'complete'); // Get the tasks currently marked as done (or ready to mark) const doneTasks = tasks.filter( - (t) => !['working_on_it', 'not_started', 'fix_and_resubmit', 'redo', 'need_help'].includes(t.status), + (t) => + !['working_on_it', 'not_started', 'fix_and_resubmit', 'redo', 'need_help'].includes( + t.status, + ), ); // last done task date) if (readyOrCompleteTasks.length === 0) { - lastTargetDate = this.unit.startDate; + // lastTargetDate = this.unit.startDate; } else { - lastTargetDate = readyOrCompleteTasks - .sort((a, b) => a.dueDate.getTime() - b.dueDate.getTime()) - .splice(-1)[0].dueDate; + // lastTargetDate = readyOrCompleteTasks + // .sort((a, b) => a.dueDate.getTime() - b.dueDate.getTime()) + // .splice(-1)[0].dueDate; } // today is used to determine when to stop adding done tasks - const today = new Date().getTime() > this.unit.endDate.getTime() ? this.unit.endDate : new Date(); + const today = + new Date().getTime() > this.unit.endDate.getTime() ? this.unit.endDate : new Date(); // use weekly completion rate to determine projected progress let completionRate: number = 0; @@ -445,7 +480,7 @@ export class Project extends Entity { if (weeksElapsed > 0) { const completedTasksWeight = readyOrCompleteTasks .map((t) => t.definition.weighting) - .reduce((prev, current, idx, arr) => prev + current, 0); + .reduce((prev, current, _idx, _arr) => prev + current, 0); completionRate = completedTasksWeight / weeksElapsed; } } @@ -524,11 +559,13 @@ export class Project extends Entity { public applySpecCon(days: number): Observable { const projectService: ProjectService = AppInjector.get(ProjectService); - return projectService.update(this, {body: {spec_con_days: days}, endpointFormat: 'projects/:id:/spec_con'}).pipe( - tap((project: Project) => { - project.specConDays = days; - }), - ); + return projectService + .update(this, {body: {spec_con_days: days}, endpointFormat: 'projects/:id:/spec_con'}) + .pipe( + tap((project: Project) => { + project.specConDays = days; + }), + ); } public tasksIncludedInPortfolioUrl(): string { @@ -540,6 +577,15 @@ export class Project extends Entity { return httpClient.get(this.tasksIncludedInPortfolioUrl()); } + public tasksStillProcessingUrl(): string { + return `${AppInjector.get(DoubtfireConstants).API_URL}/projects/${this.id}/tasks_processing`; + } + + public getTasksStillProcessing(): Observable { + const httpClient = AppInjector.get(HttpClient); + return httpClient.get(this.tasksStillProcessingUrl()); + } + public resetTargetDates(): Observable { const projectService: ProjectService = AppInjector.get(ProjectService); return projectService.update( diff --git a/src/app/api/models/scorm-datamodel.ts b/src/app/api/models/scorm-datamodel.ts index 0fdc2d3a06..b967e24c83 100644 --- a/src/app/api/models/scorm-datamodel.ts +++ b/src/app/api/models/scorm-datamodel.ts @@ -1,5 +1,5 @@ export class ScormDataModel { - dataModel: {[key: string]: any} = {}; + dataModel: Record = {}; readonly msgPrefix = 'SCORM DataModel: '; constructor() { @@ -8,19 +8,19 @@ export class ScormDataModel { public restore(dataModel: string) { // console.log(this.msgPrefix + 'restoring DataModel with provided data'); - this.dataModel = JSON.parse(dataModel); + this.dataModel = JSON.parse(dataModel) as Record; } public get(key: string): string { // console.log(`SCORM DataModel: get ${key} ${this.dataModel[key]}`); - return this.dataModel[key] ?? ''; + return String(this.dataModel[key] ?? ''); } - public dump(): {[key: string]: any} { + public dump(): Record { return this.dataModel; } - public set(key: string, value: any): string { + public set(key: string, value: string): string { // console.log(this.msgPrefix + 'set: ', key, value); this.dataModel[key] = value; if (key.match('cmi.interactions.\\d+.id')) { @@ -28,7 +28,8 @@ export class ScormDataModel { const interactionPath = key.match('cmi.interactions.\\d+'); const objectivesCounterForInteraction = interactionPath.toString() + '.objectives._count'; // console.log('Incrementing cmi.interactions._count'); - this.dataModel['cmi.interactions._count']++; + this.dataModel['cmi.interactions._count'] = + Number(this.dataModel['cmi.interactions._count'] ?? 0) + 1; // cmi.interactions.n.objectives._count must be initialized after an interaction is created // console.log(`Initializing ${objectivesCounterForInteraction}`); this.dataModel[objectivesCounterForInteraction] = 0; @@ -38,12 +39,14 @@ export class ScormDataModel { const objectivesCounterForInteraction = interactionPath.toString() + '._count'; // cmi.interactions.n.objectives._count must be incremented after objective creation // console.log(`Incrementing ${objectivesCounterForInteraction}`); - this.dataModel[objectivesCounterForInteraction.toString()]++; + this.dataModel[objectivesCounterForInteraction.toString()] = + Number(this.dataModel[objectivesCounterForInteraction.toString()] ?? 0) + 1; } if (key.match('cmi.objectives.\\d+.id')) { // cmi.objectives._count must be incremented after a new objective is created // console.log('Incrementing cmi.objectives._count'); - this.dataModel['cmi.objectives._count']++; + this.dataModel['cmi.objectives._count'] = + Number(this.dataModel['cmi.objectives._count'] ?? 0) + 1; } return 'true'; } diff --git a/src/app/api/models/staff-note.ts b/src/app/api/models/staff-note.ts index bcd1b00e34..ab3ab60a8a 100644 --- a/src/app/api/models/staff-note.ts +++ b/src/app/api/models/staff-note.ts @@ -1,8 +1,8 @@ import {Entity} from 'ngx-entity-service'; -import {Project, Unit, User, UserService} from './doubtfire-model'; import {AppInjector} from 'src/app/app-injector'; import {AlertService} from 'src/app/common/services/alert.service'; import {StaffNoteService} from '../services/staff-note.service'; +import {Project, User, UserService} from './doubtfire-model'; export class StaffNote extends Entity { id: number; @@ -41,13 +41,14 @@ export class StaffNote extends Entity { staffNoteService .delete({projectId: this.project.id, id: this.id}, {cache: this.project.staffNoteCache}) .subscribe({ - next: (response: object) => { + next: () => { AppInjector.get(AlertService).error('Successfully deleted staff note', 4000); this.project.staffNoteCount--; staffNoteService.updateStaffNoteReplies(this.project.staffNoteCache.currentValues); }, - error: (error: any) => { - AppInjector.get(AlertService).error(error?.message || error || 'Unknown error', 2000); + error: (error: Error) => { + const message = error.message || 'Unknown error'; + AppInjector.get(AlertService).error(message, 2000); }, }); } diff --git a/src/app/api/models/submission-history.ts b/src/app/api/models/submission-history.ts new file mode 100644 index 0000000000..d65888b64a --- /dev/null +++ b/src/app/api/models/submission-history.ts @@ -0,0 +1,34 @@ +import {Entity} from 'ngx-entity-service'; +import {AppInjector} from 'src/app/app-injector'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {Task} from './task'; + +export interface SubmissionArchive { + id: number; + task?: Task; + timestamp: Date; + timestampString: string; + hasSubmissionFiles?: boolean; + submissionFilesUrl(): string; +} + +export class SubmissionHistory extends Entity implements SubmissionArchive { + id: number; + task?: Task; + taskId?: number; + timestamp: Date; + timestampString: string; + createdAt?: Date; + hasSubmissionFiles?: boolean; + overseerAssessmentId?: number; + + constructor(task?: Task) { + super(); + this.task = task; + } + + public submissionFilesUrl(): string { + const constants = AppInjector.get(DoubtfireConstants); + return `${constants.API_URL}/projects/${this.task.project.id}/task_def_id/${this.task.definition.id}/submission_histories/${this.id}/files`; + } +} diff --git a/src/app/api/models/task-comment/discussion-comment.ts b/src/app/api/models/task-comment/discussion-comment.ts index 97bb24ee7e..c69968f2b7 100644 --- a/src/app/api/models/task-comment/discussion-comment.ts +++ b/src/app/api/models/task-comment/discussion-comment.ts @@ -1,6 +1,6 @@ -import { AppInjector } from 'src/app/app-injector'; -import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; -import { Task, TaskComment } from '../doubtfire-model' +import {AppInjector} from 'src/app/app-injector'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {Task, TaskComment} from '../doubtfire-model'; /** * Create a Discussion Comment, extending the base TaskComment class diff --git a/src/app/api/models/task-comment/extension-comment.ts b/src/app/api/models/task-comment/extension-comment.ts index 443bb34200..cc4da4eb0b 100644 --- a/src/app/api/models/task-comment/extension-comment.ts +++ b/src/app/api/models/task-comment/extension-comment.ts @@ -1,8 +1,8 @@ -import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; -import { AppInjector } from 'src/app/app-injector'; -import { TaskCommentService } from '../../services/task-comment.service'; -import { TaskComment, TaskStatusEnum, Task } from '../doubtfire-model'; +import {Observable} from 'rxjs'; +import {tap} from 'rxjs/operators'; +import {AppInjector} from 'src/app/app-injector'; +import {TaskCommentService} from '../../services/task-comment.service'; +import {Task, TaskComment, TaskStatusEnum} from '../doubtfire-model'; /** * Create a Discussion Comment, extending the base TaskComment class @@ -46,7 +46,7 @@ export class ExtensionComment extends TaskComment { tc.project.updateBurndownChart(); tc.project.calcTopTasks(); // Sort the task list again - }) + }), ); } diff --git a/src/app/api/models/task-comment/scorm-extension-comment.ts b/src/app/api/models/task-comment/scorm-extension-comment.ts index b8aac7909b..3f54d1c8ae 100644 --- a/src/app/api/models/task-comment/scorm-extension-comment.ts +++ b/src/app/api/models/task-comment/scorm-extension-comment.ts @@ -2,7 +2,7 @@ import {Observable} from 'rxjs'; import {tap} from 'rxjs/operators'; import {AppInjector} from 'src/app/app-injector'; import {TaskCommentService} from '../../services/task-comment.service'; -import {TaskComment, Task} from '../doubtfire-model'; +import {Task, TaskComment} from '../doubtfire-model'; export class ScormExtensionComment extends TaskComment { assessed: boolean; diff --git a/src/app/api/models/task-comment/task-comment.ts b/src/app/api/models/task-comment/task-comment.ts index e9b9bb55ce..90e83e6e72 100644 --- a/src/app/api/models/task-comment/task-comment.ts +++ b/src/app/api/models/task-comment/task-comment.ts @@ -1,12 +1,13 @@ -import {AppInjector} from 'src/app/app-injector'; import {Entity} from 'ngx-entity-service'; import {Project, Task, TaskCommentService, User} from 'src/app/api/models/doubtfire-model'; -import {UserService} from '../../services/user.service'; -import API_URL from 'src/app/config/constants/apiUrl'; -import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {AppInjector} from 'src/app/app-injector'; import {AlertService} from 'src/app/common/services/alert.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {UserService} from '../../services/user.service'; export class TaskComment extends Entity { + private static readonly EDIT_WINDOW_MS = 10 * 60 * 1000; + // Linked objects task: Task; originalComment: TaskComment = null; @@ -31,6 +32,7 @@ export class TaskComment extends Entity { shouldShowAvatar: boolean = false; firstInSeries: boolean = false; lastRead: boolean = false; + hover?: boolean; /** * Create a new TaskComment @@ -44,12 +46,12 @@ export class TaskComment extends Entity { } public get authorIsMe(): boolean { - const userService: any = AppInjector.get(UserService); + const userService: UserService = AppInjector.get(UserService); return this.author.id === userService.currentUser.id; } public get recipientIsMe(): boolean { - const userService: any = AppInjector.get(UserService); + const userService: UserService = AppInjector.get(UserService); return this.recipient.id === userService.currentUser.id; } @@ -80,6 +82,15 @@ export class TaskComment extends Entity { } public get currentUserCanEdit() { + return ( + this.authorIsMe && + this.commentType === 'text' && + this.createdAt instanceof Date && + new Date().getTime() - this.createdAt.getTime() <= TaskComment.EDIT_WINDOW_MS + ); + } + + public get currentUserCanDelete() { return this.authorIsMe || this.project?.unit.currentUserIsStaff; } @@ -91,12 +102,13 @@ export class TaskComment extends Entity { {cache: this.task.commentCache}, ) .subscribe({ - next: (response: object) => { + next: (_response: object) => { // this.task.comments = this.task.comments.filter((e: TaskComment) => e.id !== this.id); this.task.refreshCommentData(); }, - error: (error: any) => { - AppInjector.get(AlertService).error(error?.message || error || 'Unknown error', 2000); + error: (error: Error) => { + const message = error.message || 'Unknown error'; + AppInjector.get(AlertService).error(message, 2000); }, }); } diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index c8f88d1e9e..823a5bb8dd 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -1,25 +1,36 @@ -import {HttpClient} from '@angular/common/http'; import {Entity, EntityCache, EntityMapping} from 'ngx-entity-service'; +import {HttpClient} from '@angular/common/http'; import {Observable, tap} from 'rxjs'; import {AppInjector} from 'src/app/app-injector'; import {AlertService} from 'src/app/common/services/alert.service'; import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; import {TaskDefinitionService} from '../services/task-definition.service'; -import {Grade, GroupSet, LearningOutcome, Project, TutorialStream, Unit} from './doubtfire-model'; -import {Task} from './doubtfire-model'; -import {TaskPrerequisite} from './task-prerequisite'; import {DiscussionPrompt} from './discussion-prompt'; +import {GroupSet, LearningOutcome, Project, TutorialStream, Unit} from './doubtfire-model'; +import {Task} from './doubtfire-model'; import {OverseerStep} from './overseer/overseer-step'; +import {TaskPrerequisite} from './task-prerequisite'; -export type UploadRequirement = { +export interface UploadRequirement { key: string; name: string; type: string; tiiCheck?: boolean; tiiPct?: number; -}; + submissionHistory?: boolean; +} -export type SimilarityCheck = {key: string; type: string; pattern: string}; +export interface SimilarityCheck { + key: string; + type: string; + pattern: string; +} + +export interface TaskDefinitionGradeDueDate { + targetGrade: number; + targetDueDate?: Date; + startDate?: Date; +} export class TaskDefinition extends Entity { id: number; @@ -63,14 +74,7 @@ export class TaskDefinition extends Entity { discussionPromptsCount: number; overseerResourceFiles: string[] = []; - // pTargetDate: Date; - cTargetDate: Date; - dTargetDate: Date; - hdTargetDate: Date; - - cStartDate: Date; - dStartDate: Date; - hdStartDate: Date; + gradeDueDates: TaskDefinitionGradeDueDate[] = []; public readonly taskPrerequisitesCache: EntityCache = new EntityCache(); @@ -177,6 +181,43 @@ export class TaskDefinition extends Entity { return this.targetDate; } + public gradeTargetDate(targetGrade: number): Date | null { + return this.gradeDueDates.find((date) => date.targetGrade === targetGrade)?.targetDueDate; + } + + public gradeStartDate(targetGrade: number): Date | null { + return this.gradeDueDates.find((date) => date.targetGrade === targetGrade)?.startDate; + } + + public setGradeTargetDate(targetGrade: number, value: Date | null): void { + if (targetGrade === 0) { + this.targetDate = value; + return; + } + + this.gradeDueDateFor(targetGrade).targetDueDate = value; + } + + public setGradeStartDate(targetGrade: number, value: Date | null): void { + if (targetGrade === 0) { + this.startDate = value; + return; + } + + this.gradeDueDateFor(targetGrade).startDate = value; + } + + private gradeDueDateFor(targetGrade: number): TaskDefinitionGradeDueDate { + let gradeDueDate = this.gradeDueDates.find((date) => date.targetGrade === targetGrade); + + if (!gradeDueDate) { + gradeDueDate = {targetGrade}; + this.gradeDueDates.push(gradeDueDate); + } + + return gradeDueDate; + } + public localDeadlineDate(): Date { return this.dueDate; } @@ -244,11 +285,11 @@ export class TaskDefinition extends Entity { * Open the SCORM test in a new tab - using preview mode. */ public previewScormTest(): void { - window.open(`#/task_def_id/${this.id}/preview-scorm`, '_blank'); + window.open(`/task_def_id/${this.id}/preview-scorm`, '_blank'); } public get targetGradeText(): string { - return Grade.GRADES[this.targetGrade]; + return this.unit.gradeLabel(this.targetGrade); } public hasPlagiarismCheck(): boolean { @@ -303,27 +344,31 @@ export class TaskDefinition extends Entity { return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.unit.id}/task_definitions/${this.id}/jplag_report`; } - public deleteTaskSheet(): Observable { + public deleteTaskSheet(): Observable { const httpClient = AppInjector.get(HttpClient); - return httpClient.delete(this.taskSheetUploadUrl).pipe(tap(() => (this.hasTaskSheet = false))); + return httpClient + .delete(this.taskSheetUploadUrl) + .pipe(tap(() => (this.hasTaskSheet = false))); } - public deleteTaskResources(): Observable { + public deleteTaskResources(): Observable { const httpClient = AppInjector.get(HttpClient); return httpClient - .delete(this.taskResourcesUploadUrl) + .delete(this.taskResourcesUploadUrl) .pipe(tap(() => (this.hasTaskResources = false))); } - public deleteScormData(): Observable { + public deleteScormData(): Observable { const httpClient = AppInjector.get(HttpClient); - return httpClient.delete(this.scormDataUploadUrl).pipe(tap(() => (this.hasScormData = false))); + return httpClient + .delete(this.scormDataUploadUrl) + .pipe(tap(() => (this.hasScormData = false))); } - public deleteOverseerResources(): Observable { + public deleteOverseerResources(): Observable { const httpClient = AppInjector.get(HttpClient); return httpClient - .delete(this.taskOverseerResourcesUploadUrl) + .delete(this.taskOverseerResourcesUploadUrl) .pipe(tap(() => (this.hasTaskAssessmentResources = false))); } diff --git a/src/app/api/models/task-outcome-alignment.ts b/src/app/api/models/task-outcome-alignment.ts index 62414b5245..0fcf222056 100644 --- a/src/app/api/models/task-outcome-alignment.ts +++ b/src/app/api/models/task-outcome-alignment.ts @@ -1,6 +1,6 @@ -import { Entity } from 'ngx-entity-service'; -import { Project, Unit, TaskDefinition, Task } from './doubtfire-model'; -import { LearningOutcome } from './learning-outcome'; +import {Entity} from 'ngx-entity-service'; +import {Project, Task, TaskDefinition, Unit} from './doubtfire-model'; +import {LearningOutcome} from './learning-outcome'; export class TaskOutcomeAlignment extends Entity { public within: Unit | Project; @@ -18,7 +18,7 @@ export class TaskOutcomeAlignment extends Entity { } public get unit(): Unit { - if ( this.within instanceof Unit) { + if (this.within instanceof Unit) { return this.within; } else { return this.within.unit; @@ -26,7 +26,7 @@ export class TaskOutcomeAlignment extends Entity { } public get project(): Project { - if ( this.within instanceof Project) { + if (this.within instanceof Project) { return this.within; } diff --git a/src/app/api/models/task-prerequisite.ts b/src/app/api/models/task-prerequisite.ts index 9645adfe5b..4e26df0917 100644 --- a/src/app/api/models/task-prerequisite.ts +++ b/src/app/api/models/task-prerequisite.ts @@ -5,6 +5,12 @@ import {AlertService} from 'src/app/common/services/alert.service'; import {TaskPrerequisiteService} from '../services/task-prerequisite.service'; import {Project, TaskDefinition, TaskStatus, TaskStatusEnum} from './doubtfire-model'; +export interface TaskPrerequisiteData { + taskDefinitionId: number; + prerequisiteId: number; + taskStatus: TaskStatusEnum; +} + export class TaskPrerequisite extends Entity { id: number; @@ -28,7 +34,7 @@ export class TaskPrerequisite extends Entity { complete: 3, }; - constructor(json: any) { + constructor(json: TaskPrerequisiteData) { super(); this.taskDefinitionId = json.taskDefinitionId; this.prerequisiteId = json.prerequisiteId; @@ -61,12 +67,12 @@ export class TaskPrerequisite extends Entity { return false; } - public delete(): Observable { + public delete(): Observable { const taskPrerequisiteService: TaskPrerequisiteService = AppInjector.get(TaskPrerequisiteService); return taskPrerequisiteService - .delete( + .delete( { unitId: this.taskDefinition.unit.id, taskDefId: this.taskDefinitionId, diff --git a/src/app/api/models/task-similarity.ts b/src/app/api/models/task-similarity.ts index cb9640966c..fa925f91ca 100644 --- a/src/app/api/models/task-similarity.ts +++ b/src/app/api/models/task-similarity.ts @@ -1,8 +1,8 @@ import {Entity} from 'ngx-entity-service'; +import {Observable} from 'rxjs'; import {AppInjector} from 'src/app/app-injector'; -import {Task, TaskSimilarityService, User} from './doubtfire-model'; import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; -import {Observable} from 'rxjs'; +import {Task, TaskSimilarityService, User} from './doubtfire-model'; export enum TaskSimilarityType { Jplag = 'JplagTaskSimilarity', @@ -27,6 +27,7 @@ export class TaskSimilarityPart { idx: number; format: TaskSimilarityPartFormat; description: string; + panelOpenState?: boolean; } /** diff --git a/src/app/api/models/task-status.ts b/src/app/api/models/task-status.ts index eabf670adb..5544f5ea8f 100644 --- a/src/app/api/models/task-status.ts +++ b/src/app/api/models/task-status.ts @@ -14,16 +14,17 @@ export type TaskStatusEnum = | 'fail' | 'time_exceeded' | 'assess_in_portfolio' - | 'attention_required'; + | 'attention_required' + | 'rediscuss'; -export type TaskStatusUiData = { +export interface TaskStatusUiData { status: TaskStatusEnum; icon: string; materialIcon: string; label: string; class: string; help: {detail: string; reason: string; action: string}; -}; +} export class TaskStatus { public static readonly STATUS_KEYS: TaskStatusEnum[] = [ @@ -41,6 +42,7 @@ export class TaskStatus { 'time_exceeded', 'assess_in_portfolio', 'attention_required', + 'rediscuss', ]; public static readonly VALID_TOP_TASKS: TaskStatusEnum[] = [ @@ -53,6 +55,7 @@ export class TaskStatus { 'discuss', 'attention_required', 'demonstrate', + 'rediscuss', ]; public static readonly SUBMITTED_STATUSES: TaskStatusEnum[] = [ @@ -65,6 +68,7 @@ export class TaskStatus { 'time_exceeded', 'assess_in_portfolio', 'attention_required', + 'rediscuss', ]; public static readonly FINAL_STATUSES: TaskStatusEnum[] = [ @@ -78,6 +82,7 @@ export class TaskStatus { public static readonly GRADEABLE_STATUSES: TaskStatusEnum[] = [ 'fail', 'discuss', + 'rediscuss', 'demonstrate', 'complete', ]; @@ -93,6 +98,7 @@ export class TaskStatus { 'discuss', 'attention_required', 'demonstrate', + 'rediscuss', ]; public static readonly STATE_THAT_ALLOWS_EXTENSION: TaskStatusEnum[] = [ @@ -109,6 +115,7 @@ export class TaskStatus { 'demonstrate', 'ready_for_feedback', 'discuss', + 'rediscuss', 'complete', 'time_exceeded', 'fail', @@ -129,6 +136,7 @@ export class TaskStatus { 'fix_and_resubmit', 'feedback_exceeded', 'discuss', + 'rediscuss', 'demonstrate', 'complete', 'attention_required', @@ -137,6 +145,7 @@ export class TaskStatus { public static readonly FEEDBACK_TEMPLATE_STATUSES: TaskStatusEnum[] = [ 'complete', 'discuss', + 'rediscuss', 'fix_and_resubmit', 'redo', 'feedback_exceeded', @@ -156,6 +165,7 @@ export class TaskStatus { ['fix_and_resubmit', 0.3], ['ready_for_feedback', 0.5], ['discuss', 0.8], + ['rediscuss', 0.8], ['demonstrate', 0.8], ['complete', 1.0], ['time_exceeded', 0.3], @@ -175,6 +185,7 @@ export class TaskStatus { ['feedback_exceeded', 'DNR'], ['fix_and_resubmit', 'FIX'], ['discuss', 'DIS'], + ['rediscuss', 'RDS'], ['demonstrate', 'DEM'], ['complete', 'COM'], ['fail', 'FAL'], @@ -194,6 +205,7 @@ export class TaskStatus { ['ready_for_feedback', []], ['complete', ['ready_for_feedback', 'not_started', 'working_on_it', 'need_help']], ['discuss', ['ready_for_feedback', 'not_started', 'working_on_it', 'need_help']], + ['rediscuss', ['ready_for_feedback', 'not_started', 'working_on_it', 'need_help']], ['demonstrate', ['ready_for_feedback', 'not_started', 'working_on_it', 'need_help']], ['fix_and_resubmit', []], ['redo', []], @@ -204,8 +216,8 @@ export class TaskStatus { ['attention_required', ['ready_for_feedback', 'not_started', 'working_on_it', 'need_help']], ]); - public static readonly STATUS_LABELS = new Map([ - ['ready_for_feedback', 'Ready for Feedback'], + public static readonly STATUS_LABELS: Map = new Map([ + ['ready_for_feedback', 'Awaiting Feedback'], ['not_started', 'Not Started'], ['working_on_it', 'Working On It'], ['need_help', 'Need Help'], @@ -213,6 +225,7 @@ export class TaskStatus { ['feedback_exceeded', 'Feedback Exceeded'], ['fix_and_resubmit', 'Resubmit'], ['discuss', 'Discuss'], + ['rediscuss', 'Rediscuss'], ['demonstrate', 'Demonstrate'], ['complete', 'Complete'], ['fail', 'Fail'], @@ -221,25 +234,25 @@ export class TaskStatus { ['attention_required', 'Attention Required'], ]); - public static readonly STATUS_ICONS = new Map([ - ['ready_for_feedback', 'fa fa-thumbs-o-up'], - ['not_started', 'fa fa-pause'], - ['working_on_it', 'fa fa-bolt'], - ['need_help', 'fa fa-question-circle'], - ['redo', 'fa fa-refresh'], - ['feedback_exceeded', 'fa fa-low-vision'], - ['fix_and_resubmit', 'fa fa-wrench'], - ['discuss', 'fa fa-commenting'], - ['demonstrate', 'fa fa-commenting'], - ['complete', 'fa fa-check'], - ['fail', 'fa fa-times'], - ['time_exceeded', 'fa fa-clock-o'], - ['assess_in_portfolio', 'fa fa-folder-open'], - ['attention_required', 'fa fa-commenting'], + public static readonly STATUS_NAME_TO_KEY: Map = new Map([ + ['Ready for Feedback', 'ready_for_feedback'], + ['Awaiting Feedback', 'ready_for_feedback'], + ['Not Started', 'not_started'], + ['Working On It', 'working_on_it'], + ['Need Help', 'need_help'], + ['Redo', 'redo'], + ['Feedback Exceeded', 'feedback_exceeded'], + ['Resubmit', 'fix_and_resubmit'], + ['Discuss', 'discuss'], + ['Rediscuss', 'rediscuss'], + ['Re-discuss', 'rediscuss'], + ['Demonstrate', 'demonstrate'], + ['Complete', 'complete'], + ['Fail', 'fail'], + ['Time Exceeded', 'time_exceeded'], ]); - // Material icons used by newer UI elements. - public static readonly STATUS_MATERIAL_ICONS = new Map([ + public static readonly STATUS_ICONS: Map = new Map([ ['ready_for_feedback', 'thumb_up'], ['not_started', 'pause'], ['working_on_it', 'bolt'], @@ -248,16 +261,36 @@ export class TaskStatus { ['feedback_exceeded', 'visibility_off'], ['fix_and_resubmit', 'construction'], ['discuss', 'question_answer'], + ['rediscuss', 'feedback'], ['demonstrate', 'record_voice_over'], - ['complete', 'done'], + ['complete', 'done_all'], ['fail', 'close'], ['time_exceeded', 'schedule'], ['assess_in_portfolio', 'folder_open'], ['attention_required', 'sms_failed'], ]); + // Material icons used by newer UI elements. + public static readonly STATUS_MATERIAL_ICONS: Map = new Map([ + ['ready_for_feedback', 'thumb_up_off_alt'], + ['not_started', 'pause'], + ['working_on_it', 'bolt'], + ['need_help', 'help'], + ['redo', 'undo'], + ['feedback_exceeded', 'visibility_off'], + ['fix_and_resubmit', 'construction'], + ['discuss', 'question_answer'], + ['rediscuss', 'feedback'], + ['demonstrate', 'record_voice_over'], + ['complete', 'done'], + ['fail', 'close'], + ['time_exceeded', 'schedule'], + ['assess_in_portfolio', 'rate_review'], + ['attention_required', 'sms_failed'], + ]); + // Please make sure this matches task-status-colors.less - public static readonly STATUS_COLORS = new Map([ + public static readonly STATUS_COLORS: Map = new Map([ ['ready_for_feedback', '#0079D8'], ['not_started', '#CCCCCC'], ['working_on_it', '#EB8F06'], @@ -266,6 +299,7 @@ export class TaskStatus { ['feedback_exceeded', '#d46b54'], ['redo', '#804000'], ['discuss', '#31b0d5'], + ['rediscuss', '#126352'], ['demonstrate', '#428bca'], ['complete', '#5BB75B'], ['fail', '#d93713'], @@ -274,7 +308,7 @@ export class TaskStatus { ['attention_required', '#f1814d'], ]); - public static readonly STATUS_SEQ = new Map([ + public static readonly STATUS_SEQ: Map = new Map([ ['not_started', 1], ['fail', 2], ['feedback_exceeded', 3], @@ -289,6 +323,7 @@ export class TaskStatus { ['complete', 12], ['assess_in_portfolio', 13], ['attention_required', 14], + ['rediscuss', 15], ]); public static readonly SWITCHABLE_STATES = { @@ -296,6 +331,7 @@ export class TaskStatus { tutor: [ 'complete', 'discuss', + 'rediscuss', 'attention_required', 'demonstrate', 'fix_and_resubmit', @@ -309,10 +345,10 @@ export class TaskStatus { // detail = in a brief context to the student // reason = reason for this status // action = action student can take - public static readonly HELP_DESCRIPTIONS = new Map< + public static readonly HELP_DESCRIPTIONS: Map< TaskStatusEnum, {detail: string; reason: string; action: string} - >([ + > = new Map([ [ 'ready_for_feedback', { @@ -386,6 +422,16 @@ export class TaskStatus { action: 'For this to be marked as complete, attend class and discuss it with your tutor.', }, ], + [ + 'rediscuss', + { + detail: 'Your work needs another discussion.', + reason: + 'You attempted to discuss this task, but the discussion was not adequate to sign it off.', + action: + 'Brush up your knowledge and return for another discussion with your tutor to get the task signed off.', + }, + ], [ 'attention_required', { @@ -466,6 +512,6 @@ export class TaskStatus { } public static statusClass(status: TaskStatusEnum | undefined): string { - return status?.replace(new RegExp('_', 'g'), '-'); + return status?.replace(new RegExp('_', 'g'), '-') ?? 'not-started'; } } diff --git a/src/app/api/models/task.ts b/src/app/api/models/task.ts index 4b530ffea1..bac5be4451 100644 --- a/src/app/api/models/task.ts +++ b/src/app/api/models/task.ts @@ -1,35 +1,35 @@ import {Entity, EntityCache, RequestOptions} from 'ngx-entity-service'; -import {AppInjector} from 'src/app/app-injector'; import {formatDate} from '@angular/common'; +import {HttpClient} from '@angular/common/http'; +import {LOCALE_ID} from '@angular/core'; +import {Observable, firstValueFrom, map} from 'rxjs'; +import {AppInjector} from 'src/app/app-injector'; +import {AlertService} from 'src/app/common/services/alert.service'; import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {GradeTaskModalService} from 'src/app/tasks/modals/grade-task-modal/grade-task-modal.service'; +import {UploadSubmissionModalService} from 'src/app/tasks/modals/upload-submission-modal/upload-submission-modal.service'; +import {MappingFunctions} from '../services/mapping-fn'; +import {TutorNoteService} from '../services/tutor-note.service'; import { - TaskDefinition, + Group, Project, - Unit, + ScormComment, TaskComment, - TaskStatusEnum, - TaskStatus, - TaskStatusUiData, - TaskService, - Group, TaskCommentService, + TaskDefinition, + TaskService, TaskSimilarity, TaskSimilarityService, + TaskStatus, + TaskStatusEnum, + TaskStatusUiData, TestAttempt, TestAttemptService, - ScormComment, - UnitRoleService, + Unit, UnitRole, + UnitRoleService, UserService, } from './doubtfire-model'; -import {TutorNoteService} from '../services/tutor-note.service'; -import {Grade} from './grade'; -import {LOCALE_ID} from '@angular/core'; -import {HttpClient} from '@angular/common/http'; -import {Observable, firstValueFrom, map} from 'rxjs'; -import {gradeTaskModal, uploadSubmissionModal} from 'src/app/ajs-upgraded-providers'; -import {AlertService} from 'src/app/common/services/alert.service'; -import {MappingFunctions} from '../services/mapping-fn'; import {TaskPrerequisite} from './task-prerequisite'; export const FeedbackModerationAction = { @@ -66,6 +66,7 @@ export class Task extends Entity { project: Project; definition: TaskDefinition; + tutorialId: number; //TODO: map task submission details hasPdf: boolean = false; @@ -75,6 +76,8 @@ export class Task extends Entity { loadingSubmissionDetails: boolean = false; pinned: boolean = false; + hover?: boolean; + optionsOpened?: boolean; targetStartDate: Date; targetDueDate: Date; @@ -146,9 +149,9 @@ export class Task extends Entity { AppInjector.get(TaskCommentService) .addComment(this, textString, 'text') .subscribe({ - next: (tc) => {}, error: (error) => { - console.log(error); + const alerts: AlertService = AppInjector.get(AlertService); + alerts.error(`Failed to add comment: ${error}`); }, }); } @@ -166,7 +169,9 @@ export class Task extends Entity { } public get unit(): Unit { - if (this._unit) return this._unit; + if (this._unit) { + return this._unit; + } return this.project.unit; } @@ -231,7 +236,15 @@ export class Task extends Entity { } public hasTaskKey(key: {studentId: number; taskDefAbbr: string}): boolean { - return this.taskKey() === key; + if (!key) { + return false; + } + + const taskKey = this.taskKey(); + return ( + taskKey?.studentId?.toString() === key.studentId?.toString() && + taskKey?.taskDefAbbr === key.taskDefAbbr + ); } public taskKeyToUrlString(): string { @@ -240,14 +253,15 @@ export class Task extends Entity { } public get gradeWord(): string { - if (this.grade) return Grade.GRADES[this.grade]; - else { + if (this.grade !== undefined && this.grade !== null) { + return this.unit.gradeLabel(this.grade); + } else { return 'Not Graded'; } } public gradeDesc(): string { - return Grade.GRADE_ACRONYMS.get(this.grade); + return this.unit.gradeAbbreviation(this.grade); } public hasGrade(): boolean { @@ -259,7 +273,11 @@ export class Task extends Entity { } public hasQualityPoints(): boolean { - return this.definition.maxQualityPts > 0 && TaskStatus.GRADEABLE_STATUSES.includes(this.status); + return ( + this.definition.maxQualityPts > 0 && + this.qualityPts >= 0 && + TaskStatus.GRADEABLE_STATUSES.includes(this.status) + ); } public hasBeenGraded(): boolean { @@ -270,7 +288,7 @@ export class Task extends Entity { } public hasBeenGivenQualityPoints(): boolean { - return this.qualityPts > 0 || TaskStatus.GRADEABLE_STATUSES.includes(this.status); + return this.definition.maxQualityPts > 0 && this.qualityPts >= 0; } public localDueDate(): Date { @@ -280,15 +298,9 @@ export class Task extends Entity { return this.targetDueDate; } - // Unit target dates per grade guidelines - if (this.project.targetGrade === 1 && this.definition.cTargetDate) { - return this.definition.cTargetDate; - } - if (this.project.targetGrade === 2 && this.definition.dTargetDate) { - return this.definition.dTargetDate; - } - if (this.project.targetGrade === 3 && this.definition.hdTargetDate) { - return this.definition.hdTargetDate; + const gradeTargetDate = this.definition.gradeTargetDate(this.project.targetGrade); + if (gradeTargetDate) { + return gradeTargetDate; } } @@ -446,15 +458,9 @@ export class Task extends Entity { return this.targetStartDate; } - // Unit start dates per grade guidelines - if (this.project.targetGrade === 1 && this.definition.cStartDate) { - return this.definition.cStartDate; - } - if (this.project.targetGrade === 2 && this.definition.dStartDate) { - return this.definition.dStartDate; - } - if (this.project.targetGrade === 3 && this.definition.hdStartDate) { - return this.definition.hdStartDate; + const gradeStartDate = this.definition.gradeStartDate(this.project.targetGrade); + if (gradeStartDate) { + return gradeStartDate; } } @@ -515,10 +521,10 @@ export class Task extends Entity { public timeToDue(): string { const days = this.daysUntilDueDate(); - if (days < 0) { - return '!'; + if (days <= 0) { + return 'Past Due Date'; } else if (days < 11) { - return `${days}d`; + return `Due in ${this.timeUntilDueDateDescription()}`; } else { return `${Math.floor(days / 7)}w`; } @@ -573,7 +579,9 @@ export class Task extends Entity { public refreshCommentData(): void { const comments: readonly TaskComment[] = this.comments; - if (comments.length === 0) return; + if (comments.length === 0) { + return; + } comments[0].shouldShowTimestamp = true; @@ -616,12 +624,13 @@ export class Task extends Entity { comments[i].firstInSeries = i === 0 || comments[i - 1].commentType !== 'scorm'; (comments[i] as ScormComment).lastInScormSeries = i + 1 === comments.length || comments[i + 1]?.commentType !== 'scorm'; - if (!comments[i].firstInSeries) comments[i].shouldShowTimestamp = false; + if (!comments[i].firstInSeries) { + comments[i].shouldShowTimestamp = false; + } } } comments[comments.length - 1].shouldShowAvatar = true; - comments; } public taskKey(): {studentId: number; taskDefAbbr: string} { @@ -640,14 +649,14 @@ export class Task extends Entity { return this.similarityFlag; } - public getSimilarityData(match: number): Observable { + public getSimilarityData(match: number): Observable { const httpClient = AppInjector.get(HttpClient); return httpClient.get( `${AppInjector.get(DoubtfireConstants).API_URL}/tasks/${this.id}/similarity/${match}`, ); } - public updateSimilarity(match: number, other: any, dismissed: boolean): Observable { + public updateSimilarity(match: number, other: object, dismissed: boolean): Observable { const httpClient = AppInjector.get(HttpClient); return httpClient.put( `${AppInjector.get(DoubtfireConstants).API_URL}/tasks/${this.id}/similarity/${match}`, @@ -695,7 +704,7 @@ export class Task extends Entity { } public statusIcon(): string { - return TaskStatus.STATUS_ICONS.get(this.status); + return TaskStatus.STATUS_MATERIAL_ICONS.get(this.status); } public statusClass(): string { @@ -814,7 +823,7 @@ export class Task extends Entity { * Launch the SCORM player for this task in a new window. */ public launchScormPlayer(): void { - const url = `#/projects/${this.project.id}/task_def_id/${this.taskDefId}/scorm-player/normal`; + const url = `/projects/${this.project.id}/task_def_id/${this.taskDefId}/scorm-player/normal`; window.open(url, '_blank'); } @@ -869,7 +878,7 @@ export class Task extends Entity { if (!isTestSubmission) { this.status = status; } - const uploadModal: any = AppInjector.get(uploadSubmissionModal); + const uploadModal: UploadSubmissionModalService = AppInjector.get(UploadSubmissionModalService); const modal = uploadModal.show(this, reuploadEvidence, isTestSubmission); // Modal failed to present @@ -882,13 +891,15 @@ export class Task extends Entity { modal.result.then( // Grade was selected (modal closed with result) - (response) => {}, + (_response) => { + /* empty */ + }, // Grade was not selected (modal was dismissed) (_dismissed) => { if (!isTestSubmission) { this.status = oldStatus; } - const alerts: any = AppInjector.get(AlertService); + const alerts: AlertService = AppInjector.get(AlertService); alerts.message('Submission cancelled. Status was reverted.', 6000); }, ); @@ -907,6 +918,9 @@ export class Task extends Entity { } else { alerts.success(`Status changed to ${this.statusLabel()}.`); } + this.getSubmissionDetails().subscribe(); + const taskService: TaskService = AppInjector.get(TaskService); + taskService.notifyStatusChange(this); } public async markAsDiscussed(reasonText?: string) { @@ -982,6 +996,8 @@ export class Task extends Entity { triggerRecursiveFix?: boolean, ) { const oldStatus = this.status; + const oldGrade = this.grade; + const oldQualityPts = this.qualityPts; const alerts: AlertService = AppInjector.get(AlertService); if (status === 'complete' && !this.canMarkComplete) { @@ -989,27 +1005,15 @@ export class Task extends Entity { return; } - if (status === 'complete' || status === 'fix_and_resubmit') { - if (!this.commentsSinceLatestReadyForFeedback().some((comment) => comment.isManualFeedback)) { - alerts.error( - status === 'complete' - ? 'Feedback must be given before moving this task to Complete' - : 'Feedback must be given before moving this task to Fix and Resubmit', - 6000, - ); - return; - } - } - - const updateFunc = () => { + const updateFunc = (grade = this.grade, qualityPts = this.qualityPts) => { const taskService: TaskService = AppInjector.get(TaskService); const options: RequestOptions = { entity: this, cache: this.project.taskCache, body: { trigger: status, - grade: this.grade, - quality_pts: this.qualityPts, + grade: grade, + quality_pts: qualityPts, }, }; @@ -1032,7 +1036,9 @@ export class Task extends Entity { options, ) .subscribe({ - next: (response) => { + next: (_response) => { + this.grade = grade; + this.qualityPts = qualityPts; if (!hasId && this.id > 0) { this.project.taskCache.delete(this.definition.abbreviation); this.project.taskCache.add(this); @@ -1042,40 +1048,40 @@ export class Task extends Entity { }, error: (error) => { this.status = oldStatus; + this.grade = oldGrade; + this.qualityPts = oldQualityPts; alerts.error(error, 6000); }, }); }; // end update function - // Must provide grade if graded and in a final complete state + // Must provide grade if graded and in a final complete state - so use callback to run update function if ( (this.definition.isGraded || this.definition.maxQualityPts > 0) && TaskStatus.GRADEABLE_STATUSES.includes(status) ) { - const gradeModal: any = AppInjector.get(gradeTaskModal); - const modal = gradeModal.show(this); - if (modal) { - modal.result.then( - // Grade was selected (modal closed with result) - (response) => { - this.grade = response.selectedGrade; - this.qualityPts = response.qualityPts; - updateFunc(); - }, - // Grade was not selected (modal was dismissed) - () => { - this.status = oldStatus; - alerts.message('Status reverted, as no grade was specified', 6000); - }, - ); - } + const gradeModal: GradeTaskModalService = AppInjector.get(GradeTaskModalService); + gradeModal.show( + this, + // Grade was selected (modal closed with result) + (response) => { + updateFunc(response.grade, response.qualityPts); + }, + // Grade was not selected (modal was dismissed) + () => { + this.status = oldStatus; + alerts.message('Status reverted, as no grade was specified', 6000); + }, + ); } else { updateFunc(); } } public async triggerTransition(status: TaskStatusEnum): Promise { - if (this.status === status) return; + if (this.status === status) { + return; + } const alerts: AlertService = AppInjector.get(AlertService); const requiresFileUpload = @@ -1109,12 +1115,13 @@ export class Task extends Entity { return this.project.getGroupForTask(this); } - public pin(): void { + public pin(onSuccess?: () => void): void { const http = AppInjector.get(HttpClient); http.post(`${AppInjector.get(DoubtfireConstants).API_URL}/tasks/${this.id}/pin`, {}).subscribe({ - next: (data) => { + next: (_data) => { this.pinned = true; + onSuccess?.(); }, error: (message) => { (AppInjector.get(AlertService) as AlertService).error(message, 6000); @@ -1122,7 +1129,7 @@ export class Task extends Entity { }); } - public unpin(): void { + public unpin(onSuccess?: () => void): void { const http = AppInjector.get(HttpClient); http @@ -1130,6 +1137,7 @@ export class Task extends Entity { .subscribe({ next: (_data) => { this.pinned = false; + onSuccess?.(); }, error: (message) => { (AppInjector.get(AlertService) as AlertService).error(message, 6000); diff --git a/src/app/api/models/teaching-period.ts b/src/app/api/models/teaching-period.ts index 2aca186b7c..8c67b7c245 100644 --- a/src/app/api/models/teaching-period.ts +++ b/src/app/api/models/teaching-period.ts @@ -1,7 +1,7 @@ -import { Entity, EntityCache, EntityMapping } from 'ngx-entity-service'; -import { Observable } from 'rxjs'; -import { AppInjector } from 'src/app/app-injector'; -import { TeachingPeriodBreakService, TeachingPeriodService, Unit } from './doubtfire-model'; +import {Entity, EntityCache, EntityMapping} from 'ngx-entity-service'; +import {Observable} from 'rxjs'; +import {AppInjector} from 'src/app/app-injector'; +import {TeachingPeriodBreakService, TeachingPeriodService, Unit} from './doubtfire-model'; export class TeachingPeriodBreak extends Entity { id: number; @@ -27,7 +27,10 @@ export class TeachingPeriod extends Entity { * @param ignoreKeys * @returns */ - public override toJson(mappingData: EntityMapping, ignoreKeys?: string[]): object { + public override toJson( + mappingData: EntityMapping, + ignoreKeys?: string[], + ): object { return { teaching_period: super.toJson(mappingData, ignoreKeys), }; @@ -69,7 +72,10 @@ export class TeachingPeriod extends Entity { breakEntity.numberOfWeeks = weeks; const breakService: TeachingPeriodBreakService = AppInjector.get(TeachingPeriodBreakService); - return breakService.create({ teaching_period_id: this.id }, { cache: this.breaksCache, entity: breakEntity }); + return breakService.create( + {teaching_period_id: this.id}, + {cache: this.breaksCache, entity: breakEntity}, + ); } /** @@ -79,10 +85,17 @@ export class TeachingPeriod extends Entity { */ public removeBreak(teachingBreakID: number): Observable { const breakService: TeachingPeriodBreakService = AppInjector.get(TeachingPeriodBreakService); - return breakService.delete({ teaching_period_id: this.id, id: teachingBreakID }, { cache: this.breaksCache }); + return breakService.delete( + {teaching_period_id: this.id, id: teachingBreakID}, + {cache: this.breaksCache}, + ); } - public rollover(newPeriod: TeachingPeriod, rolloverInactive: boolean, searchForward: boolean): Observable { + public rollover( + newPeriod: TeachingPeriod, + rolloverInactive: boolean, + searchForward: boolean, + ): Observable { const teachingPeriodService: TeachingPeriodService = AppInjector.get(TeachingPeriodService); return teachingPeriodService.post( @@ -94,7 +107,110 @@ export class TeachingPeriod extends Entity { }, { endpointFormat: TeachingPeriodService.rolloverEndpointFormat, + }, + ); + } + + public weekNumber(date: Date | string): number | null { + if (!date || !this.startDate) { + return null; + } + + const targetDate = this.normalizeDay(date); + const startDate = this.normalizeDay(this.startDate); + if (!targetDate || !startDate) { + return null; + } + + const millisecondsPerWeek = 1000 * 60 * 60 * 24 * 7; + let result = Math.floor((targetDate.getTime() - startDate.getTime()) / millisecondsPerWeek) + 1; + + for (const teachingBreak of this.breaks) { + const breakStart = this.normalizeDay(teachingBreak.startDate); + const breakEnd = this.breakEndDate(teachingBreak); + const firstMonday = this.firstMonday(teachingBreak); + const mondayAfterBreak = this.mondayAfterBreak(teachingBreak); + + if (!breakStart || !breakEnd || !firstMonday || !mondayAfterBreak) { + continue; } + + if (targetDate >= breakStart) { + if (targetDate >= breakEnd) { + result -= teachingBreak.numberOfWeeks; + } else if (targetDate.getTime() === breakStart.getTime()) { + if (targetDate >= firstMonday) { + result -= 1; + } + } else if (targetDate >= firstMonday) { + result -= Math.ceil((targetDate.getTime() - firstMonday.getTime()) / millisecondsPerWeek); + } + + if (targetDate >= breakEnd && targetDate < mondayAfterBreak) { + result += 1; + } + } + } + + return result; + } + + private normalizeDay(date: Date | string | null | undefined): Date | null { + if (!date) { + return null; + } + + const parsed = date instanceof Date ? date : new Date(date); + if (Number.isNaN(parsed.valueOf())) { + return null; + } + + return new Date(parsed.getFullYear(), parsed.getMonth(), parsed.getDate()); + } + + private breakEndDate(teachingBreak: TeachingPeriodBreak): Date | null { + const startDate = this.normalizeDay(teachingBreak.startDate); + if (!startDate || !teachingBreak.numberOfWeeks) { + return null; + } + + return new Date( + startDate.getFullYear(), + startDate.getMonth(), + startDate.getDate() + teachingBreak.numberOfWeeks * 7, + ); + } + + private firstMonday(teachingBreak: TeachingPeriodBreak): Date | null { + const startDate = this.normalizeDay(teachingBreak.startDate); + if (!startDate) { + return null; + } + + if (startDate.getDay() === 1) { + return startDate; + } + if (startDate.getDay() === 0) { + return new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate() + 1); + } + + return new Date( + startDate.getFullYear(), + startDate.getMonth(), + startDate.getDate() + (8 - startDate.getDay()), + ); + } + + private mondayAfterBreak(teachingBreak: TeachingPeriodBreak): Date | null { + const firstMonday = this.firstMonday(teachingBreak); + if (!firstMonday || !teachingBreak.numberOfWeeks) { + return null; + } + + return new Date( + firstMonday.getFullYear(), + firstMonday.getMonth(), + firstMonday.getDate() + teachingBreak.numberOfWeeks * 7, ); } } diff --git a/src/app/api/models/test-attempt.ts b/src/app/api/models/test-attempt.ts index 4497028a75..1956c00aff 100644 --- a/src/app/api/models/test-attempt.ts +++ b/src/app/api/models/test-attempt.ts @@ -21,7 +21,7 @@ export class TestAttempt extends Entity { * Open a test attempt window in review mode */ public review() { - const url = `#/projects/${this.task.project.id}/task_def_id/${this.task.taskDefId}/scorm-player/review/${this.id}`; + const url = `/projects/${this.task.project.id}/task_def_id/${this.task.taskDefId}/scorm-player/review/${this.id}`; window.open(url, '_blank'); } diff --git a/src/app/api/models/tii-action.ts b/src/app/api/models/tii-action.ts index 0f0ed3d599..75afd42516 100644 --- a/src/app/api/models/tii-action.ts +++ b/src/app/api/models/tii-action.ts @@ -1,6 +1,4 @@ -import { Entity, EntityCache, EntityMapping } from 'ngx-entity-service'; -import { Observable } from 'rxjs'; -import { Unit } from './doubtfire-model'; +import {Entity} from 'ngx-entity-service'; export class TiiAction extends Entity { id: number; @@ -14,5 +12,4 @@ export class TiiAction extends Entity { log: string; description: string; - } diff --git a/src/app/api/models/tutorial-enrolment.ts b/src/app/api/models/tutorial-enrolment.ts index 7463175365..9af8408c51 100644 --- a/src/app/api/models/tutorial-enrolment.ts +++ b/src/app/api/models/tutorial-enrolment.ts @@ -1,6 +1,5 @@ -import { Entity } from 'ngx-entity-service'; -import { Tutorial, User } from './doubtfire-model'; - +import {Entity} from 'ngx-entity-service'; +import {Tutorial} from './doubtfire-model'; export class TutorialEnrolment extends Entity { public tutorial: Tutorial; diff --git a/src/app/api/models/tutorial-stream/tutorial-stream.ts b/src/app/api/models/tutorial-stream/tutorial-stream.ts index 2ccb96759d..17c101fa0e 100644 --- a/src/app/api/models/tutorial-stream/tutorial-stream.ts +++ b/src/app/api/models/tutorial-stream/tutorial-stream.ts @@ -1,7 +1,8 @@ -import { Entity } from 'ngx-entity-service'; -import { Unit, Tutorial } from '../doubtfire-model'; +import {Entity} from 'ngx-entity-service'; +import {Tutorial, Unit} from '../doubtfire-model'; export class TutorialStream extends Entity { + id: number; name: string; abbreviation: string; activityType: string; diff --git a/src/app/api/models/tutorial/tutorial.ts b/src/app/api/models/tutorial/tutorial.ts index 181b1ec0d0..164b8df832 100644 --- a/src/app/api/models/tutorial/tutorial.ts +++ b/src/app/api/models/tutorial/tutorial.ts @@ -1,7 +1,6 @@ -import { Entity, EntityMapping } from 'ngx-entity-service'; -import { AppInjector } from '../../../app-injector'; -import { User, Campus, UserService, CampusService, TutorialStream } from 'src/app/api/models/doubtfire-model'; -import { Unit } from '../unit'; +import {Entity, EntityMapping} from 'ngx-entity-service'; +import {Campus, TutorialStream, User} from 'src/app/api/models/doubtfire-model'; +import {Unit} from '../unit'; export class Tutorial extends Entity { unit: Unit; // TODO: Convert to a unit object once this exists @@ -65,8 +64,11 @@ export class Tutorial extends Entity { } public get tutorName(): string { - if (this.tutor) return this.tutor.name; - else return ''; + if (this.tutor) { + return this.tutor.name; + } else { + return ''; + } } public hasCapacity(): boolean { diff --git a/src/app/api/models/unit-role.ts b/src/app/api/models/unit-role.ts index 967d542381..1436c96847 100644 --- a/src/app/api/models/unit-role.ts +++ b/src/app/api/models/unit-role.ts @@ -1,5 +1,5 @@ import {Entity, EntityCache, EntityMapping} from 'ngx-entity-service'; -import {User, Unit} from './doubtfire-model'; +import {Unit, User} from './doubtfire-model'; import {TutorNote} from './tutor-note'; /** diff --git a/src/app/api/models/unit.ts b/src/app/api/models/unit.ts index cd10810ac2..6c5b87dcb7 100644 --- a/src/app/api/models/unit.ts +++ b/src/app/api/models/unit.ts @@ -1,41 +1,48 @@ import {Entity, EntityCache, EntityMapping} from 'ngx-entity-service'; +import {HttpClient, HttpParams} from '@angular/common/http'; import {Observable, tap} from 'rxjs'; import {AppInjector} from 'src/app/app-injector'; import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; +import {AlertService} from 'src/app/common/services/alert.service'; import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; import {GroupService} from '../services/group.service'; +import {MarkingSessionService} from '../services/marking-session.service'; import {ProjectService} from '../services/project.service'; import {TaskDefinitionService} from '../services/task-definition.service'; +import {TaskPrerequisiteService} from '../services/task-prerequisite.service'; +import {D2lAssessmentMapping} from './d2l/d2l_assessment_mapping'; import { - User, - UnitRole, - Task, - TeachingPeriod, - TaskDefinition, - TutorialStream, - Tutorial, - GroupSet, + Campus, + D2lAssessmentMappingService, Group, - TaskOutcomeAlignment, GroupMembership, - UnitService, + GroupSet, + OverseerImage, Project, + Task, + TaskDefinition, + TaskOutcomeAlignment, + TeachingPeriod, + Tutorial, + TutorialStream, TutorialStreamService, + UnitRole, UnitRoleService, - D2lAssessmentMappingService, - OverseerImage, - OverseerImageService, + UnitService, + User, } from './doubtfire-model'; import {LearningOutcome} from './learning-outcome'; -import {AlertService} from 'src/app/common/services/alert.service'; -import {D2lAssessmentMapping} from './d2l/d2l_assessment_mapping'; -import {SidekiqJob} from './sidekiq-job'; -import {HttpClient, HttpParams} from '@angular/common/http'; -import {TaskPrerequisiteService} from '../services/task-prerequisite.service'; import {MarkingSession} from './marking-session'; -import {MarkingSessionService} from '../services/marking-session.service'; +import {SidekiqJob} from './sidekiq-job'; import {TaskPrerequisite} from './task-prerequisite'; +export interface GradeDefinition { + id: string; + value: number; + label: string; + abbreviation: string; +} + export class Unit extends Entity { id: number; code: string; @@ -58,6 +65,7 @@ export class Unit extends Entity { startDate: Date; //TODO: or string endDate: Date; //TODO: or string portfolioAutoGenerationDate: Date; + currentUnitWeek: number | null; assessmentEnabled: boolean; overseerImageId: number = null; // image needs to be lazy loadaed @@ -79,6 +87,13 @@ export class Unit extends Entity { feedbackWarningThresholdDays: number; feedbackOverflowThresholdDays: number; + gradeDefinitions: GradeDefinition[] = [ + {id: 'fail', value: -1, label: 'Fail', abbreviation: 'F'}, + {id: 'pass', value: 0, label: 'Pass', abbreviation: 'P'}, + {id: 'credit', value: 1, label: 'Credit', abbreviation: 'C'}, + {id: 'distinction', value: 2, label: 'Distinction', abbreviation: 'D'}, + {id: 'high-distinction', value: 3, label: 'High Distinction', abbreviation: 'HD'}, + ]; d2lMapping: D2lAssessmentMapping; @@ -97,12 +112,10 @@ export class Unit extends Entity { public readonly groupSetsCache: EntityCache = new EntityCache(); - groupMemberships: Array; + groupMemberships: GroupMembership[]; readonly studentCache: EntityCache = new EntityCache(); - analytics: {} = {}; - public override toJson( mappingData: EntityMapping, ignoreKeys?: string[], @@ -134,6 +147,20 @@ export class Unit extends Entity { return this.active && (!this.teachingPeriod || this.teachingPeriod.active); } + public get gradeValues(): number[] { + return this.gradeDefinitions + .filter((definition) => definition.value >= 0) + .map((definition) => definition.value); + } + + public gradeLabel(value: number): string { + return this.gradeDefinitions.find((definition) => definition.value === value)?.label; + } + + public gradeAbbreviation(value: number): string { + return this.gradeDefinitions.find((definition) => definition.value === value)?.abbreviation; + } + public matches(text: string): boolean { return this.code.toLowerCase().indexOf(text) >= 0 || this.name.toLowerCase().indexOf(text) >= 0; } @@ -175,6 +202,28 @@ export class Unit extends Entity { return this.findStudent(id)?.enrolled; } + /** + * Enrol a student within the unit. + * + * @param idOrEmail The student id or email of the student to enrol. + * @param campus The student's campus + * @returns an observer of the post with the student project. + */ + public enrolStudent(idOrEmail: string, campus: Campus): Observable { + const projectService = AppInjector.get(ProjectService); + + return projectService.create( + { + unit_id: this.id, + student_num: idOrEmail, + campus_id: campus.id, + }, + { + cache: this.studentCache, + }, + ); + } + public get currentUserIsStaff(): boolean { return this.myRole !== 'Student'; } @@ -198,7 +247,7 @@ export class Unit extends Entity { taskDefinitionService .delete({unitId: this.id, id: taskDef.id}, {cache: this.taskDefinitionCache, entity: taskDef}) .subscribe({ - next: (response) => { + next: () => { alerts.success('Task Deleted', 2000); }, error: (message) => alerts.error(message, 6000), @@ -252,6 +301,42 @@ export class Unit extends Entity { return Math.ceil(this.totalDuration / (1000 * 60 * 60 * 24 * 7)); } + /** + * Calculate the teaching week number for a given date. + * Mirrors the Rails fallback in Unit#week_number when a teaching period + * helper is not being used on the frontend. + */ + public weekNumber(date: Date | string): number | null { + if (!date || !this.startDate) { + return null; + } + + if (this.teachingPeriod) { + return this.teachingPeriod.weekNumber(date); + } + + const targetDate = date instanceof Date ? date : new Date(date); + if (Number.isNaN(targetDate.valueOf())) { + return null; + } + const normalizedTargetDate = new Date( + targetDate.getFullYear(), + targetDate.getMonth(), + targetDate.getDate(), + ); + const normalizedStartDate = new Date( + this.startDate.getFullYear(), + this.startDate.getMonth(), + this.startDate.getDate(), + ); + const millisecondsPerWeek = 1000 * 60 * 60 * 24 * 7; + return ( + Math.floor( + (normalizedTargetDate.valueOf() - normalizedStartDate.valueOf()) / millisecondsPerWeek, + ) + 1 + ); + } + /** * Calculate how much time has elapsed in the teaching period, based on the start and * end date of the unit relative to the current date. @@ -262,17 +347,23 @@ export class Unit extends Entity { const today = new Date(); //use Math.abs to avoid sign - if (today <= this.startDate) return 0; - if (today >= this.endDate) return 100; + if (today <= this.startDate) { + return 0; + } + if (today >= this.endDate) { + return 100; + } const startToNow = Math.abs(today.valueOf() - this.startDate.valueOf()); const totalDuration = Math.abs(this.totalDuration); return Math.round((startToNow / totalDuration) * 100); } - public rolloverTo(body: {new_unit_code?: string, start_date: Date; end_date: Date}): Observable; - public rolloverTo(body: {new_unit_code?: string, teaching_period_id: number}): Observable; - public rolloverTo(body: any): Observable { + public rolloverTo( + body: + | {new_unit_code?: string; start_date: Date; end_date: Date} + | {new_unit_code?: string; teaching_period_id: number}, + ): Observable { const unitService = AppInjector.get(UnitService); return unitService.create( @@ -423,7 +514,7 @@ export class Unit extends Entity { return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.id}/grades/csv`; } - public taskStatusFactor(td: TaskDefinition): number { + public taskStatusFactor(_td: TaskDefinition): number { return 1; } @@ -586,6 +677,16 @@ export class Unit extends Entity { }`; } + public getBatchFeedbackUploadUrl(taskDefinition: TaskDefinition | number): string { + const params = new URLSearchParams({unit_id: `${this.id}`}); + const taskDefinitionId = + taskDefinition instanceof TaskDefinition ? taskDefinition.id : taskDefinition; + + params.set('task_definition_id', `${taskDefinitionId}`); + + return `${AppInjector.get(DoubtfireConstants).API_URL}/submission/batch_feedback_csv.json?${params.toString()}`; + } + public getTaskDefinitionBatchUploadUrl(): string { return `${AppInjector.get(DoubtfireConstants).API_URL}/csv/task_definitions?unit_id=${this.id}`; } @@ -608,6 +709,12 @@ export class Unit extends Entity { ); } + public downloadOverflowTaskClaimsCsv(): Observable { + return AppInjector.get(HttpClient).get( + `${AppInjector.get(DoubtfireConstants).API_URL}/csv/units/${this.id}/overflow_task_claims`, + ); + } + public downloadTutorAssessmentCsv(): Observable { return AppInjector.get(HttpClient).get( `${AppInjector.get(DoubtfireConstants).API_URL}/csv/units/${this.id}/tutor_assessments`, diff --git a/src/app/api/models/user/user.ts b/src/app/api/models/user/user.ts index b11826505a..5a98e22551 100644 --- a/src/app/api/models/user/user.ts +++ b/src/app/api/models/user/user.ts @@ -1,9 +1,8 @@ -import {HttpClient} from '@angular/common/http'; import {Entity, EntityMapping} from 'ngx-entity-service'; +import {HttpClient} from '@angular/common/http'; import {Observable, map} from 'rxjs'; import {AppInjector} from 'src/app/app-injector'; import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; -import {AuthenticationService} from '../doubtfire-model'; export type Tutor = User; @@ -22,6 +21,7 @@ export class User extends Entity { public receiveFeedbackNotifications: boolean; public hasRunFirstTimeSetup: boolean; public authenticationToken: string; + public authenticationTokenExpiry: string; public pronouns: string | null; public acceptedTiiEula: boolean; @@ -46,16 +46,16 @@ export class User extends Entity { } public get name(): string { - const fn = this.firstName.slice(0, 11).trim(); - const sn = this.lastName.slice(0, 11).trim(); + const fn = (this.firstName ?? '').slice(0, 11).trim(); + const sn = (this.lastName ?? '').slice(0, 11).trim(); const nn = this.nickname && this.nickname.trim() ? ` (${this.nickname.trim().slice(0, 11).trim()})` : ''; - return `${fn} ${sn}${nn}`; + return `${fn} ${sn}${nn}`.trim(); } public get preferredName(): string { const nickname = this.nickname?.trim(); - const firstName = this.firstName.trim(); + const firstName = this.firstName?.trim() ?? ''; if (nickname) { return nickname; } diff --git a/src/app/api/models/webcal/webcal.ts b/src/app/api/models/webcal/webcal.ts index 26dab8e18f..a77f247b14 100644 --- a/src/app/api/models/webcal/webcal.ts +++ b/src/app/api/models/webcal/webcal.ts @@ -1,4 +1,4 @@ -import { Entity, EntityMapping } from 'ngx-entity-service'; +import {Entity, EntityMapping} from 'ngx-entity-service'; export class Webcal extends Entity { enabled: boolean; @@ -15,7 +15,10 @@ export class Webcal extends Entity { // Used only when updating the webcal. Never returned from the API. shouldChangeGuid?: boolean; - public override toJson(mappingData: EntityMapping, ignoreKeys?: string[]): object { + public override toJson( + mappingData: EntityMapping, + ignoreKeys?: string[], + ): object { return { webcal: super.toJson(mappingData, ignoreKeys), }; diff --git a/src/app/api/services/activity-type.service.ts b/src/app/api/services/activity-type.service.ts index 628acde622..9d5110894e 100644 --- a/src/app/api/services/activity-type.service.ts +++ b/src/app/api/services/activity-type.service.ts @@ -1,8 +1,8 @@ -import {ActivityType} from 'src/app/api/models/doubtfire-model'; -import {Injectable} from '@angular/core'; import {CachedEntityService} from 'ngx-entity-service'; -import API_URL from 'src/app/config/constants/apiUrl'; import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {ActivityType} from 'src/app/api/models/doubtfire-model'; +import API_URL from 'src/app/config/constants/apiUrl'; @Injectable() export class ActivityTypeService extends CachedEntityService { @@ -16,7 +16,7 @@ export class ActivityTypeService extends CachedEntityService { this.mapping.mapAllKeysToJsonExcept('id'); } - public createInstanceFrom(json: object, other?: any): ActivityType { + public createInstanceFrom(_json: object): ActivityType { return new ActivityType(); } } diff --git a/src/app/api/services/authentication.service.ts b/src/app/api/services/authentication.service.ts index be59c71786..1f4ed2b7ff 100644 --- a/src/app/api/services/authentication.service.ts +++ b/src/app/api/services/authentication.service.ts @@ -1,12 +1,12 @@ -import {User, UserService} from 'src/app/api/models/doubtfire-model'; -import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; -import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; -import {StateService, UIRouter, UIRouterGlobals} from '@uirouter/angular'; -import {GlobalStateService, ViewType} from 'src/app/projects/states/index/global-state.service'; +import {Injectable} from '@angular/core'; +import {Router} from '@angular/router'; +import {AsyncSubject, Observable, catchError, map, throwError} from 'rxjs'; +import {User, UserService} from 'src/app/api/models/doubtfire-model'; import {AppInjector} from 'src/app/app-injector'; -import {AsyncSubject, catchError, map, Observable, throwError} from 'rxjs'; import {AlertService} from 'src/app/common/services/alert.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {GlobalStateService, ViewType} from 'src/app/projects/states/index/global-state.service'; /** * The format for the data returned from the auth api. @@ -14,6 +14,7 @@ import {AlertService} from 'src/app/common/services/alert.service'; interface AuthResponse { user: object; auth_token: string; + auth_token_expiry: string; lti_token?: string; } @@ -44,10 +45,8 @@ export class AuthenticationService { private httpClient: HttpClient, private userService: UserService, private alertService: AlertService, - private state: StateService, + private angularRouter: Router, private doubtfireConstants: DoubtfireConstants, - private router: UIRouter, - private uiRouterGlobals: UIRouterGlobals, ) { this.AUTH_URL = `${this.doubtfireConstants.API_URL}/auth`; // Ensure any only user data is removed from local storage @@ -177,6 +176,7 @@ export class AuthenticationService { // Set the user's authentication token for access to api. user.authenticationToken = response['auth_token']; + user.authenticationTokenExpiry = response['auth_token_expiry']; // Record the current user this.userService.currentUser = user; @@ -274,7 +274,7 @@ export class AuthenticationService { if (ssoSignOut && this.doubtfireConstants.SignoutURL) { window.location.assign(this.doubtfireConstants.SignoutURL); } else { - this.state.go('sign_in'); + this.angularRouter.navigateByUrl('/sign_in'); } }; @@ -290,9 +290,9 @@ export class AuthenticationService { } public timeoutAuthentication(): void { - if (this.uiRouterGlobals.current.name !== 'timeout') { + if (window.location.pathname !== '/timeout') { this.alertService.error('Authentication timed out', 6000); - setTimeout(() => this.router.stateService.go('timeout'), 500); + setTimeout(() => this.angularRouter.navigateByUrl('/timeout'), 500); } } diff --git a/src/app/api/services/campus.service.ts b/src/app/api/services/campus.service.ts index d46d793cc3..552e5acf47 100644 --- a/src/app/api/services/campus.service.ts +++ b/src/app/api/services/campus.service.ts @@ -1,7 +1,7 @@ -import {Campus} from 'src/app/api/models/doubtfire-model'; -import {Injectable} from '@angular/core'; import {CachedEntityService} from 'ngx-entity-service'; import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Campus} from 'src/app/api/models/doubtfire-model'; import API_URL from 'src/app/config/constants/apiUrl'; @Injectable() @@ -16,7 +16,7 @@ export class CampusService extends CachedEntityService { this.mapping.mapAllKeysToJsonExcept('id'); } - public createInstanceFrom(json: object, other?: any): Campus { + public createInstanceFrom(_json: object): Campus { return new Campus(); } } diff --git a/src/app/api/services/communication-action.service.ts b/src/app/api/services/communication-action.service.ts new file mode 100644 index 0000000000..256e579b43 --- /dev/null +++ b/src/app/api/services/communication-action.service.ts @@ -0,0 +1,49 @@ +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable, map} from 'rxjs'; +import API_URL from 'src/app/config/constants/apiUrl'; +import {CommunicationAction} from '../models/communication'; + +@Injectable() +export class CommunicationActionService { + constructor(private httpClient: HttpClient) {} + + public getForRule(unitId: number, ruleId: number): Observable { + return this.httpClient + .get[]>(this.endpoint(unitId, ruleId)) + .pipe(map((actions) => actions.map((action) => new CommunicationAction(action)))); + } + + public create( + unitId: number, + ruleId: number, + action: Partial, + ): Observable { + return this.httpClient + .post>(this.endpoint(unitId, ruleId), { + communication_action: action, + }) + .pipe(map((created) => new CommunicationAction(created))); + } + + public delete(unitId: number, ruleId: number, actionId: number): Observable { + return this.httpClient.delete(`${this.endpoint(unitId, ruleId)}/${actionId}`); + } + + public update( + unitId: number, + ruleId: number, + actionId: number, + action: Partial, + ): Observable { + return this.httpClient + .put>(`${this.endpoint(unitId, ruleId)}/${actionId}`, { + communication_action: action, + }) + .pipe(map((updated) => new CommunicationAction(updated))); + } + + private endpoint(unitId: number, ruleId: number): string { + return `${API_URL}/units/${unitId}/communication_rules/${ruleId}/actions`; + } +} diff --git a/src/app/api/services/communication-condition.service.ts b/src/app/api/services/communication-condition.service.ts new file mode 100644 index 0000000000..3024e0c4e2 --- /dev/null +++ b/src/app/api/services/communication-condition.service.ts @@ -0,0 +1,51 @@ +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable, map} from 'rxjs'; +import API_URL from 'src/app/config/constants/apiUrl'; +import {CommunicationCondition} from '../models/communication'; + +@Injectable() +export class CommunicationConditionService { + constructor(private httpClient: HttpClient) {} + + public getForRule(unitId: number, ruleId: number): Observable { + return this.httpClient + .get[]>(this.endpoint(unitId, ruleId)) + .pipe( + map((conditions) => conditions.map((condition) => new CommunicationCondition(condition))), + ); + } + + public create( + unitId: number, + ruleId: number, + condition: Partial, + ): Observable { + return this.httpClient + .post>(this.endpoint(unitId, ruleId), { + communication_condition: condition, + }) + .pipe(map((created) => new CommunicationCondition(created))); + } + + public delete(unitId: number, ruleId: number, conditionId: number): Observable { + return this.httpClient.delete(`${this.endpoint(unitId, ruleId)}/${conditionId}`); + } + + public update( + unitId: number, + ruleId: number, + conditionId: number, + condition: Partial, + ): Observable { + return this.httpClient + .put>(`${this.endpoint(unitId, ruleId)}/${conditionId}`, { + communication_condition: condition, + }) + .pipe(map((updated) => new CommunicationCondition(updated))); + } + + private endpoint(unitId: number, ruleId: number): string { + return `${API_URL}/units/${unitId}/communication_rules/${ruleId}/conditions`; + } +} diff --git a/src/app/api/services/communication-rule.service.ts b/src/app/api/services/communication-rule.service.ts new file mode 100644 index 0000000000..e7cab7b487 --- /dev/null +++ b/src/app/api/services/communication-rule.service.ts @@ -0,0 +1,67 @@ +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable, map} from 'rxjs'; +import API_URL from 'src/app/config/constants/apiUrl'; +import {CommunicationRule, CommunicationRulePreviewResponse} from '../models/communication'; +import {SidekiqJob} from '../models/sidekiq-job'; + +@Injectable() +export class CommunicationRuleService { + constructor(private httpClient: HttpClient) {} + + public getForSet(unitId: number, setId: number): Observable { + return this.httpClient + .get[]>(this.setEndpoint(unitId, setId)) + .pipe(map((rules) => rules.map((rule) => new CommunicationRule(rule)))); + } + + public createForSet( + unitId: number, + setId: number, + rule: Pick, + ): Observable { + return this.httpClient + .post>(this.setEndpoint(unitId, setId), { + communication_rule: rule, + }) + .pipe(map((created) => new CommunicationRule(created))); + } + + public updateForUnit( + unitId: number, + ruleId: number, + rule: Partial>, + ): Observable { + return this.httpClient + .put>(`${this.endpoint(unitId)}/${ruleId}`, { + communication_rule: rule, + }) + .pipe(map((updated) => new CommunicationRule(updated))); + } + + public deleteForUnit(unitId: number, ruleId: number): Observable { + return this.httpClient.delete(`${this.endpoint(unitId)}/${ruleId}`); + } + + public previewForUnit( + unitId: number, + ruleId: number, + ): Observable { + return this.httpClient.post( + `${this.endpoint(unitId)}/${ruleId}/preview`, + {}, + ); + } + + public executeForUnit(unitId: number, ruleId: number): Observable { + return this.httpClient.post(`${this.endpoint(unitId)}/${ruleId}/execute`, {}); + } + + private endpoint(unitId: number): string { + return `${API_URL}/units/${unitId}/communication_rules`; + } + + private setEndpoint(unitId: number, setId: number): string { + return `${API_URL}/units/${unitId}/communication_sets/${setId}/rules`; + } +} diff --git a/src/app/api/services/communication-set.service.ts b/src/app/api/services/communication-set.service.ts new file mode 100644 index 0000000000..73ab9767c6 --- /dev/null +++ b/src/app/api/services/communication-set.service.ts @@ -0,0 +1,67 @@ +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable, map} from 'rxjs'; +import API_URL from 'src/app/config/constants/apiUrl'; +import { + CommunicationSet, + CommunicationSetPreviewResponse, + CommunicationSetSchedule, +} from '../models/communication'; +import {SidekiqJob} from '../models/sidekiq-job'; + +@Injectable() +export class CommunicationSetService { + constructor(private httpClient: HttpClient) {} + + public getForUnit(unitId: number): Observable { + return this.httpClient + .get[]>(this.endpoint(unitId)) + .pipe(map((sets) => sets.map((set) => new CommunicationSet(set)))); + } + + public createForUnit( + unitId: number, + set: Pick & Partial>, + ): Observable { + return this.httpClient + .post>(this.endpoint(unitId), { + communication_set: set, + }) + .pipe(map((created) => new CommunicationSet(created))); + } + + public deleteForUnit(unitId: number, setId: number): Observable { + return this.httpClient.delete(`${this.endpoint(unitId)}/${setId}`); + } + + public updateForUnit( + unitId: number, + setId: number, + set: Partial> & { + schedules?: Partial[]; + }, + ): Observable { + return this.httpClient + .put>(`${this.endpoint(unitId)}/${setId}`, { + communication_set: set, + }) + .pipe(map((updated) => new CommunicationSet(updated))); + } + + public getForUnitById( + unitId: number, + setId: number, + ): Observable { + return this.httpClient.get( + `${this.endpoint(unitId)}/${setId}`, + ); + } + + public executeForUnit(unitId: number, setId: number): Observable { + return this.httpClient.post(`${this.endpoint(unitId)}/${setId}/execute`, {}); + } + + private endpoint(unitId: number): string { + return `${API_URL}/units/${unitId}/communication_sets`; + } +} diff --git a/src/app/api/services/discussion-prompt.service.ts b/src/app/api/services/discussion-prompt.service.ts index c102fbcbf1..435f1d53e8 100644 --- a/src/app/api/services/discussion-prompt.service.ts +++ b/src/app/api/services/discussion-prompt.service.ts @@ -1,6 +1,6 @@ +import {CachedEntityService, RequestOptions} from 'ngx-entity-service'; import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; -import {CachedEntityService, RequestOptions} from 'ngx-entity-service'; import {Observable} from 'rxjs'; import { Project, @@ -57,7 +57,7 @@ export class DiscussionPromptService extends CachedEntityService { + toJsonFn: (entity: DiscussionPrompt, _key: string) => { return entity.taskDefinition?.id; }, }, diff --git a/src/app/api/services/engagement-comment.service.ts b/src/app/api/services/engagement-comment.service.ts new file mode 100644 index 0000000000..bceb22511e --- /dev/null +++ b/src/app/api/services/engagement-comment.service.ts @@ -0,0 +1,116 @@ +import {CachedEntityService, RequestOptions} from 'ngx-entity-service'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable, tap} from 'rxjs'; +import {Engagement, EngagementComment, UserService} from 'src/app/api/models/doubtfire-model'; +import API_URL from 'src/app/config/constants/apiUrl'; +import {MappingFunctions} from './mapping-fn'; + +@Injectable() +export class EngagementCommentService extends CachedEntityService { + protected readonly endpointFormat = + 'projects/:projectId:/engagements/:engagementId:/comments/:id:'; + + constructor( + httpClient: HttpClient, + private userService: UserService, + ) { + super(httpClient, API_URL); + + this.mapping.addKeys( + 'id', + 'comment', + 'replyToId', + { + keys: 'user', + toEntityFn: (data: object, key: string) => { + return this.userService.cache.getOrCreate(data[key].id, this.userService, data[key]); + }, + }, + { + keys: 'createdAt', + toEntityFn: MappingFunctions.mapDate, + }, + { + keys: 'updatedAt', + toEntityFn: MappingFunctions.mapDate, + }, + ); + } + + createInstanceFrom(_json: object, other?: Engagement): EngagementComment { + return new EngagementComment(other); + } + + addComment( + engagement: Engagement, + comment: string, + replyTo?: EngagementComment, + ): Observable { + const options: RequestOptions = { + endpointFormat: this.endpointFormat, + cache: engagement.commentCache, + constructorParams: engagement, + body: { + comment, + ...(replyTo ? {reply_to_id: replyTo.id} : {}), + }, + }; + + return this.create( + { + projectId: engagement.project.id, + engagementId: engagement.id, + }, + options, + ).pipe( + tap(() => { + engagement.commentCount++; + this.updateCommentReplies(engagement.comments); + }), + ); + } + + updateComment(comment: EngagementComment, text: string): Observable { + return this.put( + { + projectId: comment.engagement.project.id, + engagementId: comment.engagement.id, + id: comment.id, + }, + { + endpointFormat: this.endpointFormat, + cache: comment.engagement.commentCache, + constructorParams: comment.engagement, + body: {comment: text}, + }, + ); + } + + deleteComment(comment: EngagementComment): Observable { + return this.delete( + { + projectId: comment.engagement.project.id, + engagementId: comment.engagement.id, + id: comment.id, + }, + { + endpointFormat: this.endpointFormat, + cache: comment.engagement.commentCache, + }, + ).pipe( + tap(() => { + comment.engagement.commentCount--; + this.updateCommentReplies(comment.engagement.comments); + }), + ); + } + + updateCommentReplies(comments: readonly EngagementComment[]): void { + for (const comment of comments) { + comment.replyTo = comment.replyToId + ? comments.find((candidate) => candidate.id === comment.replyToId) + : undefined; + } + } +} diff --git a/src/app/api/services/engagement.service.ts b/src/app/api/services/engagement.service.ts new file mode 100644 index 0000000000..89c63a189a --- /dev/null +++ b/src/app/api/services/engagement.service.ts @@ -0,0 +1,177 @@ +import {CachedEntityService, RequestOptions} from 'ngx-entity-service'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; +import { + Engagement, + EngagementCommentService, + Project, + UserService, +} from 'src/app/api/models/doubtfire-model'; +import API_URL from 'src/app/config/constants/apiUrl'; +import {MappingFunctions} from './mapping-fn'; + +export interface EngagementData { + engagementType: string; + note: string; + occurredAt: Date; + evidenceUrl?: string; + attachment?: File; +} + +export interface EngagementUpdate extends Partial { + removeEvidence?: boolean; +} + +@Injectable() +export class EngagementService extends CachedEntityService { + protected readonly endpointFormat = 'projects/:projectId:/engagements/:id:'; + + constructor( + httpClient: HttpClient, + private userService: UserService, + private engagementCommentService: EngagementCommentService, + ) { + super(httpClient, API_URL); + + this.mapping.addKeys( + 'id', + 'engagementType', + 'note', + 'evidenceUrl', + 'contentType', + 'hasAttachment', + 'attachmentFileName', + 'commentCount', + { + keys: 'user', + toEntityFn: (data: object, key: string) => { + return this.userService.cache.getOrCreate(data[key].id, this.userService, data[key]); + }, + }, + { + keys: 'occurredAt', + toEntityFn: MappingFunctions.mapDate, + }, + { + keys: 'createdAt', + toEntityFn: MappingFunctions.mapDate, + }, + { + keys: 'updatedAt', + toEntityFn: MappingFunctions.mapDate, + }, + { + keys: 'comments', + toEntityOp: (data: object, key: string, engagement: Engagement) => { + engagement.commentCache.clear(); + data[key]?.forEach((comment) => { + engagement.commentCache.getOrCreate( + comment.id, + this.engagementCommentService, + comment, + {constructorParams: engagement}, + ); + }); + this.engagementCommentService.updateCommentReplies(engagement.comments); + }, + }, + ); + } + + createInstanceFrom(_json: object, other?: Project): Engagement { + return new Engagement(other); + } + + loadEngagements(project: Project, refresh: boolean = false): Observable { + const options: RequestOptions = { + endpointFormat: this.endpointFormat, + cache: project.engagementCache, + sourceCache: project.engagementCache, + cacheBehaviourOnGet: 'cacheQuery', + constructorParams: project, + }; + const pathIds = {projectId: project.id}; + + return refresh ? this.fetchAll(pathIds, options) : this.query(pathIds, options); + } + + loadEngagement(engagement: Engagement): Observable { + return this.fetch( + { + projectId: engagement.project.id, + id: engagement.id, + }, + { + endpointFormat: this.endpointFormat, + cache: engagement.project.engagementCache, + constructorParams: engagement.project, + }, + ); + } + + createEngagement(project: Project, data: EngagementData): Observable { + return this.create( + {projectId: project.id}, + { + endpointFormat: this.endpointFormat, + cache: project.engagementCache, + constructorParams: project, + body: this.toFormData(data), + }, + ); + } + + updateEngagement(engagement: Engagement, data: EngagementUpdate): Observable { + return this.put( + { + projectId: engagement.project.id, + id: engagement.id, + }, + { + endpointFormat: this.endpointFormat, + cache: engagement.project.engagementCache, + constructorParams: engagement.project, + body: this.toFormData(data), + }, + ); + } + + deleteEngagement(engagement: Engagement): Observable { + return this.delete( + { + projectId: engagement.project.id, + id: engagement.id, + }, + { + endpointFormat: this.endpointFormat, + cache: engagement.project.engagementCache, + }, + ); + } + + private toFormData(data: EngagementUpdate): FormData { + const body = new FormData(); + + if (data.engagementType !== undefined) { + body.append('engagement_type', data.engagementType); + } + if (data.note !== undefined) { + body.append('note', data.note); + } + if (data.occurredAt !== undefined) { + body.append('occurred_at', data.occurredAt.toISOString()); + } + if (data.evidenceUrl !== undefined) { + body.append('evidence_url', data.evidenceUrl); + } + if (data.attachment !== undefined) { + body.append('attachment', data.attachment); + } + if (data.removeEvidence !== undefined) { + body.append('remove_evidence', String(data.removeEvidence)); + } + + return body; + } +} diff --git a/src/app/api/services/feedback-template.service.ts b/src/app/api/services/feedback-template.service.ts index 54d981d56b..ec2da997a6 100644 --- a/src/app/api/services/feedback-template.service.ts +++ b/src/app/api/services/feedback-template.service.ts @@ -1,8 +1,8 @@ -import {Injectable} from '@angular/core'; import {CachedEntityService} from 'ngx-entity-service'; -import {FeedbackTemplate} from '../models/feedback-template'; import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; import API_URL from 'src/app/config/constants/apiUrl'; +import {FeedbackTemplate} from '../models/feedback-template'; @Injectable() export class FeedbackTemplateService extends CachedEntityService { @@ -29,7 +29,7 @@ export class FeedbackTemplateService extends CachedEntityService { this.mapping.mapAllKeysToJsonExcept('id'); } - public createInstanceFrom(json: object, other?: any): GroupSet { - return new GroupSet(other as Unit); + public createInstanceFrom(_json: object, other?: Unit): GroupSet { + return new GroupSet(other); } } diff --git a/src/app/api/services/group.service.ts b/src/app/api/services/group.service.ts index c9a7435828..bc3e8afb11 100644 --- a/src/app/api/services/group.service.ts +++ b/src/app/api/services/group.service.ts @@ -1,7 +1,7 @@ import {CachedEntityService} from 'ngx-entity-service'; -import {Group, Unit} from 'src/app/api/models/doubtfire-model'; -import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Group, Unit} from 'src/app/api/models/doubtfire-model'; import API_URL from 'src/app/config/constants/apiUrl'; @Injectable() @@ -28,7 +28,7 @@ export class GroupService extends CachedEntityService { toEntityFn: (data: object, jsonKey: string, grp: Group) => { return grp.unit.tutorialsCache.get(data[jsonKey]); }, - toJsonFn: (group: Group, key: string) => { + toJsonFn: (group: Group, _key: string) => { return group.tutorial.id; }, }, @@ -37,7 +37,7 @@ export class GroupService extends CachedEntityService { this.mapping.mapAllKeysToJsonExcept('id', 'groupSet', 'studentCount'); } - public createInstanceFrom(json: object, other?: any): Group { - return new Group(other as Unit); + public createInstanceFrom(_json: object, other?: Unit): Group { + return new Group(other); } } diff --git a/src/app/api/services/learning-outcome.service.ts b/src/app/api/services/learning-outcome.service.ts index 2c4d6181c8..be1075257d 100644 --- a/src/app/api/services/learning-outcome.service.ts +++ b/src/app/api/services/learning-outcome.service.ts @@ -1,7 +1,7 @@ import {CachedEntityService} from 'ngx-entity-service'; -import {LearningOutcome} from 'src/app/api/models/doubtfire-model'; -import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {LearningOutcome} from 'src/app/api/models/doubtfire-model'; import API_URL from 'src/app/config/constants/apiUrl'; @Injectable() @@ -27,7 +27,7 @@ export class LearningOutcomeService extends CachedEntityService this.mapping.mapAllKeysToJsonExcept('id', 'context'); } - public createInstanceFrom(json: object, other?: any): LearningOutcome { + public createInstanceFrom(_json: object): LearningOutcome { return new LearningOutcome(); } } diff --git a/src/app/api/services/lti.service.ts b/src/app/api/services/lti.service.ts index 3f97e32fda..4cb85cc9f9 100644 --- a/src/app/api/services/lti.service.ts +++ b/src/app/api/services/lti.service.ts @@ -1,6 +1,7 @@ import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; import {Observable} from 'rxjs'; +import {CsvResult} from 'src/app/common/modals/csv-result-modal/csv-result-modal.service'; import LTI_API_URL from 'src/app/config/constants/ltiApiUrl'; import {Project} from '../models/project'; import {SidekiqJob} from '../models/sidekiq-job'; @@ -9,7 +10,7 @@ interface info { name?: string; email?: string; roles?: string[]; - custom?: any; + custom?: Record; context?: | { id?: string; @@ -34,6 +35,19 @@ export interface UnitLink { unitId: string; } +export interface LtiMembers { + members: LtiMember[]; +} + +export interface LtiMember { + email: string; + family_name: string; + given_name: string; + name: string; + user_id: string; + roles: string[]; +} + @Injectable() export class LtiService { constructor(private httpClient: HttpClient) {} @@ -58,13 +72,13 @@ export class LtiService { return this.httpClient.post(`${LTI_API_URL}/enrol`, unit); } - public getMembers(): Observable { - return this.httpClient.get(`${LTI_API_URL}/members`); + public getMembers(): Observable { + return this.httpClient.get(`${LTI_API_URL}/members`); } // Sync grades for all members in the context (course) - public syncStudentsGrades(): Observable { - return this.httpClient.post(`${LTI_API_URL}/grades`, {}); + public syncStudentsGrades(): Observable { + return this.httpClient.post(`${LTI_API_URL}/grades`, {}); } // Sync grades for all members in the context (course) diff --git a/src/app/api/services/mapping-fn.ts b/src/app/api/services/mapping-fn.ts index 8da53e0bb0..811dfca0cf 100644 --- a/src/app/api/services/mapping-fn.ts +++ b/src/app/api/services/mapping-fn.ts @@ -116,4 +116,15 @@ export class MappingFunctions { const diff = this.daysBetween(date1, date2); return Math.ceil(diff / 7); } + + /** + * Calculate the date that is a number of days after a given date + * + * @param date start date + * @param days number of days to add + * @returns the date that is that many days after the start date + */ + public static daysAfter(date: Date, days: number): Date { + return new Date(date.getTime() + this.dayMs(days)); + } } diff --git a/src/app/api/services/marking-session.service.ts b/src/app/api/services/marking-session.service.ts index a621dfa5f0..d5deeb814c 100644 --- a/src/app/api/services/marking-session.service.ts +++ b/src/app/api/services/marking-session.service.ts @@ -1,6 +1,6 @@ +import {CachedEntityService} from 'ngx-entity-service'; import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; -import {CachedEntityService} from 'ngx-entity-service'; import {Unit} from 'src/app/api/models/doubtfire-model'; import API_URL from 'src/app/config/constants/apiUrl'; import {MarkingSession} from '../models/marking-session'; diff --git a/src/app/api/services/overseer-assessment.service.ts b/src/app/api/services/overseer-assessment.service.ts index 82c41e3dbc..c206952ec5 100644 --- a/src/app/api/services/overseer-assessment.service.ts +++ b/src/app/api/services/overseer-assessment.service.ts @@ -1,10 +1,10 @@ -import {Injectable} from '@angular/core'; import {EntityService} from 'ngx-entity-service'; -import {Observable} from 'rxjs'; import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; import API_URL from 'src/app/config/constants/apiUrl'; -import {OverseerAssessment} from '../models/overseer/overseer-assessment'; import {Task} from '../models/doubtfire-model'; +import {OverseerAssessment} from '../models/overseer/overseer-assessment'; import {OverseerStepResultService} from './overseer-step-result.service'; @Injectable() @@ -24,13 +24,14 @@ export class OverseerAssessmentService extends EntityService 'id', 'submissionTimestamp', 'taskId', + 'submissionHistoryId', 'createdAt', 'updatedAt', ['taskStatus', 'result_task_status'], ['submissionStatus', 'status'], { keys: ['timestamp', 'submission_timestamp'], - toEntityFn: (data, key, entity, params?) => { + toEntityFn: (data, _key, _entity, _params?) => { return new Date(data['submission_timestamp'] * 1000); }, }, @@ -57,7 +58,7 @@ export class OverseerAssessmentService extends EntityService ); } - public createInstanceFrom(json: any, other?: any): OverseerAssessment { + public createInstanceFrom(_json: object, other?: Task): OverseerAssessment { return new OverseerAssessment(other); } diff --git a/src/app/api/services/overseer-image.service.ts b/src/app/api/services/overseer-image.service.ts index 7edc65932f..f49955fdec 100644 --- a/src/app/api/services/overseer-image.service.ts +++ b/src/app/api/services/overseer-image.service.ts @@ -1,8 +1,8 @@ import {CachedEntityService} from 'ngx-entity-service'; -import {Observable, switchMap} from 'rxjs'; -import {OverseerImage} from 'src/app/api/models/doubtfire-model'; -import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; +import {OverseerImage} from 'src/app/api/models/doubtfire-model'; import API_URL from 'src/app/config/constants/apiUrl'; import {SidekiqJob} from '../models/sidekiq-job'; @@ -32,7 +32,7 @@ export class OverseerImageService extends CachedEntityService { }); } - public createInstanceFrom(json: object, other?: any): OverseerImage { + public createInstanceFrom(_json: object): OverseerImage { return new OverseerImage(); } } diff --git a/src/app/api/services/overseer-step-result.service.ts b/src/app/api/services/overseer-step-result.service.ts index f7e873281b..ba1158714b 100644 --- a/src/app/api/services/overseer-step-result.service.ts +++ b/src/app/api/services/overseer-step-result.service.ts @@ -1,10 +1,10 @@ +import {CachedEntityService} from 'ngx-entity-service'; import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; -import {CachedEntityService} from 'ngx-entity-service'; +import {Observable} from 'rxjs'; import API_URL from 'src/app/config/constants/apiUrl'; import {OverseerAssessment} from '../models/doubtfire-model'; import {OverseerStepResult} from '../models/overseer/overseer-step-result'; -import {Observable} from 'rxjs'; @Injectable() export class OverseerStepResultService extends CachedEntityService { @@ -31,8 +31,8 @@ export class OverseerStepResultService extends CachedEntityService { diff --git a/src/app/api/services/overseer-step.service.ts b/src/app/api/services/overseer-step.service.ts index 3d8a8fca9a..dffdd95999 100644 --- a/src/app/api/services/overseer-step.service.ts +++ b/src/app/api/services/overseer-step.service.ts @@ -1,6 +1,6 @@ +import {CachedEntityService} from 'ngx-entity-service'; import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; -import {CachedEntityService} from 'ngx-entity-service'; import API_URL from 'src/app/config/constants/apiUrl'; import {OverseerStep} from '../models/overseer/overseer-step'; import {TaskDefinition} from '../models/task-definition'; @@ -23,7 +23,7 @@ export class OverseerStepService extends CachedEntityService { // 'runCommand', { keys: 'runCommand', - toEntityFn: (data: object, key: string, entity: OverseerStep, params?: any) => { + toEntityFn: (data: object, key: string, entity: OverseerStep) => { const raw = data['run_command']; if (raw?.startsWith('b64:')) { entity.decodedRunCommand = atob(raw.slice(4)); @@ -54,7 +54,7 @@ export class OverseerStepService extends CachedEntityService { this.mapping.mapAllKeysToJsonExcept('id'); } - public createInstanceFrom(json: object, other?: any): OverseerStep { - return new OverseerStep(other as TaskDefinition); + public createInstanceFrom(_json: object, other?: TaskDefinition): OverseerStep { + return new OverseerStep(other); } } diff --git a/src/app/api/services/project.service.ts b/src/app/api/services/project.service.ts index fe7aaa30d0..4156e29484 100644 --- a/src/app/api/services/project.service.ts +++ b/src/app/api/services/project.service.ts @@ -1,4 +1,7 @@ -import {CachedEntityService, RequestOptions} from 'ngx-entity-service'; +import {CachedEntityService, MappingProcess, RequestOptions} from 'ngx-entity-service'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; import { CampusService, Project, @@ -6,15 +9,11 @@ import { UnitService, UserService, } from 'src/app/api/models/doubtfire-model'; -import {Injectable} from '@angular/core'; -import {HttpClient} from '@angular/common/http'; -import API_URL from 'src/app/config/constants/apiUrl'; import {AppInjector} from 'src/app/app-injector'; -import {Observable} from 'rxjs'; -import {TaskService} from './task.service'; -import {MappingProcess} from 'ngx-entity-service/lib/mapping-process'; -import {TaskOutcomeAlignmentService} from './task-outcome-alignment.service'; +import API_URL from 'src/app/config/constants/apiUrl'; import {GroupService} from './group.service'; +import {TaskOutcomeAlignmentService} from './task-outcome-alignment.service'; +import {TaskService} from './task.service'; @Injectable() export class ProjectService extends CachedEntityService { @@ -37,20 +36,20 @@ export class ProjectService extends CachedEntityService { 'id', { keys: ['campus', 'campus_id'], - toEntityOp: (data: object, key: string, entity: Project, params?: any) => { + toEntityOp: (data: object, key: string, entity: Project) => { if (data['campus_id']) { return this.campusService.get(data['campus_id']).subscribe((campus) => { entity.campus = campus; }); } }, - toJsonFn: (entity: Project, key: string) => { + toJsonFn: (entity: Project, _key: string) => { return entity.campus ? entity.campus.id : entity.originalJson['camput_id'] ? -1 : null; }, }, { keys: 'student', - toEntityFn: (data: object, key: string, entity: Project, params?: any) => { + toEntityFn: (data: object) => { const userData = data['student']; return this.userService.cache.getOrCreate(userData.id, this.userService, userData); @@ -58,7 +57,7 @@ export class ProjectService extends CachedEntityService { }, { keys: 'userId', - toEntityOp: (data: object, key: string, entity: Project, params?: any) => { + toEntityOp: (data: object, key: string, entity: Project) => { const userId = data['user_id']; this.userService.get(userId).subscribe({ @@ -78,12 +77,16 @@ export class ProjectService extends CachedEntityService { 'staffNoteCount', { keys: 'hasPortfolio', - toEntityFn: (data: object, key: string, entity: Project, params?: any) => { + toEntityFn: (data: object, key: string, entity: Project) => { const result = data[key] === true; - if (result) entity.portfolioStatus = 1; - else if (entity.compilePortfolio) entity.portfolioStatus = 0.5; - else entity.portfolioStatus = 0; + if (result) { + entity.portfolioStatus = 1; + } else if (entity.compilePortfolio) { + entity.portfolioStatus = 0.5; + } else { + entity.portfolioStatus = 0; + } return result; }, @@ -92,7 +95,7 @@ export class ProjectService extends CachedEntityService { 'usesDraftLearningSummary', { keys: ['taskStats', 'stats'], - toEntityOp: (data: object, key: string, entity: Project, params?: any) => { + toEntityOp: (data: object, key: string, entity: Project) => { const values = data[key]; entity.taskStats = [ { @@ -124,14 +127,14 @@ export class ProjectService extends CachedEntityService { 'gradeRationale', { keys: 'unit', - toEntityFn: (data: object, key: string, entity: Project, params?: any) => { + toEntityFn: (data: object, key: string, entity: Project) => { const unitService: UnitService = AppInjector.get(UnitService); const unitData = data['unit']; const result = unitService.cache.getOrCreate(unitData.id, unitService, unitData); result.studentCache.add(entity); return result; }, - toJsonFn: (entity: Project, key: string) => { + toJsonFn: (entity: Project, _key: string) => { return entity.unit?.id; }, }, @@ -151,7 +154,7 @@ export class ProjectService extends CachedEntityService { }, { keys: 'tutorialEnrolments', - toEntityOp: (data: object, key: string, project: Project, params?: any) => { + toEntityOp: (data: object, key: string, project: Project) => { const unit: Unit = project.unit; data[key]?.forEach((tutorialEnrolment: {tutorial_id: number}) => { if (tutorialEnrolment.tutorial_id) { @@ -163,7 +166,7 @@ export class ProjectService extends CachedEntityService { }, { keys: 'groups', - toEntityOp: (data: object, key: string, project: Project, params?: any) => { + toEntityOp: (data: object, key: string, project: Project) => { data[key]?.forEach((group) => { const theGroup = project.unit.groupSetsCache .get(group.group_set_id) @@ -175,13 +178,13 @@ export class ProjectService extends CachedEntityService { theGroup.projectsCache.add(project); }); }, - toJsonFn: (entity: Project, key: string) => { + toJsonFn: (entity: Project, _key: string) => { return entity.unit?.id; }, }, { keys: 'tasks', - toEntityOp: (data: object, key: string, project: Project, params?: any) => { + toEntityOp: (data: object, key: string, project: Project) => { // create tasks from json data['tasks']?.forEach((taskData) => { project.taskCache.getOrCreate(taskData['id'], this.taskService, taskData, { @@ -194,7 +197,7 @@ export class ProjectService extends CachedEntityService { }, { keys: 'taskOutcomeAlignments', - toEntityOp: (data: object, key: string, project: Project, params?: any) => { + toEntityOp: (data: object, key: string, project: Project) => { data[key]?.forEach((alignment) => { project.taskOutcomeAlignmentsCache.getOrCreate( alignment['id'], @@ -222,8 +225,8 @@ export class ProjectService extends CachedEntityService { ); } - public createInstanceFrom(json: object, other?: any): Project { - return new Project(other as Unit); + public createInstanceFrom(_json: object, other?: Unit): Project { + return new Project(other); } public loadStudents( diff --git a/src/app/api/services/scorm-adapter.service.ts b/src/app/api/services/scorm-adapter.service.ts index bdeb14fc77..48d8971ec3 100644 --- a/src/app/api/services/scorm-adapter.service.ts +++ b/src/app/api/services/scorm-adapter.service.ts @@ -1,7 +1,7 @@ import {Injectable} from '@angular/core'; -import {UserService} from './user.service'; -import API_URL from 'src/app/config/constants/apiUrl'; import {ScormDataModel, ScormPlayerContext} from 'src/app/api/models/doubtfire-model'; +import API_URL from 'src/app/config/constants/apiUrl'; +import {UserService} from './user.service'; @Injectable({ providedIn: 'root', @@ -42,8 +42,20 @@ export class ScormAdapterService { this.context.state = 'Uninitialized'; } + private refreshUserContext(): void { + const user = this.userService.currentUser; + this.context.user = user; + this.context.learnerId = user.id; + this.context.learnerName = user.firstName + ' ' + user.lastName; + } + + private hasSuccessfulResponse(): boolean { + return this.xhr.status >= 200 && this.xhr.status < 300; + } + Initialize(): string { // console.log('API_1484_11: Initialize'); + this.refreshUserContext(); // TODO: error handling and reporting switch (this.context.state) { @@ -65,6 +77,12 @@ export class ScormAdapterService { this.xhr.send(); // console.log(this.xhr.responseText); + if (!this.hasSuccessfulResponse()) { + this.context.errorCode = 101; + console.error('Error retrieving SCORM review session:', this.xhr.responseText); + return 'false'; + } + const reviewSession = JSON.parse(this.xhr.responseText); this.dataModel.restore(reviewSession.cmi_datamodel); // console.log(this.dataModel.dump()); @@ -103,6 +121,12 @@ export class ScormAdapterService { this.xhr.send(); // console.log(this.xhr.responseText); + if (!noTestFound && !this.hasSuccessfulResponse()) { + this.context.errorCode = 101; + console.error('Error retrieving latest SCORM attempt:', this.xhr.responseText); + return 'false'; + } + if (!noTestFound) { const latestSession = JSON.parse(this.xhr.responseText); // console.log('Latest exam session:', latestSession); @@ -121,6 +145,12 @@ export class ScormAdapterService { this.xhr.send(); // console.log(this.xhr.responseText); + if (!this.hasSuccessfulResponse()) { + this.context.errorCode = 101; + console.error('Error resuming SCORM attempt:', this.xhr.responseText); + return 'false'; + } + const currentSession = JSON.parse(this.xhr.responseText); // console.log('Current exam session:', currentSession); this.context.attemptId = currentSession.id; @@ -137,6 +167,12 @@ export class ScormAdapterService { this.xhr.send(); // console.log(this.xhr.responseText); + if (!this.hasSuccessfulResponse()) { + this.context.errorCode = 101; + console.error('Error creating SCORM attempt:', this.xhr.responseText); + return 'false'; + } + const currentSession = JSON.parse(this.xhr.responseText); // console.log('Current exam session:', currentSession); this.context.attemptId = currentSession.id; @@ -150,6 +186,7 @@ export class ScormAdapterService { Terminate(): string { // console.log('API_1484_11: Terminate'); + this.refreshUserContext(); // TODO: error handling and reporting switch (this.context.state) { @@ -200,7 +237,7 @@ export class ScormAdapterService { return value; } - SetValue(element: string, value: any): string { + SetValue(element: string, value: string): string { // console.log(`API_1484_11: SetValue:`, element, value); // TODO: error reporting @@ -222,6 +259,7 @@ export class ScormAdapterService { Commit(): string { // console.log('API_1484_11: Commit'); + this.refreshUserContext(); // TODO: error reporting // TODO: can't commit until init is done @@ -275,7 +313,7 @@ export class ScormAdapterService { return errorString; } - GetDiagnostic(errorCode: string): string { + GetDiagnostic(_errorCode: string): string { // TODO: implement this // console.log(`API_1484_11: GetDiagnostic:`, errorCode); return 'GetDiagnostic is currently not implemented'; diff --git a/src/app/api/services/sidekiq-job.service.ts b/src/app/api/services/sidekiq-job.service.ts index dd5332a258..a5141f803e 100644 --- a/src/app/api/services/sidekiq-job.service.ts +++ b/src/app/api/services/sidekiq-job.service.ts @@ -1,6 +1,6 @@ +import {CachedEntityService} from 'ngx-entity-service'; import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; -import {CachedEntityService} from 'ngx-entity-service'; import {BehaviorSubject, Observable, Subject} from 'rxjs'; import API_URL from 'src/app/config/constants/apiUrl'; import {SidekiqJob} from '../models/sidekiq-job'; @@ -18,7 +18,7 @@ export class SidekiqJobService extends CachedEntityService { public jobEntries: Map = new Map(); // Allow components to track changes to jobEntries - public sidekiqJobsSubject = new BehaviorSubject([]); + public sidekiqJobsSubject: BehaviorSubject = new BehaviorSubject([]); public setJob(jobId: string, title: string, subject: Subject, job?: SidekiqJob) { this.jobEntries.set(jobId, { diff --git a/src/app/api/services/spec/campus.service.spec.ts b/src/app/api/services/spec/campus.service.spec.ts index acecf3e3d7..3373d5b15d 100644 --- a/src/app/api/services/spec/campus.service.spec.ts +++ b/src/app/api/services/spec/campus.service.spec.ts @@ -1,8 +1,14 @@ -import { TestBed, tick, fakeAsync } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { Campus } from 'src/app/api/models/doubtfire-model'; -import { CampusService } from '../campus.service'; -import { HttpRequest } from '@angular/common/http'; +import {afterEach, beforeEach, describe, expect, it} from 'vitest'; +import { + HttpRequest, + provideHttpClient, + withInterceptorsFromDi, + withXhr, +} from '@angular/common/http'; +import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {Campus} from 'src/app/api/models/doubtfire-model'; +import {CampusService} from '../campus.service'; describe('CampusService', () => { let campusService: CampusService; @@ -10,8 +16,12 @@ describe('CampusService', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [CampusService], + imports: [], + providers: [ + CampusService, + provideHttpClient(withXhr(), withInterceptorsFromDi()), + provideHttpClientTesting(), + ], }); campusService = TestBed.inject(CampusService); @@ -22,18 +32,24 @@ describe('CampusService', () => { httpMock.verify(); }); - it('should return expected campuses (HttpClient called once)', fakeAsync(() => { + it('should return expected campuses (HttpClient called once)', () => { const c = new Campus(); c.name = 'Melbourne'; c.mode = 'automatic'; c.abbreviation = 'melb'; - const expectedCampuses: Campus[] = [c]; - - campusService.query().subscribe((campuses) => expect(campuses).toEqual(expectedCampuses, 'expected campuses')); + campusService.query().subscribe((campuses) => { + expect(campuses).toHaveLength(1); + expect(campuses[0]).toMatchObject({ + id: 1, + name: 'Melbourne', + mode: 'automatic', + abbreviation: 'melb', + }); + }); - const req = httpMock.expectOne((request: HttpRequest): boolean => { + const req = httpMock.expectOne((request: HttpRequest): boolean => { expect(request.url).toEqual('http://localhost:3000/api/campuses/'); expect(request.method).toBe('GET'); return true; @@ -41,7 +57,5 @@ describe('CampusService', () => { c.id = 1; req.flush(c); - - tick(); - })); + }); }); diff --git a/src/app/api/services/spec/task-comment.service.spec.ts b/src/app/api/services/spec/task-comment.service.spec.ts new file mode 100644 index 0000000000..ae5673b28b --- /dev/null +++ b/src/app/api/services/spec/task-comment.service.spec.ts @@ -0,0 +1,72 @@ +import {afterEach, beforeEach, describe, expect, it} from 'vitest'; +import { + HttpRequest, + provideHttpClient, + withInterceptorsFromDi, + withXhr, +} from '@angular/common/http'; +import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {TaskComment} from 'src/app/api/models/doubtfire-model'; +import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; +import {EmojiService} from 'src/app/common/services/emoji.service'; +import {TaskCommentService} from '../task-comment.service'; +import {TestAttemptService} from '../test-attempt.service'; +import {UserService} from '../user.service'; + +describe('TaskCommentService discussion comments', () => { + let taskCommentService: TaskCommentService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + TaskCommentService, + provideHttpClient(withXhr(), withInterceptorsFromDi()), + provideHttpClientTesting(), + {provide: EmojiService, useValue: {}}, + {provide: UserService, useValue: {cache: {getOrCreate: () => ({})}}}, + {provide: FileDownloaderService, useValue: {}}, + {provide: TestAttemptService, useValue: {cache: {getOrCreate: () => ({})}}}, + ], + }); + + taskCommentService = TestBed.inject(TaskCommentService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('posts a discussion reply without expecting an entity response', () => { + const replyAudio = new Blob(['reply audio'], {type: 'audio/webm'}); + const comment = { + id: 69, + project: {id: 1}, + task: {definition: {id: 2}}, + } as TaskComment; + let completed = false; + + taskCommentService.postDiscussionReply(comment, replyAudio).subscribe(() => { + completed = true; + }); + + const req = httpMock.expectOne((request: HttpRequest): boolean => { + expect(request.url).toEqual( + 'http://localhost:3000/api/projects/1/task_def_id/2/comments/69/discussion_comment/reply', + ); + expect(request.method).toBe('POST'); + expect(request.body instanceof FormData).toBe(true); + const attachment = request.body.get('attachment') as Blob; + expect(attachment instanceof Blob).toBe(true); + expect(attachment.size).toBe(replyAudio.size); + expect(attachment.type).toBe(replyAudio.type); + return true; + }); + + req.flush(null); + + expect(completed).toBe(true); + }); +}); diff --git a/src/app/api/services/spec/user.service.spec.ts b/src/app/api/services/spec/user.service.spec.ts index d78a066b9e..2e03e7e9c4 100644 --- a/src/app/api/services/spec/user.service.spec.ts +++ b/src/app/api/services/spec/user.service.spec.ts @@ -1,18 +1,26 @@ -import { TestBed, tick, fakeAsync } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { User, UserService } from 'src/app/api/models/doubtfire-model'; -import { HttpRequest } from '@angular/common/http'; -import { analyticsService } from 'src/app/ajs-upgraded-providers'; +import {afterEach, beforeEach, describe, expect, it} from 'vitest'; +import { + HttpRequest, + provideHttpClient, + withInterceptorsFromDi, + withXhr, +} from '@angular/common/http'; +import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {User, UserService} from 'src/app/api/models/doubtfire-model'; describe('UserService', () => { let userService: UserService; let httpMock: HttpTestingController; - let analyticsServiceStub: jasmine.SpyObj; beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [UserService, { provide: analyticsService, useValue: analyticsServiceStub }], + imports: [], + providers: [ + UserService, + provideHttpClient(withXhr(), withInterceptorsFromDi()), + provideHttpClientTesting(), + ], }); userService = TestBed.inject(UserService); @@ -23,7 +31,7 @@ describe('UserService', () => { httpMock.verify(); }); - it('should return expected users (HttpClient called once)', fakeAsync(() => { + it('should return expected users (HttpClient called once)', () => { const u = new User(); u.id = 1; u.lastName = 'renzella'; @@ -38,22 +46,39 @@ describe('UserService', () => { u.receiveFeedbackNotifications = false; u.receiveTaskNotifications = false; - const expectedUsers: User[] = [u]; - - userService.query().subscribe((users) => expect(users).toEqual(expectedUsers, 'expected users')); + userService.query().subscribe((users) => { + expect(users).toHaveLength(1); + expect(users[0]).toMatchObject({ + id: 1, + firstName: 'Jake', + lastName: 'renzella', + email: 'jake@jake.jake', + }); + }); - const req = httpMock.expectOne((request: HttpRequest): boolean => { + const req = httpMock.expectOne((request: HttpRequest): boolean => { expect(request.url).toEqual('http://localhost:3000/api/users/'); expect(request.method).toBe('GET'); return true; }); - req.flush(u); - tick(); - })); + req.flush({ + id: 1, + last_name: 'renzella', + first_name: 'Jake', + nickname: 'jake', + has_run_first_time_setup: false, + email: 'jake@jake.jake', + student_id: '1', + username: 'test', + opt_in_to_research: true, + receive_portfolio_notifications: false, + receive_feedback_notifications: false, + receive_task_notifications: false, + }); + }); - it('should create a new user', fakeAsync(() => { + it('should create a new user', () => { const user = new User(); - user.id = 1; user.lastName = 'renzella'; user.firstName = 'Jake'; user.nickname = 'jake'; @@ -67,43 +92,57 @@ describe('UserService', () => { user.receiveTaskNotifications = false; userService.create(user).subscribe((result) => { - expect(result).toEqual(user, 'expected users'); + expect(result).toMatchObject({ + id: 1, + firstName: 'Jake', + lastName: 'renzella', + email: 'jake@jake.jake', + }); }); - const expectedUser = user; - expectedUser.id = 1; - - const req = httpMock.expectOne((request: HttpRequest): boolean => { + const req = httpMock.expectOne((request: HttpRequest): boolean => { expect(request.url).toEqual('http://localhost:3000/api/users/'); expect(request.method).toBe('POST'); return true; }); - req.flush(expectedUser); - tick(); - })); - - xit('should delete a user', fakeAsync(() => { - // let user = new User(); - // user.updateFromJson({ - // name: 'jake', lastName: 'renzella', firstName: 'Jake', nickname: 'jake', - // systemRole: 'admin', hasRunFirstTimeSetup: false, email: 'jake@jake.jake', - // student_id: '1', username: 'test', optInToResearch: true, receivePortfolioNotifications: false, - // receiveFeedbackNotifications: false, receiveTaskNotifications: false - // }); - // userService.delete(1).subscribe( - // result => expect(result).toEqual(user, 'expected users') - // ); - // const req = httpMock.expectOne((request: HttpRequest): boolean => { - // expect(request.url).toEqual('http://localhost:3000/api/users/1'); - // expect(request.method).toBe('DELETE'); - // return true; - // }); - // req.flush(user); - // tick(); - })); - - it('should update a User', fakeAsync(() => { + req.flush({ + id: 1, + last_name: 'renzella', + first_name: 'Jake', + nickname: 'jake', + has_run_first_time_setup: false, + email: 'jake@jake.jake', + student_id: '1', + username: 'test', + opt_in_to_research: true, + receive_portfolio_notifications: false, + receive_feedback_notifications: false, + receive_task_notifications: false, + }); + }); + + // it.skip('should delete a user', () => { + // let user = new User(); + // user.updateFromJson({ + // name: 'jake', lastName: 'renzella', firstName: 'Jake', nickname: 'jake', + // systemRole: 'admin', hasRunFirstTimeSetup: false, email: 'jake@jake.jake', + // student_id: '1', username: 'test', optInToResearch: true, receivePortfolioNotifications: false, + // receiveFeedbackNotifications: false, receiveTaskNotifications: false + // }); + // userService.delete(1).subscribe( + // result => expect(result).toEqual(user, 'expected users') + // ); + // const req = httpMock.expectOne((request: HttpRequest): boolean => { + // expect(request.url).toEqual('http://localhost:3000/api/users/1'); + // expect(request.method).toBe('DELETE'); + // return true; + // }); + // req.flush(user); + // tick(); + // }); + + it('should update a User', () => { const u = new User(); u.id = 1; u.lastName = 'renzella'; @@ -118,37 +157,42 @@ describe('UserService', () => { u.receiveFeedbackNotifications = false; u.receiveTaskNotifications = false; - userService.update(u).subscribe((result) => { - expect(result.firstName).toBe(u.firstName); - }, fail); + userService.update(u).subscribe( + (result) => { + expect(result.firstName).toBe(u.firstName); + }, + (error) => { + throw error; + }, + ); - let req = httpMock.expectOne((request: HttpRequest): boolean => { + let req = httpMock.expectOne((request: HttpRequest): boolean => { expect(request.url).toEqual('http://localhost:3000/api/users/1'); expect(request.method).toBe('PUT'); return true; }); req.flush(u); - tick(); u.firstName = 'andrew'; userService.update(u).subscribe({ next: (result) => { expect(result.firstName).toBe('andrew'); }, - error: fail, + error: (error) => { + throw error; + }, }); - req = httpMock.expectOne((request: HttpRequest): boolean => { + req = httpMock.expectOne((request: HttpRequest): boolean => { expect(request.url).toEqual('http://localhost:3000/api/users/1'); expect(request.method).toBe('PUT'); return true; }); req.flush(u); - tick(); - })); + }); - it('should cache the result of a get request', fakeAsync(() => { + it('should cache the result of a get request', () => { const user = new User(); user.id = 1; user.lastName = 'renzella'; @@ -163,9 +207,9 @@ describe('UserService', () => { user.receiveFeedbackNotifications = false; user.receiveTaskNotifications = false; - userService.get(1).subscribe((data) => {}); + userService.get(1).subscribe(); - const req = httpMock.expectOne((request: HttpRequest): boolean => { + const req = httpMock.expectOne((request: HttpRequest): boolean => { expect(request.url).toEqual('http://localhost:3000/api/users/1'); expect(request.method).toBe('GET'); return true; @@ -173,17 +217,15 @@ describe('UserService', () => { const user2 = user; user2.id = 1; req.flush(user2); - tick(); - userService.get(1).subscribe((data) => {}); + userService.get(1).subscribe(); - httpMock.expectNone((request: HttpRequest): boolean => { + httpMock.expectNone((_request: HttpRequest): boolean => { return true; }); - tick(); - })); + }); - it('should cache fetch/get', fakeAsync(() => { + it('should cache fetch/get', () => { let user = new User(); user.id = 1; user.lastName = 'renzella'; @@ -203,7 +245,7 @@ describe('UserService', () => { user = data; }); - let req = httpMock.expectOne((request: HttpRequest): boolean => { + let req = httpMock.expectOne((request: HttpRequest): boolean => { expect(request.url).toEqual('http://localhost:3000/api/users/1'); expect(request.method).toBe('GET'); return true; @@ -213,9 +255,8 @@ describe('UserService', () => { Object.keys(user).forEach((key) => (user2[key] = user[key])); user2.id = 1; req.flush(user2); - tick(); - let user3; + let user3: User; // 1 request here userService.fetch(1).subscribe((data) => { @@ -223,7 +264,7 @@ describe('UserService', () => { user3 = data; }); - req = httpMock.expectOne((request: HttpRequest): boolean => { + req = httpMock.expectOne((request: HttpRequest): boolean => { expect(request.url).toEqual('http://localhost:3000/api/users/1'); expect(request.method).toBe('GET'); return true; @@ -233,10 +274,10 @@ describe('UserService', () => { Object.keys(user2).forEach((key) => (user4[key] = user2[key])); user4.firstName = 'fred'; req.flush(user4); + expect(user3).toBe(user); - httpMock.expectNone((request: HttpRequest): boolean => { + httpMock.expectNone((_request: HttpRequest): boolean => { return true; }); - tick(); - })); + }); }); diff --git a/src/app/api/services/staff-note.service.ts b/src/app/api/services/staff-note.service.ts index 0a707fea8c..13e4ce5912 100644 --- a/src/app/api/services/staff-note.service.ts +++ b/src/app/api/services/staff-note.service.ts @@ -1,10 +1,10 @@ +import {CachedEntityService, RequestOptions} from 'ngx-entity-service'; import {HttpClient} from '@angular/common/http'; import {EventEmitter, Injectable} from '@angular/core'; -import {CachedEntityService, RequestOptions} from 'ngx-entity-service'; +import {Observable, tap} from 'rxjs'; import {Project, ProjectService, UserService} from 'src/app/api/models/doubtfire-model'; import API_URL from 'src/app/config/constants/apiUrl'; import {StaffNote} from '../models/staff-note'; -import {Observable, tap} from 'rxjs'; @Injectable() export class StaffNoteService extends CachedEntityService { diff --git a/src/app/api/services/submission-history.service.ts b/src/app/api/services/submission-history.service.ts new file mode 100644 index 0000000000..e65d1053ce --- /dev/null +++ b/src/app/api/services/submission-history.service.ts @@ -0,0 +1,44 @@ +import {EntityService} from 'ngx-entity-service'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; +import {SubmissionHistory} from 'src/app/api/models/submission-history'; +import {Task} from 'src/app/api/models/task'; +import API_URL from 'src/app/config/constants/apiUrl'; + +@Injectable() +export class SubmissionHistoryService extends EntityService { + protected readonly endpointFormat = + 'projects/:project_id:/task_def_id/:td_id:/submission_histories/:id:'; + + constructor(httpClient: HttpClient) { + super(httpClient, API_URL); + + this.mapping.addKeys( + 'id', + 'taskId', + 'createdAt', + 'hasSubmissionFiles', + 'overseerAssessmentId', + { + keys: ['timestamp', 'submission_timestamp'], + toEntityFn: (data) => new Date(Number(data['submission_timestamp']) * 1000), + }, + ['timestampString', 'submission_timestamp'], + ); + } + + public createInstanceFrom(_json: object, task?: Task): SubmissionHistory { + return new SubmissionHistory(task); + } + + public queryForTask(task: Task): Observable { + return this.query( + { + project_id: task.project.id, + td_id: task.definition.id, + }, + {constructorParams: task}, + ); + } +} diff --git a/src/app/api/services/task-comment.service.ts b/src/app/api/services/task-comment.service.ts index 6761bc715c..f5dd11e222 100644 --- a/src/app/api/services/task-comment.service.ts +++ b/src/app/api/services/task-comment.service.ts @@ -1,3 +1,8 @@ +import {CachedEntityService, RequestOptions} from 'ngx-entity-service'; +import {HttpClient} from '@angular/common/http'; +import {EventEmitter, Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; +import {tap} from 'rxjs/operators'; import { ScormComment, Task, @@ -5,19 +10,13 @@ import { TestAttemptService, UserService, } from 'src/app/api/models/doubtfire-model'; -import {EventEmitter, Injectable} from '@angular/core'; -import {Observable} from 'rxjs'; -import {tap} from 'rxjs/operators'; -import {CachedEntityService} from 'ngx-entity-service'; +import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; +import {EmojiService} from 'src/app/common/services/emoji.service'; +import API_URL from 'src/app/config/constants/apiUrl'; import {DiscussionComment} from '../models/task-comment/discussion-comment'; import {ExtensionComment} from '../models/task-comment/extension-comment'; -import {RequestOptions} from 'ngx-entity-service/lib/request-options'; -import {HttpClient} from '@angular/common/http'; -import API_URL from 'src/app/config/constants/apiUrl'; -import {EmojiService} from 'src/app/common/services/emoji.service'; -import {MappingFunctions} from './mapping-fn'; -import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; import {ScormExtensionComment} from '../models/task-comment/scorm-extension-comment'; +import {MappingFunctions} from './mapping-fn'; @Injectable() export class TaskCommentService extends CachedEntityService { @@ -43,13 +42,13 @@ export class TaskCommentService extends CachedEntityService { protected readonly endpointFormat = this.commentEndpointFormat; constructor( - httpClient: HttpClient, + private apiHttpClient: HttpClient, private emojiService: EmojiService, private userService: UserService, private downloader: FileDownloaderService, private testAttemptService: TestAttemptService, ) { - super(httpClient, API_URL); + super(apiHttpClient, API_URL); this.mapping.addKeys( 'id', @@ -57,13 +56,14 @@ export class TaskCommentService extends CachedEntityService { keys: 'author', toEntityFn: (data: object, key: string, comment: TaskComment) => { const user = this.userService.cache.getOrCreate(data[key]?.id, userService, data[key]); - comment.initials = `${user.preferredName[0]}${user.lastName[0]}`.toUpperCase(); + comment.initials = + `${user.preferredName[0] ?? ''}${user.lastName?.[0] ?? ''}`.toUpperCase(); return user; }, }, { keys: 'recipient', - toEntityFn: (data: object, key: string, comment: TaskComment) => { + toEntityFn: (data: object, key: string, _comment: TaskComment) => { return this.userService.cache.getOrCreate(data[key]?.id, userService, data[key]); }, }, @@ -72,7 +72,7 @@ export class TaskCommentService extends CachedEntityService { 'isNew', { keys: ['text', 'comment'], - toEntityFn: (data, key, entity) => { + toEntityFn: (data, _key, _entity) => { return this.emojiService.colonsToNative(data['comment']); }, }, @@ -133,7 +133,7 @@ export class TaskCommentService extends CachedEntityService { /** * Create a Task Comment - use the type to determine the exact object type to return. */ - public createInstanceFrom(json: any, other?: any): TaskComment { + public createInstanceFrom(json: {type?: string}, other?: Task): TaskComment { switch (json.type) { case 'discussion': return new DiscussionComment(other); @@ -161,9 +161,9 @@ export class TaskCommentService extends CachedEntityService { options?: RequestOptions, ): Observable { return super.query(pathIds, options).pipe( - tap((result) => { + tap((_result) => { // Access the task and set the number of new comments to 0 - they are now read on the server - const task = other as any; //TODO: change to Task object + const task = other as Task; task.numNewComments = 0; }), ); @@ -228,10 +228,37 @@ export class TaskCommentService extends CachedEntityService { ); } + public editComment(comment: TaskComment, text: string): Observable { + const opts: RequestOptions = { + endpointFormat: this.commentEndpointFormat, + entity: comment, + body: { + comment: text, + }, + cache: comment.task.commentCache, + constructorParams: comment.task, + }; + + return super + .update( + { + id: comment.id, + projectId: comment.project.id, + taskDefinitionId: comment.task.definition.id, + }, + opts, + ) + .pipe( + tap((_updatedComment: TaskComment) => { + comment.task.refreshCommentData(); + }), + ); + } + public requestExtension( reason: string, weeksRequested: number, - task: any, + task: Task, ): Observable { const opts: RequestOptions = { endpointFormat: this.requestExtensionEndpointFormat, @@ -266,7 +293,7 @@ export class TaskCommentService extends CachedEntityService { ); } - public requestScormExtension(reason: string, task: any): Observable { + public requestScormExtension(reason: string, task: Task): Observable { const opts: RequestOptions = { endpointFormat: this.scormRequestExtensionEndpointFormat, body: { @@ -283,17 +310,15 @@ export class TaskCommentService extends CachedEntityService { ); } - public postDiscussionReply(comment: TaskComment, replyAudio: Blob): Observable { + public postDiscussionReply(comment: TaskComment, replyAudio: Blob): Observable { const form = new FormData(); - const pathIds = { - project_id: comment.project.id, - task_definition_id: comment.task.id, - task_comment_id: comment.id, - }; form.append('attachment', replyAudio); - return this.create(pathIds, {body: form, cache: comment.task.commentCache}); + return this.apiHttpClient.post( + `${API_URL}/projects/${comment.project.id}/task_def_id/${comment.task.definition.id}/comments/${comment.id}/discussion_comment/reply`, + form, + ); } // public getDiscussionComment() -> diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index d420f2c1fc..ed24d68bff 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -1,21 +1,21 @@ import {CachedEntityService} from 'ngx-entity-service'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; import { LearningOutcomeService, TaskDefinition, TaskStatusEnum, Unit, } from 'src/app/api/models/doubtfire-model'; -import {Injectable} from '@angular/core'; -import {HttpClient} from '@angular/common/http'; -import API_URL from 'src/app/config/constants/apiUrl'; -import {MappingFunctions} from './mapping-fn'; import {AppInjector} from 'src/app/app-injector'; -import {Observable} from 'rxjs'; -import {TaskPrerequisiteService} from './task-prerequisite.service'; -import {TaskPrerequisite} from '../models/task-prerequisite'; +import API_URL from 'src/app/config/constants/apiUrl'; import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; import {SidekiqJob} from '../models/sidekiq-job'; +import {TaskPrerequisite} from '../models/task-prerequisite'; +import {MappingFunctions} from './mapping-fn'; import {OverseerStepService} from './overseer-step.service'; +import {TaskPrerequisiteService} from './task-prerequisite.service'; @Injectable() export class TaskDefinitionService extends CachedEntityService { @@ -57,7 +57,7 @@ export class TaskDefinitionService extends CachedEntityService { }, { keys: 'uploadRequirements', - toJsonFn: (taskDef: TaskDefinition, key: string) => { + toJsonFn: (taskDef: TaskDefinition, _key: string) => { return JSON.stringify( taskDef.uploadRequirements?.map((upreq) => { return { @@ -66,19 +66,21 @@ export class TaskDefinitionService extends CachedEntityService { type: upreq.type, tii_check: upreq.tiiCheck, tii_pct: upreq.tiiPct, + submission_history: upreq.submissionHistory, }; }), ); }, - toEntityFn: (data: object, key: string, taskDef: TaskDefinition, params?: any) => { + toEntityFn: (data: object, key: string) => { return ( - data[key] as Array<{ + data[key] as { key: string; name: string; type: string; tii_check: boolean; tii_pct: number; - }> + submission_history: boolean; + }[] )?.map((upreq) => { return { key: upreq.key, @@ -86,16 +88,17 @@ export class TaskDefinitionService extends CachedEntityService { type: upreq.type, tiiCheck: upreq.tii_check, tiiPct: upreq.tii_pct, + submissionHistory: upreq.submission_history, }; }); }, }, { keys: ['tutorialStream', 'tutorial_stream_abbr'], - toEntityFn: (data: object, key: string, taskDef: TaskDefinition, params?: any) => { + toEntityFn: (data: object, key: string, taskDef: TaskDefinition) => { return taskDef.unit.tutorialStreamsCache.get(data[key]); }, - toJsonFn: (taskDef: TaskDefinition, key: string) => { + toJsonFn: (taskDef: TaskDefinition, _key: string) => { return taskDef.tutorialStream?.abbreviation; }, }, @@ -103,14 +106,14 @@ export class TaskDefinitionService extends CachedEntityService { 'restrictStatusUpdates', { keys: ['groupSet', 'group_set_id'], - toEntityFn: (data: object, key: string, taskDef: TaskDefinition, params?: any) => { + toEntityFn: (data: object, key: string, taskDef: TaskDefinition) => { if (data[key]) { return taskDef.unit.groupSetsCache.get(data[key]); } else { return data[key]; } }, - toJsonFn: (taskDef: TaskDefinition, key: string) => { + toJsonFn: (taskDef: TaskDefinition, _key: string) => { return taskDef.groupSet?.id; }, }, @@ -146,6 +149,7 @@ export class TaskDefinitionService extends CachedEntityService { { keys: 'overseerSteps', toEntityOp: (data: object, key: string, taskDefinition: TaskDefinition) => { + taskDefinition.overseerStepsCache.clear(); data[key]?.forEach((overseerStep) => { taskDefinition.overseerStepsCache.getOrCreate( overseerStep['id'], @@ -159,41 +163,30 @@ export class TaskDefinitionService extends CachedEntityService { }, }, 'overseerResourceFiles', - // { - // keys: 'pTargetDate', - // toEntityFn: MappingFunctions.mapDateToDay, - // toJsonFn: MappingFunctions.mapDayToJson, - // }, - { - keys: 'cTargetDate', - toEntityFn: MappingFunctions.mapDateToDay, - toJsonFn: MappingFunctions.mapDayToJson, - }, { - keys: 'dTargetDate', - toEntityFn: MappingFunctions.mapDateToDay, - toJsonFn: MappingFunctions.mapDayToJson, - }, - { - keys: 'hdTargetDate', - toEntityFn: MappingFunctions.mapDateToDay, - toJsonFn: MappingFunctions.mapDayToJson, - }, - - { - keys: 'cStartDate', - toEntityFn: MappingFunctions.mapDateToDay, - toJsonFn: MappingFunctions.mapDayToJson, - }, - { - keys: 'dStartDate', - toEntityFn: MappingFunctions.mapDateToDay, - toJsonFn: MappingFunctions.mapDayToJson, - }, - { - keys: 'hdStartDate', - toEntityFn: MappingFunctions.mapDateToDay, - toJsonFn: MappingFunctions.mapDayToJson, + keys: ['gradeDueDates', 'grade_due_dates'], + toEntityFn: (data: object, key: string) => { + return (data[key] ?? []).map((gradeDate) => ({ + targetGrade: gradeDate.target_grade, + targetDueDate: gradeDate.target_due_date + ? MappingFunctions.mapDateToDay(gradeDate, 'target_due_date', null) + : undefined, + startDate: gradeDate.start_date + ? MappingFunctions.mapDateToDay(gradeDate, 'start_date', null) + : undefined, + })); + }, + toJsonFn: (taskDefinition: TaskDefinition) => { + return taskDefinition.gradeDueDates.map((gradeDate) => ({ + target_grade: gradeDate.targetGrade, + target_due_date: gradeDate.targetDueDate + ? MappingFunctions.mapDayToJson(gradeDate, 'targetDueDate') + : undefined, + start_date: gradeDate.startDate + ? MappingFunctions.mapDayToJson(gradeDate, 'startDate') + : undefined, + })); + }, }, ); @@ -206,8 +199,8 @@ export class TaskDefinitionService extends CachedEntityService { ); } - public override createInstanceFrom(json: object, other?: any): TaskDefinition { - return new TaskDefinition(other as Unit); + public override createInstanceFrom(_json: object, other?: Unit): TaskDefinition { + return new TaskDefinition(other); } public uploadTaskSheet(taskDefinition: TaskDefinition, file: File): Observable { diff --git a/src/app/api/services/task-outcome-alignment.service.ts b/src/app/api/services/task-outcome-alignment.service.ts index 44fd1ea07f..135df23d61 100644 --- a/src/app/api/services/task-outcome-alignment.service.ts +++ b/src/app/api/services/task-outcome-alignment.service.ts @@ -1,9 +1,8 @@ -import {HttpClient} from '@angular/common/http'; import {CachedEntityService} from 'ngx-entity-service'; -import {Project, TaskOutcomeAlignment, Unit} from 'src/app/api/models/doubtfire-model'; +import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; +import {Project, TaskOutcomeAlignment, Unit} from 'src/app/api/models/doubtfire-model'; import API_URL from 'src/app/config/constants/apiUrl'; -import {UnitTutorialsListComponent} from 'src/app/units/states/edit/directives/unit-tutorials-list/unit-tutorials-list.component'; @Injectable() export class TaskOutcomeAlignmentService extends CachedEntityService { @@ -22,7 +21,7 @@ export class TaskOutcomeAlignmentService extends CachedEntityService { + toJsonFn: (entity: TaskOutcomeAlignment, _key: string) => { return entity.learningOutcome.id; }, }, @@ -32,7 +31,7 @@ export class TaskOutcomeAlignmentService extends CachedEntityService { + toJsonFn: (entity: TaskOutcomeAlignment, _key: string) => { return entity.taskDefinition.id; }, }, @@ -42,7 +41,7 @@ export class TaskOutcomeAlignmentService extends CachedEntityService { + toJsonFn: (entity: TaskOutcomeAlignment, _key: string) => { return entity.task?.id; }, }, @@ -51,7 +50,7 @@ export class TaskOutcomeAlignmentService extends CachedEntityService { @@ -20,7 +20,7 @@ export class TaskPrerequisiteService extends CachedEntityService { @@ -33,17 +33,18 @@ export class TaskService extends CachedEntityService { 'id', { keys: 'projectId', - toEntityOp: (data: object, jsonKey: string, task: Task, _params?: any) => { + toEntityOp: (data: object, jsonKey: string, task: Task) => { // Is fetching task outside of project... task.project = task.unit.findStudent(data[jsonKey]); }, }, { keys: 'taskDefinitionId', - toEntityOp: (data: object, key: string, entity: Task, _params?: any) => { + toEntityOp: (data: object, key: string, entity: Task) => { entity.definition = entity.project.unit.taskDef(data['task_definition_id']); }, }, + 'tutorialId', 'status', { keys: 'dueDate', @@ -78,13 +79,13 @@ export class TaskService extends CachedEntityService { 'pinned', { keys: 'new_stat', - toEntityOp: (data: object, key: string, entity: Task, params?: any) => { + toEntityOp: (data: object, key: string, entity: Task) => { entity.project.taskStats = data['new_stat']; }, }, { keys: 'otherProjects', - toEntityOp: (data: object, key: string, entity: Task, params?: any) => { + toEntityOp: (data: object, key: string, entity: Task) => { data['other_projects'].forEach((details) => { const proj = entity.unit.findStudent(details.id); if (proj) { @@ -105,8 +106,8 @@ export class TaskService extends CachedEntityService { this.mapping.addJsonKey('qualityPts', 'grade', 'includeInPortfolio', 'trigger'); } - public createInstanceFrom(json: object, other?: any): Task { - return new Task(other as Project); + public createInstanceFrom(_json: object, other?: Project): Task { + return new Task(other); } public queryTasksForTaskInbox( @@ -208,7 +209,6 @@ export class TaskService extends CachedEntityService { }; this.get(pathIds, options).subscribe({ - next: (value: Task) => {}, error: (message) => { console.log(`Failed to refresh tasks ${message}`); }, @@ -231,7 +231,8 @@ export class TaskService extends CachedEntityService { public readonly statusSeq = TaskStatus.STATUS_SEQ; public readonly helpDescriptions = TaskStatus.HELP_DESCRIPTIONS; public readonly statusIcons: Map = TaskStatus.STATUS_ICONS; - public readonly statusMaterialIcons: Map = TaskStatus.STATUS_MATERIAL_ICONS; + public readonly statusMaterialIcons: Map = + TaskStatus.STATUS_MATERIAL_ICONS; public readonly rejectFutureStates = TaskStatus.REJECT_FUTURE_STATES; public statusClass(status: TaskStatusEnum): string { diff --git a/src/app/api/services/teaching-period-break.service.ts b/src/app/api/services/teaching-period-break.service.ts index a7e3b296de..5bd364aa8f 100644 --- a/src/app/api/services/teaching-period-break.service.ts +++ b/src/app/api/services/teaching-period-break.service.ts @@ -1,7 +1,7 @@ -import {HttpClient} from '@angular/common/http'; import {CachedEntityService} from 'ngx-entity-service'; -import {TeachingPeriodBreak} from 'src/app/api/models/doubtfire-model'; +import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; +import {TeachingPeriodBreak} from 'src/app/api/models/doubtfire-model'; import API_URL from 'src/app/config/constants/apiUrl'; import {MappingFunctions} from './mapping-fn'; @@ -25,7 +25,7 @@ export class TeachingPeriodBreakService extends CachedEntityService { this.cacheBehaviourOnGet = 'cacheQuery'; } - public createInstanceFrom(json: any, other?: any): TeachingPeriod { + public createInstanceFrom(_json: object): TeachingPeriod { return new TeachingPeriod(); } } diff --git a/src/app/api/services/test-attempt.service.ts b/src/app/api/services/test-attempt.service.ts index 9b418045ec..07789f4c63 100644 --- a/src/app/api/services/test-attempt.service.ts +++ b/src/app/api/services/test-attempt.service.ts @@ -1,12 +1,12 @@ -import {Injectable} from '@angular/core'; import {CachedEntityService} from 'ngx-entity-service'; -import API_URL from 'src/app/config/constants/apiUrl'; -import {Task, TestAttempt} from 'src/app/api/models/doubtfire-model'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; import {Observable} from 'rxjs'; +import {Task, TestAttempt} from 'src/app/api/models/doubtfire-model'; import {AppInjector} from 'src/app/app-injector'; -import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; import {AlertService} from 'src/app/common/services/alert.service'; -import {HttpClient} from '@angular/common/http'; +import API_URL from 'src/app/config/constants/apiUrl'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; @Injectable() export class TestAttemptService extends CachedEntityService { diff --git a/src/app/api/services/tii-action.service.ts b/src/app/api/services/tii-action.service.ts index d4bbc813ce..856d8ddd51 100644 --- a/src/app/api/services/tii-action.service.ts +++ b/src/app/api/services/tii-action.service.ts @@ -1,11 +1,9 @@ +import {CachedEntityService} from 'ngx-entity-service'; import {HttpClient} from '@angular/common/http'; -import {CachedEntityService, Entity} from 'ngx-entity-service'; -import {TiiAction, Unit, UnitService, UserService} from 'src/app/api/models/doubtfire-model'; import {Injectable} from '@angular/core'; +import {TiiAction} from 'src/app/api/models/doubtfire-model'; import API_URL from 'src/app/config/constants/apiUrl'; -import {AppInjector} from 'src/app/app-injector'; import {MappingFunctions} from './mapping-fn'; -import {MappingProcess} from 'ngx-entity-service/lib/mapping-process'; @Injectable() export class TiiActionService extends CachedEntityService { @@ -37,7 +35,7 @@ export class TiiActionService extends CachedEntityService { // this.cacheBehaviourOnGet = 'cacheQuery'; } - public createInstanceFrom(json: any, other?: any): TiiAction { + public createInstanceFrom(_json: object): TiiAction { return new TiiAction(); } } diff --git a/src/app/api/services/tii.service.spec.ts b/src/app/api/services/tii.service.spec.ts index af14851920..4cf7e6de2d 100644 --- a/src/app/api/services/tii.service.spec.ts +++ b/src/app/api/services/tii.service.spec.ts @@ -1,6 +1,6 @@ -import { TestBed } from '@angular/core/testing'; - -import { TiiService } from './tii.service'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {TestBed} from '@angular/core/testing'; +import {TiiService} from './tii.service'; describe('TiiServiceService', () => { let service: TiiService; diff --git a/src/app/api/services/tutor-note.service.ts b/src/app/api/services/tutor-note.service.ts index 3e08ce451d..f9efe14fe1 100644 --- a/src/app/api/services/tutor-note.service.ts +++ b/src/app/api/services/tutor-note.service.ts @@ -1,6 +1,6 @@ +import {CachedEntityService, RequestOptions} from 'ngx-entity-service'; import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; -import {CachedEntityService, RequestOptions} from 'ngx-entity-service'; import {Observable, tap} from 'rxjs'; import {ProjectService, Task, UnitRole, UserService} from 'src/app/api/models/doubtfire-model'; import API_URL from 'src/app/config/constants/apiUrl'; diff --git a/src/app/api/services/tutorial-stream.service.ts b/src/app/api/services/tutorial-stream.service.ts index 4a5c5a617d..4b3922d2f0 100644 --- a/src/app/api/services/tutorial-stream.service.ts +++ b/src/app/api/services/tutorial-stream.service.ts @@ -1,7 +1,7 @@ -import {HttpClient} from '@angular/common/http'; import {CachedEntityService} from 'ngx-entity-service'; -import {TutorialStream} from 'src/app/api/models/doubtfire-model'; +import {HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; +import {TutorialStream} from 'src/app/api/models/doubtfire-model'; import API_URL from 'src/app/config/constants/apiUrl'; @Injectable() @@ -11,16 +11,16 @@ export class TutorialStreamService extends CachedEntityService { constructor(httpClient: HttpClient) { super(httpClient, API_URL); - this.mapping.addKeys('name', 'abbreviation', 'activityType'); + this.mapping.addKeys('id', 'name', 'abbreviation', 'activityType'); this.mapping.mapAllKeysToJson(); } - public createInstanceFrom(json: any, other?: any): TutorialStream { + public createInstanceFrom(_json: object): TutorialStream { return new TutorialStream(); } - public override keyForJson(json: any): string { + public override keyForJson(json: {abbreviation: string}): string { return json['abbreviation']; } diff --git a/src/app/api/services/tutorial.service.ts b/src/app/api/services/tutorial.service.ts index 02cb6cc746..0451564a6b 100644 --- a/src/app/api/services/tutorial.service.ts +++ b/src/app/api/services/tutorial.service.ts @@ -1,6 +1,7 @@ -import {Inject, Injectable} from '@angular/core'; -import {analyticsService} from 'src/app/ajs-upgraded-providers'; +import {CachedEntityService, RequestOptions} from 'ngx-entity-service'; import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; import { CampusService, Project, @@ -8,10 +9,8 @@ import { Unit, UserService, } from 'src/app/api/models/doubtfire-model'; -import {CachedEntityService, RequestOptions} from 'ngx-entity-service'; -import API_URL from 'src/app/config/constants/apiUrl'; -import {Observable} from 'rxjs'; import {AlertService} from 'src/app/common/services/alert.service'; +import API_URL from 'src/app/config/constants/apiUrl'; @Injectable() export class TutorialService extends CachedEntityService { @@ -23,7 +22,6 @@ export class TutorialService extends CachedEntityService { httpClient: HttpClient, private campusService: CampusService, private userService: UserService, - @Inject(analyticsService) private AnalyticsService: any, private alerts: AlertService, ) { super(httpClient, API_URL); @@ -36,22 +34,22 @@ export class TutorialService extends CachedEntityService { 'abbreviation', { keys: ['campus', 'campus_id'], - toEntityOp: (data: object, key: string, entity: Tutorial, params?: any) => { + toEntityOp: (data: object, key: string, entity: Tutorial) => { this.campusService.get(data['campus_id']).subscribe((campus) => { entity.campus = campus; }); }, - toJsonFn: (entity: Tutorial, key: string) => { + toJsonFn: (entity: Tutorial, _key: string) => { return entity.campus ? entity.campus.id : -1; }, }, 'capacity', { keys: ['tutor', 'tutor_id'], - toEntityFn: (data: object, key: string, entity: Tutorial, params?: any) => { + toEntityFn: (data: object, key: string) => { return this.userService.cache.get(data[key]); }, - toJsonFn: (entity: Tutorial, key: string) => { + toJsonFn: (entity: Tutorial, _key: string) => { return entity.tutor?.id; }, }, @@ -59,17 +57,17 @@ export class TutorialService extends CachedEntityService { 'numStudents', { keys: ['tutorialStream', 'tutorial_stream_abbr'], - toEntityFn: (data: object, key: string, entity: Tutorial, params?: any) => { + toEntityFn: (data: object, key: string, entity: Tutorial) => { return entity.unit.tutorialStreamForAbbr(data[key]); }, - toJsonFn: (entity: Tutorial, key: string) => { + toJsonFn: (entity: Tutorial, _key: string) => { return entity.tutorialStream ? entity.tutorialStream.abbreviation : null; }, }, { keys: ['unit', 'unit_id'], - toJsonFn: (entity: Tutorial, key: string) => { + toJsonFn: (entity: Tutorial, _key: string) => { return entity.unit?.id; }, }, @@ -78,11 +76,11 @@ export class TutorialService extends CachedEntityService { this.mapping.mapAllKeysToJsonExcept('numStudents'); } - public createInstanceFrom(json: any, other?: any): Tutorial { - return new Tutorial(other as Unit); + public createInstanceFrom(_json: object, other?: Unit): Tutorial { + return new Tutorial(other); } - public override keyForJson(json: any): string | number { + public override keyForJson(json: {tutorial_id?: number}): string | number { if (json.tutorial_id) { return json.tutorial_id; } else { @@ -104,7 +102,7 @@ export class TutorialService extends CachedEntityService { body: {}, }; - var observer: Observable; + let observer: Observable<{enrolments: {tutorial_id: number}[]}>; if (isEnrol) { observer = this.post(pathIds, options); } else { diff --git a/src/app/api/services/unit-role.service.ts b/src/app/api/services/unit-role.service.ts index b67a40d41c..daaf9015d5 100644 --- a/src/app/api/services/unit-role.service.ts +++ b/src/app/api/services/unit-role.service.ts @@ -1,3 +1,6 @@ +import {CachedEntityService} from 'ngx-entity-service'; +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; import { TeachingPeriodService, Unit, @@ -5,10 +8,6 @@ import { UnitService, UserService, } from 'src/app/api/models/doubtfire-model'; -import {CachedEntityService} from 'ngx-entity-service'; -import {Inject, Injectable} from '@angular/core'; -import {analyticsService} from 'src/app/ajs-upgraded-providers'; -import {HttpClient} from '@angular/common/http'; import API_URL from 'src/app/config/constants/apiUrl'; @Injectable() @@ -20,7 +19,6 @@ export class UnitRoleService extends CachedEntityService { private userService: UserService, private unitService: UnitService, private teachingPeriodService: TeachingPeriodService, - @Inject(analyticsService) private AnalyticsService: any, ) { super(httpClient, API_URL); @@ -28,7 +26,7 @@ export class UnitRoleService extends CachedEntityService { 'id', { keys: 'unit', - toEntityFn: (data, key, entity) => { + toEntityFn: (data, _key, _entity) => { const unitData = data['unit']; const result: Unit = this.unitService.cache.getOrCreate( unitData.id, @@ -38,13 +36,13 @@ export class UnitRoleService extends CachedEntityService { result.updateFromJson(unitData, this.unitService.mapping); return result; }, - toJsonFn: (entity: UnitRole, key: string) => { + toJsonFn: (entity: UnitRole, _key: string) => { return entity.unit?.id; }, }, { keys: 'user', - toEntityFn: (data: object, key: string, entity: UnitRole, params?: any) => { + toEntityFn: (data: object) => { return this.userService.cache.getOrCreate(data['user']['id'], userService, data['user']); }, }, @@ -52,13 +50,13 @@ export class UnitRoleService extends CachedEntityService { 'roleId', { keys: 'userId', - toJsonFn: (entity: UnitRole, key: string) => { + toJsonFn: (entity: UnitRole, _key: string) => { return entity.user?.id; }, }, { keys: 'unitId', - toJsonFn: (entity: UnitRole, key: string) => { + toJsonFn: (entity: UnitRole, _key: string) => { return entity.unit?.id; }, }, @@ -79,7 +77,7 @@ export class UnitRoleService extends CachedEntityService { ); } - public createInstanceFrom(json: any, other?: any): UnitRole { + public createInstanceFrom(_json: object): UnitRole { return new UnitRole(); } } diff --git a/src/app/api/services/unit.service.ts b/src/app/api/services/unit.service.ts index cb9986c33f..0c8579415b 100644 --- a/src/app/api/services/unit.service.ts +++ b/src/app/api/services/unit.service.ts @@ -1,5 +1,7 @@ -import {Injectable} from '@angular/core'; +import {CachedEntityService} from 'ngx-entity-service'; import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; import { GroupSetService, LearningOutcomeService, @@ -11,15 +13,14 @@ import { Unit, UserService, } from 'src/app/api/models/doubtfire-model'; -import {CachedEntityService, Entity, EntityMapping} from 'ngx-entity-service'; -import API_URL from 'src/app/config/constants/apiUrl'; -import {UnitRoleService} from './unit-role.service'; +import {SidekiqJob} from 'src/app/api/models/sidekiq-job'; import {AppInjector} from 'src/app/app-injector'; -import {TaskDefinitionService} from './task-definition.service'; -import {GroupService} from './group.service'; -import {Observable} from 'rxjs'; +import API_URL from 'src/app/config/constants/apiUrl'; import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; -import {SidekiqJob} from 'src/app/api/models/sidekiq-job'; +import {GroupService} from './group.service'; +import {MappingFunctions} from './mapping-fn'; +import {TaskDefinitionService} from './task-definition.service'; +import {UnitRoleService} from './unit-role.service'; export type IloStats = { median: number; @@ -29,6 +30,35 @@ export type IloStats = { max: number; }[]; +export interface TaskStatusStat { + tutorial_stream_id: number; + status: string; + num: number; +} + +export type TaskStatusStats = Record>; + +export interface TargetGradeStat { + tutorial_id: number; + tutorial_stream_id: number; + grade: number; + num: number; +} + +export interface TaskCompletionSummary { + median: number; + lower: number; + upper: number; + min: number; + max: number; +} + +export interface TaskCompletionStats { + unit: TaskCompletionSummary; + tutorial: Record; + grade: Record; +} + @Injectable() export class UnitService extends CachedEntityService { protected readonly endpointFormat = 'units/:id:'; @@ -58,12 +88,13 @@ export class UnitService extends CachedEntityService { 'myRole', { keys: 'unitRole', - toEntityFn: (data: object, jsonKey: string, entity: Unit) => { + toEntityFn: (data: object, jsonKey: string, _entity: Unit) => { const unitRoleService = AppInjector.get(UnitRoleService); unitRoleService.cache.get(data[jsonKey]); }, }, { + // keys: 'unitRoles', keys: 'staff', toEntityOp: (data, key, entity) => { const unitRoleService = AppInjector.get(UnitRoleService); @@ -77,20 +108,20 @@ export class UnitService extends CachedEntityService { { keys: ['mainConvenor', 'main_convenor_id'], toEntityFn: (data, key, entity) => { - let result = entity.staffCache.get(data[key]); + const result = entity.staffCache.get(data[key]); entity.mainConvenorUser = result?.user; return result; }, - toJsonFn: (unit: Unit, key: string) => { + toJsonFn: (unit: Unit, _key: string) => { return unit.mainConvenor?.id; }, }, { keys: ['mainConvenorUser', 'main_convenor_user_id'], - toEntityFn: (data, key, entity) => { + toEntityFn: (data, key, _entity) => { return AppInjector.get(UserService).cache.get(data[key]); }, - toJsonFn: (unit: Unit, key: string) => { + toJsonFn: (unit: Unit, _key: string) => { return unit.mainConvenor?.user.id; }, }, @@ -105,36 +136,31 @@ export class UnitService extends CachedEntityService { return undefined; } }, - toJsonFn: (entity: Unit, key: string) => { + toJsonFn: (entity: Unit, _key: string) => { return entity.teachingPeriod ? entity.teachingPeriod.id : undefined; }, }, { keys: 'startDate', - toEntityFn: (data, key, entity, params?) => { + toEntityFn: (data, key, _entity, _params?) => { return new Date(data[key]); }, - toJsonFn: (entity, key) => { - return entity.startDate.toISOString().slice(0, 10); - }, + toJsonFn: MappingFunctions.mapDayToJson, }, { keys: 'endDate', - toEntityFn: (data, key, entity, params?) => { + toEntityFn: (data, key, _entity, _params?) => { return new Date(data[key]); }, - toJsonFn: (entity, key) => { - return entity.endDate.toISOString().slice(0, 10); - }, + toJsonFn: MappingFunctions.mapDayToJson, }, + 'currentUnitWeek', { keys: 'portfolioAutoGenerationDate', - toEntityFn: (data, key, entity, params?) => { + toEntityFn: (data, key, _entity, _params?) => { return new Date(data[key]); }, - toJsonFn: (entity, key) => { - return entity.portfolioAutoGenerationDate?.toISOString().slice(0, 10); - }, + toJsonFn: MappingFunctions.mapDayToJson, }, 'assessmentEnabled', // 'overseerImageId', @@ -218,7 +244,7 @@ export class UnitService extends CachedEntityService { { keys: 'taskDefinitions', toEntityOp: (data, key, unit) => { - var seq: number = 0; + let seq: number = 0; data['task_definitions'].forEach((taskDefinitionJson: object) => { const td = unit.taskDefinitionCache.getOrCreate( taskDefinitionJson['id'], @@ -235,7 +261,7 @@ export class UnitService extends CachedEntityService { toEntityFn: (data: object, jsonKey: string, unit: Unit) => { return unit.taskDef(data[jsonKey]); }, - toJsonFn: (unit: Unit, key: string) => { + toJsonFn: (unit: Unit, _key: string) => { return unit.draftTaskDefinition?.id; }, }, @@ -257,6 +283,9 @@ export class UnitService extends CachedEntityService { // 'groupMemberships', - map to group memberships 'feedbackWarningThresholdDays', 'feedbackOverflowThresholdDays', + { + keys: ['gradeDefinitions', 'grade_definitions'], + }, 'enforceFeedbackBeforeDiscussedInClass', ); @@ -289,11 +318,12 @@ export class UnitService extends CachedEntityService { 'allowStudentChangeTutorial', 'feedbackWarningThresholdDays', 'feedbackOverflowThresholdDays', + 'gradeDefinitions', 'enforceFeedbackBeforeDiscussedInClass', ); } - public override createInstanceFrom(json: any, other?: any): Unit { + public override createInstanceFrom(_json: object): Unit { return new Unit(); } @@ -311,25 +341,25 @@ export class UnitService extends CachedEntityService { return httpClient.get(url); } - public taskStatusCountByTutorial(unit: Unit): Observable { + public taskStatusCountByTutorial(unit: Unit): Observable { const url = `${AppInjector.get(DoubtfireConstants).API_URL}/units/${unit.id}/stats/task_status_pct`; const httpClient = AppInjector.get(HttpClient); - return httpClient.get(url); + return httpClient.get(url); } - public targetGradeStats(unit: Unit): Observable { + public targetGradeStats(unit: Unit): Observable { const url = `${AppInjector.get(DoubtfireConstants).API_URL}/units/${unit.id}/stats/student_target_grade`; const httpClient = AppInjector.get(HttpClient); - return httpClient.get(url); + return httpClient.get(url); } - public taskCompletionStats(unit: Unit): Observable { + public taskCompletionStats(unit: Unit): Observable { const url = `${AppInjector.get(DoubtfireConstants).API_URL}/units/${unit.id}/stats/task_completion_stats`; const httpClient = AppInjector.get(HttpClient); - return httpClient.get(url); + return httpClient.get(url); } public zipPortfolios(unit: Unit): Observable { diff --git a/src/app/api/services/user.service.ts b/src/app/api/services/user.service.ts index d4396e8666..c63e903b2b 100644 --- a/src/app/api/services/user.service.ts +++ b/src/app/api/services/user.service.ts @@ -1,11 +1,10 @@ -import {UnitRole, UnitService, User} from 'src/app/api/models/doubtfire-model'; -import {CachedEntityService, RequestOptions} from 'ngx-entity-service'; -import {Injectable} from '@angular/core'; +import {CachedEntityService} from 'ngx-entity-service'; import {HttpClient} from '@angular/common/http'; -import API_URL from 'src/app/config/constants/apiUrl'; +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; +import {UnitRole, UnitService, User} from 'src/app/api/models/doubtfire-model'; import {AppInjector} from 'src/app/app-injector'; -import {AuthenticationService} from './authentication.service'; -import {Observable, tap} from 'rxjs'; +import API_URL from 'src/app/config/constants/apiUrl'; @Injectable() export class UserService extends CachedEntityService { diff --git a/src/app/api/services/webcal.service.ts b/src/app/api/services/webcal.service.ts index 9644b85d73..5782fb190c 100644 --- a/src/app/api/services/webcal.service.ts +++ b/src/app/api/services/webcal.service.ts @@ -1,8 +1,8 @@ -import {Injectable} from '@angular/core'; -import {Webcal} from '../models/webcal/webcal'; -import {Entity, EntityService} from 'ngx-entity-service'; +import {EntityService} from 'ngx-entity-service'; import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; import API_URL from 'src/app/config/constants/apiUrl'; +import {Webcal} from '../models/webcal/webcal'; @Injectable() export class WebcalService extends EntityService { @@ -29,7 +29,7 @@ export class WebcalService extends EntityService { this.mapping.mapAllKeysToJsonExcept('id'); } - public createInstanceFrom(json: any, other?: any): Webcal { + public createInstanceFrom(_json: object): Webcal { return new Webcal(); } } diff --git a/src/app/app-injector.ts b/src/app/app-injector.ts index 0d113cf3fc..9a37215985 100644 --- a/src/app/app-injector.ts +++ b/src/app/app-injector.ts @@ -1,4 +1,4 @@ -import { Injector } from '@angular/core'; +import {Injector} from '@angular/core'; /** * Allows for retrieving singletons using `AppInjector.get(MyService)` (whereas diff --git a/src/app/app.component.html b/src/app/app.component.html new file mode 100644 index 0000000000..53e25cc7c8 --- /dev/null +++ b/src/app/app.component.html @@ -0,0 +1,3 @@ + + + diff --git a/src/app/app.component.ts b/src/app/app.component.ts new file mode 100644 index 0000000000..b5d35f7f32 --- /dev/null +++ b/src/app/app.component.ts @@ -0,0 +1,35 @@ +import {ChangeDetectionStrategy, Component, OnDestroy, OnInit, Renderer2} from '@angular/core'; +import {NavigationEnd, Router} from '@angular/router'; +import {Subscription, filter} from 'rxjs'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class AppComponent implements OnInit, OnDestroy { + private routerSub?: Subscription; + + constructor( + private router: Router, + private renderer: Renderer2, + ) {} + + ngOnInit(): void { + this.setBodyBackground(this.router.url); + this.routerSub = this.router.events + .pipe(filter((event) => event instanceof NavigationEnd)) + .subscribe((event: NavigationEnd) => this.setBodyBackground(event.urlAfterRedirects)); + } + + ngOnDestroy(): void { + this.routerSub?.unsubscribe(); + } + + private setBodyBackground(url: string): void { + const path = url.split('?')[0].split('#')[0]; + const background = path === '/home' || path === '/' ? '#f5f5f5' : '#fff'; + this.renderer.setStyle(document.body, 'background-color', background); + } +} diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts new file mode 100644 index 0000000000..267ffcd963 --- /dev/null +++ b/src/app/app.routes.ts @@ -0,0 +1,268 @@ +import {Routes} from '@angular/router'; +import {EditProfileComponent} from './account/edit-profile/edit-profile.component'; +import {InstitutionSettingsComponent} from './admin/institution-settings/institution-settings.component'; +import {FUnitsComponent} from './admin/states/units/units.component'; +import {FUsersComponent} from './admin/states/users/users.component'; +import {roleWhitelistGuard} from './common/guards/role-whitelist.guard'; +import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component'; +import {SubmissionFilesDownloadComponent} from './common/submission-files-download/submission-files-download.component'; +import {SuccessCloseComponent} from './common/success-close/success-close.component'; +import {CrossDashboardComponent} from './dashboard/f-cross-dashboard.component'; +import {TimeoutComponent} from './errors/states/timeout/timeout.component'; +import {UnauthorisedComponent} from './errors/states/unauthorised/unauthorised.component'; +import {AcceptEulaComponent} from './eula/accept-eula/accept-eula.component'; +import {HomeComponent} from './home/states/home/home.component'; +import {LtiDashboardComponent} from './home/states/lti-dashboard/lti-dashboard.component'; +import {LtiUnitLinkComponent} from './home/states/lti-unit-link/lti-unit-link.component'; +import {resolveProject} from './projects/project.resolver'; +import {ProjectDashboardComponent} from './projects/states/dashboard/project-dashboard/project-dashboard.component'; +import {ProjectGroupsStateComponent} from './projects/states/groups/project-groups-state.component'; +import {JplagReportViewerComponent} from './projects/states/jplag/jplag-report-viewer.component'; +import {ProjectPlanComponent} from './projects/states/plan/project-plan.component'; +import {PortfolioStateComponent} from './projects/states/portfolio/portfolio-state.component'; +import {ProjectRootStateComponent} from './projects/states/project-root-state.component'; +import {TutorDiscussionComponent} from './projects/states/tutor-discussion/tutor-discussion.component'; +import {TutorialsComponent} from './projects/states/tutorials/tutorials.component'; +import {SignInComponent} from './sessions/states/sign-in/sign-in.component'; +import {UnitAnalyticsComponent} from './units/states/analytics/unit-analytics-route.component'; +import {UnitAdminStateComponent} from './units/states/edit/unit-admin-state.component'; +import {UnitGroupsComponent} from './units/states/groups/unit-groups/unit-groups.component'; +import {PortfoliosComponent} from './units/states/portfolios/portfolios.component'; +import {RolloverComponent} from './units/states/rollover/rollover.component'; +import {StudentsListComponent} from './units/states/students-list/students-list.component'; +import {UnitTaskInboxStateComponent} from './units/states/tasks/inbox/unit-task-inbox-state.component'; +import {TaskViewerStateComponent} from './units/task-viewer/task-viewer-state.component'; +import {UnitRootStateComponent} from './units/unit-root-state.component'; +import {resolveUnit} from './units/unit.resolver'; +import {WelcomeComponent} from './welcome/welcome.component'; + +export const routes: Routes = [ + {path: '', pathMatch: 'full', redirectTo: 'home'}, + {path: 'home', component: HomeComponent}, + {path: 'sign_in', component: SignInComponent}, + {path: 'welcome', component: WelcomeComponent}, + {path: 'unauthorised', component: UnauthorisedComponent}, + {path: 'timeout', component: TimeoutComponent}, + {path: 'success-close', component: SuccessCloseComponent}, + {path: 'edit_profile', component: EditProfileComponent}, + {path: 'eula', component: AcceptEulaComponent}, + {path: 'lti', component: LtiDashboardComponent}, + {path: 'lti/link', component: LtiUnitLinkComponent}, + {path: 'jplag-report-viewer', component: JplagReportViewerComponent}, + { + path: 'projects/:projectId/task_def_id/:taskDefId/scorm-player/normal', + component: ScormPlayerComponent, + data: {mode: 'normal'}, + }, + { + path: 'projects/:projectId/task_def_id/:taskDefId/scorm-player/review/:testAttemptId', + component: ScormPlayerComponent, + data: {mode: 'review'}, + }, + { + path: 'task_def_id/:taskDefId/preview-scorm', + component: ScormPlayerComponent, + data: {mode: 'preview'}, + }, + { + path: 'projects/:projectId/task_def_id/:taskDefId/submission_files/download', + component: SubmissionFilesDownloadComponent, + }, + {path: 'view-all-units', component: FUnitsComponent, data: {mode: 'tutor'}}, + {path: 'view-all-projects', component: FUnitsComponent, data: {mode: 'student'}}, + { + path: 'dashboard', + component: CrossDashboardComponent, + canActivate: [roleWhitelistGuard], + data: {roleWhitelist: ['Student'], pageTitle: 'Dashboard'}, + }, + { + path: 'admin/units', + component: FUnitsComponent, + canActivate: [roleWhitelistGuard], + data: {mode: 'admin', roleWhitelist: ['Admin', 'Auditor', 'Convenor']}, + }, + { + path: 'admin/users', + component: FUsersComponent, + canActivate: [roleWhitelistGuard], + data: {roleWhitelist: ['Admin', 'Auditor']}, + }, + { + path: 'admin/institution-settings', + component: InstitutionSettingsComponent, + canActivate: [roleWhitelistGuard], + data: {roleWhitelist: ['Admin', 'Auditor']}, + }, + { + path: 'admin/institution-settings/:tab', + component: InstitutionSettingsComponent, + canActivate: [roleWhitelistGuard], + data: {roleWhitelist: ['Admin', 'Auditor']}, + }, + { + path: 'tutor-discussion', + component: TutorDiscussionComponent, + canActivate: [roleWhitelistGuard], + data: {task: 'Discussion', roleWhitelist: ['Admin', 'Auditor', 'Tutor']}, + }, + { + path: 'tutor-attendance', + component: TutorDiscussionComponent, + data: {attendance: true, task: 'Check-in'}, + }, + { + path: 'units', + children: [ + {path: '', pathMatch: 'full', redirectTo: '/home'}, + { + path: ':unitId', + component: UnitRootStateComponent, + resolve: { + unit: resolveUnit, + }, + children: [ + {path: '', pathMatch: 'full', redirectTo: 'tasks/inbox'}, + {path: 'analytics', component: UnitAnalyticsComponent, data: {task: 'Unit Analytics'}}, + {path: 'students/groups', component: UnitGroupsComponent, data: {task: 'Student Groups'}}, + { + path: 'students/portfolios', + component: PortfoliosComponent, + data: {task: 'Student Portfolios'}, + }, + { + path: 'students/portfolios/:projectId', + component: PortfoliosComponent, + data: {task: 'Student Portfolios'}, + }, + { + path: 'students/portfolios/:projectId/:tab', + component: PortfoliosComponent, + data: {task: 'Student Portfolios'}, + }, + { + path: 'students/portfolios/:projectId/:tab/:taskAbbreviation', + component: PortfoliosComponent, + data: {task: 'Student Portfolios'}, + }, + {path: 'students', component: StudentsListComponent, data: {task: 'Student List'}}, + { + path: 'admin', + component: UnitAdminStateComponent, + canActivate: [roleWhitelistGuard], + data: {task: 'Unit Administration', roleWhitelist: ['Convenor', 'Admin', 'Auditor']}, + }, + { + path: 'admin/:tab', + component: UnitAdminStateComponent, + canActivate: [roleWhitelistGuard], + data: {task: 'Unit Administration', roleWhitelist: ['Convenor', 'Admin', 'Auditor']}, + }, + {path: 'rollover', component: RolloverComponent, data: {task: 'Unit Rollover'}}, + {path: 'discussion', component: TutorDiscussionComponent, data: {task: 'Discussion'}}, + { + path: 'check-in', + component: TutorDiscussionComponent, + data: {attendance: true, task: 'Check-in'}, + }, + { + path: 'tasks', + pathMatch: 'full', + component: TaskViewerStateComponent, + data: {task: 'Task Lists', roleWhitelist: ['Convenor', 'Admin', 'Auditor']}, + canActivate: [roleWhitelistGuard], + }, + { + path: 'tasks', + canActivate: [roleWhitelistGuard], + data: {roleWhitelist: ['Convenor', 'Admin', 'Auditor', 'Tutor']}, + + children: [ + {path: '', pathMatch: 'full', redirectTo: 'inbox'}, + { + path: 'inbox', + component: UnitTaskInboxStateComponent, + data: {routeMode: 'inbox', task: 'Task Inbox'}, + }, + { + path: 'inbox/:studentId/:taskDefAbbr', + component: UnitTaskInboxStateComponent, + data: {routeMode: 'inbox', task: 'Task Inbox'}, + }, + { + path: 'definition', + component: UnitTaskInboxStateComponent, + data: {routeMode: 'definition', task: 'Task Explorer'}, + }, + { + path: 'definition/:studentId/:taskDefAbbr', + component: UnitTaskInboxStateComponent, + data: {routeMode: 'definition', task: 'Task Explorer'}, + }, + { + path: 'moderation', + component: UnitTaskInboxStateComponent, + data: {routeMode: 'moderation', task: 'Task Moderation'}, + }, + { + path: 'moderation/:studentId/:taskDefAbbr', + component: UnitTaskInboxStateComponent, + data: {routeMode: 'moderation', task: 'Task Moderation'}, + }, + { + path: 'overflow', + component: UnitTaskInboxStateComponent, + data: {routeMode: 'overflow', task: 'Task Overflow'}, + }, + { + path: 'overflow/:studentId/:taskDefAbbr', + component: UnitTaskInboxStateComponent, + data: {routeMode: 'overflow', task: 'Task Overflow'}, + }, + ], + }, + { + path: 'tasks/:taskAbbreviation', + component: TaskViewerStateComponent, + data: {task: 'Task Lists'}, + }, + ], + }, + ], + }, + { + path: 'projects', + children: [ + {path: '', pathMatch: 'full', redirectTo: '/home'}, + { + path: ':projectId', + component: ProjectRootStateComponent, + resolve: { + project: resolveProject, + }, + children: [ + {path: '', pathMatch: 'full', redirectTo: 'dashboard'}, + { + path: 'dashboard', + component: ProjectDashboardComponent, + data: {task: 'Dashboard'}, + }, + { + path: 'dashboard/:taskAbbreviation', + component: ProjectDashboardComponent, + data: {task: 'Dashboard'}, + }, + {path: 'plan', component: ProjectPlanComponent, data: {task: 'Plan Tasks'}}, + { + path: 'portfolio', + component: PortfolioStateComponent, + data: {task: 'Portfolio Creation'}, + }, + {path: 'groups', component: ProjectGroupsStateComponent, data: {task: 'Groups List'}}, + {path: 'tutorials', component: TutorialsComponent, data: {task: 'Tutorial List'}}, + ], + }, + ], + }, + {path: '**', redirectTo: 'home'}, +]; diff --git a/src/app/common/archive-viewer/archive-viewer.component.html b/src/app/common/archive-viewer/archive-viewer.component.html index da1de5293f..1ff2f8290e 100644 --- a/src/app/common/archive-viewer/archive-viewer.component.html +++ b/src/app/common/archive-viewer/archive-viewer.component.html @@ -1,47 +1,38 @@ -
+
@if (isLoading) { -
+
Loading archive...
} @else if (errorMessage) { -
- error_outline +
+ error_outline {{ errorMessage }}
} @else if (!archiveFile) { -
- folder_zip +
+ folder_zip Select an archive to preview.
} @else if (!hasFiles) { -
- folder_off +
+ folder_off No files to display.
} @else { @if (!readOnly && saveEndpoint) { -
+
} @@ -49,31 +40,31 @@ @if (navigationMode === 'tree') {
-
+
} @else {
@for (file of files; track trackByPath($index, file); let i = $index) { - - {{ file.tabLabel }} + + {{ file.tabLabel }} } @@ -90,7 +81,7 @@ @if (showPreview) { @if (file.isLoading || !file.isLoaded) {
Loading {{ file.name }}... @@ -102,9 +93,9 @@ [ngTemplateOutletContext]="{$implicit: file.path}" >
@@ -114,11 +105,11 @@ [ngTemplateOutlet]="filePathLabel" [ngTemplateOutletContext]="{$implicit: file.path}" > -
+
@@ -128,16 +119,16 @@ [ngTemplateOutlet]="filePathLabel" [ngTemplateOutletContext]="{$implicit: file.path}" > - +
} @else {
- insert_drive_file + insert_drive_file
-

This file type cannot be previewed.

- @@ -149,11 +140,11 @@ } - + @for (node of nodes; track trackTreeNode($index, node)) { @if (node.isDirectory) {
@@ -166,13 +157,13 @@ > } @else { diff --git a/src/app/common/audio-player/audio-player.component.scss b/src/app/common/audio-player/audio-player.component.scss index 853fecb304..22c4b968e8 100644 --- a/src/app/common/audio-player/audio-player.component.scss +++ b/src/app/common/audio-player/audio-player.component.scss @@ -11,9 +11,11 @@ -moz-box-shadow: none; box-shadow: none; - i { + mat-icon { font-size: 25px; + height: 25px; margin-left: -5px; + width: 25px; } } @@ -22,7 +24,7 @@ padding: 0%; width: 100%; - i { + mat-icon { color: white; } diff --git a/src/app/common/audio-player/audio-player.component.ts b/src/app/common/audio-player/audio-player.component.ts index 2a1243e1c2..33e8317ad9 100644 --- a/src/app/common/audio-player/audio-player.component.ts +++ b/src/app/common/audio-player/audio-player.component.ts @@ -1,21 +1,34 @@ -import { HttpResponse } from '@angular/common/http'; -import { Component, Inject, Input, ViewChild, ElementRef, OnDestroy } from '@angular/core'; -import { Project, Task, TaskComment } from 'src/app/api/models/doubtfire-model'; -import { FileDownloaderService } from '../file-downloader/file-downloader.service'; -import { AlertService } from '../services/alert.service'; +import {HttpResponse} from '@angular/common/http'; +import { + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + Inject, + Input, + OnDestroy, + Output, + ViewChild, +} from '@angular/core'; +import {Project, Task, TaskComment} from 'src/app/api/models/doubtfire-model'; +import {FileDownloaderService} from '../file-downloader/file-downloader.service'; +import {AlertService} from '../services/alert.service'; @Component({ selector: 'audio-player', templateUrl: './audio-player.component.html', styleUrls: ['./audio-player.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class AudioPlayerComponent implements OnDestroy { @Input() project: Project; @Input() task: Task; @Input() comment: TaskComment; - @Input() audioSrc: { src: string }; + @Input() audioSrc: {src: string}; + @Output() playingChange: EventEmitter = new EventEmitter(); - @ViewChild('progressBar', { read: ElementRef }) private progressBar: ElementRef; + @ViewChild('progressBar', {read: ElementRef}) private progressBar: ElementRef; private isLoaded = false; public isPlaying = false; @@ -33,12 +46,13 @@ export class AudioPlayerComponent implements OnDestroy { this.audio.onended = () => { this.isPlaying = false; + this.playingChange.emit(false); }; } ngOnDestroy(): void { // Clean up the blob - if ( this.audio.src ) { + if (this.audio.src) { this.fileDownloader.releaseBlob(this.audio.src); } } @@ -59,9 +73,12 @@ export class AudioPlayerComponent implements OnDestroy { public setSrc(src: string) { // If there was an old blob, then free the memory it uses - if ( this.audio.src ) { + if (this.audio.src) { this.fileDownloader.releaseBlob(this.audio.src); } + this.isLoaded = true; + this.isPlaying = false; + this.playingChange.emit(false); this.audio.src = src; this.audio.load(); this.audio.onloadeddata = () => { @@ -83,12 +100,12 @@ export class AudioPlayerComponent implements OnDestroy { this.fileDownloader.downloadBlob( url, - ((blobUrl: string, response: HttpResponse) => { + ((blobUrl: string, _response: HttpResponse) => { this.isLoaded = true; this.setSrc(blobUrl); this.audio.src = blobUrl; this.audio.load(); - if (onload) { + if (onLoad) { this.audio.onloadeddata = () => { fn(); }; @@ -96,25 +113,41 @@ export class AudioPlayerComponent implements OnDestroy { fn(); } }).bind(this), - ((error: any) => { + ((error: Error) => { this.alerts.error(`Error loading audio. ${error}`, 6000); - }).bind(this) + }).bind(this), ); } } + public play() { + this.execWithAudio( + true, + (() => { + this.audio.play(); + this.isPlaying = true; + this.playingChange.emit(true); + }).bind(this), + ); + } + + public stop() { + this.audio.pause(); + this.audio.currentTime = 0; + this.isPlaying = false; + this.playingChange.emit(false); + } + public pausePlay() { this.execWithAudio( true, (() => { if (this.audio.paused) { - this.audio.play(); - this.isPlaying = true; + this.play(); } else { - this.audio.pause(); - this.isPlaying = false; + this.stop(); } - }).bind(this) + }).bind(this), ); } } diff --git a/src/app/common/audio-recorder/audio/audio-comment-recorder/audio-comment-recorder.html b/src/app/common/audio-recorder/audio/audio-comment-recorder/audio-comment-recorder.html index 26d0f04358..736059be22 100644 --- a/src/app/common/audio-recorder/audio/audio-comment-recorder/audio-comment-recorder.html +++ b/src/app/common/audio-recorder/audio/audio-comment-recorder/audio-comment-recorder.html @@ -3,39 +3,51 @@ Audio recording
only supported in modern versions of Chrome, Firefox and Safari.

- +
- +
diff --git a/src/app/common/audio-recorder/audio/audio-comment-recorder/audio-comment-recorder.scss b/src/app/common/audio-recorder/audio/audio-comment-recorder/audio-comment-recorder.scss index 948428234e..c0ac8c8109 100644 --- a/src/app/common/audio-recorder/audio/audio-comment-recorder/audio-comment-recorder.scss +++ b/src/app/common/audio-recorder/audio/audio-comment-recorder/audio-comment-recorder.scss @@ -1,5 +1,3 @@ - - #audioDiscussionTemplate { display: flex; align-items: center; diff --git a/src/app/common/audio-recorder/audio/audio-comment-recorder/audio-comment-recorder.ts b/src/app/common/audio-recorder/audio/audio-comment-recorder/audio-comment-recorder.ts index 7e91fb4709..ca4798e5d3 100644 --- a/src/app/common/audio-recorder/audio/audio-comment-recorder/audio-comment-recorder.ts +++ b/src/app/common/audio-recorder/audio/audio-comment-recorder/audio-comment-recorder.ts @@ -1,18 +1,24 @@ -import { Inject, Input, Component } from '@angular/core'; -import { BaseAudioRecorderComponent } from '../base-audio-recorder'; -import { audioRecorderService } from 'src/app/ajs-upgraded-providers'; -import { TaskComment, TaskCommentService, Task } from 'src/app/api/models/doubtfire-model'; -import { AlertService } from 'src/app/common/services/alert.service'; +import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; +import {Task, TaskComment, TaskCommentService} from 'src/app/api/models/doubtfire-model'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {MediaRecorderService} from 'src/app/common/services/recorder-service'; +import {BaseAudioRecorderComponent} from '../base-audio-recorder'; -@Component({ selector: 'audio-comment-recorder', templateUrl: './audio-comment-recorder.html' }) -export class AudioCommentRecorderComponent extends BaseAudioRecorderComponent { +@Component({ + selector: 'audio-comment-recorder', + templateUrl: './audio-comment-recorder.html', + providers: [MediaRecorderService], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class AudioCommentRecorderComponent extends BaseAudioRecorderComponent implements OnInit { @Input() task: Task; canvas: HTMLCanvasElement; canvasCtx: CanvasRenderingContext2D; - isSending: boolean; + isSending: boolean = false; constructor( - @Inject(audioRecorderService) mediaRecorderService: any, + private mediaRecorderService: MediaRecorderService, private alerts: AlertService, private ts: TaskCommentService, ) { @@ -35,14 +41,16 @@ export class AudioCommentRecorderComponent extends BaseAudioRecorderComponent { this.isSending = true; if (this.blob && this.blob.size > 0) { this.ts.addComment(this.task, this.blob, 'audio').subscribe({ - next: (comment: TaskComment) => { + next: (_comment: TaskComment) => { this.isSending = false; this.scrollCommentsDown(); }, - error: (failure: { data: { error: any } }) => { - this.alerts.error(`Failed to post audio. ${failure.data != null ? failure.data.error : undefined}`); + error: (failure: {data: {error: string}}) => { + this.alerts.error( + `Failed to post audio. ${failure.data != null ? failure.data.error : undefined}`, + ); this.isSending = false; - } + }, }); this.blob = {} as Blob; diff --git a/src/app/common/audio-recorder/audio/base-audio-recorder.ts b/src/app/common/audio-recorder/audio/base-audio-recorder.ts index 8c7d2718d5..43942d7d9b 100644 --- a/src/app/common/audio-recorder/audio/base-audio-recorder.ts +++ b/src/app/common/audio-recorder/audio/base-audio-recorder.ts @@ -1,8 +1,18 @@ -import { OnInit, Directive } from '@angular/core'; +import {Directive} from '@angular/core'; +import {MediaRecorderService} from 'src/app/common/services/recorder-service'; + +export interface RecordingEvent extends Event { + detail: { + recording: { + blob: Blob; + blobUrl: string; + }; + }; +} @Directive() -export abstract class BaseAudioRecorderComponent implements OnInit { - protected mediaRecorder: any = null; +export abstract class BaseAudioRecorderComponent { + protected mediaRecorder: MediaRecorderService = null; public recordingAvailable: boolean = false; public isRecording: boolean = false; protected isPlaying: boolean = false; @@ -18,23 +28,19 @@ export abstract class BaseAudioRecorderComponent implements OnInit { return Boolean(navigator && navigator.mediaDevices && navigator.mediaDevices.getUserMedia); } - constructor(private mediaRecorderService: any) {} - - ngOnInit(): void { - this.isSending = false; - if (this.canRecord) { - this.init(); - } - } + constructor(private recorderService: MediaRecorderService) {} protected init(): void { + this.isSending = false; this.blob = new Blob(); - this.mediaRecorder = new this.mediaRecorderService(); + this.mediaRecorder = this.recorderService; // Required for recording multiple times this.mediaRecorder.config.stopTracksAndCloseCtxWhenFinished = true; // Required for visualising the stream this.mediaRecorder.config.createAnalyserNode = true; - this.mediaRecorder.em.addEventListener('recording', (evt: any) => this.onNewRecording(evt)); + this.mediaRecorder.em.addEventListener('recording', (evt: Event) => + this.onNewRecording(evt as RecordingEvent), + ); } playStop(): void { @@ -84,7 +90,7 @@ export abstract class BaseAudioRecorderComponent implements OnInit { this.mediaRecorder.processChunks(); } - onNewRecording(evt: any): void { + onNewRecording(evt: RecordingEvent): void { this.blob = evt.detail.recording.blob; this.audio.src = evt.detail.recording.blobUrl; this.audio.load(); @@ -95,8 +101,8 @@ export abstract class BaseAudioRecorderComponent implements OnInit { // Which can be overridden protected visualise(): void { const draw = () => { - let WIDTH = this.canvas.clientWidth; - let HEIGHT = this.canvas.clientHeight; + let WIDTH: number; + let HEIGHT: number; this.canvas.width = 1; this.canvas.height = 1; diff --git a/src/app/common/audio-recorder/audio/microphone-tester/microphone-tester-component.html b/src/app/common/audio-recorder/audio/microphone-tester/microphone-tester-component.html index 0f609925ab..1d3291541a 100644 --- a/src/app/common/audio-recorder/audio/microphone-tester/microphone-tester-component.html +++ b/src/app/common/audio-recorder/audio/microphone-tester/microphone-tester-component.html @@ -1,30 +1,74 @@ -
-

Audio recording
only supported in modern versions of Chrome, Firefox and Safari.

- -
-
-
-

Step 1. Record some audio!

- - +
+

+ Audio recording is only supported in modern versions of Chrome, Firefox and Safari. +

+ + + +
+
+
+

Record some audio

+

Use this quick check to confirm your microphone.

-
- +
+ + +
+ +
+
+ +
-
-

Step 2. Stop the recording, and playback the audio to make sure it's audible:

- +
+
+

Play it back

+

+ Stop the recording, then listen to make sure it is clear. +

+
+
-
-

Step 3. Check the "Ready to go" cehckbox below if you're all set!

+
+ When everything sounds right, check “Ready to go” below.
-
\ No newline at end of file +
diff --git a/src/app/common/audio-recorder/audio/microphone-tester/microphone-tester-component.scss b/src/app/common/audio-recorder/audio/microphone-tester/microphone-tester-component.scss index 4b6a37e52a..d201a8dadd 100644 --- a/src/app/common/audio-recorder/audio/microphone-tester/microphone-tester-component.scss +++ b/src/app/common/audio-recorder/audio/microphone-tester/microphone-tester-component.scss @@ -1,5 +1,4 @@ microphone-tester { - h1 { font-size: 12pt; } @@ -9,7 +8,9 @@ microphone-tester { border-radius: 72px; color: #fff; height: 72px; - transition: width 0.1s, height 0.1s; + transition: + width 0.1s, + height 0.1s; width: 72px; border: none; outline: none; @@ -18,7 +19,7 @@ microphone-tester { top: 50%; p { - font-family: "Helvetica Neue", "Segoe UI", "Helvetica", "Arial", "sans-serif"; + font-family: 'Helvetica Neue', 'Segoe UI', 'Helvetica', 'Arial', 'sans-serif'; text-rendering: optimizeLegibility; line-height: 1.3; font-size: 14px; diff --git a/src/app/common/audio-recorder/audio/microphone-tester/microphone-tester.component.ts b/src/app/common/audio-recorder/audio/microphone-tester/microphone-tester.component.ts index a899577685..fb8586156b 100644 --- a/src/app/common/audio-recorder/audio/microphone-tester/microphone-tester.component.ts +++ b/src/app/common/audio-recorder/audio/microphone-tester/microphone-tester.component.ts @@ -1,16 +1,31 @@ -import { Inject, Input, Component, AfterViewInit } from '@angular/core'; -import { BaseAudioRecorderComponent } from '../base-audio-recorder'; -import { audioRecorderService } from 'src/app/ajs-upgraded-providers'; -import { Task } from 'src/app/api/models/doubtfire-model'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + Input, + ViewChild, +} from '@angular/core'; +import {Task} from 'src/app/api/models/doubtfire-model'; +import {MediaRecorderService} from 'src/app/common/services/recorder-service'; +import {BaseAudioRecorderComponent} from '../base-audio-recorder'; -@Component({ selector: 'microphone-tester', templateUrl: './microphone-tester-component.html' }) +@Component({ + selector: 'microphone-tester', + templateUrl: './microphone-tester-component.html', + providers: [MediaRecorderService], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) export class MicrophoneTesterComponent extends BaseAudioRecorderComponent implements AfterViewInit { @Input() task: Task; + @ViewChild('micTesterAudioPlayer') audioRef: ElementRef; + @ViewChild('micTesterVisualiser') canvasRef: ElementRef; canvas: HTMLCanvasElement; canvasCtx: CanvasRenderingContext2D; - isSending: boolean; + isSending: boolean = false; - constructor(@Inject(audioRecorderService) mediaRecorderService: any) { + constructor(private mediaRecorderService: MediaRecorderService) { super(mediaRecorderService); } @@ -20,15 +35,50 @@ export class MicrophoneTesterComponent extends BaseAudioRecorderComponent implem } } - // We need to override default behaviour of the parent class. - ngOnInit() {} - init(): void { super.init(); - this.canvas = document.getElementById('micTesterVisualiser') as HTMLCanvasElement; - this.audio = document.getElementById('micTesterAudioPlayer') as HTMLAudioElement; + this.canvas = this.canvasRef.nativeElement; + this.audio = this.audioRef.nativeElement; this.canvasCtx = this.canvas.getContext('2d'); } - sendRecording(): void {} + sendRecording(): void { + /* empty */ + } + + protected visualise(): void { + const draw = () => { + let WIDTH: number; + let HEIGHT: number; + + this.canvas.width = 1; + this.canvas.height = 1; + + this.canvas.width = WIDTH = this.canvas.clientWidth; + this.canvas.height = HEIGHT = this.canvas.clientHeight; + requestAnimationFrame(draw); + analyser.getByteTimeDomainData(dataArray); + analyser.getByteFrequencyData(dataArray); + + this.canvasCtx.clearRect(0, 0, WIDTH, HEIGHT); + + const barWidth = 2; + const barGap = 2; + + for (let i = 0; i < WIDTH; i++) { + const barX = i * (barWidth + barGap); + const barY = HEIGHT / 2; + const barHeight = -(dataArray[i] / 8) + 1; + this.canvasCtx.fillStyle = '#2563eb'; + this.canvasCtx.fillRect(barX, barY, barWidth, barHeight); + this.canvasCtx.fillRect(barX, barY - barHeight, barWidth, barHeight); + } + }; + + const analyser = this.mediaRecorder.analyserNode; + analyser.fftSize = 2048; + const bufferLength = analyser.frequencyBinCount; + const dataArray = new Uint8Array(bufferLength); + draw(); + } } diff --git a/src/app/common/chart-base/chart-base-component/chart-base-component.component.html b/src/app/common/chart-base/chart-base-component/chart-base-component.component.html new file mode 100644 index 0000000000..16e336d2ba --- /dev/null +++ b/src/app/common/chart-base/chart-base-component/chart-base-component.component.html @@ -0,0 +1 @@ +

chart-base-component works!

diff --git a/src/app/common/chart-base/chart-base-component/chart-base-component.component.ts b/src/app/common/chart-base/chart-base-component/chart-base-component.component.ts new file mode 100644 index 0000000000..326b8cd75c --- /dev/null +++ b/src/app/common/chart-base/chart-base-component/chart-base-component.component.ts @@ -0,0 +1,16 @@ +import {ChangeDetectionStrategy, Component, ViewContainerRef} from '@angular/core'; + +/** + * @title chart-base-component + * @desc This is a base class to be used with the ngx-charts library. It is used to set the root view container for the tooltip service, to avoid issues with the tooltip not displaying correctly. + * + * Child classes need to extend this class and call super() in the constructor, passing in the ViewContainerRef. + */ +@Component({ + templateUrl: './chart-base-component.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class ChartBaseComponent { + constructor(public viewContainerRef: ViewContainerRef) {} +} diff --git a/src/app/common/common.coffee b/src/app/common/common.coffee deleted file mode 100644 index f330d8f6ac..0000000000 --- a/src/app/common/common.coffee +++ /dev/null @@ -1,7 +0,0 @@ -angular.module("doubtfire.common", [ - 'doubtfire.common.services' - 'doubtfire.common.filters' - 'doubtfire.common.modals' - 'doubtfire.common.file-uploader' - 'doubtfire.common.content-editable' -]) diff --git a/src/app/common/content-editable/content-editable.coffee b/src/app/common/content-editable/content-editable.coffee deleted file mode 100644 index 7cbfee3a10..0000000000 --- a/src/app/common/content-editable/content-editable.coffee +++ /dev/null @@ -1,21 +0,0 @@ -angular.module('doubtfire.common.content-editable', []) -# Workaround directive for lack of contenteditable support for divs and spans - -.directive 'contenteditable', ($sce) -> - { - restrict: 'A' - require: 'ngModel' - link: (scope, element, attrs, ngModel) -> - - read = -> - ngModel.$setViewValue element[0].innerText - return - - ngModel.$render = -> - element.html $sce.getTrustedHtml(ngModel.$viewValue or '') - return - - element.bind 'blur keyup change', -> - scope.$evalAsync(read) - return - } diff --git a/src/app/common/directives/drag-drop.directive.spec.ts b/src/app/common/directives/drag-drop.directive.spec.ts index 0d39e1f01c..50b65a31a7 100644 --- a/src/app/common/directives/drag-drop.directive.spec.ts +++ b/src/app/common/directives/drag-drop.directive.spec.ts @@ -1,4 +1,5 @@ -import { DragDropDirective } from './drag-drop.directive'; +import {describe, expect, it} from 'vitest'; +import {DragDropDirective} from './drag-drop.directive'; describe('DragDropDirective', () => { it('should create an instance', () => { diff --git a/src/app/common/directives/drag-drop.directive.ts b/src/app/common/directives/drag-drop.directive.ts index 1278cdc59e..415e2475b4 100644 --- a/src/app/common/directives/drag-drop.directive.ts +++ b/src/app/common/directives/drag-drop.directive.ts @@ -1,4 +1,4 @@ -import { Directive, Output, EventEmitter, HostBinding, HostListener } from '@angular/core'; +import {Directive, EventEmitter, HostBinding, HostListener, Output} from '@angular/core'; /** * The "appDragDrop" directive can be added to angular components to allow them to act as @@ -8,9 +8,10 @@ import { Directive, Output, EventEmitter, HostBinding, HostListener } from '@ang */ @Directive({ selector: '[appDragDrop]', + standalone: false, }) export class DragDropDirective { - @Output() fileDropped = new EventEmitter(); + @Output() fileDropped: EventEmitter = new EventEmitter(); // @HostBinding('style.background-color') private background = '#f5fcff'; // @HostBinding('style.opacity') private opacity = '1'; diff --git a/src/app/common/edit-profile-form/edit-profile-form.component.html b/src/app/common/edit-profile-form/edit-profile-form.component.html index 0825a194b8..a51b79fee2 100644 --- a/src/app/common/edit-profile-form/edit-profile-form.component.html +++ b/src/app/common/edit-profile-form/edit-profile-form.component.html @@ -1,17 +1,17 @@ -
-
+
+
-
-
+
+
-
@@ -21,7 +21,7 @@

{{ user?.firstName }}

-
+
- + Username - +
-
- +
+ First Name - +
- + Second Name - +
-
+
- + Preferred Name - +
- + Custom Pronouns @if (user.systemRole === 'Student') { - - Student ID - - + + Student ID + + } Email - + @if (canSeeSystemRole) { - - System Role - - Administrator - Convenor - Tutor - Student - Auditor - - + + System Role + + Administrator + Convenor + Tutor + Student + Auditor + + } -
- + Receive notifications for new messages
- Receive notifications when your portfolio is ready
- Receive notifications when new tasks are available
-
- + Send anonymous research statistics
- @if(tiiEnabled){ -
- Accepted TurnItIn EULA - -
+ @if (tiiEnabled) { +
+ Accepted TurnItIn EULA + +
} -
- -
- @if (mode === 'create') { - - } @if (mode === 'edit') { +
+
+ @if (mode === 'create') { + + } + @if (mode === 'edit') { + }
diff --git a/src/app/common/edit-profile-form/edit-profile-form.component.spec.ts b/src/app/common/edit-profile-form/edit-profile-form.component.spec.ts index c3d777682a..a3bfc9285a 100644 --- a/src/app/common/edit-profile-form/edit-profile-form.component.spec.ts +++ b/src/app/common/edit-profile-form/edit-profile-form.component.spec.ts @@ -1,21 +1,40 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MAT_DIALOG_DATA} from '@angular/material/dialog'; +import {MatSnackBar} from '@angular/material/snack-bar'; +import {Router} from '@angular/router'; +import {AuthenticationService} from 'src/app/api/services/authentication.service'; +import {UserService} from 'src/app/api/services/user.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {EditProfileFormComponent} from './edit-profile-form.component'; -import { EditProfileFormComponent } from './edit-profile-form.component'; +const emptyProvider = {}; -describe('EditProfileComponent', () => { +describe('EditProfileFormComponent', () => { let component: EditProfileFormComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [EditProfileFormComponent], - }).compileComponents(); + providers: [ + {provide: DoubtfireConstants, useValue: emptyProvider}, + {provide: UserService, useValue: emptyProvider}, + {provide: Router, useValue: emptyProvider}, + {provide: AuthenticationService, useValue: emptyProvider}, + {provide: MAT_DIALOG_DATA, useValue: emptyProvider}, + {provide: MatSnackBar, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(EditProfileFormComponent, {set: {template: ''}}) + .compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(EditProfileFormComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/common/edit-profile-form/edit-profile-form.component.ts b/src/app/common/edit-profile-form/edit-profile-form.component.ts index 34e58371c2..fbc51a92d6 100644 --- a/src/app/common/edit-profile-form/edit-profile-form.component.ts +++ b/src/app/common/edit-profile-form/edit-profile-form.component.ts @@ -1,7 +1,7 @@ -import {Component, Inject, Input, OnInit, Optional} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Inject, Input, OnInit, Optional} from '@angular/core'; import {MAT_DIALOG_DATA} from '@angular/material/dialog'; import {MatSnackBar} from '@angular/material/snack-bar'; -import {StateService} from '@uirouter/core'; +import {Router} from '@angular/router'; import {User} from 'src/app/api/models/user/user'; import {AuthenticationService} from 'src/app/api/services/authentication.service'; import {UserService} from 'src/app/api/services/user.service'; @@ -11,12 +11,14 @@ import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; selector: 'f-edit-profile-form', templateUrl: './edit-profile-form.component.html', styleUrls: ['./edit-profile-form.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class EditProfileFormComponent implements OnInit { constructor( private constants: DoubtfireConstants, private userService: UserService, - private state: StateService, + private router: Router, private authService: AuthenticationService, @Optional() @Inject(MAT_DIALOG_DATA) @@ -102,7 +104,7 @@ export class EditProfileFormComponent implements OnInit { this.userService.update(this.user).subscribe({ next: (updatedUser) => { if (this.mode === 'create') { - this.state.go('home'); + this.router.navigateByUrl('/home'); } else { this.user = updatedUser; this.initialFirstName = this.user.firstName; diff --git a/src/app/common/entity-form/entity-form.component.ts b/src/app/common/entity-form/entity-form.component.ts index 5ebe626f8a..8b9eddb11d 100644 --- a/src/app/common/entity-form/entity-form.component.ts +++ b/src/app/common/entity-form/entity-form.component.ts @@ -1,10 +1,11 @@ -import { AfterViewInit, Directive } from '@angular/core'; -import { UntypedFormGroup, AbstractControl } from '@angular/forms'; -import { Entity, RequestOptions } from 'ngx-entity-service'; -import { EntityService } from 'ngx-entity-service'; -import { Observable, tap } from 'rxjs'; -import { Sort } from '@angular/material/sort'; -import { MatTableDataSource } from '@angular/material/table'; +import {Entity, RequestOptions} from 'ngx-entity-service'; +import {EntityService} from 'ngx-entity-service'; +import {AfterViewInit, Directive} from '@angular/core'; +import {AbstractControl, UntypedFormGroup} from '@angular/forms'; +import {Sort} from '@angular/material/sort'; +import {MatTableDataSource} from '@angular/material/table'; +import {Observable, tap} from 'rxjs'; +import {AlertService} from 'src/app/common/services/alert.service'; export type OnSuccessMethod = (object: T, isNew: boolean) => void; @@ -47,7 +48,10 @@ export abstract class EntityFormComponent implements AfterView * * @param controls the FormControls that will make up the form. */ - constructor(controls: { [key: string]: AbstractControl }, protected entityName: string) { + constructor( + controls: Record, + protected entityName: string, + ) { this.formData = new UntypedFormGroup(controls); // Iterate over the FormControls passed in and assign the default values // For each based on the values that they are constructed with @@ -56,7 +60,10 @@ export abstract class EntityFormComponent implements AfterView } } - ngAfterViewInit() {} + // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method + ngAfterViewInit() { + /* empty */ + } /** * Cancel edit of current selected value. @@ -113,7 +120,7 @@ export abstract class EntityFormComponent implements AfterView * @param alertService the alert service used to provide alerts. * @param success the function, provided by inheritor, that is executed on success of CRUD methods. */ - submit(service: EntityService, alertService: any, success: OnSuccessMethod) { + submit(service: EntityService, alertService: AlertService, success: OnSuccessMethod) { // response is what we get back from the server // when creating or updating let response: Observable; @@ -138,14 +145,14 @@ export abstract class EntityFormComponent implements AfterView response = service.create(data, this.optionsOnRequest('create')); } else { // Nothing has changed if the selected value, so we want to inform the user - alertService.error( `${this.entityName} was not changed`, 6000); + alertService.error(`${this.entityName} was not changed`, 6000); return; } // Handle the response response.subscribe({ next: (result: T) => { - alertService.success( `${this.entityName} saved`, 2000); + alertService.success(`${this.entityName} saved`, 2000); // Success is implemented on all inheriting instances and is used // to handle the response appropriately for the context of the form success(result, this.selected ? false : true); @@ -163,7 +170,7 @@ export abstract class EntityFormComponent implements AfterView if (this.selected) { this.restoreFromBackup(); } - alertService.error( `${this.entityName} save failed: ${error}`, 6000); + alertService.error(`${this.entityName} save failed: ${error}`, 6000); }, }); } else { @@ -173,13 +180,13 @@ export abstract class EntityFormComponent implements AfterView } } - protected delete(entity: T, entities: T[], service: EntityService): Observable { - return service.delete(entity, this.optionsOnRequest('delete')).pipe( - tap((obj) => { + protected delete(entity: T, entities: T[], service: EntityService): Observable { + return service.delete(entity, this.optionsOnRequest('delete')).pipe( + tap((_obj) => { this.cancelEdit(); entities.splice(entities.indexOf(entity), 1); this.dataSource.data = entities; - }) + }), ); } @@ -207,7 +214,7 @@ export abstract class EntityFormComponent implements AfterView * to the entity constructor when an object is created. This is then passed along * in the `create` call as the `other` value to the EntityService's create method. */ - protected optionsOnRequest(kind: 'create' | 'update' | 'delete'): RequestOptions { + protected optionsOnRequest(_kind: 'create' | 'update' | 'delete'): RequestOptions { return undefined; } diff --git a/src/app/common/f-chip/chip.component.html b/src/app/common/f-chip/chip.component.html new file mode 100644 index 0000000000..552fd1cdc9 --- /dev/null +++ b/src/app/common/f-chip/chip.component.html @@ -0,0 +1,5 @@ +
+ +
diff --git a/src/app/common/f-chip/f-chip.component.scss b/src/app/common/f-chip/chip.component.scss similarity index 100% rename from src/app/common/f-chip/f-chip.component.scss rename to src/app/common/f-chip/chip.component.scss diff --git a/src/app/common/f-chip/f-chip.component.spec.ts b/src/app/common/f-chip/chip.component.spec.ts similarity index 62% rename from src/app/common/f-chip/f-chip.component.spec.ts rename to src/app/common/f-chip/chip.component.spec.ts index f431488a66..23dfb6fbc5 100644 --- a/src/app/common/f-chip/f-chip.component.spec.ts +++ b/src/app/common/f-chip/chip.component.spec.ts @@ -1,6 +1,6 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { FChipComponent } from './f-chip.component'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {FChipComponent} from './chip.component'; describe('FChipComponent', () => { let component: FChipComponent; @@ -8,9 +8,8 @@ describe('FChipComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ FChipComponent ] - }) - .compileComponents(); + declarations: [FChipComponent], + }).compileComponents(); fixture = TestBed.createComponent(FChipComponent); component = fixture.componentInstance; diff --git a/src/app/common/f-chip/chip.component.ts b/src/app/common/f-chip/chip.component.ts new file mode 100644 index 0000000000..6aa6222a72 --- /dev/null +++ b/src/app/common/f-chip/chip.component.ts @@ -0,0 +1,10 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; + +@Component({ + selector: 'f-chip', + templateUrl: './chip.component.html', + styleUrls: ['./chip.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class FChipComponent {} diff --git a/src/app/common/f-chip/f-chip.component.html b/src/app/common/f-chip/f-chip.component.html deleted file mode 100644 index 10c24dbd5a..0000000000 --- a/src/app/common/f-chip/f-chip.component.html +++ /dev/null @@ -1,5 +0,0 @@ -
- -
diff --git a/src/app/common/f-chip/f-chip.component.ts b/src/app/common/f-chip/f-chip.component.ts deleted file mode 100644 index de2a1e148f..0000000000 --- a/src/app/common/f-chip/f-chip.component.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'f-chip', - templateUrl: './f-chip.component.html', - styleUrls: ['./f-chip.component.scss'], -}) -export class FChipComponent {} diff --git a/src/app/common/feedback-template-editor/feedback-template-editor.component.html b/src/app/common/feedback-template-editor/feedback-template-editor.component.html index 7981c9aeaf..c8fe0c13a1 100644 --- a/src/app/common/feedback-template-editor/feedback-template-editor.component.html +++ b/src/app/common/feedback-template-editor/feedback-template-editor.component.html @@ -1,118 +1,120 @@ -
-
-

Edit Feedback Templates for Outcome

+
+
+

Edit Feedback Templates for Outcome

-
+
- + - + - + - + - + - + - + - + - +
- + @if (feedbackTemplate.type === 'group') { folder - - + } + @if (feedbackTemplate.type === 'template') { description - + } ParentParent {{ getParentChipText(feedbackTemplate.parentChipId) }} Chip TextChip Text {{ feedbackTemplate.chipText }} DescriptionDescription {{ feedbackTemplate.description }} Comment TextComment Text {{ feedbackTemplate.commentText }} Summary TextSummary Text {{ feedbackTemplate.type === 'group' ? '' : feedbackTemplate.summaryText }} Task StatusTask Status @if (feedbackTemplate.taskStatus) { - + + {{ taskService.statusData(feedbackTemplate.taskStatus).materialIcon }} + {{ taskService.statusData(feedbackTemplate.taskStatus).label }} } @if (feedbackTemplateHasChanges(feedbackTemplate)) { }
-
+
- +
Edit Feedback Templates for Outcome } - @if (selectedTemplate) {
-

Edit Template

+

Edit Template

-
+
@if (selectedTemplate.isNew) { Type @@ -176,9 +178,11 @@

Edit Template

Parent - - {{ parent.chipText }} - + @for (parent of getPossibleParents(); track parent) { + + {{ parent.chipText }} + + } @@ -186,11 +190,11 @@

Edit Template

Chip Text
@@ -198,11 +202,11 @@

Edit Template

Description
@@ -212,23 +216,23 @@

Edit Template

Comment Text -
+
Summary Text @@ -236,12 +240,11 @@

Edit Template

Task Status - - {{ taskService.statusData(status).label }} - + @for (status of taskService.feedbackTemplateStatuses; track status) { + + {{ taskService.statusData(status).label }} + + }
@@ -250,8 +253,8 @@

Edit Template

-
-
+
+
@if (file?.name) { - upload_file + upload_file } - {{ file?.name || message }} + {{ file?.name || message }} {{ uploadProgress }}
@if (uploadProgress) { - - } @if (!uploadProgress) { - + + } + @if (!uploadProgress) { + }
diff --git a/src/app/common/file-drop/file-drop.component.scss b/src/app/common/file-drop/file-drop.component.scss index 2c3f68d5d2..bb6482adfe 100644 --- a/src/app/common/file-drop/file-drop.component.scss +++ b/src/app/common/file-drop/file-drop.component.scss @@ -1,6 +1,6 @@ // import color .f-inner-border { - box-shadow: inset 0 0 1px rgba(0,0,0,0.8); + box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.8); clip-path: inset(-1px -1px 0px 0px); } diff --git a/src/app/common/file-drop/file-drop.component.ts b/src/app/common/file-drop/file-drop.component.ts index 24a94a1020..610dc4cbe3 100644 --- a/src/app/common/file-drop/file-drop.component.ts +++ b/src/app/common/file-drop/file-drop.component.ts @@ -1,7 +1,7 @@ -import { HttpClient, HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http'; -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { Subscription, throwError } from 'rxjs'; -import { AlertService } from '../services/alert.service'; +import {HttpClient, HttpErrorResponse, HttpEventType, HttpResponse} from '@angular/common/http'; +import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core'; +import {Subscription, throwError} from 'rxjs'; +import {AlertService} from '../services/alert.service'; /** * Allow files to be dropped for upload @@ -10,9 +10,11 @@ import { AlertService } from '../services/alert.service'; selector: 'f-file-drop', templateUrl: 'file-drop.component.html', styleUrls: ['file-drop.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class FileDropComponent { - @Input({ required: true }) mode: 'endpoint' | 'event'; + @Input({required: true}) mode: 'endpoint' | 'event'; /** The name of the file(s) you are asking the user to upload */ @Input() desiredFileName: string; @@ -26,8 +28,8 @@ export class FileDropComponent { /** The URL of the endpoint to POST the file to if mode is endpoint*/ @Input() endpoint: string; @Input() body: object; - @Output() fileChange = new EventEmitter(); - @Output() uploadSuccess = new EventEmitter>(); + @Output() fileChange: EventEmitter = new EventEmitter(); + @Output() uploadSuccess: EventEmitter> = new EventEmitter(); protected uploadProgress: number; protected uploadSub: Subscription; @@ -38,7 +40,7 @@ export class FileDropComponent { /** * Report all files dropped if mode is event */ - @Output() filesDropped = new EventEmitter(); + @Output() filesDropped: EventEmitter = new EventEmitter(); constructor( private http: HttpClient, @@ -79,21 +81,23 @@ export class FileDropComponent { const formData = new FormData(); formData.append('file', this.file); - this.http.post(this.endpoint, formData, { reportProgress: true, observe: 'events' }).subscribe( - (data) => { - if (data.type == HttpEventType.UploadProgress) { - this.uploadProgress = Math.round(100 * (data.loaded / data.total)); - } - if (data.type == HttpEventType.Response) { - if (data.ok) { - this.alert.success(`File uploaded successfully`); - this.uploadSuccess.emit(data as HttpResponse); + this.http + .post(this.endpoint, formData, {reportProgress: true, observe: 'events'}) + .subscribe( + (data) => { + if (data.type == HttpEventType.UploadProgress) { + this.uploadProgress = Math.round(100 * (data.loaded / data.total)); } - } - }, - (error) => this.handleError(error), - () => this.reset(), - ); + if (data.type == HttpEventType.Response) { + if (data.ok) { + this.alert.success(`File uploaded successfully`); + this.uploadSuccess.emit(data as HttpResponse); + } + } + }, + (error) => this.handleError(error), + () => this.reset(), + ); } } else { this.filesDropped.emit([this.file]); diff --git a/src/app/common/file-uploader/file-uploader.coffee b/src/app/common/file-uploader/file-uploader.coffee deleted file mode 100644 index 89c0c17755..0000000000 --- a/src/app/common/file-uploader/file-uploader.coffee +++ /dev/null @@ -1,298 +0,0 @@ -angular.module('doubtfire.common.file-uploader', ["ngFileUpload"]) - -.directive 'fileUploader', -> - restrict: 'E' - replace: true - templateUrl: 'common/file-uploader/file-uploader.tpl.html' - scope: - # Files map a key (file name to be uploaded) to a value (containing a - # a display name, and the type of file that is to be accepted, where - # type is one of [document, csv, archive, code, image] - # E.g.: - # { file0: { name: 'Silly Name Code', type: 'code' }, - # fileX: { name: 'Silly name Shot', type: 'image' } ... } - files: '=' - # URL to where image is to be uploaded - url: '=' - # Optional HTTP method used to post data (defaults to POST) - method: '@' - # Other payload data to pass in the upload - # E.g.: - # { unit_id: 10, other: { key: data, with: [array, of, stuff] } ... } - payload: '=?' - # Optional function to notify just prior to upload, enables injection of payload for example - onBeforeUpload: '=?' - # Optional function to perform on success (with one response parameter) - onSuccess: '=?' - # Optional function to perform on failure (with one response parameter) - onFailure: '=?' - # Optional function to perform when the upload is successful and about - # to go back into its default state - onComplete: '=?' - # This value is bound to whether or not the uploader is currently uploading - isUploading: '=?' - # This value is bound to whether or not the uploader is ready to upload - isReady: '=?' - # Shows the names of files to be uploaded (defaults to true) - showName: '=?' - # Shows initially as button - asButton: '=?' - # Exposed files that are in the zone - filesSelected: '=?' - # Whether we have one or many drop zones (default is false) - singleDropZone: '=?' - # Whether or not we show the upload button or do we hide it allowing an - # external trigger to upload (default is true) - showUploadButton: '=?' - # Sets this scope variable to a function that can then be triggered externally - # from outside the scope - initiateUpload: '=?' - # What happens when we click cancel on failure - onClickFailureCancel: '=?' - # Whether we should reset after upload - resetAfterUpload: '=?' - controller: ($scope, $timeout, newUserService) -> - # - # Accepted upload types with associated data - # - ACCEPTED_TYPES = - document: - extensions: ['pdf', 'ps'] - icon: 'fa-file-pdf-o' - name: 'PDF' - csv: - extensions: ['csv','xls','xlsx'] - icon: 'fa-file-excel-o' - name: 'CSV' - code: - extensions: ['pas', 'cpp', 'c', 'cs', 'csv', 'h', 'hpp', 'java', 'py', 'js', 'html', 'coffee', 'rb', 'css', - 'scss', 'yaml', 'yml', 'xml', 'json', 'ts', 'r', 'rmd', 'rnw', 'rhtml', 'rpres', 'tex', - 'vb', 'sql', 'txt', 'md', 'jack', 'hack', 'asm', 'hdl', 'tst', 'out', 'cmp', 'vm', 'sh', 'bat', - 'dat', 'ipynb', 'pml', 'vue'] - icon: 'fa-file-code-o' - name: 'code' - image: - extensions: ['png', 'bmp', 'tiff', 'tif', 'jpeg', 'jpg', 'gif'] - name: 'image' - icon: 'fa-file-image-o' - zip: - extensions: ['zip', 'tar.gz', 'tar'] - name: 'archive' - icon: 'fa-file-zip-o' - - # - # Error handling; check if empty files - # - throw Error "No files provided to uploader" if $scope.files?.length is 0 - - # - # Whether or not clearEnqueuedFiles is enabled - # - $scope.clearEnqueuedUpload = (upload) -> - upload.model = null - refreshShownUploadZones() - - # - # Default showName - # - $scope.showName ?= true - - # - # Default singleDropZone - # - $scope.singleDropZone ?= false - - # - # Default asButton - # - $scope.asButton ?= false - - # - # Only initially show uploader if not presenting as button - # - $scope.showUploader = !$scope.asButton - - # - # Default show upload button - # - $scope.showUploadButton ?= true - - # - # Default resetAfterUpload to true - # - $scope.resetAfterUpload ?= true - - # - # When a file is dropped, if there has been rejected files - # warn the user that that file is not okay - # - checkForError = (upload) -> - if upload.rejects?.length > 0 - upload.display.error = yes - upload.rejects = null - $timeout (-> upload.display.error = no), 4000 - return true - false - - # Called when the model has changed - $scope.modelChanged = (newFiles, upload) -> - return unless newFiles.length > 0 || upload.rejects.length > 0 - gotError = checkForError(upload) - unless gotError - $scope.filesSelected = _.flatten(_.map($scope.uploadZones, 'model')) - if $scope.singleDropZone - $scope.selectedFiles = $scope.uploadZones - refreshShownUploadZones() - - # - # Will refresh which shown drop zones are shown - # Only changes if showing one drop zone - # - refreshShownUploadZones = -> - if $scope.singleDropZone - # Find the first-most empty model in each zone - firstEmptyZone = _.find($scope.uploadZones, (zone) -> !zone.model? || zone.model.length == 0) - if firstEmptyZone? - $scope.shownUploadZones = [firstEmptyZone] - else - $scope.shownUploadZones = [] - - # - # Whether or not drop is supported by this browser - assume - # true initially, but the drop zone will alter this - # - $scope.dropSupported = true - - # - # Data required for each upload zone - # - createUploadZones = (files) -> - zones = _.map(files, (uploadData, uploadName) -> - type = uploadData.type - typeData = ACCEPTED_TYPES[type] - # No typeData found? - unless typeData? - throw Error "Invalid type provided to File Uploader #{type}" - zone = - name: uploadName - model: null - accept: "'." + typeData.extensions.join(',.') + "'" - # Rejected files - rejects: null - display: - name: uploadData.name - # Font awesome supports PDF (from Document), - # CSV, Code and Image icons - icon: typeData.icon - type: typeData.name - # Whether or not a reject error is shown - error: false - zone - ) - # Remove all but the active drop zone - if $scope.singleDropZone - $scope.shownUploadZones = [_.first(zones)] - else - $scope.shownUploadZones = zones - $scope.uploadZones = zones - createUploadZones($scope.files) - - # - # Watch for changes in the files, and recreate the zones when - # they do change - # - $scope.$watch 'files', (files, oldFiles) -> - createUploadZones(files) - - # - # Checks if okay to upload (i.e., file models exist for each drop zone) - # - $scope.readyToUpload = -> - $scope.isReady = _.compact(_.flatten (upload.model for upload in $scope.uploadZones)).length is _.keys($scope.files).length - - # - # Resets the uploader and call it - # - $scope.resetUploader = -> - # No upload info and we're not uploading - $scope.uploadingInfo = null - $scope.isUploading = false - $scope.showUploader = !$scope.asButton - for upload in $scope.uploadZones - $scope.clearEnqueuedUpload(upload) - $scope.resetUploader() - - # - # Override on click failure cancel if not set to just reset uploader - # - $scope.onClickFailureCancel ?= $scope.resetUploader - - - # - # Initiates the upload - # - $scope.initiateUpload = -> - return unless $scope.readyToUpload() - $scope.onBeforeUpload?() - - xhr = new XMLHttpRequest() - form = new FormData() - # Append data - files = ({ name: zone.name; data: zone.model[0] } for zone in $scope.uploadZones) - form.append file.name, file.data for file in files - # Append payload - payload = ({ key: k; value: v } for k, v of $scope.payload) - for payloadItem in payload - payloadItem.value = JSON.stringify(payloadItem.value) if _.isObject payloadItem.value - form.append payloadItem.key, payloadItem.value - # Set the percent - $scope.uploadingInfo = - progress: 5 - success: null - error: null - complete: false - $scope.isUploading = true - # Callbacks - xhr.onreadystatechange = -> - if xhr.readyState is 4 - $timeout (-> - # Upload is now complete - $scope.uploadingInfo.complete = true - response = null - try - response = JSON.parse xhr.responseText - catch e - if xhr.status is 0 - response = { error: 'Could not connect to the OnTrack server' } - else - response = xhr.responseText - # Success (20x success range) - if xhr.status >= 200 and xhr.status < 300 - $scope.onSuccess?(response) - $scope.uploadingInfo.success = true - $timeout((-> - $scope.onComplete?() - if $scope.resetAfterUpload - $scope.resetUploader() - ), 2500) - # Fail - else - $scope.onFailure?(response) - $scope.uploadingInfo.success = false - $scope.uploadingInfo.error = response.error or "Unknown error" - $scope.$apply() - ), 2000 - xhr.upload.onprogress = (event) -> - $scope.uploadingInfo.progress = parseInt(100.0 * event.position / event.totalSize) - $scope.$apply() - # Default the method to POST if it was not defined - $scope.method = 'POST' unless $scope.method? - - # Send it - xhr.open $scope.method, $scope.url, true - - # Add auth details - xhr.setRequestHeader('Auth-Token', newUserService.currentUser.authenticationToken) - xhr.setRequestHeader('Username', newUserService.currentUser.username) - - xhr.send form diff --git a/src/app/common/file-uploader/file-uploader.component.html b/src/app/common/file-uploader/file-uploader.component.html new file mode 100644 index 0000000000..f1f93c5526 --- /dev/null +++ b/src/app/common/file-uploader/file-uploader.component.html @@ -0,0 +1,168 @@ + + + @if (!showUploader) { + + } + +
+ @if (showUploader && uploadingInfo === null && shownUploadZones.length) { +
+ @for (upload of shownUploadZones; track upload) { + @if (!singleDropZone && showName) { +
+ {{ uploadZones.length === 1 ? '' : $index + 1 + ' - ' }} + {{ upload.display.name }} +
+ } + + @if (singleDropZone && showName) { +
Select {{ upload.display.name }}
+ } + +
+ + + +
+ + @if (!singleDropZone && upload.model?.length > 0) { +
+ {{ upload.display.icon }} + {{ upload.model[0].name }} + +
+ } + } +
+ } + + @if (showUploader && singleDropZone && uploadingInfo === null) { +
+
Upload Summary
+ + @for (upload of uploadZones; track upload) { +
+
+ {{ upload.display.icon }} + {{ upload.display.name }} +
+ + @if (upload.model?.length > 0) { + {{ upload.model[0].name }} + + } @else { + File Pending + } +
+ } +
+ } +
+ + @if (showUploader && !isUploading) { +
+ @if (showUploadButton && readyToUpload() && uploadingInfo === null) { + + } + + @if (asButton) { + + } +
+ } + + @if (showUploader && readyToUpload() && isUploading) { + @if (!uploadingInfo?.complete) { +
+
+ @for (upload of uploadZones; track upload) { + {{ upload.display.icon }} + } + arrow_right_alt + +
+ + + +
+ } + + @if (uploadingInfo?.complete) { +
+
+ + {{ uploadingInfo.success ? 'check_circle' : 'cancel' }} + + + + File Upload {{ uploadingInfo.success ? 'Successful' : 'Failed' }} + +
+ + @if (!uploadingInfo.success) { +
+
+

Error Message: {{ uploadingInfo.error }}

+ +
+ + +
+
+ } +
+ } + } +
+
diff --git a/src/app/common/file-uploader/file-uploader.component.scss b/src/app/common/file-uploader/file-uploader.component.scss new file mode 100644 index 0000000000..db00b824aa --- /dev/null +++ b/src/app/common/file-uploader/file-uploader.component.scss @@ -0,0 +1,37 @@ +.file-drop-zone .mat-icon, +.complete .mat-icon { + font-size: 50px; + height: 50px; + width: 50px; +} + +.uploading .mat-icon { + font-size: 75px; + height: 75px; + width: 75px; +} + +.file-drop-zone { + align-items: center; + background: transparent; + border: 2px dashed rgba(0, 0, 0, 0.24); + border-radius: 8px; + color: inherit; + cursor: pointer; + display: flex; + flex-direction: column; + font: inherit; + justify-content: center; + padding: 2.5rem; + text-align: center; +} + +.file-drop-zone:hover, +.file-drop-zone:focus-visible { + border-color: currentColor; + outline: none; +} + +.file-input { + display: none; +} diff --git a/src/app/common/file-uploader/file-uploader.component.ts b/src/app/common/file-uploader/file-uploader.component.ts new file mode 100644 index 0000000000..a39be59f3b --- /dev/null +++ b/src/app/common/file-uploader/file-uploader.component.ts @@ -0,0 +1,364 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, +} from '@angular/core'; +import {UserService} from 'src/app/api/services/user.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; + +export interface FileData { + name: string; + type: string; +} + +export type FileUploadSpec = FileData[] | Record; + +interface UploadDisplay { + name: string; + icon: string; + type: string; + error: boolean; +} +interface UploadZone { + name: string; + model: File[]; + accept: string; + accepts: string[]; + rejects: string[]; + display: UploadDisplay; +} + +interface UploadingInfo { + progress: number; + success: boolean; + error: string; + complete: boolean; +} + +export const ACCEPTED_TYPES = { + document: { + extensions: ['pdf', 'ps'], + // icon: 'picture_as_pdf', + icon: 'article_outlined', + name: 'PDF', + }, + csv: { + extensions: ['csv', 'xls', 'xlsx'], + icon: 'insert_chart_outlined', + name: 'CSV', + }, + code: { + // prettier-ignore + extensions: [ + 'pas', 'cpp', 'c', 'cs', 'csv', 'h', 'hpp', 'java', 'py', 'js', 'html', 'coffee', 'rb', 'css', + 'scss', 'yaml', 'yml', 'xml', 'json', 'ts', 'r', 'rmd', 'rnw', 'rhtml', 'rpres', 'tex', + 'vb', 'sql', 'txt', 'md', 'jack', 'hack', 'asm', 'hdl', 'tst', 'out', 'cmp', 'vm', 'sh', 'bat', + 'dat', 'ipynb', 'pml', 'vue' + ], + // icon: 'code', + // icon: 'code', + icon: 'integration_instructions_outlined', + name: 'code', + }, + image: { + extensions: ['png', 'bmp', 'tiff', 'tif', 'jpeg', 'jpg', 'gif'], + // icon: 'image', + icon: 'image_outlined', + name: 'image', + }, + zip: { + extensions: ['zip', 'tar.gz', 'tgz', 'tar'], + icon: 'folder_zip', + name: 'zip', + }, +} as const; + +@Component({ + selector: 'f-file-uploader', + templateUrl: './file-uploader.component.html', + styleUrls: ['./file-uploader.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class FileUploaderComponent implements OnInit, OnChanges { + @Input() files: FileUploadSpec; + @Input() url: string; + @Input() method = 'POST'; + @Input() payload?: unknown; + + @Input() onBeforeUpload?: () => void; + @Input() onSuccess?: (response) => void; + @Input() onFailure?: (response) => void; + @Input() onComplete?: () => void; + @Input() onClickFailureCancel?: () => void; + + @Input() isUploading: boolean; + @Input() isReady: boolean; + @Input() showName: boolean = true; + @Input() asButton: boolean = false; + @Input() singleDropZone: boolean = false; + @Input() showUploadButton: boolean = true; + @Input() resetAfterUpload: boolean = true; + + @Input() initiateUpload?: () => void; + + // HACK: workaround for TypeScript -> Coffeescript communication + // Once all parent components such as upload-submission-modal are migrated.. + // .. these *wont* be necessary anymore + // Parent components should declare the file-uploader using @ViewChild() and directly call initiateUpload() + @Output() isReadyChange: EventEmitter = new EventEmitter(); + @Output() uploadReady: EventEmitter<() => void> = new EventEmitter(); + + public readonly ACCEPTED_TYPES = ACCEPTED_TYPES; + + public showUploader: boolean = false; + public uploadingInfo: UploadingInfo = null; + + public shownUploadZones: UploadZone[] = []; + public uploadZones: UploadZone[] = []; + public dropSupported: boolean = true; + + constructor( + private userService: UserService, + private constants: DoubtfireConstants, + ) {} + + private externalName: string = 'OnTrack'; + + ngOnInit(): void { + this.showUploader = !this.asButton; + this.createUploadZones(this.files); + + this.uploadReady.emit(this.initiateUploadInternal.bind(this)); + + if (!this.onClickFailureCancel) { + this.onClickFailureCancel = this.resetUploader; + } + + this.resetUploader(); + + this.constants.ExternalName.subscribe((name) => { + this.externalName = name; + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['files']) { + this.createUploadZones(changes.files.currentValue); + this.updateReadyState(this.readyToUpload()); + } + } + + public backToUpload() { + this.isUploading = false; + this.uploadingInfo = null; + } + + public onDragOver(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + } + + public onDragLeave(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + } + + public onFileDropped(event: DragEvent, upload: UploadZone) { + event.preventDefault(); + event.stopPropagation(); + + const file = event.dataTransfer?.files?.[0]; + if (file) { + this.setUploadFile(upload, file); + } + } + + public onFileSelected(event: Event, upload: UploadZone) { + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + if (file) { + this.setUploadFile(upload, file); + } + input.value = ''; + } + + private setUploadFile(upload: UploadZone, file: File) { + upload.model = [file]; + this.validateFiles(); + } + + validateFiles() { + for (const upload of this.shownUploadZones) { + if (upload.model?.length) { + const name: string = upload.model[0].name.toLowerCase(); + const accepts: string[] = upload.accepts.map((ext: string) => ext.toLowerCase()); + const valid = accepts.some((ext) => name.endsWith(ext)); + if (!valid) { + upload.model = null; + upload.display.error = true; + setTimeout(() => { + upload.display.error = null; + }, 5000); + } + } + } + this.refreshShownUploadZones(); + this.updateReadyState(this.readyToUpload()); + } + + clearEnqueuedUpload(upload: UploadZone) { + upload.model = null; + this.refreshShownUploadZones(); + this.updateReadyState(this.readyToUpload()); + } + + readyToUpload(): boolean { + return this.uploadZones.every((zone) => zone.model?.length); + } + + updateReadyState(ready: boolean) { + this.isReady = ready; + this.isReadyChange.emit(ready); + } + + resetUploader() { + this.uploadingInfo = null; + this.isUploading = false; + this.showUploader = !this.asButton; + for (const upload of this.uploadZones) { + upload.model = null; + } + this.refreshShownUploadZones(); + this.updateReadyState(this.readyToUpload()); + } + + initiateUploadInternal() { + if (!this.readyToUpload()) { + return; + } + if (this.onBeforeUpload) { + this.onBeforeUpload(); + } + + this.uploadingInfo = { + progress: 5, + success: null, + error: null, + complete: false, + }; + + this.isUploading = true; + + const xhr = new XMLHttpRequest(); + const form = new FormData(); + + // Append files + for (const zone of this.uploadZones) { + if (zone.model?.length) { + form.append(zone.name, zone.model[0]); + } + } + + // Append payload + if (this.payload) { + for (const [key, value] of Object.entries(this.payload)) { + let v = value; + if (typeof v === 'object') { + v = JSON.stringify(v); + } + form.append(key, v); + } + } + + xhr.upload.onprogress = (event) => { + if (event.total) { + this.uploadingInfo.progress = Math.floor((event.loaded / event.total) * 100); + } + }; + + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + setTimeout(() => { + this.uploadingInfo.complete = true; + let response; + try { + response = JSON.parse(xhr.responseText); + } catch (e) { + console.error(e); + if (xhr.status === 0) { + response = {error: `Could not connect to ${this.externalName} the server`}; + } else { + response = xhr.responseText; + } + } + + if (xhr.status >= 200 && xhr.status < 300) { + this.onSuccess?.(response); + this.uploadingInfo.success = true; + setTimeout(() => { + this.onComplete?.(); + if (this.resetAfterUpload) { + this.resetUploader(); + } + }, 2500); + } else { + this.onFailure?.(response); + this.uploadingInfo.success = false; + this.uploadingInfo.error = (response?.error ?? 'Unknown error') as string; + } + }, 2000); + } + }; + const method = this.method ?? 'POST'; + xhr.open(method, this.url, true); + + xhr.setRequestHeader('Auth-Token', this.userService.currentUser.authenticationToken); + xhr.setRequestHeader('Username', this.userService.currentUser.username); + + xhr.send(form); + } + + // onClickFailureCancelInternal() { + // console.log('onClickFailureCancelInternal'); + // } + + refreshShownUploadZones = () => { + if (this.singleDropZone) { + const firstEmpty = this.uploadZones.find((z) => !z.model || z.model.length === 0); + this.shownUploadZones = firstEmpty ? [firstEmpty] : []; + } + }; + + createUploadZones(files: FileUploadSpec) { + const zones = Object.entries(files).map(([uploadName, uploadData]) => { + const uploadType = uploadData.type === 'archive' ? 'zip' : uploadData.type; + const typeData = ACCEPTED_TYPES[uploadType]; + if (!typeData) { + throw new Error(`Invalid type provided to File Uploader ${uploadData.type}`); + } + + return { + name: uploadName, + model: null, + accept: `.${typeData.extensions.join(',.')}`, + accepts: typeData.extensions, + rejects: null, + display: { + name: uploadData.name, + icon: typeData.icon, + type: typeData.name, + error: false, + }, + }; + }); + + this.shownUploadZones = this.singleDropZone ? [zones[0]] : zones; + this.uploadZones = zones; + } +} diff --git a/src/app/common/file-uploader/file-uploader.scss b/src/app/common/file-uploader/file-uploader.scss deleted file mode 100644 index 4ca5cb6988..0000000000 --- a/src/app/common/file-uploader/file-uploader.scss +++ /dev/null @@ -1,139 +0,0 @@ -.file-uploader { - display: block; - // Add some margin like a

- margin: 2.5em 0; - - // Colors to make it easy to understand - $hover-color: $brand-primary; - $accept-color: $brand-success; - $reject-color: $brand-danger; - - // Extra additional icons - $ban-icon: $fa-var-ban; - $download-icon: $fa-var-download; - - .upload-commit-actions { - margin-top: 1em; - .btn-upload { - margin-right: 1.5ex; - } - } - - .well.drop { - border: 2px #bbb dotted; - font-size: larger; - font-weight: bold; - color: #aaa; - text-align: center; - &, i { - @include transition(all 0.25s ease); - } - p small { - display: block; - } - &:hover { - cursor: pointer; - border-color: $hover-color; - color: $hover-color; - p small { - text-decoration: underline; - } - } - } - - // Wells which have file over - .well.drop.file-over { - cursor: copy; - border-color: $accept-color; - color: $accept-color; - // Switch the icon over - p.fa::before { - content: $download-icon; - } - } - // Rejected file over - .well.drop.file-rejected { - border-color: $reject-color; - color: $reject-color; - // Switch the icon over - p.fa::before { - content: $ban-icon; - } - } - - // File header - .selected-files { - &:not(.list-group) { - display: inline-block; - } - .selected-file { - display: block; - font-size: 1.2em; - i.file-type { - margin-right: 1ex; - font-size: 1.2em; - } - &.highlight { - animation: highlight-selected-file-animation; - animation-duration: 0.75s; - @keyframes highlight-selected-file-animation { - 0% { background: rgba(33, 150, 243, 0.4); } - 0% { box-shadow: 0 0 6px rgba(33, 150, 243, 1); } - 100% { box-shadow: 0 0 0px rgba(255, 255, 255, 0); } - } - } - } - } - a.clear-upload { - margin-left: 1ex; - &:hover i { - font-size: 1.15em; - color: $reject-color; - } - display: inline-block; - } - // Upload area/result - .upload-area { - .progress-area { - .progress { - margin-bottom: 0; - } - .icons { - width: 100%; - display: flex; - justify-content: center; - } - i.fa-arrow-right { - @include animation-wobble(); - } - i { - flex-basis: auto !important; - margin-right: 1ex; - font-size: 2em; - margin-bottom: 0.5em; - } - } - .result-area { - .result-text { - margin-bottom: 0; - display: flex; - justify-content: center; - align-items: center; - min-height: 34px; - } - i { - font-size: 2em; - @include animation-grow; - margin-right: 1ex; - } - .retry-options { - font-weight: bolder; - font-size: 1.2em; - a:first-child { - display: inline-block; - margin-right: 2ex; - } - } - } - } -} diff --git a/src/app/common/file-uploader/file-uploader.tpl.html b/src/app/common/file-uploader/file-uploader.tpl.html deleted file mode 100644 index 368f53380c..0000000000 --- a/src/app/common/file-uploader/file-uploader.tpl.html +++ /dev/null @@ -1,104 +0,0 @@ -

-
- -
-
-
-
- {{uploadZones.length == 1 ? '' : $index + 1 + ' -'}} {{upload.display.name}} -
-
- Select {{upload.display.name}} -
-
-

-

- Invalid file provided - Accepted files: {{upload.accept.split(',').join(', ')}} -

-

- Drop {{upload.display.type}} file here - or click to select one -

-

- Click to select {{upload.display.type}} file -

-
-
- - - {{upload.model[0].name}} - - - - -
-
-
-
Upload Summary
-
-
-
- - {{upload.display.name}} -
-
- {{upload.model[0].name}} - File Pending - - - -
-
-
-
-
-
- - -
-
-
-
- - - -
- -
-
-

- - File Upload {{uploadingInfo.success === true ? 'Successful' : 'Failed'}} -

-
-
-

- Error Message: - {{uploadingInfo.error}} -

-

- Retry Upload - Cancel -

-
-
-
-
diff --git a/src/app/common/file-viewer/file-viewer.component.html b/src/app/common/file-viewer/file-viewer.component.html index 476c76b464..88ad9162d4 100644 --- a/src/app/common/file-viewer/file-viewer.component.html +++ b/src/app/common/file-viewer/file-viewer.component.html @@ -1,23 +1,27 @@
- @if (blobUrl && fileType === 'pdf') { @if (!loaded) { - + @if (blobUrl && fileType === 'pdf') { + @if (!loaded) { + + } + } - - - } @if (fileType === 'html') { -
- -
+ @if (fileType === 'html') { +
+ +
}
diff --git a/src/app/common/file-viewer/file-viewer.component.spec.ts b/src/app/common/file-viewer/file-viewer.component.spec.ts index bfec618b8c..21eeaf7660 100644 --- a/src/app/common/file-viewer/file-viewer.component.spec.ts +++ b/src/app/common/file-viewer/file-viewer.component.spec.ts @@ -1,6 +1,6 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { FileViewerComponent } from './file-viewer.component'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {FileViewerComponent} from './file-viewer.component'; describe('FileViewerComponent', () => { let component: FileViewerComponent; @@ -8,9 +8,8 @@ describe('FileViewerComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ FileViewerComponent ] - }) - .compileComponents(); + declarations: [FileViewerComponent], + }).compileComponents(); fixture = TestBed.createComponent(FileViewerComponent); component = fixture.componentInstance; diff --git a/src/app/common/file-viewer/file-viewer.component.ts b/src/app/common/file-viewer/file-viewer.component.ts index 2c5bd01c73..c034f471d3 100644 --- a/src/app/common/file-viewer/file-viewer.component.ts +++ b/src/app/common/file-viewer/file-viewer.component.ts @@ -1,8 +1,15 @@ -import { Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; -import { FileDownloaderService } from '../file-downloader/file-downloader.service'; -import { HttpResponse } from '@angular/common/http'; -import { PDFProgressData } from 'ng2-pdf-viewer'; -import { AlertService } from '../services/alert.service'; +import {PDFProgressData} from 'ng2-pdf-viewer'; +import {HttpResponse} from '@angular/common/http'; +import { + ChangeDetectionStrategy, + Component, + Input, + OnChanges, + OnDestroy, + SimpleChanges, +} from '@angular/core'; +import {FileDownloaderService} from '../file-downloader/file-downloader.service'; +import {AlertService} from '../services/alert.service'; /** * The file viewer downloads a file from a URL and displays it's contents. @@ -11,6 +18,8 @@ import { AlertService } from '../services/alert.service'; selector: 'f-file-viewer', templateUrl: './file-viewer.component.html', styleUrls: ['./file-viewer.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class FileViewerComponent implements OnDestroy, OnChanges { /** @@ -50,7 +59,10 @@ export class FileViewerComponent implements OnDestroy, OnChanges { * @param fileDownloader is used to download the resources from the api * @param alerts is used to render alerts */ - constructor(private fileDownloader: FileDownloaderService, private alertService: AlertService) {} + constructor( + private fileDownloader: FileDownloaderService, + private alertService: AlertService, + ) {} /** * When destroyed, the component must free its resources. @@ -98,12 +110,12 @@ export class FileViewerComponent implements OnDestroy, OnChanges { private downloadBlob(downloadUrl: string): void { this.fileDownloader.downloadBlob( downloadUrl, - (url: string, response: HttpResponse) => { + (url: string, _response: HttpResponse) => { this.blobUrl = url; }, - (error: any) => { + (error: Error) => { this.alertService.error(`Error downloading resource. ${error}`); - } + }, ); } diff --git a/src/app/common/filters/filters.coffee b/src/app/common/filters/filters.coffee index e2a2d51ec8..3a9711cd0b 100644 --- a/src/app/common/filters/filters.coffee +++ b/src/app/common/filters/filters.coffee @@ -1,3 +1,6 @@ +# Component is no longer used and is unlinked from the app +# This file is left for reference, as these are migrated into Pipes + angular.module("doubtfire.common.filters", []) # diff --git a/src/app/common/filters/filters.pipe.ts b/src/app/common/filters/filters.pipe.ts index db4dad7478..1a130df9f1 100644 --- a/src/app/common/filters/filters.pipe.ts +++ b/src/app/common/filters/filters.pipe.ts @@ -1,10 +1,11 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import {Pipe, PipeTransform} from '@angular/core'; @Pipe({ name: 'filters', + standalone: false, }) export class FiltersPipe implements PipeTransform { - transform(value: unknown, ...args: unknown[]): unknown { + transform(_value: unknown, ..._args: unknown[]): unknown { return null; } } diff --git a/src/app/common/filters/order-by.pipe.ts b/src/app/common/filters/order-by.pipe.ts new file mode 100644 index 0000000000..b6b0b3c9d5 --- /dev/null +++ b/src/app/common/filters/order-by.pipe.ts @@ -0,0 +1,36 @@ +import {Pipe, PipeTransform} from '@angular/core'; + +@Pipe({ + name: 'orderBy', + standalone: false, +}) +export class OrderByPipe implements PipeTransform { + transform(array: readonly T[], field: string | string[], reverse: boolean = false): T[] { + if (!array || !field) { + return []; + } + + const fields = Array.isArray(field) ? field : [field]; + const valueFor = (item: T, path: string): unknown => + path.split('.').reduce((value, key) => value?.[key], item); + + const sortedArray = [...array].sort((a, b) => { + const aValue = valueFor(a, fields[0]); + const bValue = valueFor(b, fields[0]); + + if (aValue < bValue) { + return -1; + } + if (aValue > bValue) { + return 1; + } + return 0; + }); + + if (reverse) { + return sortedArray.reverse(); + } + + return sortedArray; + } +} diff --git a/src/app/common/filters/task-definition-name.pipe.ts b/src/app/common/filters/task-definition-name.pipe.ts index 155595d2ff..aed647c088 100644 --- a/src/app/common/filters/task-definition-name.pipe.ts +++ b/src/app/common/filters/task-definition-name.pipe.ts @@ -1,19 +1,19 @@ - -import { Pipe, PipeTransform } from '@angular/core'; -import { Task, TaskDefinition } from '../../api/models/doubtfire-model'; +import {Pipe, PipeTransform} from '@angular/core'; +import {TaskDefinition} from '../../api/models/doubtfire-model'; @Pipe({ name: 'taskDefinitionName', + standalone: false, }) export class TaskDefinitionNamePipe implements PipeTransform { - transform(taskDefinitions: TaskDefinition[], searchName: string): TaskDefinition[] { + transform(taskDefinitions: readonly TaskDefinition[], searchName: string): TaskDefinition[] { searchName = searchName.toLowerCase(); - return taskDefinitions.filter( // use lodash filter? - (td) => { - return td.name.toLowerCase().includes(searchName) || - td.abbreviation.toLowerCase().includes(searchName) || - td.targetGradeText.toLowerCase().includes(searchName) - } - ) + return taskDefinitions.filter((td) => { + return ( + td?.name.toLowerCase().includes(searchName) || + td?.abbreviation.toLowerCase().includes(searchName) || + td?.targetGradeText.toLowerCase().includes(searchName) + ); + }); } } diff --git a/src/app/common/filters/tasks-by-tutor.pipe.ts b/src/app/common/filters/tasks-by-tutor.pipe.ts index d99da5564b..24c9ef768f 100644 --- a/src/app/common/filters/tasks-by-tutor.pipe.ts +++ b/src/app/common/filters/tasks-by-tutor.pipe.ts @@ -3,15 +3,22 @@ import {Task, UnitRole} from '../../api/models/doubtfire-model'; @Pipe({ name: 'tasksByTutor', + standalone: false, }) export class TasksByTutorPipe implements PipeTransform { transform(currentUnitRole: UnitRole, tasks: Task[], unitRoleId?: number | string): Task[] { - if (!tasks) return tasks; + if (!tasks) { + return tasks; + } - if (!unitRoleId || unitRoleId === 'all') return tasks; + if (!unitRoleId || unitRoleId === 'all') { + return tasks; + } if (unitRoleId === 'mentoring_all') { - if (!currentUnitRole) return []; + if (!currentUnitRole) { + return []; + } return tasks.filter((task) => task.tutor?.mentorId === currentUnitRole.id); } diff --git a/src/app/common/filters/tasks-for-group-set.pipe.ts b/src/app/common/filters/tasks-for-group-set.pipe.ts new file mode 100644 index 0000000000..62842ebdf6 --- /dev/null +++ b/src/app/common/filters/tasks-for-group-set.pipe.ts @@ -0,0 +1,18 @@ +import {Pipe, PipeTransform} from '@angular/core'; +import {GroupSet, Task} from 'src/app/api/models/doubtfire-model'; + +@Pipe({ + name: 'tasksForGroupset', + standalone: false, +}) +export class TasksForGroupsetPipe implements PipeTransform { + transform(tasks: readonly Task[], groupSet: GroupSet): Task[] { + if (!tasks) { + return []; + } + + return tasks.filter((task) => { + return task.definition.groupSet === groupSet || (!task.definition.groupSet && !groupSet); + }); + } +} diff --git a/src/app/common/filters/tasks-for-inbox-search.pipe.ts b/src/app/common/filters/tasks-for-inbox-search.pipe.ts index 3b507ba43b..dbea1db779 100644 --- a/src/app/common/filters/tasks-for-inbox-search.pipe.ts +++ b/src/app/common/filters/tasks-for-inbox-search.pipe.ts @@ -1,8 +1,9 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import { Task } from 'src/app/api/models/task'; +import {Pipe, PipeTransform} from '@angular/core'; +import {Task} from 'src/app/api/models/task'; @Pipe({ name: 'tasksWithStudentName', + standalone: false, }) export class TasksForInboxSearchPipe implements PipeTransform { transform(tasks: Task[], searchText: string): Task[] { @@ -19,8 +20,8 @@ export class TasksForInboxSearchPipe implements PipeTransform { searchTerms .map((term: string) => task.matches(term)) .reduce((prev: boolean, current: boolean, currentIndex: number) => - operators[currentIndex - 1] === '&' ? prev && current : prev || current - ) + operators[currentIndex - 1] === '&' ? prev && current : prev || current, + ), ); } } diff --git a/src/app/common/filters/tasks-in-tutorials.pipe.ts b/src/app/common/filters/tasks-in-tutorials.pipe.ts index 8800feb52f..918a8da811 100644 --- a/src/app/common/filters/tasks-in-tutorials.pipe.ts +++ b/src/app/common/filters/tasks-in-tutorials.pipe.ts @@ -1,8 +1,9 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import { Task } from '../../api/models/doubtfire-model'; +import {Pipe, PipeTransform} from '@angular/core'; +import {Task} from '../../api/models/doubtfire-model'; @Pipe({ name: 'tasksInTutorials', + standalone: false, }) export class TasksInTutorialsPipe implements PipeTransform { transform(tasks: Task[], tutorialIds: number[], forceStream: boolean): Task[] { @@ -18,6 +19,10 @@ export class TasksInTutorialsPipe implements PipeTransform { // Filter the tasks to only those where the tutorial for the task is in the list of tutorial ids const result = tasks?.filter((task) => { + if (task.tutorialId) { + return tutorialIds.includes(task.tutorialId); + } + // Get the stream for the task... this may be nil or undefined if there are no streams in the unit const stream = task.definition.tutorialStream; diff --git a/src/app/common/filters/tasks-of-task-definition.pipe.ts b/src/app/common/filters/tasks-of-task-definition.pipe.ts index f909e6f35b..019e5cddd8 100644 --- a/src/app/common/filters/tasks-of-task-definition.pipe.ts +++ b/src/app/common/filters/tasks-of-task-definition.pipe.ts @@ -1,8 +1,9 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import { Task, TaskDefinition } from 'src/app/api/models/doubtfire-model'; +import {Pipe, PipeTransform} from '@angular/core'; +import {Task, TaskDefinition} from 'src/app/api/models/doubtfire-model'; @Pipe({ name: 'tasksOfTaskDefinition', + standalone: false, }) export class TasksOfTaskDefinitionPipe implements PipeTransform { transform(tasks: Task[], taskDefinition: TaskDefinition): Task[] { diff --git a/src/app/common/footer/footer.component.html b/src/app/common/footer/footer.component.html index 50233659f3..dac770abcb 100644 --- a/src/app/common/footer/footer.component.html +++ b/src/app/common/footer/footer.component.html @@ -1,6 +1,6 @@ - - @if (selectedTask?.similaritiesDetected) { -
+ + - -
+ + @if (selectedTask?.definition?.assessInPortfolioOnly) { - -
- - - @if (selectedTask && selectedTask.suggestedTaskStatus) { +
- } - + @if (selectedTask && selectedTask.suggestedTaskStatus) { + + } - @if (selectedTask?.definition?.assessInPortfolioOnly) { - } @else { +
- @if (selectedTask?.similaritiesDetected) { + @if (selectedTask?.project) { - @if (selectedTask?.definition.discussionPromptsCount) { + + - @if (canAccessTutorNotes) { + -
+ } - - + --> @if (!selectedTask?.hasPdf && selectedTask?.status === 'ready_for_feedback') { } } @if (currentUnit && currentUnitRole && currentUnitRole.tutorNoteCount > 0) { - } - @if (currentUnit) { + @if (currentUnit && !isTutorDiscussionRoute) { @@ -61,10 +60,10 @@ @if (currentUser.role === 'Admin' || currentUser.role === 'Convenor') { } - @if (currentUser.role === 'Admin') { - @@ -93,20 +92,20 @@ - diff --git a/src/app/common/header/header.component.scss b/src/app/common/header/header.component.scss index 69b0af02fe..031b4351d5 100644 --- a/src/app/common/header/header.component.scss +++ b/src/app/common/header/header.component.scss @@ -5,9 +5,9 @@ margin-bottom: 20px; &.inbox { - background-color: #F5F5F5; + background-color: #f5f5f5; box-shadow: none; - margin-bottom: 0px // nice drop shadow on bottom of toolbar + margin-bottom: 0px; // nice drop shadow on bottom of toolbar } font-family: 'Grotesk'; @@ -16,5 +16,4 @@ color: black; transform: scale(1.5); } - } diff --git a/src/app/common/header/header.component.spec.ts b/src/app/common/header/header.component.spec.ts index bc07fb1f57..b0ee436584 100644 --- a/src/app/common/header/header.component.spec.ts +++ b/src/app/common/header/header.component.spec.ts @@ -1,60 +1,56 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { MatMenuModule } from '@angular/material/menu'; -import { BehaviorSubject, Subject } from 'rxjs'; -import { Project, Unit, UnitRole } from 'src/app/api/models/doubtfire-model'; -import { GlobalStateService, ViewType } from 'src/app/projects/states/index/global-state.service'; -import { CheckForUpdateService } from 'src/app/sessions/service-worker-updater/check-for-update.service'; -import { IsActiveUnitRole } from '../pipes/is-active-unit-role.pipe'; - -import { HeaderComponent } from './header.component'; +import {MediaObserver} from 'ng-flex-layout'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Router} from '@angular/router'; +import {AuthenticationService} from 'src/app/api/models/doubtfire-model'; +import {SidekiqJobService} from 'src/app/api/services/sidekiq-job.service'; +import {UserService} from 'src/app/api/services/user.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {GlobalStateService} from 'src/app/projects/states/index/global-state.service'; +import {CheckForUpdateService} from 'src/app/sessions/service-worker-updater/check-for-update.service'; +import {AboutDoubtfireModal} from '../modals/about-doubtfire-modal/about-doubtfire-modal.component'; +import {CalendarModalService} from '../modals/calendar-modal/calendar-modal.service'; +import {QrModalService} from '../modals/qr-modal/qr-modal.service'; +import {SidekiqJobsModalService} from '../modals/sidekiq-jobs-modal/sidekiq-jobs-modal.service'; +import {TutorNotesModalService} from '../modals/tutor-notes-modal/tutor-notes-modal.service'; +import {IsActiveUnitRole} from '../pipes/is-active-unit-role.pipe'; +import {HeaderComponent} from './header.component'; + +const emptyProvider = {}; describe('HeaderComponent', () => { let component: HeaderComponent; let fixture: ComponentFixture; - // let currentUserStub: jasmine.SpyObj; - // let calendarModalStub: jasmine.SpyObj; - // let aboutDoubtfireModalStub: jasmine.SpyObj; - let isActiveUnitRoleStub: Partial; - let checkForUpdateServiceStub: Partial; - let globalStateServiceStub: Partial; - - beforeEach(waitForAsync(() => { - const showHideHeader = new Subject(); - const unitRolesSubject = new BehaviorSubject(null); - const projectsSubject = new BehaviorSubject(null); - const currentViewAndEntitySubject$ = new BehaviorSubject<{ viewType: ViewType; entity: Unit | Project | UnitRole }>( - null - ); - - // currentUserStub = { - // role: 'tutor', - // }; - globalStateServiceStub = { - showHideHeader: showHideHeader, - unitRolesSubject: unitRolesSubject, - projectsSubject: projectsSubject, - currentViewAndEntitySubject$: currentViewAndEntitySubject$, - }; - - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ declarations: [HeaderComponent], - imports: [MatMenuModule], providers: [ - // { provide: currentUser, useValue: currentUserStub }, - // { provide: calendarModal, useValue: calendarModalStub }, - // { provide: aboutDoubtfireModal, useValue: aboutDoubtfireModalStub }, - { provide: IsActiveUnitRole, useValue: isActiveUnitRoleStub }, - { provide: CheckForUpdateService, useValue: checkForUpdateServiceStub }, - { provide: GlobalStateService, useValue: globalStateServiceStub }, + {provide: CalendarModalService, useValue: emptyProvider}, + {provide: AboutDoubtfireModal, useValue: emptyProvider}, + {provide: IsActiveUnitRole, useValue: emptyProvider}, + {provide: CheckForUpdateService, useValue: emptyProvider}, + {provide: GlobalStateService, useValue: emptyProvider}, + {provide: UserService, useValue: emptyProvider}, + {provide: AuthenticationService, useValue: emptyProvider}, + {provide: MediaObserver, useValue: emptyProvider}, + {provide: DoubtfireConstants, useValue: emptyProvider}, + {provide: SidekiqJobService, useValue: emptyProvider}, + {provide: SidekiqJobsModalService, useValue: emptyProvider}, + {provide: QrModalService, useValue: emptyProvider}, + {provide: Router, useValue: emptyProvider}, + {provide: TutorNotesModalService, useValue: emptyProvider}, ], - }).compileComponents(); - })); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(HeaderComponent, {set: {template: ''}}) + .compileComponents(); + }); beforeEach(() => { fixture = TestBed.createComponent(HeaderComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/common/header/header.component.ts b/src/app/common/header/header.component.ts index c2041fb83a..d55ef7ab29 100644 --- a/src/app/common/header/header.component.ts +++ b/src/app/common/header/header.component.ts @@ -1,24 +1,34 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; -import { aboutDoubtfireModal, calendarModal } from 'src/app/ajs-upgraded-providers'; -import { CheckForUpdateService } from 'src/app/sessions/service-worker-updater/check-for-update.service'; -import { GlobalStateService, ViewType } from 'src/app/projects/states/index/global-state.service'; -import { IsActiveUnitRole } from '../pipes/is-active-unit-role.pipe'; -import { UserService } from 'src/app/api/services/user.service'; -import { AuthenticationService, Project, Task, Unit, UnitRole, User } from 'src/app/api/models/doubtfire-model'; -import { Subscription } from 'rxjs'; -import { MediaObserver } from 'ng-flex-layout'; -import { DoubtfireConstants, LogoSettings } from 'src/app/config/constants/doubtfire-constants'; +import {MediaObserver} from 'ng-flex-layout'; +import {ChangeDetectionStrategy, Component, OnDestroy, OnInit} from '@angular/core'; +import {Router} from '@angular/router'; +import {Subscription, asapScheduler, observeOn} from 'rxjs'; +import { + AuthenticationService, + Project, + Task, + Unit, + UnitRole, + User, +} from 'src/app/api/models/doubtfire-model'; import {SidekiqJobEntry, SidekiqJobService} from 'src/app/api/services/sidekiq-job.service'; -import {SidekiqJobsModalService} from '../modals/sidekiq-jobs-modal/sidekiq-jobs-modal.service'; +import {UserService} from 'src/app/api/services/user.service'; +import {DoubtfireConstants, LogoSettings} from 'src/app/config/constants/doubtfire-constants'; +import {GlobalStateService, ViewType} from 'src/app/projects/states/index/global-state.service'; +import {CheckForUpdateService} from 'src/app/sessions/service-worker-updater/check-for-update.service'; +import {AboutDoubtfireModal} from '../modals/about-doubtfire-modal/about-doubtfire-modal.component'; +import {CalendarModalService} from '../modals/calendar-modal/calendar-modal.service'; import {QrModalService} from '../modals/qr-modal/qr-modal.service'; -import {StateService} from '@uirouter/core'; +import {SidekiqJobsModalService} from '../modals/sidekiq-jobs-modal/sidekiq-jobs-modal.service'; import {TutorNotesModalService} from '../modals/tutor-notes-modal/tutor-notes-modal.service'; +import {IsActiveUnitRole} from '../pipes/is-active-unit-role.pipe'; @Component({ selector: 'app-header', templateUrl: './header.component.html', styleUrls: ['./header.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class HeaderComponent implements OnInit, OnDestroy { task: Task; @@ -46,8 +56,8 @@ export class HeaderComponent implements OnInit, OnDestroy { sidekiqJobs: SidekiqJobEntry[] = []; constructor( - @Inject(calendarModal) private CalendarModal, - @Inject(aboutDoubtfireModal) private AboutDoubtfireModal, + private calendarModal: CalendarModalService, + private aboutDoubtfireModal: AboutDoubtfireModal, private isActiveUnitRole: IsActiveUnitRole, private checkForUpdateService: CheckForUpdateService, protected globalState: GlobalStateService, @@ -58,11 +68,16 @@ export class HeaderComponent implements OnInit, OnDestroy { private sidekiqJobService: SidekiqJobService, private sidekiqJobsModalService: SidekiqJobsModalService, private qrModalService: QrModalService, - private stateService: StateService, + private router: Router, private tutorNotesModal: TutorNotesModalService, ) {} + public externalName: string; + ngOnInit(): void { + this.doubtfireConstants.ExternalName.subscribe((externalName) => { + this.externalName = externalName; + }); this.subscriptions.push( this.globalState.showHideHeader.subscribe({ next: (shouldShow) => { @@ -78,7 +93,9 @@ export class HeaderComponent implements OnInit, OnDestroy { this.subscriptions.push( this.globalState.unitRolesSubject.subscribe({ next: (unitRoles) => { - if (unitRoles == null) return; // might be signing out, or the data has been cleared + if (unitRoles == null) { + return; + } // might be signing out, or the data has been cleared this.unitRoles = unitRoles; this.filteredUnitRoles = this.isActiveUnitRole @@ -94,8 +111,10 @@ export class HeaderComponent implements OnInit, OnDestroy { this.subscriptions.push( this.globalState.projectsSubject.subscribe({ next: (projects) => { - if (projects == null) return; - this.projects = projects.filter((project) => project.unit.myRole === 'Student'); + if (!projects) { + return; + } + this.projects = projects.filter((project) => project?.unit?.myRole === 'Student'); }, error: (err) => { console.log(`Error fetching projects: ${err}`); @@ -105,14 +124,18 @@ export class HeaderComponent implements OnInit, OnDestroy { // get the current active unit or project this.subscriptions.push( - this.globalState.currentViewAndEntitySubject$.subscribe({ + this.globalState.currentViewAndEntitySubject$.pipe(observeOn(asapScheduler)).subscribe({ next: (currentViewAndEntity) => { this.currentView = currentViewAndEntity?.viewType; if (this.currentView == ViewType.PROJECT) { this.updateSelectedProject(currentViewAndEntity.entity as Project); } else if (this.currentView == ViewType.UNIT) { - this.updateSelectedUnitRole(currentViewAndEntity.entity as UnitRole); + if (currentViewAndEntity.entity instanceof UnitRole) { + this.updateSelectedUnitRole(currentViewAndEntity.entity as UnitRole); + } else if (currentViewAndEntity.entity instanceof Unit) { + this.updateSelectedUnit(currentViewAndEntity.entity as Unit); + } } else { this.currentUnit = null; this.currentProject = null; @@ -133,7 +156,7 @@ export class HeaderComponent implements OnInit, OnDestroy { }, error: (err) => { console.log(`Error getting settings: ${err}`); - } + }, }), ); @@ -160,12 +183,14 @@ export class HeaderComponent implements OnInit, OnDestroy { true, ); } else { - this.stateService.go('tutor-discussion', { - unitId: this.currentUnit.id, - }); + this.router.navigate(['/units', this.currentUnit.id, 'discussion']); } } + public get isTutorDiscussionRoute(): boolean { + return this.router.url.split('?')[0].endsWith('/discussion'); + } + showSidekiqJob() { this.sidekiqJobsModalService.show(); } @@ -193,16 +218,30 @@ export class HeaderComponent implements OnInit, OnDestroy { this.currentUnit = unitRole.unit; } + updateSelectedUnit(unit: Unit): void { + this.currentUnit = unit; + this.currentProject = null; + + this.currentUnitRole = unit.staff.find( + (ur) => ur.user?.id === this.userService.currentUser?.id, + ); + + if (this.currentUnitRole) { + // Re-map Unit onto UnitRole object + this.currentUnitRole.unit = unit; + } + } + update(): void { this.checkForUpdateService.checkForUpdate(); } openAboutModal(): void { - this.AboutDoubtfireModal.show(); + this.aboutDoubtfireModal.show(); } openCalendar(): void { - this.CalendarModal.show(); + this.calendarModal.show(null); } signOut(): void { diff --git a/src/app/common/header/task-dropdown/task-dropdown.component.html b/src/app/common/header/task-dropdown/task-dropdown.component.html index dcc4bf854d..5f7e239d25 100644 --- a/src/app/common/header/task-dropdown/task-dropdown.component.html +++ b/src/app/common/header/task-dropdown/task-dropdown.component.html @@ -2,43 +2,39 @@ @if (currentView === 'PROJECT' && currentProject !== null && currentUnit.currentUserIsStaff) { chevron_right - - - - - - - @if (currentUnit.currentUserCanViewUnitAdmin) { - @@ -48,133 +44,94 @@ } @if (currentActivity) { chevron_right - @if (currentProject !== null && currentView === 'PROJECT') { - - - - - } @if (unitRole && currentView === 'UNIT') { - - @if (isMentor || unitRole.role === 'Convenor') { - } @if (canMarkOverflowTask) { - } - - - - - - - - @if ( unitRole.role === 'Convenor' || unitRole.role === 'Admin' || unitRole.role === 'Auditor' ) { - diff --git a/src/app/common/header/task-dropdown/task-dropdown.component.spec.ts b/src/app/common/header/task-dropdown/task-dropdown.component.spec.ts index b2a4aa36ca..bc4976e202 100644 --- a/src/app/common/header/task-dropdown/task-dropdown.component.spec.ts +++ b/src/app/common/header/task-dropdown/task-dropdown.component.spec.ts @@ -1,7 +1,7 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { UIRouterModule } from '@uirouter/angular'; - -import { TaskDropdownComponent } from './task-dropdown.component'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {RouterTestingModule} from '@angular/router/testing'; +import {TaskDropdownComponent} from './task-dropdown.component'; describe('TaskDropdownComponent', () => { let component: TaskDropdownComponent; @@ -10,7 +10,7 @@ describe('TaskDropdownComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [TaskDropdownComponent], - imports: [UIRouterModule.forRoot()], + imports: [RouterTestingModule], }).compileComponents(); }); diff --git a/src/app/common/header/task-dropdown/task-dropdown.component.ts b/src/app/common/header/task-dropdown/task-dropdown.component.ts index 2b945884c4..08d3b0e861 100644 --- a/src/app/common/header/task-dropdown/task-dropdown.component.ts +++ b/src/app/common/header/task-dropdown/task-dropdown.component.ts @@ -1,5 +1,6 @@ -import {Component, Input} from '@angular/core'; -import {UIRouter} from '@uirouter/core'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {ActivatedRoute, NavigationEnd, Router} from '@angular/router'; +import {filter} from 'rxjs'; import {Project, Unit, UnitRole} from 'src/app/api/models/doubtfire-model'; import {ViewType} from 'src/app/projects/states/index/global-state.service'; import {TutorNotesModalService} from '../../modals/tutor-notes-modal/tutor-notes-modal.service'; @@ -8,17 +9,19 @@ import {TutorNotesModalService} from '../../modals/tutor-notes-modal/tutor-notes selector: 'task-dropdown', templateUrl: './task-dropdown.component.html', styleUrls: ['./task-dropdown.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TaskDropdownComponent { currentActivity: string; menuText: string; - @Input() data: { isTutor: boolean }; + @Input() data: {isTutor: boolean}; @Input() currentUnit: Unit; @Input() currentProject: Project; @Input() currentView: ViewType; @Input() unitRole: UnitRole; - taskToShortName: { [key: string]: string } = { + taskToShortName: Record = { 'Portfolio Creation': 'Portfolio', 'Staff Tasks': 'Staff Tasks', 'Student Groups': 'Groups', @@ -34,15 +37,16 @@ export class TaskDropdownComponent { 'Unit Analytics': 'Analytics', }; - taskDropdownData: {title: string; target: string; visible: any}[]; + taskDropdownData: {title: string; target: string; visible: boolean}[]; constructor( - private router: UIRouter, + private angularRouter: Router, + private route: ActivatedRoute, private tutorNotesModal: TutorNotesModalService, ) { - this.router.transitionService.onSuccess({to: '**'}, (trans) => { - this.currentActivity = trans.to().data.task; - this.menuText = this.taskToShortName?.[this.currentActivity] ?? this.currentActivity; - }); + this.angularRouter.events + .pipe(filter((event) => event instanceof NavigationEnd)) + .subscribe(() => this.setCurrentActivityFromAngularRoute()); + this.setCurrentActivityFromAngularRoute(); } public get canMarkOverflowTask() { @@ -59,4 +63,15 @@ export class TaskDropdownComponent { openTutorNotes() { this.tutorNotesModal.show(null, this.unitRole); } + + private setCurrentActivityFromAngularRoute(): void { + let route = this.route.root; + + while (route.firstChild) { + route = route.firstChild; + } + + this.currentActivity = route.snapshot.data.task; + this.menuText = this.taskToShortName?.[this.currentActivity] ?? this.currentActivity; + } } diff --git a/src/app/common/header/unit-dropdown/unit-dropdown.component.html b/src/app/common/header/unit-dropdown/unit-dropdown.component.html index 8ac2a35e17..8a3927d771 100644 --- a/src/app/common/header/unit-dropdown/unit-dropdown.component.html +++ b/src/app/common/header/unit-dropdown/unit-dropdown.component.html @@ -1,19 +1,19 @@
@if (unit) { {{ menuState.menuOpen ? 'arrow_drop_up' : 'arrow_drop_down' }} } @if (!unit) { - } diff --git a/src/app/common/header/unit-dropdown/unit-dropdown.component.scss b/src/app/common/header/unit-dropdown/unit-dropdown.component.scss index ffa6ce375c..2c3ca61f68 100644 --- a/src/app/common/header/unit-dropdown/unit-dropdown.component.scss +++ b/src/app/common/header/unit-dropdown/unit-dropdown.component.scss @@ -1,4 +1,4 @@ -@import '../../../../theme.scss'; +@use 'theme' as *; ::ng-deep .unit-dropdown-menu.mat-mdc-menu-panel { max-width: 400px !important; @@ -29,5 +29,5 @@ mat-chip-option.f-chip { color: white; font-style: bold; opacity: 1; - --mdc-chip-disabled-label-text-color: white; + --mat-chip-disabled-label-text-color: white; } diff --git a/src/app/common/header/unit-dropdown/unit-dropdown.component.spec.ts b/src/app/common/header/unit-dropdown/unit-dropdown.component.spec.ts index c3da5ae9ff..bcf7ff444d 100644 --- a/src/app/common/header/unit-dropdown/unit-dropdown.component.spec.ts +++ b/src/app/common/header/unit-dropdown/unit-dropdown.component.spec.ts @@ -1,26 +1,21 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { MatMenuModule } from '@angular/material/menu'; -import { dateService } from 'src/app/ajs-upgraded-providers'; - -import { UnitDropdownComponent } from './unit-dropdown.component'; +import {MediaObserver} from 'ng-flex-layout'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatMenuModule} from '@angular/material/menu'; +import {UnitDropdownComponent} from './unit-dropdown.component'; describe('UnitDropdownComponent', () => { let component: UnitDropdownComponent; let fixture: ComponentFixture; - let dateServiceStub: jasmine.SpyObj; - - beforeEach( - waitForAsync(() => { - dateServiceStub = jasmine.createSpy(); - dateServiceStub.showDate = true; - - TestBed.configureTestingModule({ - declarations: [UnitDropdownComponent], - imports: [MatMenuModule], - providers: [{ provide: dateService, useValue: dateServiceStub }], - }).compileComponents(); - }) - ); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [UnitDropdownComponent], + imports: [MatMenuModule], + providers: [{provide: MediaObserver, useValue: {isActive: () => false}}], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + }); beforeEach(() => { fixture = TestBed.createComponent(UnitDropdownComponent); diff --git a/src/app/common/header/unit-dropdown/unit-dropdown.component.ts b/src/app/common/header/unit-dropdown/unit-dropdown.component.ts index 871ddf4225..4190257424 100644 --- a/src/app/common/header/unit-dropdown/unit-dropdown.component.ts +++ b/src/app/common/header/unit-dropdown/unit-dropdown.component.ts @@ -1,13 +1,15 @@ -import {Component, Input, OnInit} from '@angular/core'; -import {Project, Unit, UnitRole} from 'src/app/api/models/doubtfire-model'; import {MediaObserver} from 'ng-flex-layout'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {Project, Unit, UnitRole} from 'src/app/api/models/doubtfire-model'; @Component({ selector: 'unit-dropdown', templateUrl: './unit-dropdown.component.html', styleUrls: ['./unit-dropdown.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class UnitDropdownComponent implements OnInit { +export class UnitDropdownComponent { @Input() unitRoles: UnitRole[]; @Input() projects: Project[]; @Input() unit: Unit; @@ -15,6 +17,4 @@ export class UnitDropdownComponent implements OnInit { unitTitle: string; constructor(public media: MediaObserver) {} - - ngOnInit(): void {} } diff --git a/src/app/common/hero-sidebar/hero-sidebar.component.html b/src/app/common/hero-sidebar/hero-sidebar.component.html index 8136d06734..09a24f8278 100644 --- a/src/app/common/hero-sidebar/hero-sidebar.component.html +++ b/src/app/common/hero-sidebar/hero-sidebar.component.html @@ -1,12 +1,17 @@
-
- Homepage Logo +
+ Homepage Logo

{{ externalName.value }}

-

Manage your learning, with feedback you'll want to receive.

+

Manage your learning, with feedback you'll want to receive.

-
+
diff --git a/src/app/common/hero-sidebar/hero-sidebar.component.scss b/src/app/common/hero-sidebar/hero-sidebar.component.scss index aaf7e537d5..b49aa2b548 100644 --- a/src/app/common/hero-sidebar/hero-sidebar.component.scss +++ b/src/app/common/hero-sidebar/hero-sidebar.component.scss @@ -35,8 +35,20 @@ } .pattern { - -webkit-mask-image: -webkit-gradient(linear, left top, left bottom, to(rgba(0, 0, 0, 1)), from(rgba(0, 0, 0, 0))); - mask-image: -webkit-gradient(linear, left top, left bottom, to(rgba(0, 0, 0, 1)), from(rgba(0, 0, 0, 0))); + -webkit-mask-image: -webkit-gradient( + linear, + left top, + left bottom, + to(rgba(0, 0, 0, 1)), + from(rgba(0, 0, 0, 0)) + ); + mask-image: -webkit-gradient( + linear, + left top, + left bottom, + to(rgba(0, 0, 0, 1)), + from(rgba(0, 0, 0, 0)) + ); transition: 4s linear all; background-color: #e5e5f7; opacity: 1; diff --git a/src/app/common/hero-sidebar/hero-sidebar.component.spec.ts b/src/app/common/hero-sidebar/hero-sidebar.component.spec.ts index 5cc1ec36c0..96247b980e 100644 --- a/src/app/common/hero-sidebar/hero-sidebar.component.spec.ts +++ b/src/app/common/hero-sidebar/hero-sidebar.component.spec.ts @@ -1,6 +1,8 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { HeroSidebarComponent } from './hero-sidebar.component'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {BehaviorSubject} from 'rxjs'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {HeroSidebarComponent} from './hero-sidebar.component'; describe('HeroSidebarComponent', () => { let component: HeroSidebarComponent; @@ -8,9 +10,14 @@ describe('HeroSidebarComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ HeroSidebarComponent ] - }) - .compileComponents(); + declarations: [HeroSidebarComponent], + providers: [ + { + provide: DoubtfireConstants, + useValue: {ExternalName: new BehaviorSubject('Doubtfire')}, + }, + ], + }).compileComponents(); }); beforeEach(() => { diff --git a/src/app/common/hero-sidebar/hero-sidebar.component.ts b/src/app/common/hero-sidebar/hero-sidebar.component.ts index 2910b74cb6..7c54a7bd8c 100644 --- a/src/app/common/hero-sidebar/hero-sidebar.component.ts +++ b/src/app/common/hero-sidebar/hero-sidebar.component.ts @@ -1,14 +1,14 @@ -import { Component, OnInit } from '@angular/core'; -import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; @Component({ selector: 'f-hero-sidebar', templateUrl: './hero-sidebar.component.html', styleUrls: ['./hero-sidebar.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class HeroSidebarComponent implements OnInit { +export class HeroSidebarComponent { public externalName = this.constants.ExternalName; constructor(private constants: DoubtfireConstants) {} - - ngOnInit(): void {} } diff --git a/src/app/common/learning-outcome-editor/learning-outcome-editor.component.html b/src/app/common/learning-outcome-editor/learning-outcome-editor.component.html index 9031ccde39..c8c668aabe 100644 --- a/src/app/common/learning-outcome-editor/learning-outcome-editor.component.html +++ b/src/app/common/learning-outcome-editor/learning-outcome-editor.component.html @@ -1,47 +1,47 @@ -
+
- + - + - + - + - + - +
AbbreviationAbbreviation {{ learningOutcome.abbreviation }} Short DescriptionShort Description {{ learningOutcome.shortDescription }} Full Outcome DescriptionFull Outcome Description {{ learningOutcome.fullOutcomeDescription }} Connected Learning OutcomesConnected Learning Outcomes @for (outcome of getLinkedOutcomes(learningOutcome); track outcome.abbreviation) { @@ -52,26 +52,26 @@ @if (learningOutcomeHasChanges(learningOutcome)) { }
-
+
- +
@@ -113,23 +113,23 @@ upload_file - + @if (selectedOutcome) { -
+
-

Edit Outcome

+

Edit Outcome

-
+
Abbreviation @@ -137,8 +137,8 @@

Edit Outcome

Short Description @@ -148,8 +148,8 @@

Edit Outcome

Full Outcome Description @@ -169,15 +169,15 @@

Edit Outcome

} @for (outcome of filteredOutcomes(); track outcome) { @@ -191,7 +191,7 @@

Edit Outcome

-
diff --git a/src/app/common/learning-outcome-editor/learning-outcome-editor.component.ts b/src/app/common/learning-outcome-editor/learning-outcome-editor.component.ts index 3dfa2451cf..183bb47c96 100644 --- a/src/app/common/learning-outcome-editor/learning-outcome-editor.component.ts +++ b/src/app/common/learning-outcome-editor/learning-outcome-editor.component.ts @@ -1,57 +1,60 @@ +import {LiveAnnouncer} from '@angular/cdk/a11y'; +import {COMMA, ENTER} from '@angular/cdk/keycodes'; import { AfterViewInit, + ChangeDetectionStrategy, Component, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, + ViewChild, computed, + effect, inject, - Inject, - Input, model, - OnDestroy, signal, - effect, - ViewChild, - OnChanges, - SimpleChanges, } from '@angular/core'; -import {MatTable, MatTableDataSource} from '@angular/material/table'; +import {MatAutocompleteSelectedEvent} from '@angular/material/autocomplete'; +import {MatChipInputEvent} from '@angular/material/chips'; import {MatPaginator} from '@angular/material/paginator'; +import {MatSort, Sort} from '@angular/material/sort'; +import {MatTable, MatTableDataSource} from '@angular/material/table'; +import {Subscription} from 'rxjs'; import { - TaskDefinition, - Unit, + FeedbackTemplateService, LearningOutcome, LearningOutcomeService, + TaskDefinition, TaskService, - FeedbackTemplateService, + Unit, } from 'src/app/api/models/doubtfire-model'; import {AlertService} from 'src/app/common/services/alert.service'; -import {MatSort, Sort} from '@angular/material/sort'; -import { - confirmationModal, - csvResultModalService, - csvUploadModalService, -} from 'src/app/ajs-upgraded-providers'; -import {Subscription} from 'rxjs'; -import {COMMA, ENTER} from '@angular/cdk/keycodes'; -import {LiveAnnouncer} from '@angular/cdk/a11y'; -import {MatChipInputEvent} from '@angular/material/chips'; -import {MatAutocompleteSelectedEvent} from '@angular/material/autocomplete'; +import API_URL from 'src/app/config/constants/apiUrl'; import {FileDownloaderService} from '../file-downloader/file-downloader.service'; -import {isEqual} from 'lodash'; +import {ConfirmationModalService} from '../modals/confirmation-modal/confirmation-modal.service'; +import { + CsvResult, + CsvResultModalService, +} from '../modals/csv-result-modal/csv-result-modal.service'; +import {CsvUploadModalService} from '../modals/csv-upload-modal/csv-upload-modal.service'; import {NestedCsvDownloadModalService} from './nested-csv-download-modal/nested-csv-download-modal.service'; -import API_URL from 'src/app/config/constants/apiUrl'; @Component({ selector: 'f-learning-outcome-editor', templateUrl: 'learning-outcome-editor.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class LearningOutcomeEditorComponent implements OnChanges, AfterViewInit, OnDestroy { +export class LearningOutcomeEditorComponent implements OnChanges, OnInit, AfterViewInit, OnDestroy { @Input() context?: TaskDefinition | Unit; @ViewChild('outcomeTable', {static: false}) outcomeTable: MatTable; @ViewChild(MatSort, {static: false}) outcomeSort: MatSort; @ViewChild(MatPaginator, {static: false}) outcomePaginator: MatPaginator; - public outcomeSource: MatTableDataSource; + public outcomeSource: MatTableDataSource = new MatTableDataSource([]); public outcomeColumns: string[] = [ 'abbreviation', 'shortDescription', @@ -74,28 +77,57 @@ export class LearningOutcomeEditorComponent implements OnChanges, AfterViewInit, private nestedCsvDownloadModalService: NestedCsvDownloadModalService, private feedbackTemplateService: FeedbackTemplateService, private taskService: TaskService, - @Inject(csvResultModalService) private csvResultModalService: any, - @Inject(csvUploadModalService) private csvUploadModal: any, - @Inject(confirmationModal) private confirmationModal: any, + private csvResultModalService: CsvResultModalService, + private csvUploadModal: CsvUploadModalService, + private confirmationModal: ConfirmationModalService, ) { + this.outcomeSource.filterPredicate = (data: LearningOutcome, filter: string) => { + const filterValue = filter.trim().toLowerCase(); + return ( + data.abbreviation.toLowerCase().includes(filterValue) || + data.shortDescription.toLowerCase().includes(filterValue) || + data.fullOutcomeDescription.toLowerCase().includes(filterValue) + ); + }; + effect(() => { const linkedOutcomes = this.selectedConnectedOutcomes().map((outcome) => outcome.id); if ( this.selectedOutcome && - !isEqual(linkedOutcomes.sort(), this.selectedOutcome.linkedOutcomeIds.sort()) - ) + !this.sameIds(linkedOutcomes, this.selectedOutcome.linkedOutcomeIds) + ) { this.selectedOutcome.linkedOutcomeIds = linkedOutcomes; + } }); } + private sameIds(left: number[], right: number[]): boolean { + if (left.length !== right.length) { + return false; + } + + const sortedLeft = [...left].sort((a, b) => a - b); + const sortedRight = [...right].sort((a, b) => a - b); + return sortedLeft.every((id, index) => id === sortedRight[index]); + } + + ngOnInit(): void { + this.subscribeToLearningOutcomes(); + } + ngAfterViewInit(): void { + this.outcomeSource.paginator = this.outcomePaginator; + this.outcomeSource.sort = this.outcomeSort; + } + + private subscribeToLearningOutcomes(): void { this.setAbbreviationPrefix(); if (!this.context) { this.subscriptions.push( this.learningOutcomeService.cache.values.subscribe((outcomes) => { const glos = outcomes.filter((outcome) => outcome.contextType === null); - this.initialiseTable(glos); + this.outcomeSource.data = glos; }), ); return; @@ -103,7 +135,7 @@ export class LearningOutcomeEditorComponent implements OnChanges, AfterViewInit, this.subscriptions.push( this.context.learningOutcomesCache.values.subscribe((learningOutcomes) => { - this.initialiseTable(learningOutcomes); + this.outcomeSource.data = learningOutcomes; }), ); @@ -123,15 +155,8 @@ export class LearningOutcomeEditorComponent implements OnChanges, AfterViewInit, } } - ngOnChanges(changes: SimpleChanges): void { + ngOnChanges(_changes: SimpleChanges): void { this.setAbbreviationPrefix(); - - this.subscriptions.push( - this.context.learningOutcomesCache.values.subscribe((learningOutcomes) => { - this.initialiseTable(learningOutcomes); - }), - ); - this.selectedOutcome = null; } @@ -140,23 +165,13 @@ export class LearningOutcomeEditorComponent implements OnChanges, AfterViewInit, } setAbbreviationPrefix(): void { - if (!this.context) this.abbreviationPrefix = 'GLO'; - else if (this.context instanceof TaskDefinition) this.abbreviationPrefix = 'TLO'; - else if (this.context instanceof Unit) this.abbreviationPrefix = 'ULO'; - } - - initialiseTable(learningOutcomes: LearningOutcome[]) { - this.outcomeSource = new MatTableDataSource(learningOutcomes); - this.outcomeSource.paginator = this.outcomePaginator; - this.outcomeSource.sort = this.outcomeSort; - this.outcomeSource.filterPredicate = (data: LearningOutcome, filter: string) => { - const filterValue = filter.trim().toLowerCase(); - return ( - data.abbreviation.toLowerCase().includes(filterValue) || - data.shortDescription.toLowerCase().includes(filterValue) || - data.fullOutcomeDescription.toLowerCase().includes(filterValue) - ); - }; + if (!this.context) { + this.abbreviationPrefix = 'GLO'; + } else if (this.context instanceof TaskDefinition) { + this.abbreviationPrefix = 'TLO'; + } else if (this.context instanceof Unit) { + this.abbreviationPrefix = 'ULO'; + } } public saveLearningOutcome(learningOutcome: LearningOutcome) { @@ -185,7 +200,9 @@ export class LearningOutcomeEditorComponent implements OnChanges, AfterViewInit, } else { this.selectedOutcome = learningOutcome; this.selectedConnectedOutcomes.update(() => this.getLinkedOutcomes(learningOutcome)); - if (!this.selectedOutcome.context) this.selectedOutcome.context = this.context; + if (!this.selectedOutcome.context) { + this.selectedOutcome.context = this.context; + } if (!this.selectedOutcome.hasOriginalSaveData) { this.selectedOutcome.setOriginalSaveData(this.learningOutcomeService.mapping); @@ -239,8 +256,9 @@ export class LearningOutcomeEditorComponent implements OnChanges, AfterViewInit, learningOutcome.delete().subscribe({ next: () => { this.alerts.success('Learning outcome deleted'); - if (this.selectedOutcome === learningOutcome) + if (this.selectedOutcome === learningOutcome) { this.selectLearningOutcome(this.selectedOutcome); + } }, error: () => this.alerts.error('Failed to delete learning outcome. Please try again.'), }); @@ -251,10 +269,14 @@ export class LearningOutcomeEditorComponent implements OnChanges, AfterViewInit, public uploadCsv(type: 'Learning Outcomes' | 'Feedback Templates') { let url: string; - if (type === 'Learning Outcomes') url = this.context.getOutcomeBatchUploadUrl(); - else { - if (this.context) url = this.context.getFeedbackTemplateBatchUploadUrl(); - else url = `${API_URL}/global/feedback_chips/csv`; + if (type === 'Learning Outcomes') { + url = this.context.getOutcomeBatchUploadUrl(); + } else { + if (this.context) { + url = this.context.getFeedbackTemplateBatchUploadUrl(); + } else { + url = `${API_URL}/global/feedback_chips/csv`; + } } this.csvUploadModal.show( @@ -262,7 +284,7 @@ export class LearningOutcomeEditorComponent implements OnChanges, AfterViewInit, 'Test message', {file: {name: `${type} CSV Data`, type: 'csv'}}, url, - (response: any) => { + (response: CsvResult) => { this.csvResultModalService.show(`${type} CSV Upload Results`, response); if (response.success.length > 0) { let contextType: 'units' | 'task_definitions'; @@ -288,20 +310,29 @@ export class LearningOutcomeEditorComponent implements OnChanges, AfterViewInit, public downloadCsv(type: 'Learning Outcomes' | 'Feedback Templates') { let url: string; - if (type === 'Learning Outcomes') url = this.context.getOutcomeBatchUploadUrl(); - else { - if (this.context) url = this.context.getFeedbackTemplateBatchUploadUrl(); - else url = `${API_URL}/global/feedback_chips/csv`; + if (type === 'Learning Outcomes') { + url = this.context.getOutcomeBatchUploadUrl(); + } else { + if (this.context) { + url = this.context.getFeedbackTemplateBatchUploadUrl(); + } else { + url = `${API_URL}/global/feedback_chips/csv`; + } } let name = `${type}.csv`; - if (this.context instanceof TaskDefinition) + if (this.context instanceof TaskDefinition) { name = `${this.context.unit.code}-${this.context.abbreviation}-${name}`; - else if (this.context instanceof Unit) name = `${this.context.code}-${name}`; + } else if (this.context instanceof Unit) { + name = `${this.context.code}-${name}`; + } - if (this.context instanceof Unit) this.nestedCsvDownloadModalService.show(url, name, type); - else this.fileDownloaderService.downloadFile(url, name); + if (this.context instanceof Unit) { + this.nestedCsvDownloadModalService.show(url, name, type); + } else { + this.fileDownloaderService.downloadFile(url, name); + } } public createLearningOutcome() { @@ -309,8 +340,11 @@ export class LearningOutcomeEditorComponent implements OnChanges, AfterViewInit, if (this.context) { learningOutcome.context = this.context; - if (this.context instanceof TaskDefinition) learningOutcome.contextType = 'TaskDefinition'; - else if (this.context instanceof Unit) learningOutcome.contextType = 'Unit'; + if (this.context instanceof TaskDefinition) { + learningOutcome.contextType = 'TaskDefinition'; + } else if (this.context instanceof Unit) { + learningOutcome.contextType = 'Unit'; + } learningOutcome.contextId = this.context.id; } learningOutcome.abbreviation = this.abbreviationPrefix + String(this.getNextOutcomeNumber()); diff --git a/src/app/common/learning-outcome-editor/nested-csv-download-modal/nested-csv-download-modal.component.html b/src/app/common/learning-outcome-editor/nested-csv-download-modal/nested-csv-download-modal.component.html index db56ad8b3e..8049fc8a04 100644 --- a/src/app/common/learning-outcome-editor/nested-csv-download-modal/nested-csv-download-modal.component.html +++ b/src/app/common/learning-outcome-editor/nested-csv-download-modal/nested-csv-download-modal.component.html @@ -1,6 +1,6 @@

Download the {{ data.type }} CSV

-
+

This action will download all {{ data.type.toLowerCase() }} associated with this unit.

Include task {{ data.type.toLowerCase() }}Download the {{ data.type }} CSV
-
+
- +
diff --git a/src/app/common/learning-outcome-editor/nested-csv-download-modal/nested-csv-download-modal.component.ts b/src/app/common/learning-outcome-editor/nested-csv-download-modal/nested-csv-download-modal.component.ts index 508cf87af7..d17b8fd895 100644 --- a/src/app/common/learning-outcome-editor/nested-csv-download-modal/nested-csv-download-modal.component.ts +++ b/src/app/common/learning-outcome-editor/nested-csv-download-modal/nested-csv-download-modal.component.ts @@ -1,10 +1,12 @@ -import {Component, Inject} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Inject} from '@angular/core'; import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; import {FileDownloaderService} from '../../file-downloader/file-downloader.service'; @Component({ selector: 'f-nested-csv-download-modal', templateUrl: './nested-csv-download-modal.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class NestedCsvDownloadModalComponent { public includeNested = false; diff --git a/src/app/common/learning-outcome-editor/nested-csv-download-modal/nested-csv-download-modal.service.ts b/src/app/common/learning-outcome-editor/nested-csv-download-modal/nested-csv-download-modal.service.ts index 91e34ff400..9aad82f209 100644 --- a/src/app/common/learning-outcome-editor/nested-csv-download-modal/nested-csv-download-modal.service.ts +++ b/src/app/common/learning-outcome-editor/nested-csv-download-modal/nested-csv-download-modal.service.ts @@ -1,5 +1,5 @@ import {Injectable} from '@angular/core'; -import {MatDialogRef, MatDialog} from '@angular/material/dialog'; +import {MatDialog} from '@angular/material/dialog'; import {NestedCsvDownloadModalComponent} from './nested-csv-download-modal.component'; @Injectable({ @@ -9,7 +9,7 @@ export class NestedCsvDownloadModalService { constructor(public dialog: MatDialog) {} public show(url: string, name: string, type: string) { - const dialogRef: MatDialogRef = this.dialog.open( + this.dialog.open( NestedCsvDownloadModalComponent, { data: {url, name, type}, diff --git a/src/app/common/modals/about-doubtfire-modal/about-dialog-data.ts b/src/app/common/modals/about-doubtfire-modal/about-dialog-data.ts index e85e989082..54259f51be 100644 --- a/src/app/common/modals/about-doubtfire-modal/about-dialog-data.ts +++ b/src/app/common/modals/about-doubtfire-modal/about-dialog-data.ts @@ -1,7 +1,7 @@ -import { GithubProfile } from './github-profile'; -import { ContributorData } from './contributor-data'; -import { BehaviorSubject } from 'rxjs'; -import { Sort } from '@angular/material/sort'; +import {Sort} from '@angular/material/sort'; +import {BehaviorSubject} from 'rxjs'; +import {ContributorData} from './contributor-data'; +import {GithubProfile} from './github-profile'; /** * The data shared between the AboutDoubtfireModal and its associated @@ -51,7 +51,7 @@ export class AboutDialogData { default: return 0; } - }) + }), ); } } diff --git a/src/app/common/modals/about-doubtfire-modal/about-doubtfire-modal-content.tpl.html b/src/app/common/modals/about-doubtfire-modal/about-doubtfire-modal-content.tpl.html index 08c5fd09bf..4d98486b5f 100644 --- a/src/app/common/modals/about-doubtfire-modal/about-doubtfire-modal-content.tpl.html +++ b/src/app/common/modals/about-doubtfire-modal/about-doubtfire-modal-content.tpl.html @@ -1,32 +1,51 @@ -
- + diff --git a/src/app/common/modals/extension-modal/extension-modal.component.ts b/src/app/common/modals/extension-modal/extension-modal.component.ts index ff3dcdf7d7..70ff977b6b 100644 --- a/src/app/common/modals/extension-modal/extension-modal.component.ts +++ b/src/app/common/modals/extension-modal/extension-modal.component.ts @@ -1,11 +1,11 @@ -import {Component, Inject, LOCALE_ID} from '@angular/core'; -import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog'; -import {TaskComment, TaskCommentService, Task} from 'src/app/api/models/doubtfire-model'; -import {AppInjector} from 'src/app/app-injector'; -import {FormControl, Validators, FormGroup, FormGroupDirective, NgForm} from '@angular/forms'; -import {MatDatepickerInputEvent} from '@angular/material/datepicker'; -import {differenceInWeeks, differenceInDays, isAfter, addDays} from 'date-fns'; +import {addDays, differenceInDays, differenceInWeeks, isAfter} from 'date-fns'; +import {ChangeDetectionStrategy, Component, Inject, LOCALE_ID} from '@angular/core'; +import {FormControl, FormGroup, FormGroupDirective, NgForm, Validators} from '@angular/forms'; import {ErrorStateMatcher} from '@angular/material/core'; +import {MatDatepickerInputEvent} from '@angular/material/datepicker'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {Task, TaskComment, TaskCommentService} from 'src/app/api/models/doubtfire-model'; +import {AppInjector} from 'src/app/app-injector'; import {AlertService} from '../../services/alert.service'; /** Error when invalid control is dirty, touched, or submitted. */ @@ -19,6 +19,8 @@ export class ReasonErrorStateMatcher implements ErrorStateMatcher { @Component({ selector: 'extension-modal', templateUrl: './extension-modal.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class ExtensionModalComponent { protected reasonMinLength: number = 15; diff --git a/src/app/common/modals/extension-modal/extension-modal.service.ts b/src/app/common/modals/extension-modal/extension-modal.service.ts index 1b1283fdcd..19709c908e 100644 --- a/src/app/common/modals/extension-modal/extension-modal.service.ts +++ b/src/app/common/modals/extension-modal/extension-modal.service.ts @@ -1,6 +1,6 @@ import {Injectable} from '@angular/core'; +import {MatDialog, MatDialogRef} from '@angular/material/dialog'; import {Task} from 'src/app/api/models/task'; -import {MatDialogRef, MatDialog} from '@angular/material/dialog'; import {ExtensionModalComponent} from './extension-modal.component'; @Injectable({ @@ -9,18 +9,19 @@ import {ExtensionModalComponent} from './extension-modal.component'; export class ExtensionModalService { constructor(public dialog: MatDialog) {} - public show(task: Task, afterApplication?: any) { - let dialogRef: MatDialogRef; - - dialogRef = this.dialog.open(ExtensionModalComponent, { - data: { - task, - afterApplication, + public show(task: Task, afterApplication?: () => void) { + const dialogRef: MatDialogRef = this.dialog.open( + ExtensionModalComponent, + { + data: { + task, + afterApplication, + }, }, - }); + ); - dialogRef.afterOpened().subscribe((result: any) => {}); + dialogRef.afterOpened().subscribe(); - dialogRef.afterClosed().subscribe((result: any) => {}); + dialogRef.afterClosed().subscribe(); } } diff --git a/src/app/common/modals/modals.coffee b/src/app/common/modals/modals.coffee deleted file mode 100644 index 30519a0bfb..0000000000 --- a/src/app/common/modals/modals.coffee +++ /dev/null @@ -1,3 +0,0 @@ -angular.module("doubtfire.common.modals", [ - 'doubtfire.common.modals.csv-result-modal' -]) diff --git a/src/app/common/modals/qr-modal/qr-modal.component.html b/src/app/common/modals/qr-modal/qr-modal.component.html index 8926bebe63..8cca3b1df3 100644 --- a/src/app/common/modals/qr-modal/qr-modal.component.html +++ b/src/app/common/modals/qr-modal/qr-modal.component.html @@ -1,8 +1,8 @@ -
+
{{ caption }}
- QR Code for tutor assessment + QR Code for tutor assessment @if (footer) {
{{ footer }}
} diff --git a/src/app/common/modals/qr-modal/qr-modal.component.ts b/src/app/common/modals/qr-modal/qr-modal.component.ts index 45fb24e6b1..92f1784ee7 100644 --- a/src/app/common/modals/qr-modal/qr-modal.component.ts +++ b/src/app/common/modals/qr-modal/qr-modal.component.ts @@ -1,12 +1,14 @@ -import {Component, Inject, OnInit} from '@angular/core'; -import {MAT_DIALOG_DATA} from '@angular/material/dialog'; import QRCode from 'qrcode'; +import {ChangeDetectionStrategy, Component, Inject, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA} from '@angular/material/dialog'; import {QrModalData} from './qr-modal.service'; @Component({ selector: 'f-qr-modal', templateUrl: './qr-modal.component.html', styleUrls: ['./qr-modal.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class QrModalComponent implements OnInit { constructor(@Inject(MAT_DIALOG_DATA) public data: QrModalData) {} diff --git a/src/app/common/modals/scorm-extension-modal/scorm-extension-modal.component.html b/src/app/common/modals/scorm-extension-modal/scorm-extension-modal.component.html index f39a9580b5..89b4341889 100644 --- a/src/app/common/modals/scorm-extension-modal/scorm-extension-modal.component.html +++ b/src/app/common/modals/scorm-extension-modal/scorm-extension-modal.component.html @@ -5,14 +5,14 @@

Extra attempt request

assess the request shortly.

- + Reason {{ extensionData.controls.extensionReason.value.length }} / {{ reasonMaxLength }}Extra attempt request
- + - +
{{ jobEntry.job?.status?.toUpperCase() }}
+
+ +
diff --git a/src/app/common/modals/sidekiq-jobs-modal/sidekiq-jobs-modal.component.ts b/src/app/common/modals/sidekiq-jobs-modal/sidekiq-jobs-modal.component.ts index f2f2ea7596..dceebc3455 100644 --- a/src/app/common/modals/sidekiq-jobs-modal/sidekiq-jobs-modal.component.ts +++ b/src/app/common/modals/sidekiq-jobs-modal/sidekiq-jobs-modal.component.ts @@ -1,4 +1,4 @@ -import {Component, Inject, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Inject, OnInit} from '@angular/core'; import {MatDialogRef} from '@angular/material/dialog'; import {SidekiqJobEntry, SidekiqJobService} from 'src/app/api/services/sidekiq-job.service'; import {SidekiqProgressModalService} from 'src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.service'; @@ -8,6 +8,8 @@ import {AlertService} from '../../services/alert.service'; selector: 'f-sidekiq-jobs-modal', templateUrl: './sidekiq-jobs-modal.component.html', styleUrl: './sidekiq-jobs-modal.component.scss', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class SidekiqJobsModalComponent implements OnInit { constructor( diff --git a/src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.component.html b/src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.component.html index c206750cf5..c39d262374 100644 --- a/src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.component.html +++ b/src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.component.html @@ -6,7 +6,7 @@

This job is running in the background. You can close this dialog and monitor progress anytime.
-
+
@if (job?.status === 'complete') {
COMPLETE
} @else { @@ -24,14 +24,14 @@

} @else { } @if (job?.status !== 'working' && job?.result) { - + }

diff --git a/src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.component.spec.ts b/src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.component.spec.ts index f30dd067d3..6bedbb62b2 100644 --- a/src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.component.spec.ts +++ b/src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.component.spec.ts @@ -1,6 +1,14 @@ +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; - +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {MatSnackBar} from '@angular/material/snack-bar'; +import {SidekiqJobService} from 'src/app/api/services/sidekiq-job.service'; +import {AlertService} from '../../services/alert.service'; import {SidekiqProgressModalComponent} from './sidekiq-progress-modal.component'; +import {SidekiqProgressModalService} from './sidekiq-progress-modal.service'; + +const emptyProvider = {}; describe('SidekiqProgressModalComponent', () => { let component: SidekiqProgressModalComponent; @@ -8,12 +16,24 @@ describe('SidekiqProgressModalComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SidekiqProgressModalComponent], - }).compileComponents(); + declarations: [SidekiqProgressModalComponent], + providers: [ + {provide: AlertService, useValue: emptyProvider}, + {provide: MAT_DIALOG_DATA, useValue: emptyProvider}, + {provide: MatDialogRef, useValue: emptyProvider}, + {provide: SidekiqJobService, useValue: emptyProvider}, + {provide: SidekiqProgressModalService, useValue: emptyProvider}, + {provide: MatSnackBar, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(SidekiqProgressModalComponent, {set: {template: ''}}) + .compileComponents(); + }); + beforeEach(() => { fixture = TestBed.createComponent(SidekiqProgressModalComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.component.ts b/src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.component.ts index 166f50a7b6..440d1ebf5d 100644 --- a/src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.component.ts +++ b/src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.component.ts @@ -1,4 +1,4 @@ -import {Component, Inject, OnDestroy, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit} from '@angular/core'; import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; import {MatSnackBar} from '@angular/material/snack-bar'; import {Subject} from 'rxjs'; @@ -18,6 +18,8 @@ export interface SidekiqProgressModalData { selector: 'f-sidekiq-progress-modal', templateUrl: './sidekiq-progress-modal.component.html', styleUrl: './sidekiq-progress-modal.component.scss', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class SidekiqProgressModalComponent implements OnInit, OnDestroy { private readonly pollingInterval: number = 1250; @@ -61,7 +63,6 @@ export class SidekiqProgressModalComponent implements OnInit, OnDestroy { this.sidekiqJobService.getSidekiqJob(this.data.jobId).subscribe({ next: (job) => { - this.sidekiqJobService.sidekiqJobsSubject; this.sidekiqJobService.setJob(job.id, this.data.title, this.data.subject, job); this.job = job; this.pollFailureCount = 0; diff --git a/src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.service.ts b/src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.service.ts index 8316294b1f..3881a3d7e6 100644 --- a/src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.service.ts +++ b/src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.service.ts @@ -1,12 +1,12 @@ import {Injectable} from '@angular/core'; import {MatDialog} from '@angular/material/dialog'; +import {Subject} from 'rxjs'; +import {SidekiqJob} from 'src/app/api/models/sidekiq-job'; +import {SidekiqJobService} from 'src/app/api/services/sidekiq-job.service'; import { SidekiqProgressModalComponent, SidekiqProgressModalData, } from './sidekiq-progress-modal.component'; -import {Subject} from 'rxjs'; -import {SidekiqJob} from 'src/app/api/models/sidekiq-job'; -import {SidekiqJobService} from 'src/app/api/services/sidekiq-job.service'; @Injectable({ providedIn: 'root', @@ -18,7 +18,7 @@ export class SidekiqProgressModalService { ) {} public show(title: string, jobId: string) { - const subject = new Subject(); + const subject: Subject = new Subject(); this.sidekiqJobService.setJob(jobId, title, subject); diff --git a/src/app/common/modals/spec-con-modal/spec-con-modal.component.html b/src/app/common/modals/spec-con-modal/spec-con-modal.component.html index 7bb56e4b07..182c88c0f6 100644 --- a/src/app/common/modals/spec-con-modal/spec-con-modal.component.html +++ b/src/app/common/modals/spec-con-modal/spec-con-modal.component.html @@ -6,18 +6,18 @@

Grant Extension / Special Consideration

Number of days - +
- + } - +

diff --git a/src/app/common/modals/task-assessment-modal/task-assessment-modal.component.scss b/src/app/common/modals/task-assessment-modal/task-assessment-modal.component.scss index c9e26fca56..2ca54d9c09 100644 --- a/src/app/common/modals/task-assessment-modal/task-assessment-modal.component.scss +++ b/src/app/common/modals/task-assessment-modal/task-assessment-modal.component.scss @@ -1,4 +1,3 @@ - /* TODO(mdc-migration): The following rule targets internal classes of dialog that may no longer apply for the MDC version. */ /* TODO(mdc-migration): The following rule targets internal classes of dialog that may no longer apply for the MDC version. */ mat-dialog-container.mat-mdc-dialog-container { @@ -7,7 +6,7 @@ mat-dialog-container.mat-mdc-dialog-container { max-height: unset; } -.mat-mdc-dialog-title{ +.mat-mdc-dialog-title { padding-top: 0; margin: 1em; } diff --git a/src/app/common/modals/task-assessment-modal/task-assessment-modal.component.ts b/src/app/common/modals/task-assessment-modal/task-assessment-modal.component.ts index 13bd6e9236..c37611915f 100644 --- a/src/app/common/modals/task-assessment-modal/task-assessment-modal.component.ts +++ b/src/app/common/modals/task-assessment-modal/task-assessment-modal.component.ts @@ -1,4 +1,4 @@ -import {Component, OnInit, Inject, Input} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Inject, Input, OnInit} from '@angular/core'; import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; import {Subject} from 'rxjs'; import {Task} from 'src/app/api/models/doubtfire-model'; @@ -8,6 +8,8 @@ import {TaskAssessmentModalData} from './task-assessment-modal.service'; selector: 'task-assessment-modal', templateUrl: './task-assessment-modal.component.html', styleUrls: ['./task-assessment-modal.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TaskAssessmentModalComponent implements OnInit { @Input() task: Task; diff --git a/src/app/common/modals/task-assessment-modal/task-assessment-modal.service.ts b/src/app/common/modals/task-assessment-modal/task-assessment-modal.service.ts index 31e9bc54c9..de21c465a2 100644 --- a/src/app/common/modals/task-assessment-modal/task-assessment-modal.service.ts +++ b/src/app/common/modals/task-assessment-modal/task-assessment-modal.service.ts @@ -1,7 +1,7 @@ import {Injectable} from '@angular/core'; -import {MatDialogRef, MAT_DIALOG_DATA, MatDialog} from '@angular/material/dialog'; -import {TaskAssessmentModalComponent} from './task-assessment-modal.component'; +import {MatDialog} from '@angular/material/dialog'; import {Task} from 'src/app/api/models/task'; +import {TaskAssessmentModalComponent} from './task-assessment-modal.component'; export interface TaskAssessmentModalData { task: Task; diff --git a/src/app/common/modals/tutor-notes-modal/tutor-notes-modal.component.html b/src/app/common/modals/tutor-notes-modal/tutor-notes-modal.component.html index 56d9e9acc3..176eebc049 100644 --- a/src/app/common/modals/tutor-notes-modal/tutor-notes-modal.component.html +++ b/src/app/common/modals/tutor-notes-modal/tutor-notes-modal.component.html @@ -1,5 +1,5 @@ diff --git a/src/app/common/modals/tutor-notes-modal/tutor-notes-modal.component.ts b/src/app/common/modals/tutor-notes-modal/tutor-notes-modal.component.ts index 70cf2c78af..bb34f4495f 100644 --- a/src/app/common/modals/tutor-notes-modal/tutor-notes-modal.component.ts +++ b/src/app/common/modals/tutor-notes-modal/tutor-notes-modal.component.ts @@ -1,4 +1,4 @@ -import {Component, Inject, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Inject, OnInit} from '@angular/core'; import {MAT_DIALOG_DATA} from '@angular/material/dialog'; import {Task} from 'src/app/api/models/task'; import {UnitRole} from 'src/app/api/models/unit-role'; @@ -8,6 +8,8 @@ import {TutorNotesModalData} from './tutor-notes-modal.service'; selector: 'f-tutor-notes-modal', templateUrl: './tutor-notes-modal.component.html', styleUrl: './tutor-notes-modal.component.scss', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TutorNotesModalComponent implements OnInit { constructor(@Inject(MAT_DIALOG_DATA) public data: TutorNotesModalData) {} diff --git a/src/app/common/obect-select/object-select.component.html b/src/app/common/obect-select/object-select.component.html index a553ccd7cd..a94a68f337 100644 --- a/src/app/common/obect-select/object-select.component.html +++ b/src/app/common/obect-select/object-select.component.html @@ -1,21 +1,22 @@ @if (label) { - {{ label }} + {{ label }} } @if (placeholder) { - - {{ placeholder }} - - } @for (element of source; track element) { - - {{ element.text }} - + + {{ placeholder }} + + } + @for (element of source; track element) { + + {{ element.text }} + } diff --git a/src/app/common/obect-select/object-select.component.ts b/src/app/common/obect-select/object-select.component.ts index 278925d7a3..386429f338 100644 --- a/src/app/common/obect-select/object-select.component.ts +++ b/src/app/common/obect-select/object-select.component.ts @@ -1,5 +1,5 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { MatSelectChange } from '@angular/material/select'; +import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core'; +import {MatSelectChange} from '@angular/material/select'; /** * Object select component used to overcome limitations with the angularjs version used. @@ -9,14 +9,15 @@ import { MatSelectChange } from '@angular/material/select'; @Component({ selector: 'object-select', templateUrl: 'object-select.component.html', - // styleUrls: ['object-select.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class ObjectSelectComponent { - @Input() source: { value: T; text: string }[]; + @Input() source: {value: T; text: string}[]; @Input() target: T; @Input() label: string; @Input() placeholder: string = null; - @Output() targetChange = new EventEmitter(); + @Output() targetChange: EventEmitter = new EventEmitter(); selectionChange($event: MatSelectChange) { this.target = $event.value; diff --git a/src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.html b/src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.html index ef5d4765ef..2e65ecfe24 100644 --- a/src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.html +++ b/src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.html @@ -5,19 +5,21 @@
@if (!hideFooter) { - diff --git a/src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.scss b/src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.scss index 96d34e7765..1a00bb118f 100644 --- a/src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.scss +++ b/src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.scss @@ -1,4 +1,4 @@ -@import '../../../styles/common/doubtfire-panel.scss'; +@use 'styles/common/doubtfire-panel' as *; .pdf-viewer-panel { .pdf-viewer-body { diff --git a/src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.spec.ts b/src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.spec.ts index d9b08196f1..ff24c419cf 100644 --- a/src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.spec.ts +++ b/src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.spec.ts @@ -1,28 +1,28 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { HttpClientModule } from '@angular/common/http'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {FileDownloaderService} from '../file-downloader/file-downloader.service'; +import {PdfViewerPanelComponent} from './pdf-viewer-panel.component'; -import { FileDownloaderService } from '../file-downloader/file-downloader.service'; -import { PdfViewerPanelComponent } from './pdf-viewer-panel.component'; +const emptyProvider = {}; describe('PdfViewerPanelComponent', () => { let component: PdfViewerPanelComponent; let fixture: ComponentFixture; - let fileDownloaderServiceStub: Partial; - beforeEach( - waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [PdfViewerPanelComponent], - imports: [HttpClientModule], - providers: [{ provide: FileDownloaderService, useValue: fileDownloaderServiceStub }], - }).compileComponents(); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [PdfViewerPanelComponent], + providers: [{provide: FileDownloaderService, useValue: emptyProvider}], + schemas: [NO_ERRORS_SCHEMA], }) - ); + .overrideComponent(PdfViewerPanelComponent, {set: {template: ''}}) + .compileComponents(); + }); beforeEach(() => { fixture = TestBed.createComponent(PdfViewerPanelComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.ts b/src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.ts index 1d5a948ef4..5fa911c55a 100644 --- a/src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.ts +++ b/src/app/common/pdf-viewer-panel/pdf-viewer-panel.component.ts @@ -1,22 +1,22 @@ -import { Component, OnInit, Input, Inject } from '@angular/core'; -import { FileDownloaderService } from '../file-downloader/file-downloader.service'; +import {ChangeDetectionStrategy, Component, Inject, Input} from '@angular/core'; +import {FileDownloaderService} from '../file-downloader/file-downloader.service'; @Component({ selector: 'pdf-viewer-panel', templateUrl: './pdf-viewer-panel.component.html', styleUrls: ['./pdf-viewer-panel.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class PdfViewerPanelComponent implements OnInit { +export class PdfViewerPanelComponent { @Input() pdfUrl: string; @Input() footerText: string; @Input() resourcesUrl: string; @Input() hideFooter: boolean; constructor(@Inject(FileDownloaderService) private fileDownloader: FileDownloaderService) {} - ngOnInit(): void {} - downloadPdf() { - this.fileDownloader.downloadFile(this.pdfUrl + "?as_attachment=true", 'displayed-pdf.pdf'); + this.fileDownloader.downloadFile(this.pdfUrl + '?as_attachment=true', 'displayed-pdf.pdf'); } downloadResources() { diff --git a/src/app/common/pdf-viewer/pdf-viewer.component.html b/src/app/common/pdf-viewer/pdf-viewer.component.html index 268190c700..c509ce5fdd 100644 --- a/src/app/common/pdf-viewer/pdf-viewer.component.html +++ b/src/app/common/pdf-viewer/pdf-viewer.component.html @@ -1,45 +1,45 @@
- -
- + search @@ -50,20 +50,22 @@ @if (pdfBlobUrl) { @if (useNativePdfViewer) { - + @if (pdfBlobUrl) { + PDF Preview + } } @else { } } @else { - + }
diff --git a/src/app/common/pdf-viewer/pdf-viewer.component.scss b/src/app/common/pdf-viewer/pdf-viewer.component.scss index da9fc20f12..b5787040ba 100644 --- a/src/app/common/pdf-viewer/pdf-viewer.component.scss +++ b/src/app/common/pdf-viewer/pdf-viewer.component.scss @@ -1,4 +1,4 @@ -@import '../../../styles/mixins/scrollable.scss'; +@use 'styles/mixins/scrollable' as *; #pdfContainer { flex-direction: column; diff --git a/src/app/common/pdf-viewer/pdf-viewer.component.ts b/src/app/common/pdf-viewer/pdf-viewer.component.ts index 97c4ff5100..a8a2de519f 100644 --- a/src/app/common/pdf-viewer/pdf-viewer.component.ts +++ b/src/app/common/pdf-viewer/pdf-viewer.component.ts @@ -1,6 +1,8 @@ +import {PDFDocumentProxy, PdfViewerComponent} from 'ng2-pdf-viewer'; import {HttpResponse} from '@angular/common/http'; import { AfterViewInit, + ChangeDetectionStrategy, Component, Inject, Input, @@ -9,7 +11,6 @@ import { SimpleChanges, ViewChild, } from '@angular/core'; -import {PDFDocumentProxy, PdfViewerComponent} from 'ng2-pdf-viewer'; import {FileDownloaderService} from '../file-downloader/file-downloader.service'; import {AlertService} from '../services/alert.service'; @@ -17,6 +18,8 @@ import {AlertService} from '../services/alert.service'; selector: 'f-pdf-viewer', templateUrl: './pdf-viewer.component.html', styleUrls: ['./pdf-viewer.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class fPdfViewerComponent implements OnDestroy, OnChanges, AfterViewInit { private readonly ZOOM_MIN = 0.5; diff --git a/src/app/common/pipes/humanized-date.pipe.spec.ts b/src/app/common/pipes/humanized-date.pipe.spec.ts index 0698daa95c..62839a0c26 100644 --- a/src/app/common/pipes/humanized-date.pipe.spec.ts +++ b/src/app/common/pipes/humanized-date.pipe.spec.ts @@ -1,4 +1,5 @@ -import { HumanizedDatePipe } from './humanized-date.pipe'; +import {describe, expect, it} from 'vitest'; +import {HumanizedDatePipe} from './humanized-date.pipe'; describe('HumanizedDatePipe', () => { it('create an instance', () => { diff --git a/src/app/common/pipes/humanized-date.pipe.ts b/src/app/common/pipes/humanized-date.pipe.ts index fcffa6ae65..7341035833 100644 --- a/src/app/common/pipes/humanized-date.pipe.ts +++ b/src/app/common/pipes/humanized-date.pipe.ts @@ -1,13 +1,14 @@ -import { Pipe, PipeTransform } from '@angular/core'; import moment from 'moment'; +import {Pipe, PipeTransform} from '@angular/core'; @Pipe({ name: 'humanizedDate', + standalone: false, }) export class HumanizedDatePipe implements PipeTransform { - transform(value: unknown, ...args: unknown[]): unknown { + transform(value: unknown, ..._args: unknown[]): string { if (value == null) { - return; + return ''; } return moment(value).calendar(null, { sameDay: '', diff --git a/src/app/common/pipes/is-active-unit-role.pipe.ts b/src/app/common/pipes/is-active-unit-role.pipe.ts index d6675aac9d..e78f149260 100644 --- a/src/app/common/pipes/is-active-unit-role.pipe.ts +++ b/src/app/common/pipes/is-active-unit-role.pipe.ts @@ -1,11 +1,12 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import { UnitRole } from 'src/app/api/models/unit-role'; +import {Pipe, PipeTransform} from '@angular/core'; +import {UnitRole} from 'src/app/api/models/unit-role'; @Pipe({ name: 'isActiveUnitRole', + standalone: false, }) export class IsActiveUnitRole implements PipeTransform { - transform(array: UnitRole[], ...args: any[]): UnitRole[] { + transform(array: UnitRole[]): UnitRole[] { if (array == null) { return; } diff --git a/src/app/common/pipes/localized-date.pipe.ts b/src/app/common/pipes/localized-date.pipe.ts index e7eceb2baf..1cec7ca02a 100644 --- a/src/app/common/pipes/localized-date.pipe.ts +++ b/src/app/common/pipes/localized-date.pipe.ts @@ -2,6 +2,7 @@ import {Pipe, PipeTransform} from '@angular/core'; @Pipe({ name: 'localizedDateTime', + standalone: false, }) export class LocalizedDatePipe implements PipeTransform { transform(value: string | Date): string { diff --git a/src/app/common/pipes/marked.pipe.spec.ts b/src/app/common/pipes/marked.pipe.spec.ts index 4173514ebc..5ac4060da5 100644 --- a/src/app/common/pipes/marked.pipe.spec.ts +++ b/src/app/common/pipes/marked.pipe.spec.ts @@ -1,4 +1,5 @@ -import { MarkedPipe } from './marked.pipe'; +import {describe, expect, it} from 'vitest'; +import {MarkedPipe} from './marked.pipe'; describe('MarkedPipe', () => { it('create an instance', () => { diff --git a/src/app/common/pipes/marked.pipe.ts b/src/app/common/pipes/marked.pipe.ts index 1f19fa6b26..3a0e09f2f3 100644 --- a/src/app/common/pipes/marked.pipe.ts +++ b/src/app/common/pipes/marked.pipe.ts @@ -1,8 +1,9 @@ -import {Pipe, PipeTransform} from '@angular/core'; import * as marked from 'marked'; +import {Pipe, PipeTransform} from '@angular/core'; @Pipe({ name: 'marked', + standalone: false, }) export class MarkedPipe implements PipeTransform { // Set the options for the markdown renderer @@ -15,9 +16,9 @@ export class MarkedPipe implements PipeTransform { }); } - transform(value: string, ...args: any[]): string { + transform(value: string): string { if (value && value.length > 0) { - return marked.parse(value.replaceAll(/\r\n|\r|\n/g, '
'), {async: false}); + return marked.parse(value.replaceAll(/\r\n|\r|\n/g, '
'), {async: false}) as string; } return value; } diff --git a/src/app/common/pipes/safe.pipe.spec.ts b/src/app/common/pipes/safe.pipe.spec.ts index 283c5ece0b..1a1332fcd4 100644 --- a/src/app/common/pipes/safe.pipe.spec.ts +++ b/src/app/common/pipes/safe.pipe.spec.ts @@ -1,7 +1,7 @@ -import { Sanitizer } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; -import { SafePipe } from './safe.pipe'; +import {describe, it} from 'vitest'; describe('SafePipe', () => { - it('create an instance', () => {}); + it('create an instance', () => { + /* empty */ + }); }); diff --git a/src/app/common/pipes/safe.pipe.ts b/src/app/common/pipes/safe.pipe.ts index 41044b40a5..27fd6a6c11 100644 --- a/src/app/common/pipes/safe.pipe.ts +++ b/src/app/common/pipes/safe.pipe.ts @@ -1,7 +1,10 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; +import {Pipe, PipeTransform} from '@angular/core'; +import {DomSanitizer} from '@angular/platform-browser'; -@Pipe({ name: 'safe' }) +@Pipe({ + name: 'safe', + standalone: false, +}) export class SafePipe implements PipeTransform { constructor(private sanitizer: DomSanitizer) {} transform(url: string) { diff --git a/src/app/common/project-progress-bar/project-progress-bar.component.spec.ts b/src/app/common/project-progress-bar/project-progress-bar.component.spec.ts index 648e4d54c0..0bd437cb91 100644 --- a/src/app/common/project-progress-bar/project-progress-bar.component.spec.ts +++ b/src/app/common/project-progress-bar/project-progress-bar.component.spec.ts @@ -1,6 +1,7 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ProjectProgressBarComponent } from './project-progress-bar.component'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {ProjectProgressBarComponent} from './project-progress-bar.component'; describe('ProjectProgressBarComponent', () => { let component: ProjectProgressBarComponent; @@ -8,13 +9,16 @@ describe('ProjectProgressBarComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ ProjectProgressBarComponent ] + declarations: [ProjectProgressBarComponent], + schemas: [NO_ERRORS_SCHEMA], }) - .compileComponents(); + .overrideComponent(ProjectProgressBarComponent, {set: {template: ''}}) + .compileComponents(); + }); + beforeEach(() => { fixture = TestBed.createComponent(ProjectProgressBarComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/common/project-progress-bar/project-progress-bar.component.ts b/src/app/common/project-progress-bar/project-progress-bar.component.ts index ff9b79ff73..d1935aa561 100644 --- a/src/app/common/project-progress-bar/project-progress-bar.component.ts +++ b/src/app/common/project-progress-bar/project-progress-bar.component.ts @@ -1,17 +1,16 @@ -import { Component, Input, OnInit, SimpleChanges } from '@angular/core'; -import { Project } from 'src/app/api/models/project'; +import {ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges} from '@angular/core'; @Component({ selector: 'f-project-progress-bar', templateUrl: './project-progress-bar.component.html', styleUrls: ['./project-progress-bar.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class ProjectProgressBarComponent { - @Input() progress: any[]; +export class ProjectProgressBarComponent implements OnChanges { + @Input() progress: {value: number}[]; public percentProgress: number = 0; - constructor() {} - ngOnChanges(changes: SimpleChanges) { if (changes.progress) { if (changes.progress.currentValue) { diff --git a/src/app/common/project-progress/project-progress-gauge.component.css b/src/app/common/project-progress/project-progress-gauge.component.css new file mode 100644 index 0000000000..5d4e87f30f --- /dev/null +++ b/src/app/common/project-progress/project-progress-gauge.component.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/src/app/common/project-progress/project-progress-gauge.component.html b/src/app/common/project-progress/project-progress-gauge.component.html new file mode 100644 index 0000000000..f6543fc09a --- /dev/null +++ b/src/app/common/project-progress/project-progress-gauge.component.html @@ -0,0 +1,18 @@ + + diff --git a/src/app/common/project-progress/project-progress-gauge.component.ts b/src/app/common/project-progress/project-progress-gauge.component.ts new file mode 100644 index 0000000000..40e3f9a2f0 --- /dev/null +++ b/src/app/common/project-progress/project-progress-gauge.component.ts @@ -0,0 +1,43 @@ +import {LegendPosition} from '@swimlane/ngx-charts'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {Project} from 'src/app/api/models/project'; + +@Component({ + selector: 'f-project-progress-gauge', + templateUrl: './project-progress-gauge.component.html', + styleUrl: './project-progress-gauge.component.css', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class ProjectProgressGaugeComponent { + @Input() project: Project; + + protected gaugeData = [ + { + 'name': 'Pass', + 'value': 100, + }, + { + 'name': 'Credit', + 'value': 79, + }, + { + 'name': 'Distinction', + 'value': 40, + }, + { + 'name': 'HD', + 'value': 19, + }, + ]; + + smallView: [number, number] = [90, 90]; + view: [number, number] = [500, 500]; + legend: boolean = true; + legendPosition: LegendPosition = LegendPosition.Below; + rightLegendPosition: LegendPosition = LegendPosition.Right; + + colorScheme = { + domain: ['#5AA454', '#E44D25', '#CFC0BB', '#7aa3e5', '#a8385d', '#aae3f5'], + }; +} diff --git a/src/app/common/scorm-player/scorm-player.component.html b/src/app/common/scorm-player/scorm-player.component.html index 990a9ef4a7..0e36d95f4f 100644 --- a/src/app/common/scorm-player/scorm-player.component.html +++ b/src/app/common/scorm-player/scorm-player.component.html @@ -1 +1,3 @@ - +@if (iframeSrc) { + +} diff --git a/src/app/common/scorm-player/scorm-player.component.spec.ts b/src/app/common/scorm-player/scorm-player.component.spec.ts index 7980df7c3c..677d27327f 100644 --- a/src/app/common/scorm-player/scorm-player.component.spec.ts +++ b/src/app/common/scorm-player/scorm-player.component.spec.ts @@ -1,6 +1,14 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {DomSanitizer} from '@angular/platform-browser'; +import {ActivatedRoute} from '@angular/router'; +import {AuthenticationService, UserService} from 'src/app/api/models/doubtfire-model'; +import {ScormAdapterService} from 'src/app/api/services/scorm-adapter.service'; +import {GlobalStateService} from 'src/app/projects/states/index/global-state.service'; +import {ScormPlayerComponent} from './scorm-player.component'; -import { ScormPlayerComponent } from './scorm-player.component'; +const emptyProvider = {}; describe('ScormPlayerComponent', () => { let component: ScormPlayerComponent; @@ -8,13 +16,24 @@ describe('ScormPlayerComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ ScormPlayerComponent ] + declarations: [ScormPlayerComponent], + providers: [ + {provide: GlobalStateService, useValue: emptyProvider}, + {provide: ScormAdapterService, useValue: emptyProvider}, + {provide: UserService, useValue: emptyProvider}, + {provide: AuthenticationService, useValue: emptyProvider}, + {provide: DomSanitizer, useValue: emptyProvider}, + {provide: ActivatedRoute, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], }) - .compileComponents(); + .overrideComponent(ScormPlayerComponent, {set: {template: ''}}) + .compileComponents(); + }); + beforeEach(() => { fixture = TestBed.createComponent(ScormPlayerComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/common/scorm-player/scorm-player.component.ts b/src/app/common/scorm-player/scorm-player.component.ts index 6c2bdadd20..2673912134 100644 --- a/src/app/common/scorm-player/scorm-player.component.ts +++ b/src/app/common/scorm-player/scorm-player.component.ts @@ -1,5 +1,6 @@ -import {Component, OnInit, Input, HostListener} from '@angular/core'; +import {ChangeDetectionStrategy, Component, HostListener, Input, OnInit} from '@angular/core'; import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser'; +import {ActivatedRoute} from '@angular/router'; import { AuthenticationService, ScormPlayerContext, @@ -29,6 +30,8 @@ declare global { selector: 'f-scorm-player', templateUrl: './scorm-player.component.html', styleUrls: ['./scorm-player.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class ScormPlayerComponent implements OnInit { context: ScormPlayerContext; @@ -53,12 +56,24 @@ export class ScormPlayerComponent implements OnInit { private userService: UserService, private authService: AuthenticationService, private sanitizer: DomSanitizer, + private route: ActivatedRoute, ) {} ngOnInit(): void { + this.projectId = this.projectId ?? Number(this.route.snapshot.paramMap.get('projectId')); + this.taskDefId = this.taskDefId ?? Number(this.route.snapshot.paramMap.get('taskDefId')); + this.testAttemptId = + this.testAttemptId ?? Number(this.route.snapshot.paramMap.get('testAttemptId')); + this.mode = this.mode ?? (this.route.snapshot.data.mode as ScormPlayerComponent['mode']); + this.globalState.setView(ViewType.OTHER); this.globalState.hideHeader(); - this.authService.getScormToken().subscribe((value: string) => this.setupScorm(value)); + this.authService.afterAuthCall((result) => { + if (!result) { + return; + } + this.authService.getScormToken().subscribe((value: string) => this.setupScorm(value)); + }); } private setupScorm(token: string): void { diff --git a/src/app/common/services/alert-service.service.spec.ts b/src/app/common/services/alert-service.service.spec.ts index e2a7a5e972..72cf8816c6 100644 --- a/src/app/common/services/alert-service.service.spec.ts +++ b/src/app/common/services/alert-service.service.spec.ts @@ -1,6 +1,6 @@ -import { TestBed } from '@angular/core/testing'; - -import { AlertService } from './alert.service'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {TestBed} from '@angular/core/testing'; +import {AlertService} from './alert.service'; describe('AlertServiceService', () => { let service: AlertService; diff --git a/src/app/common/services/alert.component.html b/src/app/common/services/alert.component.html new file mode 100644 index 0000000000..e565e82c95 --- /dev/null +++ b/src/app/common/services/alert.component.html @@ -0,0 +1,10 @@ + + + {{ data?.icon }} + + {{ data?.message }} + + + + + diff --git a/src/app/common/services/alert.service.ts b/src/app/common/services/alert.service.ts index bd14ae14b3..bf27c6f386 100644 --- a/src/app/common/services/alert.service.ts +++ b/src/app/common/services/alert.service.ts @@ -1,8 +1,12 @@ -import {Component, Inject, Injectable, inject} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Inject, Injectable, inject} from '@angular/core'; import {MAT_SNACK_BAR_DATA, MatSnackBar, MatSnackBarRef} from '@angular/material/snack-bar'; - import {ConfettiService} from './confetti.service'; +interface AlertData { + message: string; + icon: string; +} + @Injectable({ providedIn: 'root', }) @@ -44,18 +48,11 @@ export class AlertService { @Component({ selector: 'f-alert', - template: ` - - {{ data?.icon }} - - {{ data?.message }} - - - - `, + templateUrl: './alert.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class AlertComponent { snackBarRef = inject(MatSnackBarRef); - constructor(@Inject(MAT_SNACK_BAR_DATA) public data: any) {} + constructor(@Inject(MAT_SNACK_BAR_DATA) public data: AlertData) {} } diff --git a/src/app/common/services/analytics-service.coffee b/src/app/common/services/analytics-service.coffee deleted file mode 100644 index 971fc0f37a..0000000000 --- a/src/app/common/services/analytics-service.coffee +++ /dev/null @@ -1,42 +0,0 @@ -angular.module("doubtfire.common.services.analytics", []) -# -# Services for analytics -# -.factory("analyticsService", ($analytics, newUserService) -> - analyticsService = {} - - # - # Logs a new event with the specified category and event name - # - # For consistency, use like this: - # category: 'Visualisations' (Pluralised) - # eventName: 'Refreshed All' (Past-Tense) - # - # Label is optional and should be a string - # Value is optional and must be a positive numerical value - # - analyticsService.event = (category, eventName, label, value) -> - # Critical! Don't log unless user has opted in - # Do not remove this as we'd be breaching the law! - return unless newUserService.currentUser.optInToResearch - - if value? and typeof value isnt 'number' and value < 0 - throw new Error "Value needs to be a positive number" - $analytics.eventTrack eventName, { - category: category - label: label - value: value - } - - analyticsService.watchEvent = ( scope, toWatch, category, label) -> - scope.$watch toWatch, (newVal, oldVal) -> - if newVal? && newVal != oldVal - if _.isFunction label - analyticsService.event category, "Changed #{toWatch}", label(newVal) - else if _.isInteger newVal - analyticsService.event category, "Changed #{toWatch}", label, newVal - else - analyticsService.event category, "Changed #{toWatch}", newVal - - analyticsService -) diff --git a/src/app/common/services/comment-draft.service.ts b/src/app/common/services/comment-draft.service.ts index 3394435564..26f4d51f64 100644 --- a/src/app/common/services/comment-draft.service.ts +++ b/src/app/common/services/comment-draft.service.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; -import { Observable, of } from 'rxjs'; +import {Injectable} from '@angular/core'; +import {Observable, of} from 'rxjs'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class CommentDraftService { private readonly PREFIX = 'df_comment_draft_'; // Prefix for localStorage keys diff --git a/src/app/common/services/confetti.service.spec.ts b/src/app/common/services/confetti.service.spec.ts index 803fa8a931..5adbef2d05 100644 --- a/src/app/common/services/confetti.service.spec.ts +++ b/src/app/common/services/confetti.service.spec.ts @@ -1,6 +1,6 @@ -import { TestBed } from '@angular/core/testing'; - -import { ConfettiService } from './confetti.service'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {TestBed} from '@angular/core/testing'; +import {ConfettiService} from './confetti.service'; describe('ConfettiService', () => { let service: ConfettiService; diff --git a/src/app/common/services/confetti.service.ts b/src/app/common/services/confetti.service.ts index d65a3f5eb6..92a51c8ad2 100644 --- a/src/app/common/services/confetti.service.ts +++ b/src/app/common/services/confetti.service.ts @@ -1,18 +1,16 @@ -import { Injectable } from '@angular/core'; import confetti from 'canvas-confetti'; +import {Injectable} from '@angular/core'; @Injectable({ providedIn: 'root', }) export class ConfettiService { - constructor() {} - public canon(x: number = 0, y: number = 0, angle = 210): void { confetti({ angle: angle, spread: 80, particleCount: 100, - origin: { y: y, x: x }, + origin: {y: y, x: x}, }); } } diff --git a/src/app/common/services/date-service.coffee b/src/app/common/services/date-service.coffee deleted file mode 100644 index 10cffe1980..0000000000 --- a/src/app/common/services/date-service.coffee +++ /dev/null @@ -1,31 +0,0 @@ -angular.module("doubtfire.common.services.dates", []) -# -# Services for making alerts -# -.factory("dateService", -> - - dateService = {} - - monthNames = [ - "Jan", "Feb", "Mar", - "Apr", "May", "Jun", "Jul", - "Aug", "Sep", "Oct", - "Nov", "Dec" - ] - - dateService.showDate = (dateValue) -> - if (dateValue?) - date = new Date(dateValue) - "#{monthNames[date.getMonth()]} #{date.getFullYear()}" - else - "-" - - dateService.showFullDate = (dateValue) -> - if (dateValue?) - date = new Date(dateValue) - "#{date.getDate()} #{monthNames[date.getMonth()]} #{date.getFullYear()}" - else - "-" - - dateService -) diff --git a/src/app/common/services/date.service.spec.ts b/src/app/common/services/date.service.spec.ts new file mode 100644 index 0000000000..9f4df416bb --- /dev/null +++ b/src/app/common/services/date.service.spec.ts @@ -0,0 +1,16 @@ +import {beforeEach, describe, expect, it} from 'vitest'; +import {TestBed} from '@angular/core/testing'; +import {DateService} from './date.service'; + +describe('DateService', () => { + let service: DateService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(DateService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/common/services/date.service.ts b/src/app/common/services/date.service.ts new file mode 100644 index 0000000000..915fd96fcc --- /dev/null +++ b/src/app/common/services/date.service.ts @@ -0,0 +1,69 @@ +import {Injectable} from '@angular/core'; + +@Injectable({ + providedIn: 'root', // Available throughout the app. +}) +export class DateService { + private monthNames: string[] = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + + constructor() { + // Bind the methods to ensure `this` context is correct + this.showDate = this.showDate.bind(this); + this.showFullDate = this.showFullDate.bind(this); + } + + /** + * Returns a dateString for the passed-in `date`, + * in the format of `MMM-YYYY`. + * + * Note: If you are going to pass in a `dateString`, please ensure it is in + * `ISO 8601` format. + * + * @param {string | Date} dateValue + * + * @returns {string} + */ + showDate(dateValue?: string | Date): string { + if (dateValue) { + const date = new Date(dateValue); + + return `${this.monthNames[date.getMonth()]} ${date.getFullYear()}`; + } else { + return '-'; + } + } + + /** + * Returns a dateString for the passed-in `date`, + * in the format of `D-MM-YYYY`. + * + * Note: If you are going to pass in a `dateString`, please ensure it is in + * `ISO 8601` format. + * + * @param {string | Date} dateValue + * + * @returns {string} + */ + showFullDate(dateValue?: string | Date): string { + if (dateValue) { + const date = new Date(dateValue); + + return `${date.getDate()} ${this.monthNames[date.getMonth()]} ${date.getFullYear()}`; + } else { + return '-'; + } + } +} diff --git a/src/app/common/services/emoji.service.spec.ts b/src/app/common/services/emoji.service.spec.ts new file mode 100644 index 0000000000..7361195639 --- /dev/null +++ b/src/app/common/services/emoji.service.spec.ts @@ -0,0 +1,24 @@ +import {EmojiSearch} from '@ctrl/ngx-emoji-mart'; +import {describe, expect, it} from 'vitest'; +import {EmojiService} from './emoji.service'; + +describe('EmojiService', () => { + const emojiSearch = { + emojisList: { + v: {colons: ':v:', native: '✌️'}, + thumbsup: {colons: ':thumbsup:', native: '👍'}, + }, + } as unknown as EmojiSearch; + + const service = new EmojiService(emojiSearch); + + it('does not convert slash-delimited SharePoint URL path segments to emoji', () => { + const url = 'https://example.com/:v:/g/abc123'; + + expect(service.colonsToNative(url)).toBe(url); + }); + + it('still converts emoji shortcodes in comment text to native emoji', () => { + expect(service.colonsToNative('Nice work :thumbsup:')).toBe('Nice work 👍'); + }); +}); diff --git a/src/app/common/services/emoji.service.ts b/src/app/common/services/emoji.service.ts index 28fbf0a6c7..3e129c3d42 100644 --- a/src/app/common/services/emoji.service.ts +++ b/src/app/common/services/emoji.service.ts @@ -1,17 +1,17 @@ -/* eslint-disable @typescript-eslint/no-inferrable-types */ -import { Injectable } from '@angular/core'; -import { EmojiSearch } from '@ctrl/ngx-emoji-mart'; -import { EmojiData } from '@ctrl/ngx-emoji-mart/ngx-emoji'; +import {EmojiSearch} from '@ctrl/ngx-emoji-mart'; +import {EmojiData} from '@ctrl/ngx-emoji-mart/ngx-emoji'; +import {Injectable} from '@angular/core'; @Injectable({ providedIn: 'root', }) export class EmojiService { // DO NOT CHANGE THIS LINE EVEN WITH AN ESLINT RULE - // eslint-disable-next-line max-len, prettier/prettier + // prettier-ignore + // eslint-disable-next-line max-len, no-misleading-character-class emojiMatch: RegExp = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|[\ud83c\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c\ude32-\ude3a]|[\ud83c\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/; // eslint-disable-next-line no-useless-escape - colonPairMatch: RegExp = /(\:)(\w|\+|\-)+(\:)/; + colonPairMatch: RegExp = /(^|[^\w/])(:[\w+-]+:)(?!\/)/; constructor(private emojiSearch: EmojiSearch) {} @@ -34,6 +34,7 @@ export class EmojiService { const emojiList: EmojiData[] = Object.values(this.emojiSearch.emojisList); let result = emojiList.find((e) => e.colons === emojiString)?.native; if (result === undefined) { + // eslint-disable-next-line no-useless-escape result = emojiString.replace(/\:/, '<><>'); } return result; @@ -47,12 +48,16 @@ export class EmojiService { let replaced = true; while (replaced) { replaced = false; - s = s.replace(this.colonPairMatch, (matched: string, p1?: string) => { - replaced = true; - return this.mapStringToEmoji(matched); - }); + s = s.replace( + this.colonPairMatch, + (_matched: string, prefix: string, emojiString: string) => { + replaced = true; + return `${prefix}${this.mapStringToEmoji(emojiString)}`; + }, + ); } - const result = s.replace(/\<\>\<\>/, ':'); + // eslint-disable-next-line no-useless-escape + const result = s.replaceAll('<><>', ':'); return result; } @@ -64,7 +69,7 @@ export class EmojiService { let replaced = true; while (replaced) { replaced = false; - s = s.replace(this.emojiMatch, (matched: string, p1?: string) => { + s = s.replace(this.emojiMatch, (matched: string, _p1?: string) => { replaced = true; return this.mapEmojiToString(matched); }); diff --git a/src/app/common/services/grade.service.spec.ts b/src/app/common/services/grade.service.spec.ts index 640228e3c3..86789ec7d8 100644 --- a/src/app/common/services/grade.service.spec.ts +++ b/src/app/common/services/grade.service.spec.ts @@ -1,6 +1,6 @@ -import { TestBed } from '@angular/core/testing'; - -import { GradeService } from './grade.service'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {TestBed} from '@angular/core/testing'; +import {GradeService} from './grade.service'; describe('GradeService', () => { let service: GradeService; @@ -13,4 +13,19 @@ describe('GradeService', () => { it('should be created', () => { expect(service).toBeTruthy(); }); + + it('uses all target grades when a unit has no custom configuration', () => { + expect(service.gradeValuesFor()).toEqual([0, 1, 2, 3]); + }); + + it('uses the grades enabled for a unit', () => { + const unit = {gradeValues: [0]}; + + expect(service.gradeValuesFor(unit)).toEqual([0]); + expect(service.allGradeValuesFor(unit)).toEqual([-1, 0]); + expect(service.gradeViewDataFor(unit, true)).toEqual([ + {value: -1, viewValue: 'Fail'}, + {value: 0, viewValue: 'Pass'}, + ]); + }); }); diff --git a/src/app/common/services/grade.service.ts b/src/app/common/services/grade.service.ts index d2c0ee6213..e0242e123a 100644 --- a/src/app/common/services/grade.service.ts +++ b/src/app/common/services/grade.service.ts @@ -1,14 +1,27 @@ import {Injectable} from '@angular/core'; -import {Project} from 'src/app/api/models/project'; +import type {GradeDefinition} from 'src/app/api/models/unit'; + +interface UnitGradeConfiguration { + gradeValues?: number[]; + gradeDefinitions?: GradeDefinition[]; +} @Injectable({ providedIn: 'root', }) export class GradeService { + public readonly defaultGradeDefinitions: GradeDefinition[] = [ + {id: 'fail', value: -1, label: 'Fail', abbreviation: 'F'}, + {id: 'pass', value: 0, label: 'Pass', abbreviation: 'P'}, + {id: 'credit', value: 1, label: 'Credit', abbreviation: 'C'}, + {id: 'distinction', value: 2, label: 'Distinction', abbreviation: 'D'}, + {id: 'high-distinction', value: 3, label: 'High Distinction', abbreviation: 'HD'}, + ]; + allGradeValues = [-1, 0, 1, 2, 3]; gradeValues = [0, 1, 2, 3]; - grades = { + public grades = { '-1': 'Fail', 0: 'Pass', 1: 'Credit', @@ -16,7 +29,7 @@ export class GradeService { 3: 'High Distinction', }; - gradeIndex = { + public gradeIndex = { Fail: -1, Pass: 0, Credit: 1, @@ -24,7 +37,7 @@ export class GradeService { 'High Distinction': 3, }; - gradeViewData = [ + public gradeViewData = [ {value: -1, viewValue: 'Fail'}, {value: 0, viewValue: 'Pass'}, {value: 1, viewValue: 'Credit'}, @@ -71,7 +84,57 @@ export class GradeService { HD: '#80FF00', }; - public stringToGrade(value: string): number { - return this.gradeIndex[value]; + public stringToGrade(value: string, unit?: UnitGradeConfiguration): number { + return ( + this.gradeDefinitionsFor(unit).find((definition) => definition.label === value)?.value ?? + this.gradeIndex[value] + ); + } + + public gradeDefinitionsFor(unit?: UnitGradeConfiguration): GradeDefinition[] { + if (unit?.gradeDefinitions?.length) { + return unit.gradeDefinitions; + } + + if (unit?.gradeValues?.length) { + return this.defaultGradeDefinitions.filter( + (definition) => definition.value === -1 || unit.gradeValues.includes(definition.value), + ); + } + + return this.defaultGradeDefinitions; + } + + public gradeValuesFor(unit?: UnitGradeConfiguration): number[] { + return this.gradeDefinitionsFor(unit) + .filter((definition) => definition.value >= 0) + .map((definition) => definition.value); + } + + public allGradeValuesFor(unit?: UnitGradeConfiguration): number[] { + return [-1, ...this.gradeValuesFor(unit)]; + } + + public gradeViewDataFor( + unit?: UnitGradeConfiguration, + includeFail: boolean = false, + ): {value: number; viewValue: string}[] { + return this.gradeDefinitionsFor(unit) + .filter((definition) => includeFail || definition.value >= 0) + .map((definition) => ({value: definition.value, viewValue: definition.label})); + } + + public gradeLabel(value: number, unit?: UnitGradeConfiguration): string { + return ( + this.gradeDefinitionsFor(unit).find((definition) => definition.value === value)?.label ?? + this.grades[value] + ); + } + + public gradeAbbreviation(value: number, unit?: UnitGradeConfiguration): string { + return ( + this.gradeDefinitionsFor(unit).find((definition) => definition.value === value) + ?.abbreviation ?? this.gradeAcronyms[value] + ); } } diff --git a/src/app/common/services/http-authentication.interceptor.ts b/src/app/common/services/http-authentication.interceptor.ts index 920d47f4dd..359495a24d 100644 --- a/src/app/common/services/http-authentication.interceptor.ts +++ b/src/app/common/services/http-authentication.interceptor.ts @@ -1,15 +1,18 @@ +import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'; import {Injectable} from '@angular/core'; -import {HttpRequest, HttpHandler, HttpEvent, HttpInterceptor} from '@angular/common/http'; import {Observable} from 'rxjs'; -import API_URL from 'src/app/config/constants/apiUrl'; import {UserService} from 'src/app/api/services/user.service'; +import API_URL from 'src/app/config/constants/apiUrl'; import LTI_API_URL from 'src/app/config/constants/ltiApiUrl'; @Injectable() export class HttpAuthenticationInterceptor implements HttpInterceptor { constructor(private userService: UserService) {} - intercept(request: HttpRequest, next: HttpHandler): Observable> { + intercept( + request: HttpRequest, + next: HttpHandler, + ): Observable> { if (request.url.startsWith(API_URL) || request.url.startsWith(LTI_API_URL)) { request = request.clone({ setHeaders: { diff --git a/src/app/common/services/http-error.interceptor.ts b/src/app/common/services/http-error.interceptor.ts index a182d04baf..fe205397cd 100644 --- a/src/app/common/services/http-error.interceptor.ts +++ b/src/app/common/services/http-error.interceptor.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import * as Sentry from '@sentry/angular'; import { HttpErrorResponse, HttpEvent, @@ -37,27 +38,19 @@ export class HttpErrorInterceptor implements HttpInterceptor { } intercept(request: HttpRequest, next: HttpHandler): Observable> { - // const retryTimes: number = 3; - // const delayDuration: number = 100; - - // TODO: Check for access token / refresh token expiration before trying the initial request - // .. This way we can avoid spamming console with 409 errors - - return next.handle(request).pipe( - // retryWhen(errors => errors - // .pipe( - // concatMap((error, count) => { - // if (count < retryTimes && (error.status === 400 || error.status === 0)) { - // return of(error.status); - // } - // return throwError(error); - // }), - // delay(delayDuration) - // ) - // ), + const request$ = this.isAccessTokenExpired(request) + ? throwError(() => new HttpErrorResponse({status: 419})) + : next.handle(request); + + return request$.pipe( catchError((error: HttpErrorResponse) => { if (this.isAuthError(error)) { + if (this.isAccessTokenRequest(request)) { + return throwError(() => this.extractErrorMessage(error)); + } + if (!this.refreshTokenInProgress) { + console.log('Refreshing access token'); this.refreshTokenInProgress = true; this.refreshTokenSubject.next(null); return this.attemptRefresh$().pipe( @@ -70,6 +63,9 @@ export class HttpErrorInterceptor implements HttpInterceptor { if (this.isAuthError(err)) { this.authenticationService.timeoutAuthentication(); } + if (!(err instanceof HttpErrorResponse)) { + return throwError(() => err); + } return throwError(() => this.extractErrorMessage(err)); }), finalize(() => (this.refreshTokenInProgress = false)), @@ -78,8 +74,9 @@ export class HttpErrorInterceptor implements HttpInterceptor { return this.refreshTokenSubject.pipe( filter((result) => result !== null), take(1), - switchMap((_res) => { - return next.handle(this.injectToken(request)); + switchMap(() => next.handle(this.injectToken(request))), + catchError((err: HttpErrorResponse) => { + return throwError(() => this.extractErrorMessage(err)); }), ); } @@ -94,8 +91,24 @@ export class HttpErrorInterceptor implements HttpInterceptor { return error.status === 419 || (error.status === 403 && this.userService.isAnonymousUser()); } + private isAccessTokenExpired(request: HttpRequest) { + const user = this.userService.currentUser; + const expiry = Date.parse(user.authenticationTokenExpiry); + + return ( + !this.isAccessTokenRequest(request) && + !!user.authenticationToken && + !Number.isNaN(expiry) && + expiry <= Date.now() + ); + } + + private isAccessTokenRequest(request: HttpRequest) { + return request.url.endsWith('/auth/access-token'); + } + private extractErrorMessage(error: HttpErrorResponse) { - let errorMessage: string = ''; + let errorMessage: string; let logMessage: string = ''; if (error.error instanceof ErrorEvent) { // client-side error @@ -114,6 +127,8 @@ export class HttpErrorInterceptor implements HttpInterceptor { logMessage = `Error Code: ${error.status}`; } + this.throwError(`${logMessage}: ${errorMessage}`, error.status); + console.error(`${logMessage}: ${errorMessage}`); return errorMessage; } @@ -126,4 +141,31 @@ export class HttpErrorInterceptor implements HttpInterceptor { }, }); } + + throwError(message: string, statusCode: number) { + Sentry.diagnoseSdkConnectivity().then(() => { + Sentry.startSpan( + { + name: `Error ${statusCode}`, + op: 'http.client_error', + attributes: { + 'http.response.status_code': statusCode, + }, + }, + () => { + throw new HttpRequestError(message, statusCode); + }, + ); + }); + } +} + +class HttpRequestError extends Error { + constructor( + message: string | undefined, + public readonly statusCode: number, + ) { + super(message); + this.name = 'HttpRequestError'; + } } diff --git a/src/app/common/services/listener-service.coffee b/src/app/common/services/listener-service.coffee deleted file mode 100644 index 120365765c..0000000000 --- a/src/app/common/services/listener-service.coffee +++ /dev/null @@ -1,11 +0,0 @@ -angular.module("doubtfire.common.services.listener", []) - -.factory("listenerService", -> - listeners = {} - listenerService = {} - listenerService.listenTo = (scope) -> - listeners[scope.$id] ?= [] - scope.$on '$destroy', -> _.each(listeners[scope.$id], (l) -> l()) - listeners[scope.$id] - listenerService -) diff --git a/src/app/common/services/media-service.coffee b/src/app/common/services/media-service.coffee deleted file mode 100644 index 21c6cefa08..0000000000 --- a/src/app/common/services/media-service.coffee +++ /dev/null @@ -1,20 +0,0 @@ -angular.module("doubtfire.common.services.media-service", []) -# -# Services for working with media APIs -# -.factory("mediaService", ($rootScope, $timeout, $sce) -> - mediaService = {} - - mediaService.audioCtx = mediaService.audioCtx? || (new (window.AudioContext || webkitAudioContext)()) - - mediaService.getMimeType = () -> - mimeType = 'audio/webm' - if !MediaRecorder.isTypeSupported(mimeType) - if navigator.userAgent.toLowerCase().indexOf('firefox') > -1 - mimeType = 'audio/ogg' - else - mimeType = '' - mimeType - - mediaService -) diff --git a/src/app/common/services/outcome-service.coffee b/src/app/common/services/outcome-service.coffee deleted file mode 100644 index e87fce2613..0000000000 --- a/src/app/common/services/outcome-service.coffee +++ /dev/null @@ -1,146 +0,0 @@ -# Component not used - -angular.module("doubtfire.common.services.outcome-service", []) - -# -# Services for handling Outcomes -# -.factory("outcomeService", (gradeService, newTaskService) -> - outcomeService = {} - - # outcomeService.unitTaskStatusFactor = -> - # (taskDefinitionId) -> 1 - - # outcomeService.projectTaskStatusFactor = (project) -> - # (taskDefinitionId) -> - # task = project.findTaskForDefinition(taskDefinitionId) - # if task? - # newTaskService.learningWeight.get(task.status) - # else - # 0 - - outcomeService.alignmentLabels = [ - "The task is not related to this outcome at all", - "The task is slightly related to this outcome", - "The task is related to this outcome", - "The task is a reasonable example for this outcome", - "The task is a strong example of this outcome", - "The task is the best example of this outcome", - ] - - outcomeService.individualTaskStatusFactor = (project, task) -> - (taskDefinitionId) -> - if task.definition.id == taskDefinitionId - newTaskService.learningWeight.get(project.findTaskForDefinition(taskDefinitionId).status) - else - 0 - - outcomeService.individualTaskPotentialFactor = (project, task) -> - (taskDefinitionId) -> - if task.definition.id == taskDefinitionId then 1 else 0 - - outcomeService.calculateTargets = (unit, source, taskStatusFactor) -> - outcomes = {} - # For each learning outcome (LO) -- produce a map with grades containing task scores - # calculated from alignment details, task target grade, and task status factor. - # - # The Task Status Factor for projects will be a value between 0 and 1 - # In the unit the taskStatusFactor will always be 1 (100%) to show potential values - # for the unit -- in effect removing the task status from unit calculations - _.each unit.ilos, (outcome) -> - # Add grade map for this LO to outcomes map - outcomes[outcome.id] = { - # Using 0..3 so that it can be used to calculate the grade scale below - 0: [] # Pass grade... -- will contain scores for pass grade tasks - 1: [] # Credit grade... -- etc. - 2: [] - 3: [] - } - - # For each outcome / task alignment... - _.each source.taskOutcomeAlignments, (align) -> - # Get the task definition - td = unit.taskDef(align.taskDefinition.id) - # Store a partial score for this task in the relevant outcomes ( outcomes[outcome id][grade] << score ) - # At this stage it is just rating * taskFactor (1 to 5 times 0 to 1) - outcomes[align.learningOutcome.id][td.targetGrade].push align.rating * taskStatusFactor(td) - - # Finally reduce all of these into one score for each outcome / grade - _.each outcomes, (outcome, key) -> - # For this outcome - _.each outcome, (tmp, key1) -> - # get a scale for the grade - scale = Math.pow(2, parseInt(key1,10)) - # Reduce all task partial scores and * grade scale -- replace array with single value - outcome[key1] = _.reduce(tmp, ((memo, num) -> memo + num), 0) * scale - - # Returns map of... - # { - # : { - # 0: - # 1: ... - # }, - # 86: { <--- OutcomeID 86 --> "Programming Principles" - # 0: 27 <--- Pass: 27 score (from rating * task status factor * scale reduced) - # 1: 53 ... - # }, - # } - outcomes - - outcomeService.calculateTaskContribution = (unit, project, task) -> - outcome_set = [] - outcome_set[0] = outcomeService.calculateTargets(unit, unit, outcomeService.individualTaskStatusFactor(project, task)) - - _.each outcome_set[0], (outcome, key) -> - outcome_set[0][key] = _.reduce(outcome, ((memo, num) -> memo + num), 0) - - outcome_set[0].title = 'Current Task Contribution' - outcome_set - - outcomeService.calculateTaskPotentialContribution = (unit, project, task) -> - outcomes = outcomeService.calculateTargets(unit, unit, outcomeService.individualTaskPotentialFactor(project, task)) - - _.each outcomes, (outcome, key) -> - outcomes[key] = _.reduce(outcome, ((memo, num) -> memo + num), 0) - - outcomes['title'] = 'Potential Task Contribution' - outcomes - - outcomeService.calculateProgress = (unit, project) -> - outcome_set = [] - - outcome_set[0] = outcomeService.calculateTargets(unit, unit, project.taskStatusFactor.bind(project)) - # outcome_set[1] = outcomeService.calculateTargets(unit, project, outcomeService.projectTaskStatusFactor(project)) - - _.each outcome_set, (outcomes, key) -> - _.each outcomes, (outcome, key) -> - outcomes[key] = _.reduce(outcome, ((memo, num) -> memo + num), 0) - - outcome_set[0].title = "Your Progress" # - Staff Suggestion" - # outcome_set[1].title = "Your Progress - Your Reflection" - - outcome_set - - - outcomeService.targetsByGrade = (unit, source) -> - result = [] - outcomes = outcomeService.calculateTargets(unit, source, unit.taskStatusFactor) - - values = { - '0': [] - '1': [] - '2': [] - '3': [] - } - - _.each outcomes, (outcome, key) -> - _.each outcome, (tmp, key1) -> - values[key1].push { label: $sce.getTrustedHtml(unit.outcome(parseInt(key,10)).abbreviation), value: tmp } - - _.each values, (vals, idx) -> - result.push { key: gradeService.grades[idx], values: vals } - - result - - outcomeService -) diff --git a/src/app/common/services/recorder-service.coffee b/src/app/common/services/recorder-service.coffee deleted file mode 100644 index b3e6408e4e..0000000000 --- a/src/app/common/services/recorder-service.coffee +++ /dev/null @@ -1,259 +0,0 @@ -# Parts adapted from https://github.com/kaliatech/web-audio-recording-tests - -angular.module("doubtfire.common.services.recorder-service", []) -# -# Services for working with cross-platform, media Recording APIs -# -.factory("recorderService", ($rootScope, $timeout, $sce) -> - return class RecorderService - constructor: () -> - window.AudioContext = window.AudioContext || window.webkitAudioContext - - @em = document.createDocumentFragment() - @state = 'inactive' - @audioCtx = {} - @chunks = [] - @chunkType = '' - - @usingMediaRecorder = window.MediaRecorder || false - - # MediaRecording on Safari is broken for us in some specific way which I'm not sure how to fix yet. - if /^((?!chrome|android).)*safari/i.test(navigator.userAgent) then @usingMediaRecorder = false - - @encoderMimeType - - @config = { - broadcastAudioProcessEvents: false, - createAnalyserNode: true, - createDynamicsCompressorNode: false, - forceScriptProcessor: false, - manualEncoderId: 'wav', - micGain: 1.0, - processorBufferSize: 2048, - stopTracksAndCloseCtxWhenFinished: true, - userMediaConstraints: { - audio: true - } - audioBitsPerSecond: 128000 - } - return - - # Called once when the recording is initated - startRecording: () -> - if (@state != 'inactive') - return - - # This is the case on ios/chrome, when clicking links from within ios/slack (sometimes), etc. - if (!navigator || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) - console.error('Missing support for navigator.mediaDevices.getUserMedia') # temp: helps when testing for strange issues on ios/safari - return - - @audioCtx = new AudioContext() - @micGainNode = @audioCtx.createGain() - @outputGainNode = @audioCtx.createGain() - - if (@config.createDynamicsCompressorNode) - @dynamicsCompressorNode = audioCtx.createDynamicsCompressor() - - - if (@config.createAnalyserNode) - @analyserNode = @audioCtx.createAnalyser() - - - # If not using MediaRecorder(i.e. safari and edge), then a script processor is required. It's optional - # on browsers using MediaRecorder and is only useful if wanting to do custom analysis or manipulation of - # recorded audio data. - if (@config.forceScriptProcessor || @config.broadcastAudioProcessEvents || !@usingMediaRecorder) - @processorNode = @audioCtx.createScriptProcessor(@config.processorBufferSize, 1, 1) # TODO: Get the number of channels from mic - - # Create stream destination on chrome/firefox because, AFAICT, we have no other way of feeding audio graph output - # in to MediaRecorderSafari/Edge don't have this method as of 2018-04. - if (@audioCtx.createMediaStreamDestination) - @destinationNode = @audioCtx.createMediaStreamDestination() - else - @destinationNode = @audioCtx.destination - - # Create web worker for doing the encoding - if (!@usingMediaRecorder) - @encoderWorker = new Worker('/assets/wav-worker.js') - @encoderMimeType = 'audio/wav' - - that = this - @encoderWorker.addEventListener('message', (e) -> - event = new Event('dataavailable') - if (that.config.manualEncoderId == 'ogg') - event.data = e.data - else - event.data = new Blob(e.data, { type: that.encoderMimeType }) - that._onDataAvailable(event) - ) - - # This will prompt user for permission if needed - that = this - return navigator.mediaDevices.getUserMedia(@config.userMediaConstraints) - .then((stream) -> - that._startRecordingWithStream(stream) - ) - .catch((error) -> - return - ) - return - - setMicGain: (newGain) -> - @config.micGain = newGain - if (@audioCtx && @micGainNode) - @micGainNode.gain.setValueAtTime(newGain, @audioCtx.currentTime) - return - - _startRecordingWithStream: (stream) -> - @micAudioStream = stream - @inputStreamNode = @audioCtx.createMediaStreamSource(@micAudioStream) - @audioCtx = @inputStreamNode.context - - # Kind-of a hack to allow hooking in to audioGraph mediaRecorder.inputStreamNode - if (@onGraphSetupWithInputStream) - @onGraphSetupWithInputStream(@inputStreamNode) - - @inputStreamNode.connect(@micGainNode) - @micGainNode.gain.setValueAtTime(@config.micGain, @audioCtx.currentTime) - - nextNode = @micGainNode - if (@dynamicsCompressorNode) - @micGainNode.connect(@dynamicsCompressorNode) - nextNode = @dynamicsCompressorNode - - @state = 'recording' - - if (@processorNode) - nextNode.connect(@processorNode) - @processorNode.connect(@outputGainNode) - that = this - @processorNode.onaudioprocess = (e) -> that._onAudioProcess(e) - else - nextNode.connect(@outputGainNode) - - if (@analyserNode) - nextNode.connect(@analyserNode) - - @outputGainNode.connect(@destinationNode) - - if (@usingMediaRecorder) - @mediaRecorder = new MediaRecorder(@destinationNode.stream, { audioBitsPerSecond: @config.audioBitsPerSecond }) - that = this - @mediaRecorder.addEventListener('dataavailable', (evt) -> that._onDataAvailable(evt)) - @mediaRecorder.addEventListener('error', (evt) -> @_onError(evt)) - - @mediaRecorder.start() - else - @outputGainNode.gain.setValueAtTime(0, @audioCtx.currentTime) - return - - _onAudioProcess: (e) -> - if (@config.broadcastAudioProcessEvents) - @em.dispatchEvent(new CustomEvent('onaudioprocess', { - detail: { - inputBuffer: e.inputBuffer, - outputBuffer: e.outputBuffer - } - })) - if (!@usingMediaRecorder) - if (@state == 'recording') - if (@config.broadcastAudioProcessEvents) - @encoderWorker.postMessage(['encode', e.outputBuffer.getChannelData(0)]) - else - @encoderWorker.postMessage(['encode', e.inputBuffer.getChannelData(0)]) - return - - processChunks: () -> - if (@state == 'inactive') - return - this._dumpChunks() - return - - _dumpChunks: () -> - if(@usingMediaRecorder) - @mediaRecorder.requestData() - - if (!@usingMediaRecorder) - @encoderWorker.postMessage(['dump', @audioCtx.sampleRate]) - clearInterval(@slicing) - - # Called once when the recording has been stopped - stopRecording: () -> - if (@state == 'inactive') - return - if (@usingMediaRecorder) - @state = 'inactive' - @mediaRecorder.stop() - else - @state = 'inactive' - @encoderWorker.postMessage(['dump', @audioCtx.sampleRate]) - clearInterval(@slicing) - return - - # Called each time a chunk of recording becomes available - _onDataAvailable: (evt) -> - @chunks.push(evt.data) - @chunkType = evt.data.type - - blob = new Blob(@chunks, { 'type': @chunkType }) - blobUrl = URL.createObjectURL(blob) - recording = { - ts: new Date().getTime(), - blobUrl: blobUrl, - mimeType: blob.type, - size: blob.size - blob: blob - } - - @em.dispatchEvent(new CustomEvent('recording', { detail: { recording: recording } })) - - @chunks = [] - - if (@state != 'inactive') - return - - this._cleanup() - return - - _cleanup: () -> - @chunkType = null - if (@destinationNode) - @destinationNode.disconnect() - @destinationNode = null - if (@outputGainNode) - @outputGainNode.disconnect() - @outputGainNode = null - if (@analyserNode) - @analyserNode.disconnect() - @analyserNode = null - if (@processorNode) - @processorNode.disconnect() - @processorNode = null - if (@encoderWorker) - @encoderWorker.postMessage(['close']) - @encoderWorker = null - if (@dynamicsCompressorNode) - @dynamicsCompressorNode.disconnect() - @dynamicsCompressorNode = null - if (@micGainNode) - @micGainNode.disconnect() - @micGainNode = null - if (@inputStreamNode) - @inputStreamNode.disconnect() - @inputStreamNode = null - - if (@config.stopTracksAndCloseCtxWhenFinished) - # This removes the red bar in iOS/Safari - @micAudioStream.getTracks().forEach((track) -> track.stop()) - @micAudioStream = null - - @audioCtx.close() - @audioCtx = null - - return - - _onError: (evt) -> - @em.dispatchEvent(new Event('error')) - return -) diff --git a/src/app/common/services/recorder-service.ts b/src/app/common/services/recorder-service.ts new file mode 100644 index 0000000000..2c50415efc --- /dev/null +++ b/src/app/common/services/recorder-service.ts @@ -0,0 +1,324 @@ +import {Injectable} from '@angular/core'; + +type RecorderState = 'inactive' | 'recording'; + +interface RecorderConfig { + broadcastAudioProcessEvents: boolean; + createAnalyserNode: boolean; + createDynamicsCompressorNode: boolean; + forceScriptProcessor: boolean; + manualEncoderId: 'wav' | 'ogg'; + micGain: number; + processorBufferSize: number; + stopTracksAndCloseCtxWhenFinished: boolean; + userMediaConstraints: MediaStreamConstraints; + audioBitsPerSecond: number; +} + +@Injectable() +export class MediaRecorderService { + em: DocumentFragment; + state: RecorderState; + audioCtx: AudioContext | null; + chunks: BlobPart[]; + chunkType: string | null; + usingMediaRecorder: boolean; + encoderMimeType?: string; + config: RecorderConfig; + + micGainNode: GainNode | null; + outputGainNode: GainNode | null; + dynamicsCompressorNode: DynamicsCompressorNode | null; + analyserNode: AnalyserNode | null; + processorNode: ScriptProcessorNode | null; + destinationNode: MediaStreamAudioDestinationNode | AudioDestinationNode | null; + encoderWorker: Worker | null; + micAudioStream: MediaStream | null; + inputStreamNode: MediaStreamAudioSourceNode | null; + mediaRecorder: MediaRecorder | null; + onGraphSetupWithInputStream?: (inputStream: MediaStreamAudioSourceNode) => void; + + constructor() { + const audioWindow = window as typeof window & { + webkitAudioContext?: typeof AudioContext; + }; + audioWindow.AudioContext = audioWindow.AudioContext || audioWindow.webkitAudioContext; + + this.em = document.createDocumentFragment(); + this.state = 'inactive'; + this.audioCtx = null; + this.chunks = []; + this.chunkType = ''; + + this.micGainNode = null; + this.outputGainNode = null; + this.dynamicsCompressorNode = null; + this.analyserNode = null; + this.processorNode = null; + this.destinationNode = null; + this.encoderWorker = null; + this.micAudioStream = null; + this.inputStreamNode = null; + this.mediaRecorder = null; + + this.usingMediaRecorder = Boolean(window.MediaRecorder); + + if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { + this.usingMediaRecorder = false; + } + + this.config = { + broadcastAudioProcessEvents: false, + createAnalyserNode: true, + createDynamicsCompressorNode: false, + forceScriptProcessor: false, + manualEncoderId: 'wav', + micGain: 1.0, + processorBufferSize: 2048, + stopTracksAndCloseCtxWhenFinished: true, + userMediaConstraints: { + audio: true, + }, + audioBitsPerSecond: 128000, + }; + } + + startRecording(): Promise | void { + if (this.state !== 'inactive') { + return; + } + + if (!navigator?.mediaDevices?.getUserMedia) { + console.error('Missing support for navigator.mediaDevices.getUserMedia'); + return; + } + + this.audioCtx = new AudioContext(); + this.micGainNode = this.audioCtx.createGain(); + this.outputGainNode = this.audioCtx.createGain(); + + if (this.config.createDynamicsCompressorNode) { + this.dynamicsCompressorNode = this.audioCtx.createDynamicsCompressor(); + } + + if (this.config.createAnalyserNode) { + this.analyserNode = this.audioCtx.createAnalyser(); + } + + if ( + this.config.forceScriptProcessor || + this.config.broadcastAudioProcessEvents || + !this.usingMediaRecorder + ) { + this.processorNode = this.audioCtx.createScriptProcessor( + this.config.processorBufferSize, + 1, + 1, + ); + } + + if (this.audioCtx.createMediaStreamDestination) { + this.destinationNode = this.audioCtx.createMediaStreamDestination(); + } else { + this.destinationNode = this.audioCtx.destination; + } + + if (!this.usingMediaRecorder) { + this.encoderWorker = new Worker('/assets/wav-worker.js'); + this.encoderMimeType = 'audio/wav'; + + this.encoderWorker.addEventListener('message', (e: MessageEvent) => { + const event = new Event('dataavailable') as Event & {data: Blob}; + if (this.config.manualEncoderId === 'ogg') { + event.data = e.data as Blob; + } else { + event.data = new Blob(e.data as BlobPart[], {type: this.encoderMimeType}); + } + this._onDataAvailable(event); + }); + } + + return navigator.mediaDevices + .getUserMedia(this.config.userMediaConstraints) + .then((stream) => { + this._startRecordingWithStream(stream); + }) + .catch(() => undefined); + } + + setMicGain(newGain: number): void { + this.config.micGain = newGain; + if (this.audioCtx && this.micGainNode) { + this.micGainNode.gain.setValueAtTime(newGain, this.audioCtx.currentTime); + } + } + + processChunks(): void { + if (this.state === 'inactive') { + return; + } + this._dumpChunks(); + } + + stopRecording(): void { + if (this.state === 'inactive') { + return; + } + this.state = 'inactive'; + + if (this.usingMediaRecorder) { + this.mediaRecorder?.stop(); + return; + } + + this.encoderWorker?.postMessage(['dump', this.audioCtx?.sampleRate]); + } + + private _startRecordingWithStream(stream: MediaStream): void { + if (!this.audioCtx || !this.micGainNode || !this.outputGainNode || !this.destinationNode) { + return; + } + + this.micAudioStream = stream; + this.inputStreamNode = this.audioCtx.createMediaStreamSource(this.micAudioStream); + this.audioCtx = this.inputStreamNode.context as AudioContext; + + this.onGraphSetupWithInputStream?.(this.inputStreamNode); + + this.inputStreamNode.connect(this.micGainNode); + this.micGainNode.gain.setValueAtTime(this.config.micGain, this.audioCtx.currentTime); + + let nextNode: AudioNode = this.micGainNode; + if (this.dynamicsCompressorNode) { + this.micGainNode.connect(this.dynamicsCompressorNode); + nextNode = this.dynamicsCompressorNode; + } + + this.state = 'recording'; + + if (this.processorNode) { + nextNode.connect(this.processorNode); + this.processorNode.connect(this.outputGainNode); + this.processorNode.onaudioprocess = (e: AudioProcessingEvent) => this._onAudioProcess(e); + } else { + nextNode.connect(this.outputGainNode); + } + + if (this.analyserNode) { + nextNode.connect(this.analyserNode); + } + + this.outputGainNode.connect(this.destinationNode); + + if (this.usingMediaRecorder) { + const streamDestination = this.destinationNode as MediaStreamAudioDestinationNode; + this.mediaRecorder = new MediaRecorder(streamDestination.stream, { + audioBitsPerSecond: this.config.audioBitsPerSecond, + }); + this.mediaRecorder.addEventListener('dataavailable', (evt) => this._onDataAvailable(evt)); + this.mediaRecorder.addEventListener('error', (evt) => this._onError(evt)); + this.mediaRecorder.start(); + } else { + this.outputGainNode.gain.setValueAtTime(0, this.audioCtx.currentTime); + } + } + + private _onAudioProcess(e: AudioProcessingEvent): void { + if (this.config.broadcastAudioProcessEvents) { + this.em.dispatchEvent( + new CustomEvent('onaudioprocess', { + detail: { + inputBuffer: e.inputBuffer, + outputBuffer: e.outputBuffer, + }, + }), + ); + } + + if (!this.usingMediaRecorder && this.state === 'recording' && this.encoderWorker) { + if (this.config.broadcastAudioProcessEvents) { + this.encoderWorker.postMessage(['encode', e.outputBuffer.getChannelData(0)]); + } else { + this.encoderWorker.postMessage(['encode', e.inputBuffer.getChannelData(0)]); + } + } + } + + private _dumpChunks(): void { + if (this.usingMediaRecorder) { + this.mediaRecorder?.requestData(); + return; + } + + this.encoderWorker?.postMessage(['dump', this.audioCtx?.sampleRate]); + } + + private _onDataAvailable(evt: BlobEvent | (Event & {data: Blob})): void { + this.chunks.push(evt.data); + this.chunkType = evt.data.type; + + const blob = new Blob(this.chunks, {type: this.chunkType}); + const blobUrl = URL.createObjectURL(blob); + const recording = { + ts: new Date().getTime(), + blobUrl, + mimeType: blob.type, + size: blob.size, + blob, + }; + + this.em.dispatchEvent(new CustomEvent('recording', {detail: {recording}})); + + this.chunks = []; + + if (this.state !== 'inactive') { + return; + } + + this._cleanup(); + } + + private _cleanup(): void { + this.chunkType = null; + + this.destinationNode?.disconnect(); + this.destinationNode = null; + + this.outputGainNode?.disconnect(); + this.outputGainNode = null; + + this.analyserNode?.disconnect(); + this.analyserNode = null; + + this.processorNode?.disconnect(); + this.processorNode = null; + + if (this.encoderWorker) { + this.encoderWorker.postMessage(['close']); + this.encoderWorker = null; + } + + this.dynamicsCompressorNode?.disconnect(); + this.dynamicsCompressorNode = null; + + this.micGainNode?.disconnect(); + this.micGainNode = null; + + this.inputStreamNode?.disconnect(); + this.inputStreamNode = null; + + if (this.config.stopTracksAndCloseCtxWhenFinished) { + this.micAudioStream?.getTracks().forEach((track) => track.stop()); + this.micAudioStream = null; + + this.audioCtx?.close(); + this.audioCtx = null; + } + + this.mediaRecorder = null; + } + + private _onError(_evt: Event): void { + this.em.dispatchEvent(new Event('error')); + } +} diff --git a/src/app/common/services/services.coffee b/src/app/common/services/services.coffee deleted file mode 100644 index e88e7a7bdc..0000000000 --- a/src/app/common/services/services.coffee +++ /dev/null @@ -1,7 +0,0 @@ -angular.module("doubtfire.common.services", [ - 'doubtfire.common.services.outcome-service' - 'doubtfire.common.services.analytics' - 'doubtfire.common.services.dates' - 'doubtfire.common.services.listener' - 'doubtfire.common.services.recorder-service' -]) diff --git a/src/app/common/services/task-submission.service.ts b/src/app/common/services/task-submission.service.ts index 47249fd975..453b6450bb 100644 --- a/src/app/common/services/task-submission.service.ts +++ b/src/app/common/services/task-submission.service.ts @@ -1,15 +1,15 @@ -import {Injectable, Inject} from '@angular/core'; import {HttpClient} from '@angular/common/http'; -import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {Injectable} from '@angular/core'; import {Observable} from 'rxjs'; import { - Task, OverseerAssessment, OverseerAssessmentService, OverseerImage, OverseerImageService, + Task, } from 'src/app/api/models/doubtfire-model'; import {AppInjector} from 'src/app/app-injector'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; export interface TaskAssessmentResult { id?: number; @@ -27,6 +27,15 @@ export interface TestResult { is_successful: boolean; } +export interface TaskAssessmentResponse { + result: string; +} + +export interface SubmissionResult { + label: string; + result: string; +} + export interface DockerImageInfo { name: string; packages?: string[]; @@ -50,22 +59,25 @@ export class TaskSubmissionService { private overseerAssessmentService: OverseerAssessmentService, ) {} - public getLatestTaskAssessment(taskInfo: Task): Observable { + public getLatestTaskAssessment(taskInfo: Task): Observable { const url = `${AppInjector.get(DoubtfireConstants).API_URL}/projects/${ taskInfo.project.id }/task_def_id/${taskInfo.definition.id}/submissions/latest`; - return this.http.get(url); + return this.http.get(url); } public getLatestSubmissionsTimestamps(taskInfo: Task): Observable { return this.overseerAssessmentService.queryForTask(taskInfo); } - public getSubmissionByTimestamp(taskInfo: Task, timestamp: string): Observable { + public getSubmissionByTimestamp( + taskInfo: Task, + timestamp: string, + ): Observable { const url = `${AppInjector.get(DoubtfireConstants).API_URL}/projects/${ taskInfo.project.id }/task_def_id/${taskInfo.definition.id}/submissions/timestamps/${timestamp}`; - return this.http.get(url); + return this.http.get(url); } public getDockerImages(): Observable { diff --git a/src/app/common/status-icon/status-icon.component.html b/src/app/common/status-icon/status-icon.component.html index b87e0eac58..1c61c04b16 100644 --- a/src/app/common/status-icon/status-icon.component.html +++ b/src/app/common/status-icon/status-icon.component.html @@ -1,10 +1,14 @@
- + {{ statusIcon }}
diff --git a/src/app/common/status-icon/status-icon.component.scss b/src/app/common/status-icon/status-icon.component.scss index ee89d37718..86633746ce 100644 --- a/src/app/common/status-icon/status-icon.component.scss +++ b/src/app/common/status-icon/status-icon.component.scss @@ -1,4 +1,4 @@ -@import '../../../styles/mixins/task-status-colors-generator.scss'; +@use 'styles/mixins/task-status-colors-generator' as *; :host { font-size: 1em; @@ -41,6 +41,9 @@ &.discuss { @include status-icon('discuss'); } + &.rediscuss { + @include status-icon('rediscuss'); + } &.demonstrate { @include status-icon('demonstrate'); } diff --git a/src/app/common/status-icon/status-icon.component.spec.ts b/src/app/common/status-icon/status-icon.component.spec.ts index f09c362828..15cba56103 100644 --- a/src/app/common/status-icon/status-icon.component.spec.ts +++ b/src/app/common/status-icon/status-icon.component.spec.ts @@ -1,25 +1,24 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; - -import { StatusIconComponent } from './status-icon.component'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {StatusIconComponent} from './status-icon.component'; describe('StatusIconComponent', () => { let component: StatusIconComponent; let fixture: ComponentFixture; - beforeEach( - waitForAsync(() => { - - TestBed.configureTestingModule({ - declarations: [StatusIconComponent], - providers: [], - }).compileComponents(); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [StatusIconComponent], + schemas: [NO_ERRORS_SCHEMA], }) - ); + .overrideComponent(StatusIconComponent, {set: {template: ''}}) + .compileComponents(); + }); beforeEach(() => { fixture = TestBed.createComponent(StatusIconComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/common/status-icon/status-icon.component.ts b/src/app/common/status-icon/status-icon.component.ts index 57811fa153..7a9667694a 100644 --- a/src/app/common/status-icon/status-icon.component.ts +++ b/src/app/common/status-icon/status-icon.component.ts @@ -1,27 +1,39 @@ -import { Component, Input, Inject, OnInit } from '@angular/core'; -import { TaskStatus, TaskStatusEnum } from 'src/app/api/models/task-status'; +import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; +import {TaskStatus, TaskStatusEnum} from 'src/app/api/models/task-status'; @Component({ selector: 'status-icon', templateUrl: './status-icon.component.html', styleUrls: ['./status-icon.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class StatusIconComponent implements OnInit { - @Input() status: TaskStatusEnum = 'not_started'; + @Input() status?: TaskStatusEnum = 'not_started'; @Input() showTooltip: boolean; - - statusIcon: (status: TaskStatusEnum) => string; - statusLabel: (status: TaskStatusEnum) => string; - statusClass: (status: TaskStatusEnum) => string; - - constructor() {} + @Input() compact = false; ngOnInit(): void { if (this.showTooltip == null) { this.showTooltip = true; } - this.statusIcon = (status: TaskStatusEnum) => TaskStatus.STATUS_ICONS.get(status); - this.statusLabel = (status: TaskStatusEnum) => TaskStatus.STATUS_LABELS.get(status); - this.statusClass = (status: TaskStatusEnum) => TaskStatus.statusClass(status); + } + + get statusIcon(): string { + return TaskStatus.STATUS_MATERIAL_ICONS.get(this.resolvedStatus) ?? 'pause'; + } + + get statusLabel(): string { + return TaskStatus.STATUS_LABELS.get(this.resolvedStatus) ?? 'Not Started'; + } + + get statusClass(): string { + return TaskStatus.statusClass(this.resolvedStatus); + } + + get resolvedStatus(): TaskStatusEnum { + return this.status && TaskStatus.STATUS_KEYS.includes(this.status) + ? this.status + : 'not_started'; } } diff --git a/src/app/common/submission-files-download/submission-files-download.component.html b/src/app/common/submission-files-download/submission-files-download.component.html new file mode 100644 index 0000000000..f3348a38bd --- /dev/null +++ b/src/app/common/submission-files-download/submission-files-download.component.html @@ -0,0 +1,26 @@ +
+ @switch (downloadState) { + @case ('downloading') { + +

Downloading submitted files...

+ } + + @case ('downloaded') { + check_circle +

Submitted files downloaded.

+ + } + + @case ('failed') { + error +

Could not download submitted files.

+ + } + } +
diff --git a/src/app/common/submission-files-download/submission-files-download.component.ts b/src/app/common/submission-files-download/submission-files-download.component.ts new file mode 100644 index 0000000000..5d0e75f629 --- /dev/null +++ b/src/app/common/submission-files-download/submission-files-download.component.ts @@ -0,0 +1,57 @@ +import {HttpResponse} from '@angular/common/http'; +import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {FileDownloaderService} from '../file-downloader/file-downloader.service'; + +type DownloadState = 'downloading' | 'downloaded' | 'failed'; + +@Component({ + selector: 'f-submission-files-download', + templateUrl: './submission-files-download.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class SubmissionFilesDownloadComponent implements OnInit { + protected downloadState: DownloadState = 'downloading'; + private downloadUrl = ''; + + constructor( + private readonly route: ActivatedRoute, + private readonly constants: DoubtfireConstants, + private readonly fileDownloader: FileDownloaderService, + ) {} + + public ngOnInit(): void { + const projectId = this.route.snapshot.paramMap.get('projectId'); + const taskDefId = this.route.snapshot.paramMap.get('taskDefId'); + + this.downloadUrl = `${this.constants.API_URL}/projects/${projectId}/task_def_id/${taskDefId}/submission_files?as_attachment=true`; + this.download(); + } + + protected download(): void { + this.downloadState = 'downloading'; + + this.fileDownloader.downloadBlob( + this.downloadUrl, + (resourceUrl: string, response: HttpResponse) => { + this.fileDownloader.downloadBlobToFile( + resourceUrl, + this.filenameFromResponse(response) ?? 'submitted-files.zip', + ); + this.downloadState = 'downloaded'; + }, + () => { + this.downloadState = 'failed'; + }, + ); + } + + private filenameFromResponse(response: HttpResponse): string | null { + const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/; + const matches = filenameRegex.exec(response.headers.get('Content-Disposition')); + + return matches?.[1]?.replace(/['"]/g, '') ?? null; + } +} diff --git a/src/app/common/success-close/success-close.component.ts b/src/app/common/success-close/success-close.component.ts index ca298ac680..558e7b7ecc 100644 --- a/src/app/common/success-close/success-close.component.ts +++ b/src/app/common/success-close/success-close.component.ts @@ -1,9 +1,11 @@ -import { Component, OnInit } from '@angular/core'; +import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core'; @Component({ selector: 'f-success-close', templateUrl: 'success-close.component.html', - styleUrls: ['success-close.component.scss'] + styleUrls: ['success-close.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class SuccessCloseComponent implements OnInit { ngOnInit(): void { diff --git a/src/app/common/task-badge/task-badge.component.css b/src/app/common/task-badge/task-badge.component.css index 8c791270f5..01f2399341 100644 --- a/src/app/common/task-badge/task-badge.component.css +++ b/src/app/common/task-badge/task-badge.component.css @@ -8,7 +8,7 @@ } .task-badge.task-badge-highlight { - background-color: var(--mdc-theme-primary, #3939ff) !important; + background-color: var(--mat-theme-primary, #3939ff) !important; color: #ffffff; } diff --git a/src/app/common/task-badge/task-badge.component.html b/src/app/common/task-badge/task-badge.component.html index dfa63dd941..c0801d02c6 100644 --- a/src/app/common/task-badge/task-badge.component.html +++ b/src/app/common/task-badge/task-badge.component.html @@ -1,5 +1,5 @@

diff --git a/src/app/common/task-badge/task-badge.component.ts b/src/app/common/task-badge/task-badge.component.ts index d050a8fe4f..9868e4fb80 100644 --- a/src/app/common/task-badge/task-badge.component.ts +++ b/src/app/common/task-badge/task-badge.component.ts @@ -1,12 +1,14 @@ -import {Component, Input, type OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; import {TaskDefinition} from 'src/app/api/models/task-definition'; @Component({ selector: 'f-task-badge', templateUrl: './task-badge.component.html', styleUrl: './task-badge.component.css', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class FTaskBadgeComponent implements OnInit { +export class FTaskBadgeComponent { @Input() taskDef: TaskDefinition; @Input() size = 100; @Input() highlight = false; @@ -15,11 +17,9 @@ export class FTaskBadgeComponent implements OnInit { get abbreviation(): string { // return the first 3 characters of the task abbreviation - return this.taskDef.abbreviation.substring(0, 4); + return this.taskDef?.abbreviation.substring(0, 4); } - ngOnInit(): void {} - calculateFontSize(length: number): string { const baseFontSize = 1.5; // Base font size in rem const maxLength = 3; // Maximum length before font size reduction diff --git a/src/app/common/unit-code/unit-code.component.css b/src/app/common/unit-code/unit-code.component.css index a571170e3c..5d4e87f30f 100644 --- a/src/app/common/unit-code/unit-code.component.css +++ b/src/app/common/unit-code/unit-code.component.css @@ -1,5 +1,3 @@ :host { display: block; - } - diff --git a/src/app/common/unit-code/unit-code.component.html b/src/app/common/unit-code/unit-code.component.html index 8e33ff9a06..cd8724577c 100644 --- a/src/app/common/unit-code/unit-code.component.html +++ b/src/app/common/unit-code/unit-code.component.html @@ -1,10 +1,5 @@ -

+
@if (isDualBadge && shiftBetweenBadges) { @for (part of unitCodeParts; track part; let i = $index) { @if (i === currentIndex) { diff --git a/src/app/common/unit-code/unit-code.component.ts b/src/app/common/unit-code/unit-code.component.ts index 462597afe1..a01278f7dc 100644 --- a/src/app/common/unit-code/unit-code.component.ts +++ b/src/app/common/unit-code/unit-code.component.ts @@ -1,5 +1,5 @@ -import {trigger, state, style, animate, transition} from '@angular/animations'; -import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {animate, state, style, transition, trigger} from '@angular/animations'; +import {ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit} from '@angular/core'; import {Subscription} from 'rxjs'; import {UnitCodeService} from './unit-code.service'; @@ -20,6 +20,8 @@ import {UnitCodeService} from './unit-code.service'; ]), ]), ], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class UnitCodeComponent implements OnInit, OnDestroy { @Input() unit_code: string; @@ -34,12 +36,20 @@ export class UnitCodeComponent implements OnInit, OnDestroy { constructor(private unitCodeService: UnitCodeService) {} get isDualBadge() { - return this.unit_code?.includes('/'); + return this.unit_code?.includes('/') || this.unit_code?.includes('-'); } get unitCodeParts() { if (this.shiftBetweenBadges) { - return this.isDualBadge ? this.unit_code.split('/') : [this.unit_code]; + if (this.isDualBadge) { + if (this.unit_code.includes('/')) { + return this.unit_code.split('/'); + } else { + return this.unit_code.split('-'); + } + } else { + return [this.unit_code]; + } } return this.unit_code; } diff --git a/src/app/common/user-badge/user-badge.component.html b/src/app/common/user-badge/user-badge.component.html index 20cfa08553..b1e2669deb 100644 --- a/src/app/common/user-badge/user-badge.component.html +++ b/src/app/common/user-badge/user-badge.component.html @@ -1,32 +1,32 @@ -
+
-
-
+
+
-

+

{{ selectedTask?.project.student.firstName }} {{ selectedTask?.project.student.lastName }}

{{ selectedTask?.definition.name }}

diff --git a/src/app/common/user-badge/user-badge.component.scss b/src/app/common/user-badge/user-badge.component.scss index ed194fa2c5..d4762075c1 100644 --- a/src/app/common/user-badge/user-badge.component.scss +++ b/src/app/common/user-badge/user-badge.component.scss @@ -1,46 +1,46 @@ +@use 'sass:color'; + :host { - white-space: nowrap; - text-overflow: ellipsis; + white-space: nowrap; + text-overflow: ellipsis; } a h4 { - font-weight: 400; - // font-family: 'Grotesk'; - font-size: 14px; + font-weight: 400; + // font-family: 'Grotesk'; + font-size: 14px; } a { - color: black + color: black; } a h4, a p { - margin: 0; - line-height: 1.2em; + margin: 0; + line-height: 1.2em; } a p { - color: (lighten($color: #000000, $amount: 15)); + color: color.adjust(#000000, $lightness: 15%); font-size: 12px; } #placeholder1 { - margin-left: -4px; - line-height: 1.2em; - width: 200px; - height: 16px; - background: rgba(0, 0, 0, 0.08); - border-radius: 10px; - margin-bottom: 3px; + margin-left: -4px; + line-height: 1.2em; + width: 200px; + height: 16px; + background: rgba(0, 0, 0, 0.08); + border-radius: 10px; + margin-bottom: 3px; } #placeholder2 { - margin-left: -4px; - line-height: 1.2em; - width: 200px; - height: 12px; - background: rgba(0, 0, 0, 0.08); - border-radius: 10px; + margin-left: -4px; + line-height: 1.2em; + width: 200px; + height: 12px; + background: rgba(0, 0, 0, 0.08); + border-radius: 10px; } - - diff --git a/src/app/common/user-badge/user-badge.component.spec.ts b/src/app/common/user-badge/user-badge.component.spec.ts index 6c161dfacf..87b04a194d 100644 --- a/src/app/common/user-badge/user-badge.component.spec.ts +++ b/src/app/common/user-badge/user-badge.component.spec.ts @@ -1,6 +1,7 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { UserBadgeComponent } from './user-badge.component'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {UserBadgeComponent} from './user-badge.component'; describe('UserBadgeComponent', () => { let component: UserBadgeComponent; @@ -9,11 +10,15 @@ describe('UserBadgeComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [UserBadgeComponent], - }).compileComponents(); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(UserBadgeComponent, {set: {template: ''}}) + .compileComponents(); + }); + beforeEach(() => { fixture = TestBed.createComponent(UserBadgeComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/common/user-badge/user-badge.component.ts b/src/app/common/user-badge/user-badge.component.ts index a07521fa2e..52f1a7b93d 100644 --- a/src/app/common/user-badge/user-badge.component.ts +++ b/src/app/common/user-badge/user-badge.component.ts @@ -1,14 +1,14 @@ -import {Component, Input} from '@angular/core'; -import {UIRouter} from '@uirouter/angular'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; import {Task} from 'src/app/api/models/doubtfire-model'; @Component({ selector: 'f-user-badge', templateUrl: './user-badge.component.html', styleUrls: ['./user-badge.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class UserBadgeComponent { - constructor(private router: UIRouter) {} @Input() selectedTask: Task; get unselected(): boolean { @@ -19,43 +19,18 @@ export class UserBadgeComponent { return this.selectedTask == null; } - get studentRouteParams(): {projectId: number; tutor: boolean; taskAbbr: string} | undefined { - if (this.unselected) { - return undefined; - } - - return { - projectId: this.selectedTask.project.id, - tutor: true, - taskAbbr: '', - }; - } - - get studentTaskRouteParams(): {projectId: number; tutor: boolean; taskAbbr: string} | undefined { - if (this.unselected) { - return undefined; - } - - return { - projectId: this.selectedTask.project.id, - taskAbbr: this.selectedTask.definition.abbreviation, - tutor: true, - }; - } - - goToStudent(): void { - this.router.stateService.go('projects/dashboard', { - projectId: this.selectedTask.project.id, - tutor: true, - taskAbbr: '', - }); + get studentDashboardRoute(): unknown[] | null { + return this.unselected ? null : ['/projects', this.selectedTask.project.id, 'dashboard']; } - goToStudentTask(): void { - this.router.stateService.go('projects/dashboard', { - projectId: this.selectedTask.project.id, - taskAbbr: this.selectedTask.definition.abbreviation, - tutor: true, - }); + get studentTaskRoute(): unknown[] | null { + return this.unselected + ? null + : [ + '/projects', + this.selectedTask.project.id, + 'dashboard', + this.selectedTask.definition.abbreviation, + ]; } } diff --git a/src/app/common/user-icon/user-icon.component.html b/src/app/common/user-icon/user-icon.component.html index 0d24efdceb..82690f70c2 100644 --- a/src/app/common/user-icon/user-icon.component.html +++ b/src/app/common/user-icon/user-icon.component.html @@ -1,4 +1,4 @@ - + @if (unselected) { -account_circle + account_circle } diff --git a/src/app/common/user-icon/user-icon.component.scss b/src/app/common/user-icon/user-icon.component.scss index 3d001bf71f..357a10175c 100644 --- a/src/app/common/user-icon/user-icon.component.scss +++ b/src/app/common/user-icon/user-icon.component.scss @@ -9,4 +9,4 @@ width: 50px; margin-left: -6px; color: rgba(0, 0, 0, 0.12); -} \ No newline at end of file +} diff --git a/src/app/common/user-icon/user-icon.component.ts b/src/app/common/user-icon/user-icon.component.ts index 07f3a8d27e..212eee6e02 100644 --- a/src/app/common/user-icon/user-icon.component.ts +++ b/src/app/common/user-icon/user-icon.component.ts @@ -1,23 +1,60 @@ -import { Component, Input, ViewChild, AfterViewInit, OnChanges, SimpleChanges } from '@angular/core'; -import { User, UserService } from 'src/app/api/models/doubtfire-model'; -import { Md5 } from 'ts-md5/dist/md5'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + Input, + OnChanges, + SimpleChanges, + ViewChild, +} from '@angular/core'; +import {User, UserService} from 'src/app/api/models/doubtfire-model'; + +interface D3Selection { + append(name: string): D3Selection; + attr( + name: string, + value: string | number | ((datum: IconLine, index: number) => string | number), + ): D3Selection; + call( + callback: (selection: D3Selection, size: number, radius: number) => void, + size: number, + radius: number, + ): D3Selection; + data(data: IconLine[]): D3Selection; + enter(): D3Selection; + remove(): D3Selection; + selectAll(selector: string): D3Selection; + style(name: string, value: string): D3Selection; + text(value: (datum: IconLine) => string): D3Selection; +} + +interface IconLine { + width: number; + text: string; +} -declare var d3: any; +declare const d3: { + select(element: SVGElement): D3Selection; +}; @Component({ selector: 'user-icon', templateUrl: './user-icon.component.html', styleUrls: ['./user-icon.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class UserIconComponent implements AfterViewInit, OnChanges { @Input() user: User; @Input() unselected: boolean; @Input() size = 100; - @ViewChild('svg') svg: { nativeElement: any }; + @ViewChild('svg') svg: ElementRef; lineHeight = 12; usingCurrentUser: boolean; + private renderSequence = 0; ngAfterViewInit(): void { if (this.user == null) { @@ -35,18 +72,26 @@ export class UserIconComponent implements AfterViewInit, OnChanges { constructor(private userService: UserService) {} - get backgroundUrl(): string { - const hash = this.email != null ? Md5.hashStr(this.email.trim().toLowerCase()) : Md5.hashStr(''); + private async backgroundUrl(): Promise { + const hash = await this.sha256(this.email?.trim().toLowerCase() ?? ''); return `https://www.gravatar.com/avatar/${hash}.png?default=blank&size=${this.size * 4}`; } + private async sha256(value: string): Promise { + const bytes = new TextEncoder().encode(value); + const digest = await crypto.subtle.digest('SHA-256', bytes); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join( + '', + ); + } + get email(): string { return this.user?.email; } get initials(): string { - const result = this.user?.name != null ? this.user.name.split(' ') : ' '; - return result.length > 1 ? `${result[0][0]}${result[1][0]}`.toUpperCase() : ' '; + const words = this.user?.name.split(' ').filter(Boolean) ?? []; + return words.length > 1 ? `${words[0][0]}${words[1][0]}`.toUpperCase() : ' '; } get words(): string[] { @@ -61,8 +106,8 @@ export class UserIconComponent implements AfterViewInit, OnChanges { return Math.max(this.size / 2, 4); } - private generateLines(): any[] { - let line; + private generateLines(): IconLine[] { + let line: IconLine; let lineWidth0 = Infinity; const result = []; for (let i = 0, n = this.words.length; i < n; ++i) { @@ -73,7 +118,7 @@ export class UserIconComponent implements AfterViewInit, OnChanges { line.text = lineText1; } else { lineWidth0 = this.measureWidth(this.words[i]); - line = { width: lineWidth0, text: this.words[i] }; + line = {width: lineWidth0, text: this.words[i]}; result.push(line); } } @@ -115,21 +160,28 @@ export class UserIconComponent implements AfterViewInit, OnChanges { return context.measureText(text).width; } - drawUserIcon(): void { + async drawUserIcon(): Promise { + const svgElement = this.svg?.nativeElement; + if (!svgElement) { + return; + } + + const renderSequence = ++this.renderSequence; + const backgroundUrl = await this.backgroundUrl(); + if (renderSequence !== this.renderSequence) { + return; + } + // TODO: Consider caching SVG on a per-user basis // clear svg - d3.select(this.svg?.nativeElement).selectAll('*').remove(); + d3.select(svgElement).selectAll('*').remove(); // if this.unselected is undefined or true - if (!this.unselected == null || this.unselected) { - if (this.svg?.nativeElement) { - // hide div from DOM (but don't remove it) - this.svg.nativeElement.style.display = 'none'; - } + if (this.unselected) { + // hide div from DOM (but don't remove it) + svgElement.style.display = 'none'; } else { - if (this.svg?.nativeElement) { - // add div to DOM - this.svg.nativeElement.style.display = 'block'; - } + // add div to DOM + svgElement.style.display = 'block'; } const lines = this.generateLines(); @@ -141,7 +193,7 @@ export class UserIconComponent implements AfterViewInit, OnChanges { } const svg = d3 - .select(this.svg?.nativeElement) + .select(svgElement) .style('font', '8px sans-serif') .attr('width', this.size) .attr('shape-rendering', 'geometricPrecision') @@ -149,7 +201,7 @@ export class UserIconComponent implements AfterViewInit, OnChanges { .attr('height', this.size) .attr('text-anchor', 'middle'); - function appendCircle(selection, size, radius) { + function appendCircle(selection: D3Selection, size: number, radius: number) { selection .append('circle') .attr('cx', size / 2) @@ -160,7 +212,10 @@ export class UserIconComponent implements AfterViewInit, OnChanges { const id = this.generateUniqueId(); const defs = svg.append('defs'); - defs.append('clipPath').attr('id', `image-clip-${id}`).call(appendCircle, this.size, this.radius); + defs + .append('clipPath') + .attr('id', `image-clip-${id}`) + .call(appendCircle, this.size, this.radius); svg .append('circle') @@ -171,7 +226,10 @@ export class UserIconComponent implements AfterViewInit, OnChanges { svg .append('text') - .attr('transform', `translate(${this.size / 2},${this.size / 2}) scale(${this.radius / textRadius})`) + .attr( + 'transform', + `translate(${this.size / 2},${this.size / 2}) scale(${this.radius / textRadius})`, + ) .selectAll('tspan') .data(lines) .enter() @@ -183,7 +241,7 @@ export class UserIconComponent implements AfterViewInit, OnChanges { svg .append('image') - .attr('xlink:href', this.backgroundUrl) + .attr('xlink:href', backgroundUrl) .attr('width', this.size) .attr('height', this.size) .attr('x', 0) diff --git a/src/app/config/analytics/analytics.coffee b/src/app/config/analytics/analytics.coffee deleted file mode 100644 index 641d80653b..0000000000 --- a/src/app/config/analytics/analytics.coffee +++ /dev/null @@ -1,8 +0,0 @@ -angular.module('doubtfire.config.analytics', []) -# -# Configuration for analytics -# -.config( ($analyticsProvider) -> - # Disable virtual page views for analytics - $analyticsProvider.virtualPageviews(false) -) diff --git a/src/app/config/config.coffee b/src/app/config/config.coffee deleted file mode 100644 index b7699efbf5..0000000000 --- a/src/app/config/config.coffee +++ /dev/null @@ -1,16 +0,0 @@ -# -# The Doubtfire configuration module stores all configuration settings -# for Doubtfire loaded at runtime. -# -# The order in which the modules load here is IMPORTANT so do not rearrange -# them -# -angular.module('doubtfire.config', [ - 'doubtfire.config.vendor-dependencies' - 'doubtfire.config.routing' - 'doubtfire.config.analytics' - 'doubtfire.config.runtime' - 'doubtfire.config.root-controller' - 'doubtfire.config.debug' - 'doubtfire.config.privacy-policy' -]) diff --git a/src/app/config/constants/apiUrl.ts b/src/app/config/constants/apiUrl.ts index 6e502f57ce..74c355b7ce 100644 --- a/src/app/config/constants/apiUrl.ts +++ b/src/app/config/constants/apiUrl.ts @@ -1,3 +1,4 @@ import HOST_URL from './hostUrl'; + const API_URL: string = `${HOST_URL}/api`; export default API_URL; diff --git a/src/app/config/constants/doubtfire-constants.ts b/src/app/config/constants/doubtfire-constants.ts index 72d1140bfc..16207391e7 100644 --- a/src/app/config/constants/doubtfire-constants.ts +++ b/src/app/config/constants/doubtfire-constants.ts @@ -1,7 +1,6 @@ -import {HttpClient, HttpBackend} from '@angular/common/http'; +import {HttpBackend, HttpClient} from '@angular/common/http'; import {Injectable} from '@angular/core'; import {BehaviorSubject} from 'rxjs'; - import API_URL from 'src/app/config/constants/apiUrl'; import HOST_URL from 'src/app/config/constants/hostUrl'; @@ -29,7 +28,7 @@ interface SignOutUrlResponseFormat { export class DoubtfireConstants { private http: HttpClient; - public mainContributors: ReadonlyArray = [ + public mainContributors: readonly string[] = [ 'macite', // Andrew Cain 'alexcu', // Alex Cummaudo 'jakerenzella', // Jake Renzella diff --git a/src/app/config/constants/ltiApiUrl.ts b/src/app/config/constants/ltiApiUrl.ts index a3c45ed8d6..a29f000427 100644 --- a/src/app/config/constants/ltiApiUrl.ts +++ b/src/app/config/constants/ltiApiUrl.ts @@ -1,3 +1,4 @@ import HOST_URL from './hostUrl'; + const LTI_API_URL: string = `${HOST_URL}/lti/api`; export default LTI_API_URL; diff --git a/src/app/config/debug/debug.coffee b/src/app/config/debug/debug.coffee deleted file mode 100644 index 06dafe8555..0000000000 --- a/src/app/config/debug/debug.coffee +++ /dev/null @@ -1,10 +0,0 @@ -angular.module('doubtfire.config.debug', []) - -# -# You can define any debug helpers here -# - -# Debug helper method -scope = ($0) -> - throw new Error "Select a DOM element using 'Inspect Element' first, then call using scope($0)" unless $0? - angular.element($0).scope() diff --git a/src/app/config/privacy-policy/privacy-policy.coffee b/src/app/config/privacy-policy/privacy-policy.coffee deleted file mode 100644 index a7a1a84efa..0000000000 --- a/src/app/config/privacy-policy/privacy-policy.coffee +++ /dev/null @@ -1,17 +0,0 @@ -angular.module("doubtfire.config.privacy-policy", []) - -.factory('PrivacyPolicy', ($http, DoubtfireConstants) -> - privacyPolicy = { - privacy: '', - plagiarism: '', - loaded: false, - } - - $http.get("#{DoubtfireConstants.API_URL}/settings/privacy").then ((response) -> - privacyPolicy.privacy = response.data.privacy - privacyPolicy.plagiarism = response.data.plagiarism - privacyPolicy.loaded = true - ) - - privacyPolicy -) diff --git a/src/app/config/privacy-policy/privacy-policy.spec.ts b/src/app/config/privacy-policy/privacy-policy.spec.ts new file mode 100644 index 0000000000..634447dd0a --- /dev/null +++ b/src/app/config/privacy-policy/privacy-policy.spec.ts @@ -0,0 +1,33 @@ +import {afterEach, beforeEach, describe, expect, it} from 'vitest'; +import {provideHttpClient, withXhr} from '@angular/common/http'; +import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing'; +import {TestBed} from '@angular/core/testing'; +import {PrivacyPolicy} from './privacy-policy'; + +describe('PrivacyPolicy', () => { + let service: PrivacyPolicy; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideHttpClient(withXhr()), provideHttpClientTesting()], + }); + service = TestBed.inject(PrivacyPolicy); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should load the privacy and plagiarism policies', () => { + httpMock + .expectOne('http://localhost:3000/api/settings/privacy') + .flush({privacy: 'Privacy policy', plagiarism: 'Plagiarism policy'}); + + expect(service).toBeTruthy(); + expect(service.privacy).toBe('Privacy policy'); + expect(service.plagiarism).toBe('Plagiarism policy'); + expect(service.loaded).toBe(true); + }); +}); diff --git a/src/app/config/privacy-policy/privacy-policy.ts b/src/app/config/privacy-policy/privacy-policy.ts new file mode 100644 index 0000000000..9f18c9e4af --- /dev/null +++ b/src/app/config/privacy-policy/privacy-policy.ts @@ -0,0 +1,29 @@ +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import API_URL from 'src/app/config/constants/apiUrl'; + +interface Response { + privacy: string; + plagiarism: string; +} + +@Injectable({ + providedIn: 'root', +}) +export class PrivacyPolicy { + privacy = ''; + plagiarism = ''; + loaded = false; + + public API_URL: string = API_URL; + + constructor(private http: HttpClient) { + const url: string = `${this.API_URL}/settings/privacy`; + + this.http.get(url).subscribe((response) => { + this.privacy = response.privacy; + this.plagiarism = response.plagiarism; + this.loaded = true; + }); + } +} diff --git a/src/app/config/root-controller/root-controller.coffee b/src/app/config/root-controller/root-controller.coffee deleted file mode 100644 index 4a9854fc0e..0000000000 --- a/src/app/config/root-controller/root-controller.coffee +++ /dev/null @@ -1,8 +0,0 @@ -angular.module('doubtfire.config.root-controller', []) - -# -# The Doubtfire root application controller -# -.controller("AppCtrl", (GlobalStateService) -> - -) diff --git a/src/app/config/routing/routing.coffee b/src/app/config/routing/routing.coffee deleted file mode 100644 index 0091a888a7..0000000000 --- a/src/app/config/routing/routing.coffee +++ /dev/null @@ -1,13 +0,0 @@ -angular.module('doubtfire.config.routing', []) -# -# Configuration for angular routing -# -.config(($urlRouterProvider, $httpProvider) -> - # Catch bad URLs. - # $urlRouterProvider.otherwise "/not_found" - $urlRouterProvider.when "", "/" - - # Map root/home URL to a default state of our choosing. - # TODO: (@alexcu) probably change it to map to /dashboard at some point. - $urlRouterProvider.when "/", "/home" -) diff --git a/src/app/config/runtime/runtime.coffee b/src/app/config/runtime/runtime.coffee deleted file mode 100644 index b5b9f95f0d..0000000000 --- a/src/app/config/runtime/runtime.coffee +++ /dev/null @@ -1,47 +0,0 @@ -# -# Runtime settings for when Doubtfire is about to launch -# -angular.module('doubtfire.config.runtime', []) - -.run(($rootScope, $state, $filter, $location, authenticationService, editableOptions, editableThemes, $transitions) -> - # Angular xeditable - editableOptions.theme = 'bs3' - editableThemes.bs3.inputClass = 'input-sm' - editableThemes.bs3.buttonsClass = 'btn-sm' - - handleUnauthorisedDest = (toState) -> - if authenticationService.isAuthenticated() - $state.go "unauthorised" - else if $state.current.name isnt "sign_in" - $state.go "sign_in" - - handleTokenTimeout = -> - if $state.current.name isnt "timeout" - $state.go "timeout" - - handleUnauthorised = -> - handleUnauthorisedDest($state.current) - - # Don't let the user see pages not intended for their role - - # Redirect the user if they make an unauthorised API request - $rootScope.$on "unauthorisedRequestIntercepted", handleUnauthorised - - # Redirect the user if their token expires - $rootScope.$on("tokenTimeout", handleTokenTimeout) - - # Watch for state transition and check role whitelist - $transitions.onStart {}, (trans) -> - toState = trans.to() - return true unless toState.data.roleWhitelist - - # Get the auth service to check this when the auth is complete - authenticationService.afterAuthCall( - (isAuthenticated) -> - unless isAuthenticated && authenticationService.isAuthorised(toState.data.roleWhitelist) - handleUnauthorisedDest(toState) - ) - - # We can always transition... but may have to redirect after auth call... - return true -) diff --git a/src/app/config/vendor-dependencies/vendor-dependencies.coffee b/src/app/config/vendor-dependencies/vendor-dependencies.coffee deleted file mode 100644 index 2d1aca3a25..0000000000 --- a/src/app/config/vendor-dependencies/vendor-dependencies.coffee +++ /dev/null @@ -1,30 +0,0 @@ -# -# Use this module to define all third-party dependencies -# that are used in Doubtfire -# -angular.module('doubtfire.config.vendor-dependencies', [ - # ng* - 'ngCsv' - 'ngSanitize' - - # templates - 'templates-app' - - # ui.* - 'ui.router' - 'ui.router.upgrade' - 'ui.bootstrap' - 'ui.codemirror' - - # other libraries - 'angular.filter' - 'localization' - 'markdown' - 'nvd3' - 'xeditable' - 'angular-md5' - - # analytics - 'angulartics' - 'angulartics.google.analytics' -]) diff --git a/src/app/dashboard/f-cross-dashboard.component.html b/src/app/dashboard/f-cross-dashboard.component.html index 3ea01a6f18..5a2338670f 100644 --- a/src/app/dashboard/f-cross-dashboard.component.html +++ b/src/app/dashboard/f-cross-dashboard.component.html @@ -1,24 +1,105 @@ -
-
-
-
-

{{ unit.code }}

- - sort - filter_list_alt -
+
+
+ + Unit scope + + Active units + Previous units + All units + + +
-
-
-
- +
+ @for (unit of displayedUnits; track unit.projectId) { +
+
+
+

+ {{ unit.code }} +

+

+ {{ unit.isPrevious ? 'Previous' : 'Active' }} +

+ + + + + + @for (mode of sortOptions; track mode) { + + } + + + + + @for (mode of filterOptions; track mode) { +
+ + {{ mode }} +
+ } +
+
+ +
+ @for (task of unit.tasks; track task.abbreviation) { +
+ +
+ } +
+
+ } + + @if (unitScope !== 'active' && loadingPreviousUnits) { +
+ Loading previous units... +
+ } + + @if (unitScope !== 'active' && previousUnitsLoadError) { +
+

Previous units

+

Previous units could not be loaded.

+
+ } + + @if ( + unitScope !== 'active' && + previousUnitsLoaded && + !loadingPreviousUnits && + previousUnits.length === 0 + ) { +
+
+

Previous units

+
+
+

No previous units are available.

-
+ }
diff --git a/src/app/dashboard/f-cross-dashboard.component.spec.ts b/src/app/dashboard/f-cross-dashboard.component.spec.ts new file mode 100644 index 0000000000..86a9d0bbc6 --- /dev/null +++ b/src/app/dashboard/f-cross-dashboard.component.spec.ts @@ -0,0 +1,156 @@ +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatMenuModule} from '@angular/material/menu'; +import {BehaviorSubject, of, throwError} from 'rxjs'; +import {Project} from '../api/models/project'; +import {ProjectService} from '../api/services/project.service'; +import {GlobalStateService} from '../projects/states/index/global-state.service'; +import {CrossDashboardComponent} from './f-cross-dashboard.component'; + +describe('CrossDashboardComponent', () => { + let component: CrossDashboardComponent; + let fixture: ComponentFixture; + let projectsSubject: BehaviorSubject; + let projectServiceQuery: ReturnType; + + const makeProject = (id: number, code: string, isActive: boolean): Project => { + const tasks = []; + + return { + id, + tasks, + unit: { + code, + name: `${code} Unit`, + isActive, + taskDefinitions: [], + }, + calcTopTasks: vi.fn(), + activeTasks: vi.fn().mockReturnValue(tasks), + } as unknown as Project; + }; + + beforeEach(async () => { + projectsSubject = new BehaviorSubject([]); + projectServiceQuery = vi.fn().mockReturnValue(of([])); + + const globalStateServiceStub = { + onLoad: (callback: () => void): void => callback(), + currentUserProjects: { + values: projectsSubject.asObservable(), + }, + }; + + const projectServiceStub = { + query: projectServiceQuery, + }; + + await TestBed.configureTestingModule({ + declarations: [CrossDashboardComponent], + imports: [MatMenuModule], + providers: [ + { + provide: GlobalStateService, + useValue: globalStateServiceStub, + }, + { + provide: ProjectService, + useValue: projectServiceStub, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(CrossDashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + afterEach(() => { + projectsSubject.complete(); + }); + + it('uses Active units as the default scope', () => { + expect(component.unitScope).toBe('active'); + expect(projectServiceQuery).not.toHaveBeenCalled(); + }); + + it('keeps only active projects in the active-unit collection', () => { + projectsSubject.next([makeProject(1, 'COS10001', true), makeProject(2, 'COS30046', false)]); + + expect(component.activeUnits.map((unit) => unit.code)).toEqual(['COS10001']); + expect(component.displayedUnits.map((unit) => unit.code)).toEqual(['COS10001']); + }); + + it('loads and displays previous units in Previous units mode', () => { + projectServiceQuery.mockReturnValue( + of([makeProject(1, 'COS10001', true), makeProject(2, 'COS30046', false)]), + ); + + component.setUnitScope('previous'); + + expect(projectServiceQuery).toHaveBeenCalledTimes(1); + expect(component.previousUnitsLoaded).toBe(true); + expect(component.loadingPreviousUnits).toBe(false); + expect(component.previousUnits.map((unit) => unit.code)).toEqual(['COS30046']); + expect(component.displayedUnits.map((unit) => unit.code)).toEqual(['COS30046']); + expect(component.previousUnits[0].isPrevious).toBe(true); + }); + + it('shows active units first and previous units afterward in All units mode', () => { + projectsSubject.next([makeProject(1, 'COS10001', true), makeProject(2, 'COS20007', true)]); + + projectServiceQuery.mockReturnValue( + of([ + makeProject(1, 'COS10001', true), + makeProject(2, 'COS20007', true), + makeProject(3, 'COS30046', false), + ]), + ); + + component.setUnitScope('all'); + + expect(component.displayedUnits.map((unit) => unit.code)).toEqual([ + 'COS10001', + 'COS20007', + 'COS30046', + ]); + }); + + it('does not request previous units again after they have loaded', () => { + projectServiceQuery.mockReturnValue(of([makeProject(3, 'COS30046', false)])); + + component.setUnitScope('previous'); + component.setUnitScope('active'); + component.setUnitScope('all'); + + expect(projectServiceQuery).toHaveBeenCalledTimes(1); + }); + + it('renders the no-previous-units empty state', () => { + projectServiceQuery.mockReturnValue(of([])); + + component.setUnitScope('previous'); + fixture.detectChanges(); + + expect(component.previousUnitsLoaded).toBe(true); + expect(component.loadingPreviousUnits).toBe(false); + expect(component.previousUnits).toEqual([]); + expect(fixture.nativeElement.textContent).toContain('Previous units'); + expect(fixture.nativeElement.textContent).toContain('No previous units are available.'); + }); + + it('shows an error state when previous units cannot be loaded', () => { + projectServiceQuery.mockReturnValue( + throwError(() => new Error('Unable to load previous units')), + ); + + component.setUnitScope('previous'); + fixture.detectChanges(); + + expect(component.previousUnitsLoadError).toBe(true); + expect(component.loadingPreviousUnits).toBe(false); + expect(fixture.nativeElement.textContent).toContain('Previous units could not be loaded.'); + }); +}); diff --git a/src/app/dashboard/f-cross-dashboard.component.ts b/src/app/dashboard/f-cross-dashboard.component.ts index 6c2107b1af..477faf22cd 100644 --- a/src/app/dashboard/f-cross-dashboard.component.ts +++ b/src/app/dashboard/f-cross-dashboard.component.ts @@ -1,62 +1,222 @@ -import {Component, OnInit} from '@angular/core'; +import {EntityCache} from 'ngx-entity-service'; +import {ChangeDetectorRef, Component, OnInit} from '@angular/core'; import {GlobalStateService} from 'src/app/projects/states/index/global-state.service'; import {Project} from '../api/models/project'; -import {TaskStatus} from '../api/models/task-status'; -import {DashboardTask} from './list-item/dashboard-list-item.component'; import {Task} from '../api/models/task'; -import {TaskDefinition} from '../api/models/task-definition'; +import {TaskStatus, TaskStatusEnum} from '../api/models/task-status'; +import {ProjectService} from '../api/services/project.service'; +import {DashboardTask} from './list-item/dashboard-list-item.component'; + +type UnitScope = 'active' | 'previous' | 'all'; + +enum Filter { + HideCompleted = 'Hide Completed', +} + +enum SortMode { + Recommended = 'Recommended', + SubmissionDate = 'Due Date', + Default = 'Default', +} + +const completedTypes: readonly TaskStatusEnum[] = ['complete']; type DashboardUnit = { projectId: number; code: string; name: string; tasks: DashboardTask[]; + isPrevious: boolean; }; @Component({ selector: 'f-cross-dashboard', + standalone: false, templateUrl: './f-cross-dashboard.component.html', }) export class CrossDashboardComponent implements OnInit { - constructor(private globalStateService: GlobalStateService,) {} + activeUnits: DashboardUnit[] = []; + previousUnits: DashboardUnit[] = []; + + unitScope: UnitScope = 'active'; + + previousUnitsLoaded = false; + loadingPreviousUnits = false; + previousUnitsLoadError = false; + + filterOptions = Object.values(Filter); + sortOptions = Object.values(SortMode); + unitsProcessed: DashboardUnit[] = []; + + private readonly previousProjectsCache: EntityCache = new EntityCache(); + private filters: Map = new Map(); + private sorting: Map = new Map(); - units: DashboardUnit[] = []; + constructor( + private globalStateService: GlobalStateService, + private projectService: ProjectService, + private changeDetectorRef: ChangeDetectorRef, + ) {} ngOnInit(): void { this.globalStateService.onLoad(() => { this.globalStateService.currentUserProjects.values.subscribe((projects) => { - this.units = this.mapProjects(projects); + const activeProjects = projects.filter((project) => project.unit.isActive); + this.activeUnits = this.mapProjects(activeProjects); + this.processTasks(); }); }); } - mapProjects(projects: readonly Project[]): DashboardUnit[] { + get displayedUnits(): DashboardUnit[] { + return this.unitsProcessed; + } + + setUnitScope(scope: UnitScope): void { + this.unitScope = scope; + this.processTasks(); + + const needsPreviousUnits = scope === 'previous' || scope === 'all'; + + if (needsPreviousUnits && !this.previousUnitsLoaded && !this.loadingPreviousUnits) { + this.loadPreviousUnits(); + } + } + + setSort(project: number, mode: SortMode): void { + this.sorting.set(project, mode); + this.processTasks(); + } + + toggleFilter(project: number, filter: Filter): void { + let filters = this.filters.get(project) ?? []; + if (filters.includes(filter)) { + filters = filters.filter((currentFilter) => currentFilter !== filter); + } else { + filters = [...filters, filter]; + } + this.filters.set(project, filters); + this.processTasks(); + } + + isFilterEnabled(project: number, filter: Filter): boolean { + return this.filters.get(project)?.includes(filter) === true; + } + + private loadPreviousUnits(): void { + this.loadingPreviousUnits = true; + this.previousUnitsLoadError = false; + + this.projectService + .query(undefined, { + cache: this.previousProjectsCache, + params: { + include_inactive: true, + include_task_definitions: true, + }, + }) + .subscribe({ + next: (projects: Project[]) => { + const previousProjects = projects.filter((project) => !project.unit.isActive); + + this.previousUnits = this.mapProjects(previousProjects); + this.previousUnitsLoaded = true; + this.loadingPreviousUnits = false; + this.processTasks(); + + this.changeDetectorRef.detectChanges(); + }, + error: () => { + this.previousUnitsLoadError = true; + this.loadingPreviousUnits = false; + this.processTasks(); + + this.changeDetectorRef.detectChanges(); + }, + }); + } + + private processTasks(): void { + const units = this.getUnitsForCurrentScope(); + + this.unitsProcessed = units.map((unit) => ({ + ...unit, + tasks: unit.tasks + .filter((task) => { + const filters = this.filters.get(unit.projectId) ?? []; + return !(filters.includes(Filter.HideCompleted) && completedTypes.includes(task.status)); + }) + .sort((a, b) => { + const sort = this.sorting.get(unit.projectId) ?? SortMode.Recommended; + + if (completedTypes.includes(a.status) && !completedTypes.includes(b.status)) { + return -1; + } + + if (!completedTypes.includes(a.status) && completedTypes.includes(b.status)) { + return 1; + } + + switch (sort) { + case SortMode.Recommended: + // TODO: Connect to recommender's points. + return 0; + case SortMode.SubmissionDate: + return a.dueDate.getTime() - b.dueDate.getTime(); + case SortMode.Default: + return a.weight - b.weight; + } + + return 0; + }), + })); + } + + private getUnitsForCurrentScope(): DashboardUnit[] { + if (this.unitScope === 'previous') { + return this.previousUnits; + } + + if (this.unitScope === 'all') { + return [...this.activeUnits, ...this.previousUnits]; + } + + return this.activeUnits; + } + + private mapProjects(projects: readonly Project[]): DashboardUnit[] { return projects.map((project) => { + project.calcTopTasks(); const unit = project.unit; + return { projectId: project.id, code: unit.code, name: unit.name, - tasks: this.mapTasks(project.tasks, unit.taskDefinitions, project.id, unit.code), + tasks: this.mapTasks(project.activeTasks(), project.id, unit.code), + isPrevious: !unit.isActive, }; }); } - mapTasks(tasks: readonly Task[], taskDefs: readonly TaskDefinition[], projectId: number, unitCode: string): DashboardTask[] { - return taskDefs.map((def) => { - const task = tasks.find((t) => t.taskDefId == def.id); + private mapTasks(tasks: readonly Task[], projectId: number, unitCode: string): DashboardTask[] { + return tasks.map((task) => { + const def = task.definition; + return { title: def.name, subtitle: `${def.abbreviation} - ${def.targetGradeText} Task`, + statusLabel: TaskStatus.STATUS_LABELS.get(task.status), abbreviation: def.abbreviation, - color: TaskStatus.STATUS_COLORS.get(task?.status ?? 'not_started'), - comments: task?.numNewComments ?? 0, - projectId: projectId, - statusLabel: TaskStatus.STATUS_LABELS.get(task?.status ?? 'not_started'), + color: TaskStatus.STATUS_COLORS.get(task.status), + comments: task.numNewComments ?? 0, + status: task.status, + weight: task.topWeight, + projectId, description: def.description, - unitCode: unitCode, - dueDate: def.targetDate, taskDef: def, + unitCode, + dueDate: def.targetDate, }; }); } diff --git a/src/app/dashboard/list-item/dashboard-list-item.component.html b/src/app/dashboard/list-item/dashboard-list-item.component.html index 2bbc5cb854..6498999a9b 100644 --- a/src/app/dashboard/list-item/dashboard-list-item.component.html +++ b/src/app/dashboard/list-item/dashboard-list-item.component.html @@ -1,28 +1,27 @@
-
-
+
+

{{ task.title }}

{{ task.subtitle }}

-
-
-
- {{ task.comments }} +
+ @if (!isExpanded && task.comments > 0) { +
+ {{ task.comments }}
-
- + } + {{ isExpanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }}
-
+ @if (isExpanded) { -
+ }
diff --git a/src/app/dashboard/list-item/dashboard-list-item.component.ts b/src/app/dashboard/list-item/dashboard-list-item.component.ts index 32800afac2..2a28c3ded6 100644 --- a/src/app/dashboard/list-item/dashboard-list-item.component.ts +++ b/src/app/dashboard/list-item/dashboard-list-item.component.ts @@ -1,5 +1,6 @@ import {Component, Input} from '@angular/core'; import {TaskDefinition} from '../../api/models/task-definition'; +import {TaskStatusEnum} from '../../api/models/task-status'; export type DashboardTask = { title: string; @@ -8,6 +9,8 @@ export type DashboardTask = { abbreviation: string; color: string; comments: number; + status: TaskStatusEnum; + weight: number; projectId: number; description: string; taskDef: TaskDefinition; @@ -17,6 +20,7 @@ export type DashboardTask = { @Component({ selector: 'f-dashboard-list-item', + standalone: false, templateUrl: './dashboard-list-item.component.html', }) export class DashboardListItemComponent { diff --git a/src/app/dashboard/list-item/expanded-list-item/expanded-list-item.component.html b/src/app/dashboard/list-item/expanded-list-item/expanded-list-item.component.html index 21ed3d2345..7d8293dd38 100644 --- a/src/app/dashboard/list-item/expanded-list-item/expanded-list-item.component.html +++ b/src/app/dashboard/list-item/expanded-list-item/expanded-list-item.component.html @@ -1,6 +1,6 @@
-
-
+
+

{{ task.statusLabel }}

{{ task.description }}

@@ -9,32 +9,31 @@

+
diff --git a/src/app/units/states/tasks/viewer/directives/f-task-sheet-view/f-task-sheet-view.component.scss b/src/app/errors/states/unauthorised/unauthorised.component.scss similarity index 100% rename from src/app/units/states/tasks/viewer/directives/f-task-sheet-view/f-task-sheet-view.component.scss rename to src/app/errors/states/unauthorised/unauthorised.component.scss diff --git a/src/app/errors/states/unauthorised/unauthorised.component.spec.ts b/src/app/errors/states/unauthorised/unauthorised.component.spec.ts new file mode 100644 index 0000000000..b0902a02aa --- /dev/null +++ b/src/app/errors/states/unauthorised/unauthorised.component.spec.ts @@ -0,0 +1,31 @@ +import {beforeEach, describe, expect, it} from 'vitest'; +import {Location} from '@angular/common'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {UnauthorisedComponent} from './unauthorised.component'; + +const emptyProvider = {}; + +describe('UnauthorisedComponent', () => { + let component: UnauthorisedComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [UnauthorisedComponent], + providers: [{provide: Location, useValue: emptyProvider}], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(UnauthorisedComponent, {set: {template: ''}}) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UnauthorisedComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/errors/states/unauthorised/unauthorised.component.ts b/src/app/errors/states/unauthorised/unauthorised.component.ts new file mode 100644 index 0000000000..da3f9dddb8 --- /dev/null +++ b/src/app/errors/states/unauthorised/unauthorised.component.ts @@ -0,0 +1,17 @@ +import {Location} from '@angular/common'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; + +@Component({ + selector: 'unauthorised', + templateUrl: 'unauthorised.component.html', + styleUrls: ['unauthorised.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class UnauthorisedComponent { + constructor(private location: Location) {} + + goBack() { + this.location.back(); + } +} diff --git a/src/app/errors/states/unauthorised/unauthorised.tpl.html b/src/app/errors/states/unauthorised/unauthorised.tpl.html deleted file mode 100644 index a0f26d25c2..0000000000 --- a/src/app/errors/states/unauthorised/unauthorised.tpl.html +++ /dev/null @@ -1,7 +0,0 @@ -
-
- -

Unauthorised

-

You do not have sufficient permissions to access this resource, or your session has expired.

-
-
\ No newline at end of file diff --git a/src/app/errors/unavailable-card/unavailable-card.component.html b/src/app/errors/unavailable-card/unavailable-card.component.html index 86b05b2f34..151f295a19 100644 --- a/src/app/errors/unavailable-card/unavailable-card.component.html +++ b/src/app/errors/unavailable-card/unavailable-card.component.html @@ -1,8 +1,8 @@ -
Temporarily Unavailable
+
Temporarily Unavailable
engineering
We apologise for the inconvenience.
Please check back again soon. diff --git a/src/app/errors/unavailable-card/unavailable-card.component.ts b/src/app/errors/unavailable-card/unavailable-card.component.ts index a33f38bb90..f949856e31 100644 --- a/src/app/errors/unavailable-card/unavailable-card.component.ts +++ b/src/app/errors/unavailable-card/unavailable-card.component.ts @@ -1,8 +1,10 @@ -import {Component} from '@angular/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; @Component({ selector: 'f-unavailable-card', templateUrl: './unavailable-card.component.html', styleUrls: ['./unavailable-card.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class UnavailableCardComponent {} diff --git a/src/app/eula/accept-eula/accept-eula.component.html b/src/app/eula/accept-eula/accept-eula.component.html index b0cc9f6c5c..5de43cf604 100644 --- a/src/app/eula/accept-eula/accept-eula.component.html +++ b/src/app/eula/accept-eula/accept-eula.component.html @@ -1,5 +1,5 @@
-
+

End User License Agreements

In order to use {{ toolName | async }}, you need to accept the following end user license @@ -7,10 +7,10 @@

End User License Agreements

-
diff --git a/src/app/eula/accept-eula/accept-eula.component.spec.ts b/src/app/eula/accept-eula/accept-eula.component.spec.ts index cb749c8c4b..9912d785e5 100644 --- a/src/app/eula/accept-eula/accept-eula.component.spec.ts +++ b/src/app/eula/accept-eula/accept-eula.component.spec.ts @@ -1,6 +1,19 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Router} from '@angular/router'; +import {EMPTY} from 'rxjs'; +import {UserService} from 'src/app/api/models/doubtfire-model'; +import {TiiService} from 'src/app/api/services/tii.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {AcceptEulaComponent} from './accept-eula.component'; -import { AcceptEulaComponent } from './accept-eula.component'; +const constantsStub = { + ExternalName: EMPTY, + IsTiiEnabled: EMPTY, +}; +const emptyProvider = {}; describe('AcceptEulaComponent', () => { let component: AcceptEulaComponent; @@ -8,13 +21,23 @@ describe('AcceptEulaComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ AcceptEulaComponent ] + declarations: [AcceptEulaComponent], + providers: [ + {provide: DoubtfireConstants, useValue: constantsStub}, + {provide: TiiService, useValue: emptyProvider}, + {provide: UserService, useValue: emptyProvider}, + {provide: AlertService, useValue: emptyProvider}, + {provide: Router, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], }) - .compileComponents(); + .overrideComponent(AcceptEulaComponent, {set: {template: ''}}) + .compileComponents(); + }); + beforeEach(() => { fixture = TestBed.createComponent(AcceptEulaComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/eula/accept-eula/accept-eula.component.ts b/src/app/eula/accept-eula/accept-eula.component.ts index fa6c4abadb..a8c0ede0e0 100644 --- a/src/app/eula/accept-eula/accept-eula.component.ts +++ b/src/app/eula/accept-eula/accept-eula.component.ts @@ -1,5 +1,5 @@ -import {Component} from '@angular/core'; -import {StateService} from '@uirouter/core'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {Router} from '@angular/router'; import {Observable, ReplaySubject, take} from 'rxjs'; import {UserService} from 'src/app/api/models/doubtfire-model'; import {TiiService} from 'src/app/api/services/tii.service'; @@ -10,25 +10,27 @@ import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; selector: 'f-accept-eula', templateUrl: './accept-eula.component.html', styleUrls: ['./accept-eula.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class AcceptEulaComponent { public toolName: Observable; public eulaHtml: string; - public iframeDoc$ = new ReplaySubject(1); + public iframeDoc$: ReplaySubject = new ReplaySubject(1); constructor( private constants: DoubtfireConstants, private tiiService: TiiService, private userService: UserService, private alertService: AlertService, - private state: StateService, + private router: Router, ) { this.constants.IsTiiEnabled.subscribe((enabled) => { if (enabled) { this.getEulaHtml(); } else { - this.state.go('home'); + this.router.navigateByUrl('/home'); } }); @@ -45,15 +47,17 @@ export class AcceptEulaComponent { public acceptEula(): void { this.userService.currentUser.acceptTiiEula().subscribe(() => { this.alertService.success('You have accepted the EULAs'); - this.state.go('home'); + this.router.navigateByUrl('/home'); }); } - public onIframeLoad(iframe): void { - this.iframeDoc$.next(iframe.contentDocument || iframe.contentWindow); + public onIframeLoad(iframe: HTMLIFrameElement): void { + if (iframe.contentDocument) { + this.iframeDoc$.next(iframe.contentDocument); + } } - getIframeDoc(): Observable { + getIframeDoc(): Observable { return this.iframeDoc$.asObservable(); } diff --git a/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.coffee b/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.coffee deleted file mode 100644 index b949a95d98..0000000000 --- a/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.coffee +++ /dev/null @@ -1,74 +0,0 @@ -angular.module('doubtfire.groups.group-member-contribution-assigner', []) - -# -# Directive to rate each student's contributions -# in a group task assessment -# -.directive('groupMemberContributionAssigner', -> - restrict: 'E' - templateUrl: 'groups/group-member-contribution-assigner/group-member-contribution-assigner.tpl.html' - replace: true - scope: - task: '=' - project: '=' - team: '=' #out parameter - - controller: ($scope, gradeService) -> - $scope.selectedGroupSet = $scope.task.definition.groupSet - unless $scope.task.isTestSubmission - $scope.selectedGroup = $scope.project.getGroupForTask($scope.task) - - $scope.memberSortOrder = 'project.student.name' - $scope.numStars = 5 - $scope.initialStars = 3 - - $scope.percentages = { - danger: 0, - warning: 25, - info: 50, - success: 100 - } - - $scope.checkClearRating = (contrib) -> - if contrib.confRating == 1 && contrib.overStar == 1 && contrib.rating == 0 - contrib.rating = contrib.percent = 0 - else if contrib.confRating == 1 && contrib.overStar == 1 && contrib.rating == 0 - contrib.rating = 1 - contrib.confRating = contrib.rating - - memberPercentage = (contrib, rating) -> - (100 * (rating / $scope.selectedGroup.contributionSum($scope.team.memberContributions, contrib, rating))).toFixed() - - $scope.hoveringOver = (contrib, value) -> - contrib.overStar = value - contrib.percent = memberPercentage(contrib, value) - - $scope.gradeFor = gradeService.gradeFor - - if $scope.selectedGroup && $scope.selectedGroupSet - $scope.selectedGroup.getMembers().subscribe({ - next: (members) -> - $scope.team.memberContributions = _.map(members, (member) -> - result = { - project: member, - rating: $scope.initialStars, - confRating: $scope.initialStars, - percent: 0 - } - result.percent = memberPercentage(result, $scope.initialStars) - result - ) - # Need the '+' to convert to number - $scope.percentages.warning = +(25 / members.length).toFixed() - $scope.percentages.info = +(50 / members.length).toFixed() - $scope.percentages.success = +(95 / members.length).toFixed() - }) - else - $scope.team.memberContributions = [] - - $scope.percentClass = (pct) -> - return 'label-success' if pct >= $scope.percentages.success - return 'label-info' if $scope.percentages.info <= pct < $scope.percentages.success - return 'label-warning' if $scope.percentages.warning <= pct < $scope.percentages.info - return 'label-danger' if $scope.percentages.danger <= pct < $scope.percentages.warning -) diff --git a/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.component.html b/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.component.html new file mode 100644 index 0000000000..4d62d51d5b --- /dev/null +++ b/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.component.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + +
Team Member + {{ member.project.student.name }} + Target Grade + + Contribution + @for (i of [].constructor(numStars); track $index) { + + person + + } +
diff --git a/src/app/units/states/tasks/viewer/directives/f-task-sheet-view/f-task-sheet-view.component.spec.ts b/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.component.scss similarity index 100% rename from src/app/units/states/tasks/viewer/directives/f-task-sheet-view/f-task-sheet-view.component.spec.ts rename to src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.component.scss diff --git a/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.component.ts b/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.component.ts new file mode 100644 index 0000000000..6f75ea24ea --- /dev/null +++ b/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.component.ts @@ -0,0 +1,161 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, +} from '@angular/core'; +import {Sort} from '@angular/material/sort'; +import {MatTableDataSource} from '@angular/material/table'; +import {GroupSet} from 'src/app/api/models/doubtfire-model'; +import {Group, MemberContribution} from 'src/app/api/models/groups/group'; +import {Project} from 'src/app/api/models/project'; +import {Task} from 'src/app/api/models/task'; + +@Component({ + selector: 'f-group-member-contribution-assigner', + templateUrl: './group-member-contribution-assigner.component.html', + styleUrls: ['./group-member-contribution-assigner.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class GroupMemberContributionAssignerComponent implements OnInit, OnChanges { + @Input() isTestSubmission: boolean; + + @Input() task: Task; + @Input() project: Project; + @Input() team = {memberContributions: [] as MemberContribution[]}; + @Output() teamChange: EventEmitter<{memberContributions: MemberContribution[]}> = + new EventEmitter(); + + selectedGroupSet: GroupSet; + selectedGroup: Group; + + numStars = 5; + initialStars = 3; + + percentages = { + danger: 0, + warning: 25, + info: 50, + success: 100, + }; + + displayedColumns = ['name', 'target-grade', 'contribution']; + dataSource: MatTableDataSource = new MatTableDataSource([]); + + ngOnInit(): void { + this.initializeGroupData(); + this.loadMembers(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['task'] || changes['project']) { + this.initializeGroupData(); + this.loadMembers(); + } + } + + private initializeGroupData(): void { + this.selectedGroupSet = this.task?.definition?.groupSet; + // Check if this is an overseer test submission + if (!this.isTestSubmission) { + const group = this.project?.getGroupForTask(this.task); + this.selectedGroup = group; + if (!this.selectedGroup && this.selectedGroupSet?.groups?.length > 0) { + this.selectedGroup = this.selectedGroupSet.groups[0]; + } + } + } + + private loadMembers(): void { + if (!this.selectedGroup && this.selectedGroupSet?.groups?.length > 0) { + console.error(`Could not find project's group`); + this.team.memberContributions = []; + return; + } + if (this.selectedGroup && this.selectedGroupSet) { + this.selectedGroup.getMembers().subscribe({ + next: (members) => { + this.team.memberContributions = members.map((member) => { + const result: MemberContribution = { + project: member, + rating: this.initialStars, + percent: 0, + overStar: null, + }; + result.percent = this.memberPercentage(result, this.initialStars); + return result; + }); + + // Update percentages based on member count + this.percentages.warning = +(25 / members.length).toFixed(); + this.percentages.info = +(50 / members.length).toFixed(); + this.percentages.success = +(95 / members.length).toFixed(); + + this.teamChange.emit(this.team); + this.dataSource.data = [...this.team.memberContributions]; + }, + }); + } else { + this.team.memberContributions = []; + this.teamChange.emit(this.team); + } + } + + private memberPercentage(contrib: MemberContribution, rating: number): number { + return +( + 100 * + (rating / + this.selectedGroup.contributionSum(this.team.memberContributions, contrib.project, rating)) + ).toFixed(); + } + + selectRating(contrib: MemberContribution, rating: number) { + if (contrib.rating !== rating) { + contrib.rating = rating; + this.hoveringOver(contrib, rating); + } else { + contrib.rating = 0; + this.hoveringOver(contrib, 0); + } + } + + hoveringOver(contrib: MemberContribution, value: number): void { + contrib.overStar = value; + contrib.percent = this.memberPercentage(contrib, value); + } + + private sortCompare(aValue: number | string, bValue: number | string, isAsc: boolean) { + return (aValue < bValue ? -1 : 1) * (isAsc ? 1 : -1); + } + + sortTableData(sort: Sort) { + if (!sort.active || sort.direction === '') { + return; + } + this.dataSource.data = this.dataSource.data.sort((a, b) => { + switch (sort.active) { + case 'name': + return this.sortCompare( + a.project.student.name, + b.project.student.name, + sort.direction === 'asc', + ); + case 'target-grade': + return this.sortCompare( + a.project.targetGrade, + b.project.targetGrade, + sort.direction === 'asc', + ); + case 'contribution': + return this.sortCompare(a.rating, b.rating, sort.direction === 'asc'); + default: + return 0; + } + }); + } +} diff --git a/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.scss b/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.scss deleted file mode 100644 index 8199fd2757..0000000000 --- a/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.scss +++ /dev/null @@ -1,19 +0,0 @@ -.group-member-contribution-assigner { - .group-member-contribution-rating { - &:focus { - outline: none; - } - i { - font-size: 2em; - cursor: pointer; - } - .icon-colorful { - color: rgb(255, 247, 141); - -webkit-text-stroke-width: 1px; - -webkit-text-stroke-color: orange; - } - .icon-disable { - color: #ccc; - } - } -} diff --git a/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.tpl.html b/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.tpl.html deleted file mode 100644 index 92c4f2ef43..0000000000 --- a/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.tpl.html +++ /dev/null @@ -1,41 +0,0 @@ -
- - - - - - - - - - - - - - - -
Team MemberTarget GradeContribution
{{contrib.project.student.name}} - - - - - - - {{contrib.percent}} % effort - - - No effort - - -
-
diff --git a/src/app/groups/group-member-list/group-member-list.coffee b/src/app/groups/group-member-list/group-member-list.coffee deleted file mode 100644 index ddb80ece67..0000000000 --- a/src/app/groups/group-member-list/group-member-list.coffee +++ /dev/null @@ -1,58 +0,0 @@ -angular.module('doubtfire.groups.group-member-list', []) - -# -# Lists members in a group -# -.directive('groupMemberList', -> - restrict: 'E' - templateUrl: 'groups/group-member-list/group-member-list.tpl.html' - scope: - unit: '=' - project: '=' - unitRole: '=' - selectedGroup: '=' - onMembersLoaded: '=?' - controller: ($scope, $timeout, gradeService, alertService, listenerService) -> - # Cleanup - listeners = listenerService.listenTo($scope) - - # Initial sort orders - $scope.tableSort = - order: 'student_name' - reverse: false - - # Table sorting - $scope.sortTableBy = (column) -> - $scope.tableSort.order = column - $scope.tableSort.reverse = !$scope.tableSort.reverse - - # Loading - startLoading = -> $scope.loaded = false - finishLoading = -> $timeout(-> - $scope.loaded = true - $scope.onMembersLoaded?() - , 500) - - # Initially not loaded - $scope.loaded = false - - # Remove group members - $scope.removeMember = (member) -> - $scope.selectedGroup.removeMember(member) - - # Listen for changes to group - listeners.push $scope.$watch "selectedGroup.id", (newGroupId) -> - return unless newGroupId? - startLoading() - $scope.canRemoveMembers = $scope.unitRole || ($scope.selectedGroup.groupSet.allowStudentsToManageGroups && !$scope.selectedGroup.locked) - - $scope.selectedGroup.getMembers().subscribe({ - next: (members) -> - finishLoading() - error: (failure) -> - $timeout((-> - alertService.error( "Unauthorised to view members in this group", 3000) - $scope.selectedGroup = null - ), 1000) - }) -) diff --git a/src/app/groups/group-member-list/group-member-list.component.html b/src/app/groups/group-member-list/group-member-list.component.html new file mode 100644 index 0000000000..e87db81839 --- /dev/null +++ b/src/app/groups/group-member-list/group-member-list.component.html @@ -0,0 +1,55 @@ +@if (loading) { +
+ Loading members... +
+} @else if (!selectedGroup || selectedGroup.members.length === 0) { +
+ group_off +

There are no members in this group

+
+} @else { + + + + + + + + + + + + + + + + + + + + + + + +
{{ unitRole ? 'Student ID' : '' }} + @if (unitRole) { + {{ member.student.username || 'N/A' }} + } + Name + {{ member.student.name }} + {{ unitRole ? 'Target Grade' : '' }} + @if (unitRole) { + + } + {{ canRemoveMembers ? 'Actions' : '' }} + @if (canRemoveMembers) { + @if (!project && unitRole) { + + } @else if (project && project.id === member.id) { + + } + } +
+} diff --git a/src/app/groups/group-member-list/group-member-list.component.scss b/src/app/groups/group-member-list/group-member-list.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/groups/group-member-list/group-member-list.component.ts b/src/app/groups/group-member-list/group-member-list.component.ts new file mode 100644 index 0000000000..162c552139 --- /dev/null +++ b/src/app/groups/group-member-list/group-member-list.component.ts @@ -0,0 +1,81 @@ +import { + ChangeDetectionStrategy, + Component, + Input, + OnChanges, + OnInit, + SimpleChanges, +} from '@angular/core'; +import {MatTableDataSource} from '@angular/material/table'; +import {Subscription} from 'rxjs'; +import {Group, UnitRole} from 'src/app/api/models/doubtfire-model'; +import {Project} from 'src/app/api/models/project'; +import {Unit} from 'src/app/api/models/unit'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-group-member-list', + templateUrl: './group-member-list.component.html', + styleUrls: ['./group-member-list.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class GroupMemberListComponent implements OnInit, OnChanges { + @Input() unit: Unit; + @Input() unitRole: UnitRole; + @Input() project: Project; + @Input() selectedGroup: Group; + @Input() onMembersLoaded: () => void; + + loading = false; + + canRemoveMembers = false; + + displayedColumns: string[] = ['student_id', 'name', 'target_grade', 'actions']; + groupMembers: Project[] = []; + dataSource = new MatTableDataSource(); + + private groupMembersSub?: Subscription; + + constructor(private alertService: AlertService) {} + + ngOnInit() { + if (!this.selectedGroup) { + return; + } + + this.groupMembersSub = this.selectedGroup.projectsCache.values.subscribe((values) => { + this.dataSource.data = values; + }); + } + + public removeMember(member: Project) { + this.selectedGroup.removeMember(member); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['selectedGroup'] && this.selectedGroup) { + this.loading = true; + this.selectedGroup.getMembers().subscribe({ + next: (members) => { + this.loading = false; + this.onMembersLoaded?.(); + this.canRemoveMembers = + !!this.unitRole || + (this.selectedGroup.groupSet.allowStudentsToManageGroups && !this.selectedGroup.locked); + + this.dataSource.data = members; + + this.groupMembersSub?.unsubscribe(); + this.groupMembersSub = this.selectedGroup.projectsCache.values.subscribe((values) => { + this.dataSource.data = values; + }); + }, + error: (error) => { + this.alertService.error(`Failed to fetch group members: ${error}`, 6000); + this.selectedGroup = null; + }, + }); + } + } +} diff --git a/src/app/groups/group-member-list/group-member-list.scss b/src/app/groups/group-member-list/group-member-list.scss deleted file mode 100644 index 75fb259794..0000000000 --- a/src/app/groups/group-member-list/group-member-list.scss +++ /dev/null @@ -1,6 +0,0 @@ -group-member-list table { - th.student-id { width: 25%; } - th.student-name { width: 50%; } - th.actions { width: 25%; } - th.student-grade { width: 50%; } -} diff --git a/src/app/groups/group-member-list/group-member-list.tpl.html b/src/app/groups/group-member-list/group-member-list.tpl.html deleted file mode 100644 index fd28367eb5..0000000000 --- a/src/app/groups/group-member-list/group-member-list.tpl.html +++ /dev/null @@ -1,55 +0,0 @@ -
- Loading Members... -
-
-
-

No members in group

-

There are no members in this group

-
-
- - - - - - - - - - - - - - - - - -
- - Student ID - - - - - Name - - - - - Target Grade - - - - Actions -
{{member.student.username || "N/A"}}{{member.student.name}} - - - - -
diff --git a/src/app/groups/group-selector/group-selector.coffee b/src/app/groups/group-selector/group-selector.coffee deleted file mode 100644 index f6e0a56745..0000000000 --- a/src/app/groups/group-selector/group-selector.coffee +++ /dev/null @@ -1,210 +0,0 @@ -angular.module('doubtfire.groups.group-selector', []) - -# -# Allows tutors and students to select (and create if applicable) -# new groups for teamwork -# -.directive('groupSelector', -> - restrict: 'E' - templateUrl: 'groups/group-selector/group-selector.tpl.html' - scope: - unit: "=" - # Use project for student context - project: "=?" - # Use unit role for tutor context - unitRole: "=?" - # Pass in a groupset to set the groupset context - selectedGroupSet: '=' - # Bind the selected group for switching - selectedGroup: '=?' - # Shows the groupset selector - showGroupSetSelector: '=?' - # On change of a group - onSelect: '=?' - controller: ($scope, $filter, $timeout, alertService, listenerService, newUserService, newGroupService) -> - # Cleanup - listeners = listenerService.listenTo($scope) - - # Unit role or project should be included in $scope - if !$scope.unitRole? && !$scope.project? || $scope.unitRole? && $scope.project? - throw Error "Group selector must have exactly one unit role or one project" - - # Filtering - applyFilters = -> - if $scope.unitRole? # apply staff filter - filteredGroups = $filter('groupsInTutorials')($scope.selectedGroupSet.groups, $scope.unitRole, $scope.staffFilter) - else # apply project filter - filteredGroups = $scope.selectedGroupSet.groups - # Apply remaining filters - $scope.filteredGroups = $filter('paginateAndSort')(filteredGroups, $scope.pagination, $scope.tableSort) - - $scope.setStaffFilter = (scope) -> - $scope.staffFilter = scope - applyFilters() - - # Pagination values - $scope.pagination = - currentPage: 1 - maxSize: 10 - pageSize: 10 - totalSize: null - show: false - onChange: applyFilters - - # Initial sort orders - $scope.tableSort = - order: 'name' - reverse: false - - # Table sorting - $scope.sortTableBy = (column) -> - $scope.tableSort.order = column - $scope.tableSort.reverse = !$scope.tableSort.reverse - applyFilters() - - # Loading - startLoading = -> $scope.loaded = false - finishLoading = -> $timeout((-> - $scope.loaded = true - if $scope.project? - $scope.selectGroup($scope.project.groupForGroupSet($scope.selectedGroupSet)) - ), 500) - - # Select group function - $scope.selectGroup = (group) -> - return if $scope.project? && ! $scope.project.inGroup(group) # its the student view - - $scope.selectedGroup = group - $scope.onSelect?(group) - - # Sets the placeholder text (useful to know named - # groups are technically optional) - resetNewGroupForm = () -> - $scope.newGroupName = "" - - # Group set selector - $scope.selectedGroupSet ?= _.first($scope.unit.groupSets) - $scope.showGroupSetSelector ?= $scope.unit.groupSets.length > 1 - $scope.selectGroupSet = (groupSet) -> - return unless groupSet? - startLoading() - $scope.selectGroup(null) - # Can only create groups if unitRole provided and selectedGroupSet - $scope.canCreateGroups = $scope.unitRole? || groupSet?.allowStudentsToCreateGroups - $scope.unit.getGroups(groupSet).subscribe({ - next: (groups) -> - $scope.selectedGroupSet = groupSet - finishLoading() - resetNewGroupForm() - applyFilters() - error: (message) -> - finishLoading() - alertService.error( "Unable to get groups #{message}", 6000) - }) - - $scope.selectGroupSet($scope.selectedGroupSet) - - # Load groups if not loaded - # $scope.unit.getGroups($scope.selectedGroupSet.id) if $scope.selectedGroupSet?.groups? - - # Staff filter options (convenor should see all) - $scope.staffFilter = { - Convenor: 'all', - Tutor: 'mine' - }[$scope.unitRole.role] if $scope.unitRole? - - # Changing staff filter reapplies filter - $scope.onChangeStaffFilter = applyFilters - - # Search text reapplies filter - $scope.searchTextChanged = applyFilters - - # Adds a group to the unit - $scope.addGroup = (name) -> - if $scope.unit.tutorials.length == 0 - alertService.error( "Please ensure there is at least one tutorial before groups are created", 6000) - # Student context - if $scope.project - #TODO: Need to add stream to group set - tutorialId = $scope.project.tutorials[0].id || $scope.unit.tutorials[0].id - else - # Convenor or Tutor - tutorName = $scope.unitRole?.name || newUserService.currentUser.name - tutorialId = _.find($scope.unit.tutorials, (tute) -> tute.tutor?.name == tutorName)?.id - # Default to first tutorial if can't find - tutorialId ?= _.first($scope.unit.tutorials).id - - newGroupService.create({ - unitId: $scope.unit.id, - groupSetId: $scope.selectedGroupSet.id, - }, { - cache: $scope.selectedGroupSet.groupsCache, - constructorParams: $scope.unit - body: { - group: { - name: name, - tutorial_id: tutorialId - } - } - }).subscribe({ - next: (group) -> - resetNewGroupForm() - applyFilters() - $scope.selectedGroup = group - error: (message) -> alertService.error( message, 6000) - }) - - # Join or leave group as project - $scope.projectInGroup = (group) -> - $scope.project?.inGroup(group) - - $scope.joinGroup = (group) -> - return unless $scope.project? - partOfGroup = $scope.projectInGroup(group) - return alertService.error( "You are already member of this group") if partOfGroup - group.addMember($scope.project, - () -> - $scope.selectedGroup = group - () -> - ) - - # Update group function - $scope.updateGroup = (data, group) -> - group.capacityAdjustment = data.capacityAdjustment - group.tutorial = data.tutorial - group.name = data.name - - newGroupService.update(group).subscribe({ - next: () -> - alertService.success( "Updated group", 2000) - applyFilters() - error: (message) -> alertService.error( "Failed to update group. #{message}", 6000) - }) - - # Remove group function - $scope.deleteGroup = (group) -> - newGroupService.delete(group, { cache: $scope.selectedGroupSet.groupsCache }).subscribe({ - next: () -> - alertService.success( "Deleted group", 2000) - $scope.selectedGroup = null if group.id == $scope.selectedGroup?.id - resetNewGroupForm() - applyFilters() - error: () -> alertService.error( "Failed to delete group. #{message}", 6000) - }) - - # Toggle lockable group - $scope.toggleLocked = (group) -> - group.locked = !group.locked - newGroupService.update(group).subscribe({ - next: (success) -> - group.locked = success.locked - alertService.success( "Group updated", 2000) - error: () -> alertService.error( "Failed to lock group. #{message}", 6000) - }) - - # Watch selected group set changes - listeners.push $scope.$on 'UnitGroupSetEditor/SelectedGroupSetChanged', (evt, args) -> - newGroupSet = $scope.unit.findGroupSet(args.id) - # return if newGroupSet == $scope.selectedGroupSet - $scope.selectGroupSet(newGroupSet) -) diff --git a/src/app/groups/group-selector/group-selector.component.html b/src/app/groups/group-selector/group-selector.component.html new file mode 100644 index 0000000000..ccc01c2506 --- /dev/null +++ b/src/app/groups/group-selector/group-selector.component.html @@ -0,0 +1,203 @@ + + +
+
+
+ + Groups for + @if (!showGroupSetSelector && selectedGroup) { + "{{ selectedGroupSet?.name }}" + } + + + @if (showGroupSetSelector) { + + + @for (gs of unit.groupSets; track gs.id) { + {{ gs.name }} + } + + + } +
+ @if (unitRole || selectedGroupSet?.allowStudentsToCreateGroups) { +
+ + + + +
+ } +
+ @if (unitRole) { +
+ + All Tutorials + My Tutorials + +
+ } +
+
+ + @if (selectedGroupSet && selectedGroupSet.groups.length === 0) { +
+ group_off +

There are no groups in this set

+
+ } @else { + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name + @if (editing(group)) { + + + + } @else { + {{ group.name || 'Not set' }} + } + Tutorial + @if (editing(group)) { + + + @for (tutorial of unit.tutorials; track tutorial) { + {{ tutorial.abbreviation }} + } + + + } @else { + {{ group.tutorial.abbreviation }} + } + + @if (unitRole) { + Capacity Adjustment + } + + @if (unitRole) { + @if (editing(group)) { + + + + } @else { + {{ group.capacityAdjustment }} + } + } + Capacity + @if (group.hasSpace()) { + Available + } @else { + Full + } + + @if (unitRole || (project && selectedGroupSet.allowStudentsToManageGroups)) { + Actions + } + + @if (isPartOfGroup(project, group)) { +
Joined
+ } @else if (project && group.hasSpace() && selectedGroupSet.allowStudentsToManageGroups) { +
+ @if (!group.locked && !selectedGroupSet.locked) { + + } @else { + lock + } +
+ } + @if (unitRole) { +
+ @if (editing(group)) { +
+ + +
+ } @else { + + + + } +
+ } +
+ + } + + @if ( + selectedGroupSet && + selectedGroupSet.keepGroupsInSameClass && + selectedGroupSet.groups.length > 0 && + !unitRole + ) { +

+ Can't see the group you need to join? Groups shown are limited to those in your allocated + tutorials. Use the + Tutorial List to check and update + your tutorial enrolment if needed. +

+ } +
diff --git a/src/app/groups/group-selector/group-selector.component.scss b/src/app/groups/group-selector/group-selector.component.scss new file mode 100644 index 0000000000..200fdfce50 --- /dev/null +++ b/src/app/groups/group-selector/group-selector.component.scss @@ -0,0 +1,9 @@ +.mat-mdc-row .mat-mdc-cell { + border-bottom: 1px solid transparent; + border-top: 1px solid transparent; + cursor: pointer; +} + +.mat-mdc-row:hover { + background-color: #eee; +} diff --git a/src/app/groups/group-selector/group-selector.component.ts b/src/app/groups/group-selector/group-selector.component.ts new file mode 100644 index 0000000000..0a9b475738 --- /dev/null +++ b/src/app/groups/group-selector/group-selector.component.ts @@ -0,0 +1,267 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + Input, + OnChanges, + OnInit, + SimpleChanges, + ViewChild, +} from '@angular/core'; +import {UntypedFormControl, Validators} from '@angular/forms'; +import {MatButtonToggleChange} from '@angular/material/button-toggle'; +import {MatPaginator} from '@angular/material/paginator'; +import {MatTableDataSource} from '@angular/material/table'; +import {Subscription} from 'rxjs'; +import {Group, GroupSet, UnitRole, UserService} from 'src/app/api/models/doubtfire-model'; +import {Project} from 'src/app/api/models/project'; +import {Unit} from 'src/app/api/models/unit'; +import {GroupService} from 'src/app/api/services/group.service'; +import {EntityFormComponent} from 'src/app/common/entity-form/entity-form.component'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-group-selector', + templateUrl: './group-selector.component.html', + styleUrls: ['./group-selector.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class GroupSelectorComponent + extends EntityFormComponent + implements OnInit, OnChanges, AfterViewInit +{ + @Input() unit: Unit; + @Input() unitRole: UnitRole; + @Input() project: Project; + @Input() selectedGroup: Group; + @Input() selectedGroupSet: GroupSet; + @Input() onSelect: (group: Group) => void; + + @ViewChild(MatPaginator) paginator!: MatPaginator; + displayedColumns: string[] = ['name', 'tutorial', 'capacity_adjustment', 'capacity', 'actions']; + public groups: Group[] = []; + + public newGroupName: string; + public staffTutorialFilter: 'all' | 'mine' = 'all'; + + private groupsSub?: Subscription; + + constructor( + private userService: UserService, + private groupService: GroupService, + private alertService: AlertService, + ) { + super( + { + name: new UntypedFormControl('', [Validators.required]), + tutorial: new UntypedFormControl(null, [Validators.required]), + capacityAdjustment: new UntypedFormControl('', [Validators.required]), + }, + 'Group', + ); + } + + public get showGroupSetSelector() { + return this.unit.groupSets.length > 1; + } + + ngOnInit(): void { + if (this.unit.groupSets.length > 0) { + this.selectedGroupSet = this.unit.groupSets[0]; + } + } + + selectGroupSet(groupSet: GroupSet) { + this.selectedGroupSet = groupSet; + this.refreshGroups(); + } + + ngAfterViewInit() { + this.dataSource = new MatTableDataSource(); + this.dataSource.paginator = this.paginator; + + if (this.unit.groupSets.length > 0) { + this.selectedGroupSet = this.unit.groupSets[0]; + } + + this.refreshGroups(); + } + + refreshGroups() { + this.groupsSub?.unsubscribe(); + this.groupsSub = this.selectedGroupSet?.groupsCache.values.subscribe((values) => { + this.groups = [...values]; + }); + this.applyFilters(); + } + + onGroupNameChange() { + this.applyFilters(); + } + + applyFilters() { + const filteredGroups = this.groups + .filter( + (g) => + this.staffTutorialFilter === 'all' || + (this.unitRole && g.tutorial.tutor.id === this.unitRole.user.id), + ) + .filter( + (g) => !this.newGroupName || g.name.toLowerCase().includes(this.newGroupName.toLowerCase()), + ); + + this.dataSource.data = filteredGroups.sort((a, b) => a.name.localeCompare(b.name)); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['selectedGroupSet'] && this.selectedGroupSet) { + if (!this.dataSource) { + this.dataSource = new MatTableDataSource(); + } + this.refreshGroups(); + } + } + + onTutorialFilterChange(event: MatButtonToggleChange) { + this.staffTutorialFilter = event.value; + this.applyFilters(); + } + + addGroup(name: string) { + if (this.unit.tutorials.length == 0) { + this.alertService.error( + `Please ensure there is at least one tutorial before groups are created`, + 6000, + ); + return; + } + let tutorialId; + if (this.project) { + tutorialId = this.project.tutorials[0].id || this.unit.tutorials[0].id; + } else { + const tutorName = this.unitRole?.user.name || this.userService.currentUser.name; + tutorialId = + this.unit.tutorials.find((t) => t.tutor?.name === tutorName)?.id ?? + this.unit.tutorials[0].id; + } + + this.groupService + .create( + { + unitId: this.unit.id, + groupSetId: this.selectedGroupSet.id, + }, + { + cache: this.selectedGroupSet.groupsCache, + constructorParams: this.unit, + body: { + group: { + name, + tutorial_id: tutorialId, + }, + }, + }, + ) + .subscribe({ + next: (group) => { + this.alertService.success('Successfully created group', 3000); + this.selectedGroup = group; + this.newGroupName = ''; + this.applyFilters(); + }, + error: (error) => { + this.alertService.error(`Failed to create group: ${error}`); + }, + }); + } + + isPartOfGroup(project: Project, group: Group) { + return group && project?.inGroup(group); + } + + joinGroup(group: Group) { + if (!this.project) { + return; + } + + if (this.isPartOfGroup(this.project, group)) { + this.alertService.error('You are already member of this group'); + return; + } + + group.addMember(this.project, () => { + this.selectedGroup = group; + this.selectGroup(group); + }); + } + + selectGroup(group: Group) { + if (this.project && !this.project.inGroup(group)) { + // Return because we're in the student view + return; + } + + if (this.editing(group)) { + return; + } + + this.selectedGroup = group; + this.onSelect(group); + } + + deleteGroup(event: Event, group: Group) { + event.stopPropagation(); + + this.groupService.delete(group, {cache: this.selectedGroupSet.groupsCache}).subscribe({ + next: () => { + this.alertService.success('Deleted group', 3000); + if (group.id === this.selectedGroup?.id) { + this.selectedGroup = null; + this.selectGroup(null); + } + }, + error: (error) => { + this.alertService.error(`Failed to delete group: ${error}`, 6000); + }, + }); + } + + toggleLocked(event: Event, group: Group) { + event.stopPropagation(); + + const originalLockedState = group.locked; + group.locked = !group.locked; + + this.groupService.update(group).subscribe({ + next: (success) => { + group.locked = success.locked; + this.alertService.success(`Group has been ${!group.locked ? 'un' : ''}locked`, 3000); + }, + error: (error) => { + this.alertService.error(`Failed to ${!group.locked ? 'un' : ''}lock group: ${error}`, 6000); + group.locked = originalLockedState; + }, + }); + } + + startEditGroup(event: Event, group: Group) { + event.stopPropagation(); + this.flagEdit(group); + } + + cancelEditGroup(event: Event) { + event.stopPropagation(); + this.cancelEdit(); + } + + saveEdit(event: Event) { + event.stopPropagation(); + super.submit(this.groupService, this.alertService, this.onSuccess.bind(this)); + this.cancelEdit(); + } + + onSuccess(): void { + this.refreshGroups(); + } +} diff --git a/src/app/groups/group-selector/group-selector.scss b/src/app/groups/group-selector/group-selector.scss deleted file mode 100644 index c176bf951a..0000000000 --- a/src/app/groups/group-selector/group-selector.scss +++ /dev/null @@ -1,45 +0,0 @@ -group-selector { - display: block; -} -group-selector table { - th.name { - width: 25%; - } - th.tutorial { - width: 15%; - } - th.capacity_adjustment { - width: 15%; - } - th.capacity { - width: 15%; - } - th.actions { - width: 25%; - } -} -group-selector .panel-title > group-set-selector { - display: inline-block; - max-width: 50%; - padding-left: 1ex; -} -@media (max-width: $screen-md) { - group-selector .input-group.staff-filter { - margin-bottom: 1em; - &, - .btn-group { - width: 100%; - } - .btn { - width: 50%; - } - } -} - -.lockButton { - width: 70px; -} - -.joinButton { - width: 70px; -} diff --git a/src/app/groups/group-selector/group-selector.tpl.html b/src/app/groups/group-selector/group-selector.tpl.html deleted file mode 100644 index e0a9446a14..0000000000 --- a/src/app/groups/group-selector/group-selector.tpl.html +++ /dev/null @@ -1,220 +0,0 @@ -
-

- Groups for - {{selectedGroupSet.name}} - - -

-
- -
-
-
-
- - -
-
- -
- - - - -
- -
-
- -
Loading Groups...
- -
-
-

No Groups To Show

-

- There are no groups available for {{selectedGroupSet.name}}{{staffFilter == 'mine' || - selectedGroupSet.keepGroupsInSameClass ? " in your tutorials." : ""}}{{newGroupName.length > 0 ? " with name " + newGroupName + "." : "."}} -

-

- Please make sure that you are enrolled in the correct tutorial. You can only join a group that is running in your - allocated tutorial. Use the Tutorial List to - check and update your tutorial enrolment. -

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - -
- - Name - - - - Tutorial - - - - - Capacity Adjustment - - - - - Capacity - - - - Actions -
- - {{ group.name || 'Not Set' }} - - - - - {{group.tutorial.abbreviation}} - - - - - {{group.capacityAdjustment}} - - - Available - Full - -
- - - - -
-
- - - - - - -
-
- diff --git a/src/app/groups/group-set-manager/group-set-manager.coffee b/src/app/groups/group-set-manager/group-set-manager.coffee deleted file mode 100644 index 5e8506f7cb..0000000000 --- a/src/app/groups/group-set-manager/group-set-manager.coffee +++ /dev/null @@ -1,44 +0,0 @@ -angular.module('doubtfire.groups.group-set-manager', []) - -# -# Manager directive for tutors to add and remove group -# members from a group within a group set context -# -.directive('groupSetManager', -> - restrict: 'E' - templateUrl: 'groups/group-set-manager/group-set-manager.tpl.html' - scope: - unit: '=' - unitRole: '=' - project: '=' - selectedGroupSet: '=' - showGroupSetSelector: '=?' - controller: ($scope, newGroupService, gradeService, alertService) -> - if !$scope.unitRole? && !$scope.project? - throw Error "Group set group manager must have exactly one unit role or project" - # Reset member panel toolbar visibility - $scope.newGroupSelected = -> - $scope.showMemberPanelToolbar = false if $scope.unitRole? - $scope.groupMembersLoaded = -> - $scope.showMemberPanelToolbar = true if $scope.unitRole? - - # Add new member to the group - $scope.addMember = (member) -> - $scope.selectedGroup.addMember(member) - $scope.selectedStudent = null - - # Update name of group - $scope.updateGroup = (data) -> - newGroupService.update({ - unitId: $scope.unit.id, - groupSetId: $scope.selectedGroupSet.id, - id: $scope.selectedGroup.id, - }, { - entity: data - }).subscribe({ - next: (response) -> - alertService.success( "Group changed", 2000) - error: (response) -> - alertService.error( response, 6000) - }) -) diff --git a/src/app/groups/group-set-manager/group-set-manager.component.html b/src/app/groups/group-set-manager/group-set-manager.component.html new file mode 100644 index 0000000000..96f29aed21 --- /dev/null +++ b/src/app/groups/group-set-manager/group-set-manager.component.html @@ -0,0 +1,76 @@ +
+ + + + @if (selectedGroup) { + + + Members of + @if (!editingGroupName) { + {{ selectedGroup?.name }} + @if (unitRole || selectedGroup.groupSet?.allowStudentsToManageGroups) { + + } + } @else { + @if (unitRole || selectedGroup.groupSet?.allowStudentsToManageGroups) { + + + + + + } + } + + @if (selectedGroup.locked) { + lock + } + + + + + @if (unitRole) { + + + + + @for (project of filteredProjects | async; track project) { + {{ project.student.name }} + } + + + + } + + } +
diff --git a/src/app/groups/group-set-manager/group-set-manager.component.scss b/src/app/groups/group-set-manager/group-set-manager.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/groups/group-set-manager/group-set-manager.component.ts b/src/app/groups/group-set-manager/group-set-manager.component.ts new file mode 100644 index 0000000000..5d986592e6 --- /dev/null +++ b/src/app/groups/group-set-manager/group-set-manager.component.ts @@ -0,0 +1,115 @@ +import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; +import {FormControl} from '@angular/forms'; +import {Observable, map, startWith} from 'rxjs'; +import {Group, GroupSet, Project, Unit, UnitRole} from 'src/app/api/models/doubtfire-model'; +import {GroupService} from 'src/app/api/services/group.service'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-group-set-manager', + templateUrl: './group-set-manager.component.html', + styleUrls: ['./group-set-manager.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class GroupSetManagerComponent implements OnInit { + @Input() project: Project; + @Input() unit: Unit; + @Input() selectedGroupSet: GroupSet; + @Input() showGroupSetSelector: boolean; + @Input() unitRole: UnitRole; + + public selectedGroup: Group; + + editingGroupName = false; + + control = new FormControl(''); + projects: Project[] = []; + filteredProjects: Observable; + + constructor( + private groupService: GroupService, + private alertService: AlertService, + ) {} + + ngOnInit(): void { + this.filteredProjects = this.control.valueChanges.pipe( + startWith(''), + map((value) => this._filter(value)), + ); + } + + get groupSelectHandler() { + return (group: Group) => this.newGroupSelected(group); + } + + displayFn(project: Project): string { + return project && project.student.name ? project.student.name : ''; + } + + newGroupSelected(group: Group) { + if (this.selectedGroup) { + this.selectedGroup.name = this.originalGroupName; + } + this.editingGroupName = false; + this.selectedGroup = group; + + const students = this.unit.studentsForGroupTypeAhead(group) || []; + this.projects = students.filter((project) => !group.projects.find((p) => project.id === p.id)); + + this.originalGroupName = group.name; + } + + private _filter(value: string | Project): Project[] { + if (typeof value !== 'string') { + return; + } + + const filterValue = value.toLowerCase(); + return this.projects.filter( + (project) => + project.student.name.toLowerCase().includes(filterValue.toLowerCase()) && // Find by name + !this.selectedGroup.projects.find((p) => project.id === p.id), // Not already assigned to the group + ); + } + + addMember(project: Project) { + this.selectedGroup.addMember(project); + this.control.setValue(''); + } + + private originalGroupName: string; + startEditingGroupName() { + this.originalGroupName = this.selectedGroup.name; + this.editingGroupName = true; + } + + stopEditinGroupName() { + this.selectedGroup.name = this.originalGroupName; + this.editingGroupName = false; + } + + updateGroup() { + this.editingGroupName = false; + this.groupService + .update( + { + unitId: this.unit.id, + groupSetId: this.selectedGroup.groupSet.id, + id: this.selectedGroup.id, + }, + { + entity: this.selectedGroup, + }, + ) + .subscribe({ + next: () => { + this.alertService.success('Successfully updated group', 3000); + }, + error: (error) => { + this.selectedGroup.name = this.originalGroupName; + this.alertService.error(`Failed to update gorup: ${error}`, 6000); + }, + }); + } +} diff --git a/src/app/groups/group-set-manager/group-set-manager.scss b/src/app/groups/group-set-manager/group-set-manager.scss deleted file mode 100644 index b65662abca..0000000000 --- a/src/app/groups/group-set-manager/group-set-manager.scss +++ /dev/null @@ -1,6 +0,0 @@ -@media (min-width: $screen-lg) { - group-set-manager { - display: block; - @include panel-row; - } -} diff --git a/src/app/groups/group-set-manager/group-set-manager.tpl.html b/src/app/groups/group-set-manager/group-set-manager.tpl.html deleted file mode 100644 index 09386b1daa..0000000000 --- a/src/app/groups/group-set-manager/group-set-manager.tpl.html +++ /dev/null @@ -1,68 +0,0 @@ - - -
-
-
-
-

- Members of - {{selectedGroup.name}} -

-
-
- -
-
-
- -
-
- -
- -
-
-
- - - -
- diff --git a/src/app/groups/group-set-selector/group-set-selector.component.html b/src/app/groups/group-set-selector/group-set-selector.component.html index 46486cf3f8..09ff83321d 100644 --- a/src/app/groups/group-set-selector/group-set-selector.component.html +++ b/src/app/groups/group-set-selector/group-set-selector.component.html @@ -1,10 +1,7 @@ - + @for (gs of unit.groupSets; track gs.id) { - {{gs.name}} + {{ gs.name }} } diff --git a/src/app/groups/group-set-selector/group-set-selector.component.ts b/src/app/groups/group-set-selector/group-set-selector.component.ts index 60ece272be..b6b7103d67 100644 --- a/src/app/groups/group-set-selector/group-set-selector.component.ts +++ b/src/app/groups/group-set-selector/group-set-selector.component.ts @@ -1,15 +1,24 @@ -import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; -import { Unit, GroupSet } from 'src/app/api/models/doubtfire-model'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; +import {GroupSet, Unit} from 'src/app/api/models/doubtfire-model'; @Component({ selector: 'group-set-selector', templateUrl: './group-set-selector.component.html', - styleUrls: ['./group-set-selector.component.scss'] + styleUrls: ['./group-set-selector.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class GroupSetSelectorComponent implements OnInit { @Input() unit: Unit; @Input() selectedGroupSet: GroupSet; - @Output() selectedGroupSetChange = new EventEmitter(); + @Output() selectedGroupSetChange: EventEmitter = new EventEmitter(); ngOnInit(): void { if (!this.unit) { diff --git a/src/app/groups/groups.coffee b/src/app/groups/groups.coffee deleted file mode 100644 index 391a78d492..0000000000 --- a/src/app/groups/groups.coffee +++ /dev/null @@ -1,6 +0,0 @@ -angular.module('doubtfire.groups', [ - 'doubtfire.groups.group-member-contribution-assigner' - 'doubtfire.groups.group-member-list' - 'doubtfire.groups.group-selector' - 'doubtfire.groups.group-set-manager' -]) diff --git a/src/app/home/splash-screen/LoadingService.service.ts b/src/app/home/splash-screen/LoadingService.service.ts index 471a6f3356..ff66ce1229 100644 --- a/src/app/home/splash-screen/LoadingService.service.ts +++ b/src/app/home/splash-screen/LoadingService.service.ts @@ -5,7 +5,7 @@ import {BehaviorSubject} from 'rxjs'; providedIn: 'root', }) export class LoadingService { - private loadingSubject = new BehaviorSubject(false); + private loadingSubject: BehaviorSubject = new BehaviorSubject(false); loading$ = this.loadingSubject.asObservable(); @@ -16,6 +16,4 @@ export class LoadingService { loadingOff() { this.loadingSubject.next(false); } - - constructor() {} } diff --git a/src/app/home/splash-screen/splash-screen.component.spec.ts b/src/app/home/splash-screen/splash-screen.component.spec.ts index 152b2a2bea..6f13f45e5f 100644 --- a/src/app/home/splash-screen/splash-screen.component.spec.ts +++ b/src/app/home/splash-screen/splash-screen.component.spec.ts @@ -1,35 +1,32 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { BehaviorSubject } from 'rxjs'; -import { GlobalStateService } from 'src/app/projects/states/index/global-state.service'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {GlobalStateService} from 'src/app/projects/states/index/global-state.service'; +import {LoadingService} from './LoadingService.service'; +import {SplashScreenComponent} from './splash-screen.component'; -import { SplashScreenComponent } from './splash-screen.component'; +const emptyProvider = {}; describe('SplashScreenComponent', () => { let component: SplashScreenComponent; let fixture: ComponentFixture; - let globalStateServiceStub: Partial; - beforeEach( - waitForAsync(() => { - const isLoadingSubject = new BehaviorSubject(true); - - globalStateServiceStub = { - isLoadingSubject: isLoadingSubject, - }; - - TestBed.configureTestingModule({ - declarations: [SplashScreenComponent], - imports: [BrowserAnimationsModule], - providers: [{ provide: GlobalStateService, useValue: globalStateServiceStub }], - }).compileComponents(); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SplashScreenComponent], + providers: [ + {provide: GlobalStateService, useValue: emptyProvider}, + {provide: LoadingService, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], }) - ); + .overrideComponent(SplashScreenComponent, {set: {template: ''}}) + .compileComponents(); + }); beforeEach(() => { fixture = TestBed.createComponent(SplashScreenComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/home/splash-screen/splash-screen.component.ts b/src/app/home/splash-screen/splash-screen.component.ts index a037652d4f..f53ae1d1ad 100644 --- a/src/app/home/splash-screen/splash-screen.component.ts +++ b/src/app/home/splash-screen/splash-screen.component.ts @@ -1,13 +1,15 @@ -import {Component, ContentChild, OnInit, TemplateRef} from '@angular/core'; import {AnimationOptions} from 'ngx-lottie'; +import {ChangeDetectionStrategy, Component, ContentChild, OnInit, TemplateRef} from '@angular/core'; import {Observable} from 'rxjs'; import {GlobalStateService} from 'src/app/projects/states/index/global-state.service'; import {LoadingService} from './LoadingService.service'; -import {AnimationItem} from 'lottie-web'; + @Component({ selector: 'splash-screen', templateUrl: './splash-screen.component.html', styleUrls: ['./splash-screen.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class SplashScreenComponent implements OnInit { constructor( @@ -39,5 +41,4 @@ export class SplashScreenComponent implements OnInit { } }); } - } diff --git a/src/app/home/states/home/home.component.html b/src/app/home/states/home/home.component.html index 4b02d137f0..08b25da246 100644 --- a/src/app/home/states/home/home.component.html +++ b/src/app/home/states/home/home.component.html @@ -1,76 +1,83 @@ -
+

You are not enrolled in {{ externalName.value }}.

Contact your unit convenor or tutor to enrol you in a subject.

-
-

You are not enrolled in any {{ externalName.value }} units.

-

Contact your unit convenor or tutor to enrol you in a subject.

-
- + @if (!notEnrolled && projects?.length === 0 && unitRoles?.length === 0) { +
+

You are not enrolled in any {{ externalName.value }} units.

+

Contact your unit convenor or tutor to enrol you in a subject.

+
+ }

Units you teach

-
- +
+
-
-
-
- - - {{ unitRole.unit?.name }} - {{ unitRole.unit?.code }} - - - - - - - {{ unitRole.teachingPeriod?.name || showDate(unitRole.unit.startDate) }} - - - {{ unitRole.role }} - - - + @for (unitRole of unitRoles | isActiveUnitRole; track unitRole) { +
+ @if (!unitRole.unit.teachingPeriod || unitRole.unit.teachingPeriod?.active) { +
- - - + + + {{ unitRole.unit?.name }} + {{ unitRole.unit?.code }} + + + + + + + {{ unitRole.unit.teachingPeriod?.name || showDate(unitRole.unit.startDate) }} + + + {{ unitRole.role }} + + + + + + +
+ }
-
+ }

You do not teach any active units

- +
-
+ @if (unitRoles.length && projects?.length) { + + } +

Enrolled units

-
- +
+
-
- diff --git a/src/app/home/states/home/home.component.scss b/src/app/home/states/home/home.component.scss index db6c82d11d..75b8c7ae8e 100644 --- a/src/app/home/states/home/home.component.scss +++ b/src/app/home/states/home/home.component.scss @@ -1,4 +1,4 @@ -@import '../../../../theme.scss'; +@use 'theme' as *; #home { padding-top: 10px; @@ -55,7 +55,7 @@ color: white; opacity: 1; border-style: none; - --mdc-chip-disabled-label-text-color: white; + --mat-chip-disabled-label-text-color: white; } } @@ -86,6 +86,6 @@ } :host { - --mdc-linear-progress-track-height: 16px; - --mdc-linear-progress-active-indicator-height: 16px; + --mat-progress-bar-track-height: 16px; + --mat-progress-bar-active-indicator-height: 16px; } diff --git a/src/app/home/states/home/home.component.ts b/src/app/home/states/home/home.component.ts index e066368acf..5fa8f2e9f8 100644 --- a/src/app/home/states/home/home.component.ts +++ b/src/app/home/states/home/home.component.ts @@ -1,15 +1,17 @@ -import {Component, Inject, OnDestroy, OnInit, Renderer2} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit} from '@angular/core'; +import {Router} from '@angular/router'; +import {Subscription} from 'rxjs'; +import {Project, UnitRole, User, UserService} from 'src/app/api/models/doubtfire-model'; +import {DateService} from 'src/app/common/services/date.service'; import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; -import {analyticsService, dateService} from 'src/app/ajs-upgraded-providers'; -import {UIRouter} from '@uirouter/angular'; import {GlobalStateService, ViewType} from 'src/app/projects/states/index/global-state.service'; -import {Project, UnitRole, User, UserService} from 'src/app/api/models/doubtfire-model'; -import {Subscription} from 'rxjs'; @Component({ selector: 'home', templateUrl: 'home.component.html', styleUrls: ['home.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class HomeComponent implements OnInit, OnDestroy { projects: Project[]; @@ -23,15 +25,12 @@ export class HomeComponent implements OnInit, OnDestroy { loadingProjects: boolean; constructor( - private renderer: Renderer2, private constants: DoubtfireConstants, private globalState: GlobalStateService, private userService: UserService, - @Inject(analyticsService) private AnalyticsService: any, - @Inject(dateService) private DateService: any, - @Inject(UIRouter) private router: UIRouter, + @Inject(DateService) private DateService: DateService, + private router: Router, ) { - // this.renderer.setStyle(document.body, 'background-color', '#f0f2f5'); // projects and units are loaded as part of global state service at login } @@ -41,12 +40,11 @@ export class HomeComponent implements OnInit, OnDestroy { private subscriptions: Subscription[] = []; ngOnDestroy(): void { - // this.renderer.setStyle(document.body, 'background-color', '#fff'); this.subscriptions.forEach((sub) => sub.unsubscribe()); } ngOnInit(): void { - this.AnalyticsService.event('Home', 'Viewed Home page'); + this.globalState.showHeader(); this.globalState.setView(ViewType.OTHER); this.loadingUnitRoles = true; @@ -55,7 +53,6 @@ export class HomeComponent implements OnInit, OnDestroy { this.subscriptions.push( this.globalState.unitRolesSubject.subscribe({ next: (unitRoles) => this.unitRolesLoaded(unitRoles), - error: (err) => {}, }), ); @@ -65,14 +62,13 @@ export class HomeComponent implements OnInit, OnDestroy { projects = projects.filter((project) => project.unit.myRole === 'Student'); this.projectsLoaded(projects); }, - error: (err) => {}, }), ); this.notEnrolled = this.checkEnrolled(); if (this.currentUser.role === 'Auditor') { - this.router.stateService.go('admin/units'); + this.router.navigateByUrl('/admin/units'); } this.ifAdmin = this.currentUser.role === 'Admin'; @@ -94,7 +90,9 @@ export class HomeComponent implements OnInit, OnDestroy { } checkEnrolled(): boolean { - if (this.unitRoles != null || this.projects != null) return false; + if (this.unitRoles != null || this.projects != null) { + return false; + } return ( (this.unitRoles?.length === 0 && this.currentUser.role === 'Tutor') || diff --git a/src/app/home/states/lti-dashboard/lti-dashboard.component.html b/src/app/home/states/lti-dashboard/lti-dashboard.component.html index ba12db70e8..6be88fafcf 100644 --- a/src/app/home/states/lti-dashboard/lti-dashboard.component.html +++ b/src/app/home/states/lti-dashboard/lti-dashboard.component.html @@ -1,18 +1,17 @@ -
-
-
+
+
+
-

OnTrack

+

OnTrack

@if (isLoading) { @if (unauthorised) { -
+
An error occurred. Please refresh the page.
} @else { @@ -20,66 +19,66 @@

OnTrack

Loading...

} } @else { -
+
@if (currentUser?.systemRole === 'Convenor' || currentUser?.systemRole === 'Admin') { }
-
+
@if (linkedUnit) { {{ linkedUnit.code }} — {{ linkedUnit.name }} @if (currentUser?.systemRole === 'Convenor' || currentUser?.systemRole === 'Admin') {
- } @@ -88,7 +87,7 @@

OnTrack

@if (currentUser?.systemRole === 'Convenor' || currentUser?.systemRole === 'Admin') {
- } @else { diff --git a/src/app/home/states/lti-dashboard/lti-dashboard.component.ts b/src/app/home/states/lti-dashboard/lti-dashboard.component.ts index 0c0ad93d57..dd775f824d 100644 --- a/src/app/home/states/lti-dashboard/lti-dashboard.component.ts +++ b/src/app/home/states/lti-dashboard/lti-dashboard.component.ts @@ -1,6 +1,5 @@ -import {AfterViewInit, Component, Inject, Input} from '@angular/core'; -import {StateService, UIRouter} from '@uirouter/angular'; -import {csvResultModalService} from 'src/app/ajs-upgraded-providers'; +import {AfterViewInit, ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {ActivatedRoute, Router} from '@angular/router'; import {ProjectService, User} from 'src/app/api/models/doubtfire-model'; import {Unit} from 'src/app/api/models/unit'; import {AuthenticationService} from 'src/app/api/services/authentication.service'; @@ -8,6 +7,7 @@ import {LtiService} from 'src/app/api/services/lti.service'; import {UnitService} from 'src/app/api/services/unit.service'; import {UserService} from 'src/app/api/services/user.service'; import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; +import {CsvResultModalService} from 'src/app/common/modals/csv-result-modal/csv-result-modal.service'; import {SidekiqProgressModalService} from 'src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.service'; import {AlertService} from 'src/app/common/services/alert.service'; @@ -15,20 +15,21 @@ import {AlertService} from 'src/app/common/services/alert.service'; selector: 'f-lti-dashboard', templateUrl: 'lti-dashboard.component.html', styleUrls: ['lti-dashboard.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class LtiDashboardComponent implements AfterViewInit { constructor( - @Inject(UIRouter) private router: UIRouter, + private router: Router, + private route: ActivatedRoute, private ltiService: LtiService, private userService: UserService, private authenticationService: AuthenticationService, - private stateService: StateService, private alertsService: AlertService, private unitService: UnitService, private projectService: ProjectService, private confirmationModalService: ConfirmationModalService, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - @Inject(csvResultModalService) private _csvResultModalService: any, + private csvResultModalService: CsvResultModalService, private sidekiqProgressModalService: SidekiqProgressModalService, ) {} @@ -46,6 +47,8 @@ export class LtiDashboardComponent implements AfterViewInit { isSyncingEnrolments: boolean; ngAfterViewInit(): void { + this.ltik = this.ltik ?? this.route.snapshot.queryParamMap.get('ltik'); + // Scroll to the bottom of the page in case the header is visible // Ensures our action buttons are centered setTimeout(() => window.scrollTo(0, document.body.scrollHeight), 100); @@ -98,9 +101,7 @@ export class LtiDashboardComponent implements AfterViewInit { } goToLinkUnit(): void { - this.stateService.go('lti/link', { - ltik: this.ltik, - }); + this.router.navigate(['/lti/link'], {queryParams: {ltik: this.ltik}}); } removeLink(): void { @@ -168,7 +169,7 @@ export class LtiDashboardComponent implements AfterViewInit { .show('Syncing users into OnTrack', job.id) .subscribe((completedJob) => { this.isSyncingEnrolments = false; - this._csvResultModalService.show( + this.csvResultModalService.show( 'Enrolment sync', JSON.parse(completedJob.result), ); @@ -200,7 +201,7 @@ export class LtiDashboardComponent implements AfterViewInit { next: (result) => { this.isSyncingGrades = false; this.alertsService.success('Successfully synced grades from OnTrack', 5000); - this._csvResultModalService.show('Grade sync', result); + this.csvResultModalService.show('Grade sync', result); }, error: (error) => { console.log(error); diff --git a/src/app/home/states/lti-unit-link/lti-unit-link.component.html b/src/app/home/states/lti-unit-link/lti-unit-link.component.html index 2ab7465ca9..452bfc8bc6 100644 --- a/src/app/home/states/lti-unit-link/lti-unit-link.component.html +++ b/src/app/home/states/lti-unit-link/lti-unit-link.component.html @@ -1,24 +1,23 @@ -
-
-
+
+
+
-

OnTrack

+

OnTrack

@if (loadingUnits) {

Loading...

} @else if (!activeUnits.length) { -

+

You must already be a Convenor or Admin to link a unit. You are not currently assigned to any units.

} @else { -

+

Students will be automatically enrolled in this unit when they launch the OnTrack tool from this course.

@@ -33,11 +32,11 @@

OnTrack

diff --git a/src/app/home/states/lti-unit-link/lti-unit-link.component.ts b/src/app/home/states/lti-unit-link/lti-unit-link.component.ts index bb305a8ed3..74f4c8bbb3 100644 --- a/src/app/home/states/lti-unit-link/lti-unit-link.component.ts +++ b/src/app/home/states/lti-unit-link/lti-unit-link.component.ts @@ -1,5 +1,5 @@ -import {AfterViewInit, Component, Input} from '@angular/core'; -import {StateService} from '@uirouter/core'; +import {AfterViewInit, ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {ActivatedRoute, Router} from '@angular/router'; import {CreateNewUnitModal} from 'src/app/admin/modals/create-new-unit-modal/create-new-unit-modal.component'; import {Unit} from 'src/app/api/models/unit'; import {AuthenticationService} from 'src/app/api/services/authentication.service'; @@ -13,6 +13,8 @@ import {AlertService} from 'src/app/common/services/alert.service'; selector: 'f-lti-unit-link', templateUrl: 'lti-unit-link.component.html', styleUrls: ['lti-unit-link.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class LtiUnitLinkComponent implements AfterViewInit { constructor( @@ -23,7 +25,8 @@ export class LtiUnitLinkComponent implements AfterViewInit { private alertsService: AlertService, private ltiService: LtiService, private userService: UserService, - private stateService: StateService, + private router: Router, + private route: ActivatedRoute, ) {} @Input() ltik: string; @@ -36,6 +39,7 @@ export class LtiUnitLinkComponent implements AfterViewInit { public loadingUnits: boolean; ngAfterViewInit(): void { + this.ltik = this.ltik ?? this.route.snapshot.queryParamMap.get('ltik'); this.loadingUnits = true; // Scroll to the bottom of the page in case the header is visible @@ -89,9 +93,7 @@ export class LtiUnitLinkComponent implements AfterViewInit { this.alertsService.success(`Successfully linked ${unit.code}`, 5000); - this.stateService.go('lti', { - ltik: this.ltik, - }); + this.router.navigate(['/lti'], {queryParams: {ltik: this.ltik}}); }, error: (error) => { console.log(error); @@ -119,7 +121,7 @@ export class LtiUnitLinkComponent implements AfterViewInit { ); this.loadingUnits = false; }, - error: (error) => { + error: (_error) => { this.alertsService.error(`Failed to fetch units`, 6000); }, }); diff --git a/src/app/legacy-route-placeholder.component.html b/src/app/legacy-route-placeholder.component.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/legacy-route-placeholder.component.scss b/src/app/legacy-route-placeholder.component.scss new file mode 100644 index 0000000000..cc36f78bfa --- /dev/null +++ b/src/app/legacy-route-placeholder.component.scss @@ -0,0 +1,3 @@ +:host { + display: none; +} diff --git a/src/app/legacy-route-placeholder.component.ts b/src/app/legacy-route-placeholder.component.ts new file mode 100644 index 0000000000..dc364e55dc --- /dev/null +++ b/src/app/legacy-route-placeholder.component.ts @@ -0,0 +1,10 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; + +@Component({ + selector: 'legacy-route-placeholder', + templateUrl: './legacy-route-placeholder.component.html', + styleUrl: './legacy-route-placeholder.component.scss', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class LegacyRoutePlaceholderComponent {} diff --git a/src/app/projects/project-outcome-alignment/project-outcome-alignment.coffee b/src/app/projects/project-outcome-alignment/project-outcome-alignment.coffee deleted file mode 100644 index 3b45694da3..0000000000 --- a/src/app/projects/project-outcome-alignment/project-outcome-alignment.coffee +++ /dev/null @@ -1,50 +0,0 @@ -# Component not used - -angular.module("doubtfire.projects.project-outcome-alignment", []) - -.directive("projectOutcomeAlignment", -> - restrict: 'E' - templateUrl: 'projects/project-outcome-alignment/project-outcome-alignment.tpl.html' - controller: ($scope, $rootScope, $timeout, outcomeService, alertService, analyticsService, Visualisation, newUnitService) -> - $scope.poaView = { - activeTab: 'list' - } - $scope.targets = outcomeService.calculateTargets($scope.unit, $scope.unit, $scope.unit.taskStatusFactor) - $scope.currentProgress = outcomeService.calculateProgress($scope.unit, $scope.project) - - $scope.refreshCharts = Visualisation.refreshAll - - refreshAlignmentData = -> - $scope.currentProgress.length = 0 - $scope.currentProgress = _.extend $scope.currentProgress, outcomeService.calculateProgress($scope.unit, $scope.project) - - # $scope.$watch 'project', -> - # refreshAlignmentData() - # $rootScope.$broadcast('ProgressUpdated') - - # $scope.$watch 'project.tasks', -> - # refreshAlignmentData() - # $rootScope.$broadcast('ProgressUpdated') - - $scope.selectTab = (tab) -> - if tab is 'progress' - if !$scope.classStats? - newUnitService.loadLearningProgressClassStats($scope.unit).subscribe({ - next: (response) -> $scope.classStats = response - error: (response) -> - alertService.error( response, 6000) - $scope.classStats = {} - }) - $scope.poaView.activeTab = tab - eventName = if tab is 'progress' then "View Learning Progress Tab" else "Reflect on Learning Tab" - $scope.refreshCharts() - - # Default tab - $scope.selectTab('progress') - - $scope.$on('UpdateAlignmentChart', -> - refreshAlignmentData() - $rootScope.$broadcast('ProgressUpdated') - ) - -) diff --git a/src/app/projects/project-outcome-alignment/project-outcome-alignment.tpl.html b/src/app/projects/project-outcome-alignment/project-outcome-alignment.tpl.html deleted file mode 100644 index 66de997bd9..0000000000 --- a/src/app/projects/project-outcome-alignment/project-outcome-alignment.tpl.html +++ /dev/null @@ -1,47 +0,0 @@ - - - - Outcome Achievement - - - - - Outcome Alignment - - - -
-
-

- Outcome Achievement -

-
-
- Overall progress on unit outcome are shown below -
- -
-
-
-

- Visualise Achievement -

-
-
- Your achievement with all ILOs are visualised below -
- - -
-
- -
diff --git a/src/app/projects/project-progress-dashboard/project-progress-dashboard.coffee b/src/app/projects/project-progress-dashboard/project-progress-dashboard.coffee deleted file mode 100644 index d28a0d31b5..0000000000 --- a/src/app/projects/project-progress-dashboard/project-progress-dashboard.coffee +++ /dev/null @@ -1,46 +0,0 @@ -angular.module('doubtfire.projects.project-progress-dashboard',[]) - -# -# Progress tab for the student's project -# -# Basically a dashboard where students can see everything about their -# project in one area including burndown chart, tasks to work on -# and their target grade -# -.directive('projectProgressDashboard', -> - restrict: 'E' - templateUrl: 'projects/project-progress-dashboard/project-progress-dashboard.tpl.html' - controller: ($scope, $state, $rootScope, $stateParams, newProjectService, alertService, gradeService, newTaskService, listenerService) -> - if $stateParams.projectId? - $scope.studentProjectId = $stateParams.projectId - else if $scope.project? - $scope.studentProjectId = $scope.project.id - - $scope.grades = gradeService.grades - - $scope.currentVisualisation = 'burndown' - - $scope.chooseGrade = (idx) -> - $scope.project.targetGrade = idx - newProjectService.update($scope.project).subscribe( - (response) -> - alertService.success( "Target updated") - ) - updateTaskCompletionStats() - - $scope.taskCount = -> - $scope.unit.taskDefinitionCount - - $scope.taskStats = {} - - # Update move to task and project... - updateTaskCompletionStats = -> - $scope.taskStats.numberOfTasksCompleted = $scope.project.tasksByStatus(newTaskService.completeStatus).length - $scope.taskStats.numberOfTasksRemaining = $scope.project.activeTasks().length - $scope.taskStats.numberOfTasksCompleted - - $scope.$on 'TaskStatusUpdated', -> - updateTaskCompletionStats() - - - updateTaskCompletionStats() -) diff --git a/src/app/projects/project-progress-dashboard/project-progress-dashboard.component.html b/src/app/projects/project-progress-dashboard/project-progress-dashboard.component.html new file mode 100644 index 0000000000..a0929e54e4 --- /dev/null +++ b/src/app/projects/project-progress-dashboard/project-progress-dashboard.component.html @@ -0,0 +1,68 @@ +@if (project$ | async; as project) { +
+ +
+ + + +

+ {{ project.student.nickname || project.student.firstName }}'s {{ project.unit.name }} +

+

{{ project.unit.description }}

+
+
+
+
+ + + +

Targetting

+ + info +
+ + Target grade + + @for (grade of grades; track grade.value) { + {{ + grade.viewValue + }} + } + + +

+
+
+
+ +
+ + + Your progress + + +
+ + + +
+
+
+
+ + +
+} diff --git a/src/app/projects/project-progress-dashboard/project-progress-dashboard.component.scss b/src/app/projects/project-progress-dashboard/project-progress-dashboard.component.scss new file mode 100644 index 0000000000..3a2b24d37b --- /dev/null +++ b/src/app/projects/project-progress-dashboard/project-progress-dashboard.component.scss @@ -0,0 +1,11 @@ +:host { + display: block; +} + +h1, +h2, +h3, +h4, +p { + color: black; +} diff --git a/src/app/projects/project-progress-dashboard/project-progress-dashboard.component.ts b/src/app/projects/project-progress-dashboard/project-progress-dashboard.component.ts new file mode 100644 index 0000000000..c6cc3cb2de --- /dev/null +++ b/src/app/projects/project-progress-dashboard/project-progress-dashboard.component.ts @@ -0,0 +1,49 @@ +import {ChangeDetectionStrategy, Component, Input, type OnInit} from '@angular/core'; +import {Observable} from 'rxjs'; +import {Project} from 'src/app/api/models/project'; +import {ProjectService} from 'src/app/api/services/project.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {GradeService} from 'src/app/common/services/grade.service'; + +@Component({ + selector: 'f-project-progress-dashboard', + templateUrl: './project-progress-dashboard.component.html', + styleUrl: './project-progress-dashboard.component.scss', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class ProjectProgressDashboardComponent implements OnInit { + @Input() project$: Observable; + private project: Project; + protected grades; + + constructor( + private gradeService: GradeService, + private projectService: ProjectService, + private alertService: AlertService, + ) {} + + ngOnInit(): void { + this.project$.subscribe((project) => { + this.project = project; + this.grades = this.gradeService.gradeViewDataFor(project.unit); + }); + + setTimeout(() => { + console.log(this.project.taskStats); + }, 3000); + } + + protected targetGradeClicked(grade: number): void { + this.project.targetGrade = grade; + this.projectService.update(this.project).subscribe({ + next: (_project) => { + this.alertService.success('Target grade updated'); + }, + error: (error) => { + console.error(error); + this.alertService.error('Error updating target grade', error); + }, + }); + } +} diff --git a/src/app/projects/project-progress-dashboard/project-progress-dashboard.tpl.html b/src/app/projects/project-progress-dashboard/project-progress-dashboard.tpl.html deleted file mode 100644 index 4570c5fb1c..0000000000 --- a/src/app/projects/project-progress-dashboard/project-progress-dashboard.tpl.html +++ /dev/null @@ -1,87 +0,0 @@ -
-
-
-
-
-
-
-

Task List

-
-
-
- -
-
-
-
-
-
-

Target Grade

- Select the grade you wish to achieve in the unit. -
-
-

- -

-
-
-
-
-
- -
-
-
-
-
-

Burndown Chart

- The Burndown chart shows how much work remains for you to achieve your target grade. -
-
-

Task Summary Chart

- Summary of each of your task statuses -
-
-
-
-
- - -
-
-
-
-
-
- -
-
- -
-
- -
-
-
diff --git a/src/app/projects/project.resolver.ts b/src/app/projects/project.resolver.ts new file mode 100644 index 0000000000..0fe0af8cb6 --- /dev/null +++ b/src/app/projects/project.resolver.ts @@ -0,0 +1,42 @@ +import {inject} from '@angular/core'; +import {ResolveFn} from '@angular/router'; +import {Observable} from 'rxjs'; +import {Project, ProjectService} from 'src/app/api/models/doubtfire-model'; +import {GlobalStateService, ViewType} from './states/index/global-state.service'; + +export const resolveProject: ResolveFn = (route, state) => { + const projectService = inject(ProjectService); + const globalState = inject(GlobalStateService); + const projectId = Number(route.paramMap.get('projectId')); + const resolveProgressively = state.url.split('?')[0].includes('/dashboard'); + + return new Observable((observer) => { + const mappingCompleteCallback = (project: Project) => { + globalState.setView(ViewType.PROJECT, project); + if (!resolveProgressively) { + observer.next(project); + observer.complete(); + } + }; + + globalState.onLoad(() => { + if (resolveProgressively) { + observer.next(projectService.cache.getOrCreate(projectId, projectService, {id: projectId})); + observer.complete(); + return; + } + + projectService + .get( + {id: projectId}, + { + cacheBehaviourOnGet: 'cacheQuery', + mappingCompleteCallback, + }, + ) + .subscribe({ + error: (error) => observer.error(error), + }); + }); + }); +}; diff --git a/src/app/projects/projects.coffee b/src/app/projects/projects.coffee deleted file mode 100644 index 8499f03af4..0000000000 --- a/src/app/projects/projects.coffee +++ /dev/null @@ -1,5 +0,0 @@ -angular.module('doubtfire.projects', [ - 'doubtfire.projects.states' - 'doubtfire.projects.project-outcome-alignment' - 'doubtfire.projects.project-progress-dashboard' -]) diff --git a/src/app/projects/states/dashboard/dashboard.coffee b/src/app/projects/states/dashboard/dashboard.coffee deleted file mode 100644 index d4d073c8ad..0000000000 --- a/src/app/projects/states/dashboard/dashboard.coffee +++ /dev/null @@ -1,58 +0,0 @@ -angular.module('doubtfire.projects.states.dashboard', [ - 'doubtfire.projects.states.dashboard.directives' -]) - -# -# Tasks state for projects -# -.config(($stateProvider) -> - $stateProvider.state 'projects/dashboard', { - parent: 'projects/index' - url: '/dashboard/:taskAbbr?tutor' - controller: 'ProjectsDashboardStateCtrl' - templateUrl: 'projects/states/dashboard/dashboard.tpl.html' - params: - taskAbbr: dynamic: true - data: - task: "Dashboard" - pageTitle: "_Home_" - roleWhitelist: ['Tutor', 'Convenor', 'Admin', 'Student', 'Auditor'] - } -) - -.controller("ProjectsDashboardStateCtrl", ($scope, $urlRouter, $state, $stateParams, listenerService) -> - # Cleanup - listeners = listenerService.listenTo($scope) - - # Load in task task abbreviation - $scope.taskData = { - selectedTask: null - onSelectedTaskChange: (task) -> - setTaskAbbrAsUrlParams(task) - } - - # Sets URL parameters for the task key - setTaskAbbrAsUrlParams = (task) -> - taskAbbr = if _.isString(task) then task else task?.definition.abbreviation - taskAbbr = if taskAbbr then taskAbbr else '' - # Change URL of new task without notify - $state.go($state.$current, {taskAbbr: taskAbbr}, {notify: false}) - - # Sets selected task from URL parameters - setSelectedTaskFromUrlParams = (taskAbbr) -> - $scope.taskData.selectedTask = null unless taskAbbr? - $scope.taskData.selectedTask = $scope.project.activeTasks().find((t) -> - t.definition.abbreviation.toLowerCase() == taskAbbr?.toLowerCase() - ) - - # False task abbreviation provided? - unless setSelectedTaskFromUrlParams($stateParams.taskAbbr)? - setTaskAbbrAsUrlParams(null) - - - # Task complete - listeners.push $scope.$on('TaskSubmissionUploadComplete', ($event) -> - # Go back to the dashboard - $scope.taskData.selectedTask = null - ) -) diff --git a/src/app/projects/states/dashboard/dashboard.tpl.html b/src/app/projects/states/dashboard/dashboard.tpl.html index e79f1af1ca..2171877d44 100644 --- a/src/app/projects/states/dashboard/dashboard.tpl.html +++ b/src/app/projects/states/dashboard/dashboard.tpl.html @@ -7,14 +7,14 @@ style="padding: 0 8px 0 8px" > - - Add Engagement Stamp + + +

+ Students can see engagement stamps, including any notes and evidence you add. They can also + leave comments under each engagement. +

+
+ + Engagement type + + + @for (type of engagementTypes; track type) { + {{ type }} + } + + Choose a suggestion or enter another type. + @if (form.controls.engagementType.hasError('required')) { + Enter an engagement type. + } + + + + Note + + {{ form.controls.note.value.length }} / 4095 + + +
+ + Date + + + + @if (form.controls.occurredDate.hasError('required')) { + Select when the engagement occurred. + } + + + + Time + + @if (form.controls.occurredTime.hasError('required')) { + Select a time. + } + +
+ + + Evidence + + No evidence + External URL + Upload image or PDF + + + + @if (form.controls.evidenceMode.value === 'url') { + + Evidence URL + + @if ( + form.controls.evidenceUrl.hasError('pattern') || + (form.controls.evidenceUrl.touched && !form.controls.evidenceUrl.value.trim()) + ) { + Enter a valid HTTP or HTTPS URL. + } + + } + + @if (form.controls.evidenceMode.value === 'attachment') { +
+ + +

Maximum file size: 30 MB.

+ @if (attachmentError) { +

{{ attachmentError }}

+ } +
+ } +
+
+ + + + + diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/add-engagement-dialog/add-engagement-dialog.component.scss b/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/add-engagement-dialog/add-engagement-dialog.component.scss new file mode 100644 index 0000000000..cdca2a91a1 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/add-engagement-dialog/add-engagement-dialog.component.scss @@ -0,0 +1,12 @@ +// Prevent mobile devices from zooming in engagement stamp modal inputs +@media (max-width: 768px) { + :host ::ng-deep { + .mat-mdc-input-element, + .mat-mdc-select, + .mat-mdc-select-value, + .mat-mdc-select-trigger, + .mat-mdc-select-min-line { + font-size: 20px !important; + } + } +} diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/add-engagement-dialog/add-engagement-dialog.component.ts b/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/add-engagement-dialog/add-engagement-dialog.component.ts new file mode 100644 index 0000000000..523b4e6a69 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/add-engagement-dialog/add-engagement-dialog.component.ts @@ -0,0 +1,177 @@ +import {ChangeDetectionStrategy, Component, Inject} from '@angular/core'; +import {FormControl, FormGroup, Validators} from '@angular/forms'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {Engagement, EngagementService, Project} from 'src/app/api/models/doubtfire-model'; +import {AlertService} from 'src/app/common/services/alert.service'; + +type EvidenceMode = 'none' | 'url' | 'attachment'; + +interface AddEngagementForm { + engagementType: FormControl; + note: FormControl; + occurredDate: FormControl; + occurredTime: FormControl; + evidenceMode: FormControl; + evidenceUrl: FormControl; +} + +@Component({ + selector: 'f-add-engagement-dialog', + templateUrl: './add-engagement-dialog.component.html', + styleUrl: './add-engagement-dialog.component.scss', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class AddEngagementDialogComponent { + readonly engagementTypes = ['Discuss', 'Attendance', 'Forum', 'Email', 'Attention']; + readonly notePlaceholders: Record = { + attendance: 'Attended tutorial and participated in class activities.', + discuss: 'Discussed tasks during tutorial.', + discussion: 'Discussed tasks during tutorial.', + forum: 'Posted to the unit forum and engaged with discussion.', + email: 'Discussed unit progress with the teaching team via email.', + attention: 'Engagement concern noted for follow-up.', + }; + readonly maxAttachmentSize = 30 * 1024 * 1024; + readonly form: FormGroup; + + attachment?: File; + attachmentError?: string; + saving = false; + + constructor( + @Inject(MAT_DIALOG_DATA) readonly data: {project: Project}, + private dialogRef: MatDialogRef, + private engagementService: EngagementService, + private alerts: AlertService, + ) { + const now = new Date(); + this.form = new FormGroup({ + engagementType: new FormControl('', { + nonNullable: true, + validators: [Validators.required, Validators.maxLength(255)], + }), + note: new FormControl('', { + nonNullable: true, + validators: [Validators.maxLength(4095)], + }), + occurredDate: new FormControl(now, { + nonNullable: true, + validators: [Validators.required], + }), + occurredTime: new FormControl(this.formatTime(now), { + nonNullable: true, + validators: [Validators.required], + }), + evidenceMode: new FormControl('none', {nonNullable: true}), + evidenceUrl: new FormControl('', { + nonNullable: true, + validators: [Validators.pattern(/^https?:\/\/.+/i)], + }), + }); + } + + get canSubmit(): boolean { + if (this.form.invalid || this.saving || this.attachmentError !== undefined) { + return false; + } + + const mode = this.form.controls.evidenceMode.value; + if (mode === 'url') { + return this.form.controls.evidenceUrl.value.trim().length > 0; + } + if (mode === 'attachment') { + return this.attachment !== undefined; + } + + return true; + } + + get notePlaceholder(): string { + const engagementType = this.form.controls.engagementType.value.trim().toLowerCase(); + return ( + this.notePlaceholders[engagementType] ?? 'Describe how the student engaged with the unit.' + ); + } + + engagementTypeSelected(input: HTMLInputElement): void { + window.setTimeout(() => input.blur()); + } + + evidenceModeChanged(): void { + const mode = this.form.controls.evidenceMode.value; + + if (mode !== 'url') { + this.form.controls.evidenceUrl.setValue(''); + } + this.attachment = undefined; + this.attachmentError = undefined; + } + + fileSelected(event: Event): void { + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + + this.attachment = undefined; + this.attachmentError = undefined; + + if (!file) { + return; + } + if (file.size === 0) { + this.attachmentError = 'The selected file is empty.'; + input.value = ''; + return; + } + if (file.size > this.maxAttachmentSize) { + this.attachmentError = 'The selected file must be no larger than 30 MB.'; + input.value = ''; + return; + } + if (file.type !== 'application/pdf' && !file.type.startsWith('image/')) { + this.attachmentError = 'Select an image or PDF file.'; + input.value = ''; + return; + } + + this.attachment = file; + } + + submit(): void { + if (!this.canSubmit) { + this.form.markAllAsTouched(); + return; + } + + const values = this.form.getRawValue(); + const occurredAt = new Date(values.occurredDate); + const [hours, minutes] = values.occurredTime.split(':').map(Number); + occurredAt.setHours(hours, minutes, 0, 0); + + this.saving = true; + this.engagementService + .createEngagement(this.data.project, { + engagementType: values.engagementType.trim(), + note: values.note.trim() || this.notePlaceholder, + occurredAt, + evidenceUrl: values.evidenceMode === 'url' ? values.evidenceUrl.trim() : undefined, + attachment: values.evidenceMode === 'attachment' ? this.attachment : undefined, + }) + .subscribe({ + next: (engagement: Engagement) => { + this.alerts.success('Engagement stamp added.'); + this.dialogRef.close(engagement); + }, + error: (error) => { + this.saving = false; + this.alerts.error(error?.error ?? 'Unable to add the engagement stamp.'); + }, + }); + } + + private formatTime(date: Date): string { + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${hours}:${minutes}`; + } +} diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/engagement-detail-dialog/engagement-detail-dialog.component.html b/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/engagement-detail-dialog/engagement-detail-dialog.component.html new file mode 100644 index 0000000000..bc976ca6c5 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/engagement-detail-dialog/engagement-detail-dialog.component.html @@ -0,0 +1,288 @@ +
+
+
+
+
{{ engagement.engagementType }} Engagement
+

+ Added by {{ engagement.user?.firstName }} {{ engagement.user?.lastName }} + · + + {{ engagement.occurredAt | humanizedDate }} + +

+
+ + +
+
+ + + @if (loading) { +
+ +
+ } @else if (loadFailed) { +

Unable to load this engagement.

+ } @else { +
+ +
+ + + + {{ engagement.user?.firstName }} {{ engagement.user?.lastName }} + + +
+ + {{ engagement.createdAt | humanizedDate }} + +
+
+ + +
{{ engagement.note }}
+ + @if (engagement.evidenceUrl || engagement.hasAttachment) { +
+

Evidence

+ + @if (engagement.evidenceUrl) { + + link + {{ engagement.evidenceUrl }} + + } + + @if (engagement.hasAttachment && engagement.contentType === 'image') { + @if (evidenceLoading) { +
+ +
+ } @else if (evidenceBlobUrl) { + + } + } @else if (engagement.hasAttachment) { + + } + + @if (evidenceLoadFailed) { +

Unable to load the attached evidence.

+ } +
+ } +
+
+ + @for (comment of comments; track comment.id) { + @if (comment.replyToId) { +
+
+ reply +
+ @if (comment.replyTo) { + + Replying to {{ comment.replyTo.user?.preferredName }} + {{ comment.replyTo.user?.lastName }} + + {{ comment.replyTo.comment }} + } @else { + Replying to: Deleted comment + } +
+
+
+ } + + +
+ @if (comment.currentUserCanEdit) { + + edit + + } + + reply + + @if (comment.currentUserCanDelete) { + + delete + + } +
+ +
+ + + {{ comment.user?.firstName }} {{ comment.user?.lastName }} + +
+ + {{ comment.createdAt | humanizedDate }} + +
+
+ + + @if (editingComment?.id === comment.id) { + + Update Comment + +
+ + +
+
+ } @else { +
+ } +
+
+ } + + @if (comments.length === 0) { +

No comments yet.

+ } + + +
+ } +
+ + @if (!loading && !loadFailed) { +
+ @if (replyingToComment) { +
+
+
+ reply +
+ + Replying to {{ replyingToComment.user?.preferredName }} + {{ replyingToComment.user?.lastName }} + + + {{ replyingToComment.comment }} + +
+
+ +
+
+ } + + + Comment + +
+ +
+
+
+ } +
diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/engagement-detail-dialog/engagement-detail-dialog.component.scss b/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/engagement-detail-dialog/engagement-detail-dialog.component.scss new file mode 100644 index 0000000000..684d95154b --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/engagement-detail-dialog/engagement-detail-dialog.component.scss @@ -0,0 +1,14 @@ +.action .mat-icon { + color: #9696969d; + font-size: 20px; + width: 20px; + height: 20px; + cursor: pointer; + vertical-align: middle; + text-align: center; + margin-left: 0.3em; +} + +.action .mat-icon:hover { + color: black; +} diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/engagement-detail-dialog/engagement-detail-dialog.component.ts b/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/engagement-detail-dialog/engagement-detail-dialog.component.ts new file mode 100644 index 0000000000..f63ddd1790 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/engagement-detail-dialog/engagement-detail-dialog.component.ts @@ -0,0 +1,205 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + Inject, + OnDestroy, + OnInit, + ViewChild, +} from '@angular/core'; +import {MAT_DIALOG_DATA} from '@angular/material/dialog'; +import { + Engagement, + EngagementComment, + EngagementCommentService, + EngagementService, +} from 'src/app/api/models/doubtfire-model'; +import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-engagement-detail-dialog', + templateUrl: './engagement-detail-dialog.component.html', + styleUrl: './engagement-detail-dialog.component.scss', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class EngagementDetailDialogComponent implements OnInit, OnDestroy { + @ViewChild('commentsEnd') commentsEnd?: ElementRef; + + engagement: Engagement; + commentText = ''; + loading = true; + loadFailed = false; + submitting = false; + replyingToComment?: EngagementComment; + hoveredCommentId?: number; + editingComment?: EngagementComment; + editingCommentText = ''; + evidenceBlobUrl?: string; + evidenceLoading = false; + evidenceLoadFailed = false; + + constructor( + @Inject(MAT_DIALOG_DATA) readonly data: {engagement: Engagement}, + private engagementService: EngagementService, + private engagementCommentService: EngagementCommentService, + private fileDownloader: FileDownloaderService, + private alerts: AlertService, + private confirmationModal: ConfirmationModalService, + ) { + this.engagement = data.engagement; + } + + get comments(): readonly EngagementComment[] { + return [...this.engagement.comments].sort( + (first, second) => first.createdAt.getTime() - second.createdAt.getTime(), + ); + } + + ngOnInit(): void { + this.engagementService.loadEngagement(this.engagement).subscribe({ + next: (engagement) => { + this.engagement = engagement; + this.loading = false; + this.loadAttachment(); + this.scrollToBottom(); + }, + error: () => { + this.loadFailed = true; + this.loading = false; + }, + }); + } + + ngOnDestroy(): void { + if (this.evidenceBlobUrl) { + this.fileDownloader.releaseBlob(this.evidenceBlobUrl); + } + } + + openAttachment(): void { + if (this.evidenceBlobUrl) { + window.open(this.evidenceBlobUrl, '_blank', 'noopener,noreferrer'); + } + } + + submitComment(): void { + const comment = this.commentText.trim(); + if (!comment || this.submitting) { + return; + } + + this.submitting = true; + this.engagementCommentService + .addComment(this.engagement, comment, this.replyingToComment) + .subscribe({ + next: () => { + this.commentText = ''; + this.submitting = false; + this.replyingToComment = undefined; + this.scrollToBottom(); + }, + error: (error) => { + this.submitting = false; + this.alerts.error(error?.error ?? 'Unable to add your comment.'); + }, + }); + } + + replyToComment(comment: EngagementComment): void { + this.replyingToComment = comment; + } + + cancelReply(): void { + this.replyingToComment = undefined; + } + + editComment(comment: EngagementComment): void { + if (!comment.currentUserCanEdit) { + return; + } + + this.editingComment = comment; + this.editingCommentText = comment.comment; + } + + cancelEdit(): void { + this.editingComment = undefined; + this.editingCommentText = ''; + } + + updateComment(): void { + const text = this.editingCommentText.trim(); + if (!this.editingComment || !text) { + return; + } + + this.engagementCommentService.updateComment(this.editingComment, text).subscribe({ + next: () => this.cancelEdit(), + error: (error) => this.alerts.error(error?.error ?? 'Unable to update this comment.'), + }); + } + + deleteComment(comment: EngagementComment): void { + if (!comment.currentUserCanDelete) { + return; + } + + this.confirmationModal.show( + 'Delete comment', + 'Are you sure you want to delete this engagement comment?', + () => { + this.engagementCommentService.deleteComment(comment).subscribe({ + next: () => { + if (this.replyingToComment?.id === comment.id) { + this.cancelReply(); + } + }, + error: (error) => this.alerts.error(error?.error ?? 'Unable to delete this comment.'), + }); + }, + ); + } + + scrollToComment(comment?: EngagementComment): void { + if (!comment) { + return; + } + + const element = document.getElementById(`engagement-comment-${comment.id}`); + element?.scrollIntoView({behavior: 'smooth', block: 'center'}); + } + + private loadAttachment(): void { + if (!this.engagement.hasAttachment) { + return; + } + + this.evidenceLoading = true; + this.fileDownloader.downloadBlob( + this.engagement.attachmentUrl, + (blobUrl) => { + this.evidenceBlobUrl = blobUrl; + this.evidenceLoading = false; + this.scrollToBottom(); + }, + () => { + this.evidenceLoadFailed = true; + this.evidenceLoading = false; + }, + ); + } + + scrollToBottom(): void { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + const container = this.commentsEnd?.nativeElement.closest( + '.mat-mdc-dialog-content', + ) as HTMLElement | null; + container?.scrollTo({top: container.scrollHeight}); + }); + }); + } +} diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/engagement-passport-card.component.html b/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/engagement-passport-card.component.html new file mode 100644 index 0000000000..aa79b48871 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/engagement-passport-card.component.html @@ -0,0 +1,100 @@ + + +
+
+ Engagement Passport + + A semester view of your engagement with the unit and teaching team. + +
+ + @if (currentUserCanAddEngagement) { + + } +
+
+ + +
+ @for (item of legend; track item.type) { +
+ + + + {{ item.label }} +
+ } +
+ + @if (loading) { +
+ +
+ } @else { +
+
+ @for (week of weeks; track week.week) { +
+
+ @for (column of stampColumns(week.stamps); track $index) { +
+ @for (stamp of column; track $index) { + + } +
+ } +
+ +
+ Week + {{ week.week }} +
+
+ } +
+
+ } + + @if (loadFailed) { +

Unable to load engagement stamps.

+ } + +

+ Each stamp records a moment of engagement. Hover over or focus a stamp for more detail. +

+
+
diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/engagement-passport-card.component.scss b/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/engagement-passport-card.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/engagement-passport-card.component.ts b/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/engagement-passport-card.component.ts new file mode 100644 index 0000000000..305e2c4e60 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/engagement-passport-card/engagement-passport-card.component.ts @@ -0,0 +1,202 @@ +import {ChangeDetectionStrategy, Component, Input, OnChanges} from '@angular/core'; +import {MatDialog} from '@angular/material/dialog'; +import { + Engagement, + EngagementService, + Project, + UserService, +} from 'src/app/api/models/doubtfire-model'; +import {AddEngagementDialogComponent} from './add-engagement-dialog/add-engagement-dialog.component'; +import {EngagementDetailDialogComponent} from './engagement-detail-dialog/engagement-detail-dialog.component'; + +interface EngagementPresentation { + label: string; + icon: string; + classes: string; +} + +interface EngagementStamp { + engagement: Engagement; + type: string; + label: string; + icon: string; + classes: string; +} + +interface EngagementWeek { + week: number; + stamps: EngagementStamp[]; +} + +interface EngagementLegendItem extends EngagementPresentation { + type: string; +} + +@Component({ + selector: 'f-engagement-passport-card', + templateUrl: './engagement-passport-card.component.html', + styleUrl: './engagement-passport-card.component.scss', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class EngagementPassportCardComponent implements OnChanges { + @Input() project: Project; + + loading = false; + loadFailed = false; + weeks: EngagementWeek[] = []; + + private readonly fallbackPresentation: EngagementPresentation = { + label: 'Other engagement', + icon: 'star', + classes: 'border-gray-300 bg-gray-50 text-gray-700', + }; + + private readonly presentations: Record = { + attendance: { + label: 'Class attendance', + icon: 'groups', + classes: 'border-green-300 bg-green-50 text-green-700', + }, + discussion: { + label: 'Discussion', + icon: 'record_voice_over', + classes: 'border-cyan-300 bg-cyan-50 text-cyan-700', + }, + forum: { + label: 'Forum post', + icon: 'forum', + classes: 'border-blue-300 bg-blue-50 text-blue-700', + }, + email: { + label: 'Tutor email', + icon: 'mail', + classes: 'border-violet-300 bg-violet-50 text-violet-700', + }, + attention: { + label: 'Needs attention', + icon: 'feedback', + classes: 'border-yellow-300 bg-yellow-50 text-yellow-700', + }, + }; + + readonly legend: EngagementLegendItem[] = Object.entries(this.presentations).map( + ([type, presentation]) => ({type, ...presentation}), + ); + + constructor( + private engagementService: EngagementService, + private dialog: MatDialog, + private userService: UserService, + ) {} + + get currentWeek(): number | null { + return this.project?.unit?.currentUnitWeek ?? null; + } + + get currentUserCanAddEngagement(): boolean { + const currentUserId = this.userService.currentUser?.id; + return ( + currentUserId !== undefined && + this.project?.unit?.staff.some((unitRole) => unitRole.user.id === currentUserId) + ); + } + + ngOnChanges(): void { + if (!this.project?.id) { + return; + } + + const cachedEngagements = this.project.engagementCache.currentValues; + this.buildWeeks(cachedEngagements); + this.loading = cachedEngagements.length === 0; + this.loadFailed = false; + + this.engagementService.loadEngagements(this.project, true).subscribe({ + next: (engagements) => { + this.buildWeeks(engagements); + this.loading = false; + }, + error: () => { + this.loadFailed = true; + this.loading = false; + }, + }); + } + + stampColumns(stamps: EngagementStamp[]): EngagementStamp[][] { + const columns: EngagementStamp[][] = []; + + for (let index = 0; index < stamps.length; index += 5) { + columns.push(stamps.slice(index, index + 5)); + } + + return columns; + } + + weekWidth(stamps: EngagementStamp[]): number { + const columnCount = Math.max(1, Math.ceil(stamps.length / 5)); + const stampWidth = 35; + const columnGap = 5; + const horizontalPadding = 16; + + return Math.max( + 58, + columnCount * stampWidth + (columnCount - 1) * columnGap + horizontalPadding, + ); + } + + openAddEngagementDialog(): void { + const dialogRef = this.dialog.open(AddEngagementDialogComponent, { + data: {project: this.project}, + width: 'calc(100vw - 32px)', + maxWidth: '640px', + autoFocus: false, + }); + + dialogRef.afterClosed().subscribe((engagement?: Engagement) => { + if (engagement) { + this.buildWeeks(this.project.engagementCache.currentValues); + } + }); + } + + openEngagement(engagement: Engagement): void { + this.dialog.open(EngagementDetailDialogComponent, { + data: {engagement}, + width: 'calc(100vw - 32px)', + maxWidth: '900px', + autoFocus: false, + }); + } + + private buildWeeks(engagements: readonly Engagement[]): void { + const totalWeeks = Math.max(1, this.project.unit.totalWeeks); + this.weeks = Array.from({length: totalWeeks}, (_, index) => ({ + week: index + 1, + stamps: [], + })); + + for (const engagement of engagements) { + const weekNumber = this.project.unit.weekNumber(engagement.occurredAt); + if (weekNumber === null || weekNumber < 1 || weekNumber > totalWeeks) { + continue; + } + + const type = this.normalizeEngagementType(engagement.engagementType); + const presentation = this.presentations[type] ?? this.fallbackPresentation; + this.weeks[weekNumber - 1].stamps.push({ + engagement, + type, + label: engagement.note, + icon: presentation.icon, + classes: presentation.classes, + }); + } + } + + private normalizeEngagementType(engagementType: string): string { + const type = engagementType?.trim().toLowerCase(); + return type === 'discuss' ? 'discussion' : type; + } +} diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.coffee b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.coffee deleted file mode 100644 index 0a82f1ab9a..0000000000 --- a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.coffee +++ /dev/null @@ -1,44 +0,0 @@ -angular.module('doubtfire.projects.states.dashboard.directives.progress-dashboard', []) -# -# Summary dashboard showing some graphs and way to change the -# current target grade -# -.directive('progressDashboard', -> - restrict: 'E' - templateUrl: 'projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html' - scope: - project: '=' - onUpdateTargetGrade: '=' - controller: ($scope, $stateParams, newProjectService, gradeService, analyticsService, alertService) -> - # Is the current user a tutor? - $scope.tutor = $stateParams.tutor - # Number of tasks completed and remaining - updateTaskCompletionValues = -> - completedTasks = $scope.project.numberTasks("complete") - $scope.numberOfTasks = - completed: completedTasks - remaining: $scope.project.activeTasks().length - completedTasks - updateTaskCompletionValues() - - # Expose grade names and values - $scope.grades = - names: gradeService.grades - values: gradeService.gradeValues - - $scope.updateTargetGrade = (newGrade) -> - $scope.project.targetGrade = newGrade - newProjectService.update($scope.project).subscribe( - (project) -> - project.refreshBurndownChartData() - - # Update task completions and re-render task status graph - updateTaskCompletionValues() - $scope.renderTaskStatusPieChart?() - $scope.onUpdateTargetGrade?() - analyticsService.event("Student Project View - Progress Dashboard", "Grade Changed", $scope.grades.names[newGrade]) - alertService.success( "Updated target grade successfully", 2000) - - (failure) -> - alertService.error( "Failed to update target grade", 4000) - ) -) diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.html b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.html new file mode 100644 index 0000000000..dfc62a5318 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.html @@ -0,0 +1,138 @@ +
+
+

+ Progress Dashboard + @if (viewingOtherStudentProject) { + for {{ project?.student?.name }} + } +

+
+
+ +
+ +
+ + +
+ + + Target Grade + +

Your target grade changes which tasks you need to complete.

+
+
+ + +
+ + Select Target Grade + + @for (grade of grades.values; track grade) { + + {{ grades.names[grade] }} + + } + + +
+ To change your target grade, use the + + Task Planner + +
+
+
+
+
+ + @if (showSubmittedGrade) { + +
+ + + Submitted Grade + +

The grade you are submitting your portfolio for.

+
+
+ + +
+ + Select Submitted Grade + + @for (grade of grades.values; track grade) { + + {{ grades.names[grade] }} + + } + + +
+
+
+
+ } + + +
+ +
+ + +
+ +
+ +
+ + + Progress Burndown + + The burndown chart shows how much work remains for you to achieve your target grade. + + + + +
+ Aim to keep your + Complete + line close to or ahead of the + Target + line to keep on track. +
+
+
+ + + + Task Statuses + Breakdown summary of each of your task statuses + + + + + +
+
+
diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.scss b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.ts b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.ts new file mode 100644 index 0000000000..7cb2445c90 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.ts @@ -0,0 +1,82 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; +import {Project} from 'src/app/api/models/project'; +import {ProjectService} from 'src/app/api/services/project.service'; +import {UserService} from 'src/app/api/services/user.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {GradeService} from 'src/app/common/services/grade.service'; + +@Component({ + selector: 'f-progress-dashboard', + templateUrl: './progress-dashboard.component.html', + styleUrls: ['./progress-dashboard.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class ProgressDashboardComponent implements OnInit { + @Input() project: Project; + @Input() showSubmittedGrade?: boolean = false; + @Output() doUpdateTargetGrade: EventEmitter = new EventEmitter(); + + grades: {names: Record; values: number[]} = { + names: this.gradeService.grades, + values: this.gradeService.gradeValues, + }; + numberOfTasks = { + completed: 0, + remaining: 0, + }; + + constructor( + private gradeService: GradeService, + private projectService: ProjectService, + private alertService: AlertService, + private userService: UserService, + ) {} + + ngOnInit(): void { + this.grades.values = this.gradeService.gradeValuesFor(this.project.unit); + this.grades.names = Object.fromEntries( + this.project.unit.gradeDefinitions.map((definition) => [definition.value, definition.label]), + ); + this.updateTaskCompletionValues(); + this.project?.refreshBurndownChartData(); + } + + public get viewingOtherStudentProject(): boolean { + const role = this.project?.unit?.myRole; + const currentUser = this.userService.currentUser; + + return !!role && role !== 'Student' && this.project?.student?.id !== currentUser?.id; + } + + updateTargetGrade(newGrade: number): void { + this.project.targetGrade = newGrade; + this.projectService.update(this.project).subscribe( + (project) => { + project.refreshBurndownChartData(); + this.updateTaskCompletionValues(); + this.doUpdateTargetGrade.emit(); + this.alertService.success('Updated target grade successfully', 2000); + }, + (error) => { + console.error('Error updating target grade:', error); + this.alertService.error('Failed to update target grade', 4000); + }, + ); + } + + private updateTaskCompletionValues(): void { + const completedTasks = this.project.numberTasks('complete'); + this.numberOfTasks = { + completed: completedTasks, + remaining: this.project.activeTasks().length - completedTasks, + }; + } +} diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html deleted file mode 100644 index 95a5664aaf..0000000000 --- a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html +++ /dev/null @@ -1,80 +0,0 @@ -
-
-

- Progress Dashboard for {{project.student.name}} -

-
-
-
-
- -
-
-
-
-
-

Target Grade

-
-
- - -
- To change your target grade, use the - - Task Planner - -
-
- -
- -
- -
-
-
-

Progress Burndown

-
- The burndown chart shows how much work remains for you to achieve your target grade. -
-
-
- -
- -
-
-
-
-
-

Task Statuses

-
- Breakdown summary of each of your task statuses. -
-
-
- - -
-
-
-
-
diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component.html b/src/app/projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component.html index da584e5670..c11ab4c597 100644 --- a/src/app/projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component.html +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/task-planner-card/task-planner-card.component.html @@ -1,7 +1,7 @@ Plan Your Tasks - +

The Task Planner shows a timeline of your tasks, their due dates, and prerequisite relationships. Use it to plan when to start and submit tasks, ensuring prerequisites are @@ -18,8 +18,8 @@ -

- {{task.gradeDesc()}} + + {{task.gradeDesc()}} + {{task.qualityQts}}{{task.definition.maxQualityPts}} - + {{task.definition.n > - + !
@@ -77,7 +88,10 @@

{{task.definition.n -
  • +
  • No tasks to display.
  • diff --git a/src/app/projects/states/dashboard/directives/student-task-list/task-list-item/task-list-item.component.html b/src/app/projects/states/dashboard/directives/student-task-list/task-list-item/task-list-item.component.html index 538bfa1c08..b206215363 100644 --- a/src/app/projects/states/dashboard/directives/student-task-list/task-list-item/task-list-item.component.html +++ b/src/app/projects/states/dashboard/directives/student-task-list/task-list-item/task-list-item.component.html @@ -4,15 +4,15 @@

    @if (task.isGroupTask()) { - + } @if (!task.isGroupTask()) { - + } {{ task.definition.abbreviation }} - {{ gradeNames[task.definition.targetGrade] }} Task @if (task.isBeforeStartDate() && !task.inSubmittedState()) { - {{ task.timeToStart() }} + hourglass_empty {{ task.timeToStart() }} } @if (!task.isBeforeStartDate() && !task.inSubmittedState()) { @@ -22,7 +22,7 @@

    task.isOverdue() ? 'Task Overdue!' : 'Complete task in ' + task.timeToDue() }}" > - {{ task.timeToDue() }} + hourglass_bottom {{ task.timeToDue() }} }

    @@ -34,19 +34,19 @@

    {{ task.numNewComments }} - + visibility
    @@ -62,7 +62,7 @@

    class="task-subscript-badge soon-badge" [hidden]="!(task.isDueSoon() && !task.inFinalState())" > - + schedule !(task.betweenDueDateAndDeadlineDate() && !task.isPastDeadline() && !task.inFinalState()) " > - + schedule - ! + schedule!

    diff --git a/src/app/projects/states/dashboard/directives/student-task-list/task-list-item/task-list-item.component.ts b/src/app/projects/states/dashboard/directives/student-task-list/task-list-item/task-list-item.component.ts index b6c8d1c30f..2f2351cc40 100644 --- a/src/app/projects/states/dashboard/directives/student-task-list/task-list-item/task-list-item.component.ts +++ b/src/app/projects/states/dashboard/directives/student-task-list/task-list-item/task-list-item.component.ts @@ -1,4 +1,4 @@ -import {Component, Input, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; import {Task} from 'src/app/api/models/doubtfire-model'; import {GradeService} from 'src/app/common/services/grade.service'; @@ -6,13 +6,15 @@ import {GradeService} from 'src/app/common/services/grade.service'; selector: 'task-list-item', templateUrl: 'task-list-item.component.html', styleUrls: ['task-list-item.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TaskListItemComponent implements OnInit { @Input() task: Task; - @Input() setSelectedTask: any; - @Input() isSelectedTask: any; + @Input() setSelectedTask: (task: Task) => void; + @Input() isSelectedTask: (task: Task) => boolean; - public gradeNames: {}; + public gradeNames: GradeService['grades']; constructor(private gs: GradeService) {} diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.html index 8cc634a7fd..781e0aa244 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.html @@ -1,6 +1,6 @@ -
    +
    - comment + comment

    Discussion Prompts for {{ project?.student?.name }}

    diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.ts index 6156a4b191..864f64226f 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/discussion-prompts-view/discussion-prompts-view.component.ts @@ -1,9 +1,11 @@ -import {Component, Input} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; @Component({ selector: 'f-discussion-prompts-view', templateUrl: './discussion-prompts-view.component.html', styleUrls: ['./discussion-prompts-view.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class DiscussionPromptsViewComponent { @Input() project; diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/staff-notes-view/staff-notes-view.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/staff-notes-view/staff-notes-view.component.html index 8b57557240..b2ea951404 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/staff-notes-view/staff-notes-view.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/staff-notes-view/staff-notes-view.component.html @@ -1,8 +1,9 @@ -
    +
    - comment -

    Staff Notes for {{ project?.student?.name }}

    + comment +

    Student Notes for {{ project?.student?.name }}

    +

    Use these notes for private staff discussions about the student. Students cannot view them.

    diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/staff-notes-view/staff-notes-view.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/staff-notes-view/staff-notes-view.component.ts index 45e4e76048..e61a9d1a43 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/staff-notes-view/staff-notes-view.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/staff-notes-view/staff-notes-view.component.ts @@ -1,9 +1,11 @@ -import {Component, Input} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; @Component({ selector: 'f-staff-notes-view', templateUrl: './staff-notes-view.component.html', styleUrls: ['./staff-notes-view.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class StaffNotesViewComponent { @Input() project; diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component.html index 54ca0810cb..65f939f5be 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component.html @@ -1,16 +1,19 @@ - + Assessment Information
    -
    +

    This task will be graded against a grade standard. Your work will be assessed and assigned - a grade according to a Pass, Credit, Distinction or High Distinction standard. + a grade according to the {{ gradeStandardLabels }} standards configured for this unit.

    -
    +
    This task has been assigned a grade.

    Your tutor has marked you on this task to a @@ -21,13 +24,13 @@

    -

    +

    This task will be graded against a quality scale from 0 to {{ task.definition.maxQualityPts }}. Your work will assessed and assigned a star rating based on the quality of your submission.

    -
    +
    This task has been assessed for quality.

    You have been awarded diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component.spec.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component.spec.ts index d9dc27547d..ced1674ccc 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component.spec.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component.spec.ts @@ -1,6 +1,11 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TaskService} from 'src/app/api/services/task.service'; +import {GradeService} from 'src/app/common/services/grade.service'; +import {TaskAssessmentCardComponent} from './task-assessment-card.component'; -import { TaskAssessmentCardComponent } from './task-assessment-card.component'; +const emptyProvider = {}; describe('TaskAssessmentCardComponent', () => { let component: TaskAssessmentCardComponent; @@ -8,13 +13,20 @@ describe('TaskAssessmentCardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ TaskAssessmentCardComponent ] + declarations: [TaskAssessmentCardComponent], + providers: [ + {provide: TaskService, useValue: emptyProvider}, + {provide: GradeService, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], }) - .compileComponents(); + .overrideComponent(TaskAssessmentCardComponent, {set: {template: ''}}) + .compileComponents(); + }); + beforeEach(() => { fixture = TestBed.createComponent(TaskAssessmentCardComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component.ts index 34885faa0e..db8db18949 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component.ts @@ -1,16 +1,33 @@ -import { Component, Input } from '@angular/core'; -import { Task } from 'src/app/api/models/task'; -import { TaskService } from 'src/app/api/services/task.service'; -import { GradeService } from 'src/app/common/services/grade.service'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {Task} from 'src/app/api/models/task'; +import {TaskService} from 'src/app/api/services/task.service'; +import {GradeService} from 'src/app/common/services/grade.service'; @Component({ selector: 'f-task-assessment-card', templateUrl: './task-assessment-card.component.html', styleUrls: ['./task-assessment-card.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TaskAssessmentCardComponent { - constructor(private taskService: TaskService, private gradeService: GradeService) {} + constructor( + private taskService: TaskService, + private gradeService: GradeService, + ) {} @Input() task: Task; - gradeNames = this.gradeService.grades; + + get gradeNames() { + return Object.fromEntries( + this.task.unit.gradeDefinitions.map((definition) => [definition.value, definition.label]), + ); + } + + get gradeStandardLabels(): string { + return this.task.unit.gradeDefinitions + .filter((definition) => definition.value >= 0) + .map((definition) => definition.label) + .join(', '); + } } diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.tpl.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.tpl.html index c849365e0f..0aa6c58092 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.tpl.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.tpl.html @@ -1,4 +1,7 @@ -

    +

    Assessment Information

    @@ -7,38 +10,38 @@

    Assessment Information

    This task {{assessmentCards.hasBeenGraded ? 'has been' : 'will be'}} assigned a grade

    - This task will be graded against a grade standard. Your work will - be assessed and assigned a grade according to a Pass, Credit, - Distinction or High Distinction standard. + This task will be graded against a grade standard. Your work will be assessed and assigned + a grade according to a Pass, Credit, Distinction or High Distinction standard.

    Advice for achieving a {{task.project.targetGradeWord}}

    - As you are attempting to achieve a {{task.project.targetGradeWord}} in this unit, - you should attempt to achieve a {{task.project.targetGradeWord}} grade - on this task. Ask your tutor to find out more on what they are looking for when they are assessing - this work to a specific grade. + As you are attempting to achieve a {{task.project.targetGradeWord}} in this unit, you + should attempt to achieve a {{task.project.targetGradeWord}} grade on + this task. Ask your tutor to find out more on what they are looking for when they are + assessing this work to a specific grade.

    -
    +
    +
    Your tutor has marked you on this task to a {{task.gradeWord}} standard. -
    -
    -
    +
    + +
    + +
    This task will be assessed on a scale to {{task.definition.maxQualityPts}} - - This task has been assessed for quality - + This task has been assessed for quality

    This task will be graded against a quality scale from - 0 to {{task.definition.maxQualityPts}}. Your work will assessed - and assigned a star rating based on the quality of your submission. + 0 to {{task.definition.maxQualityPts}}. Your work will assessed and + assigned a star rating based on the quality of your submission.

    max="task.definition.maxQualityPts" state-on="'fa fa-star rating-outline'" state-off="'fa fa-star rating-disabled'" - readonly="true"> + readonly="true" + >

    You have been awarded out @@ -55,6 +59,9 @@

    avaliable points for this task.

    -
    -
    -
    +
    + +
    + +
    + diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component.html index 2e44b47ce8..e5f1ce9ae5 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component.html @@ -1,19 +1,22 @@ - + -
    - +
    + {{ taskDef?.name }}
    -
    +
    @if (task) { }
    • {{ grades.names[taskDef?.targetGrade] }} Task
    • - @if (taskDef.unit.allowFlexibleDates) { + @if (unit?.allowFlexibleDates ?? taskDef?.unit?.allowFlexibleDates) {
    • I'm planning to start this task by {{ startDate() | date: 'EEE d MMM' }}.
    • The due date has been extended by {{ task?.extensions }} week{{ @@ -42,23 +45,23 @@
    - diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component.ts index 73da8076c8..87696430e1 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-description-card/task-description-card.component.ts @@ -1,5 +1,11 @@ -import {Component, Input, Inject, EventEmitter, Output} from '@angular/core'; - +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Inject, + Input, + Output, +} from '@angular/core'; import {Task, TaskDefinition, Unit} from 'src/app/api/models/doubtfire-model'; import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; import {GradeService} from 'src/app/common/services/grade.service'; @@ -8,6 +14,8 @@ import {GradeService} from 'src/app/common/services/grade.service'; selector: 'f-task-description-card', templateUrl: 'task-description-card.component.html', styleUrls: ['task-description-card.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TaskDescriptionCardComponent { @Output() switchView$: EventEmitter = new EventEmitter(); @@ -16,7 +24,10 @@ export class TaskDescriptionCardComponent { @Input() taskDef: TaskDefinition; @Input() unit: Unit; - public grades: {names: any; acronyms: any}; + public grades: { + names: GradeService['grades']; + acronyms: GradeService['gradeAcronyms']; + }; constructor( private GradeService: GradeService, @@ -47,9 +58,13 @@ export class TaskDescriptionCardComponent { } public dueDate(): Date { - if (this.task) return this.task.localDueDate(); - else if (this.taskDef) return this.taskDef.targetDate; - else return undefined; + if (this.task) { + return this.task.localDueDate(); + } else if (this.taskDef) { + return this.taskDef.targetDate; + } else { + return undefined; + } } public startDate(): Date { @@ -60,7 +75,7 @@ export class TaskDescriptionCardComponent { if (this.task) { return this.task.localDeadlineDate(); } - return this.taskDef.localDeadlineDate(); + return this.taskDef?.localDeadlineDate(); } public shouldShowDeadline(): boolean { diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.html index 97c882ca8e..0e308f0ffa 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.html @@ -15,13 +15,13 @@ @if (flexibleDatesEnabled) { -

    +

    Your target due date for this task is {{ task?.localDueDateString() }}. You should aim to complete this task before then to keep your progress on track.

    } @else { -

    +

    This task's due date is {{ task?.localDueDateString() }}. You should aim to complete this task before then to keep your progress on track.

    @@ -56,27 +56,32 @@ @if (task?.betweenDueDateAndDeadlineDate()) { - warning @if (flexibleDatesEnabled) { - Past Target Date By {{ task?.timePastDueDateDescription() }} + Past Target Date By {{ task?.timePastDueDateDescription() }} } @else { - Past Due Date By {{ task?.timePastDueDateDescription() }} + Past Due Date By {{ task?.timePastDueDateDescription() }} } - + @if (flexibleDatesEnabled) { -

    +

    You should have submitted this task by {{ task?.localDueDateString() }} to keep your progress on track. Try to finish it as soon as possible to avoid delays. You can revise your project plan dates, but ensure you submit before the deadline to receive feedback.

    } @else { -

    +

    You should have completed this task by {{ task?.localDueDateString() }}. Try and finish it as soon as possible to avoid falling behind. You will need to @@ -116,16 +121,19 @@ @if (task?.isPastDeadline()) { - error - Passed Due Date By {{ task?.timePastDueDateDescription() }} - + -

    +

    @if (task?.definition?.unit.markLateSubmissionsAsAssessInPortfolio) { You should have completed this task by {{ task?.localDueDateString() }} { let component: TaskDueCardComponent; @@ -8,13 +9,16 @@ describe('TaskDueCardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ TaskDueCardComponent ] + declarations: [TaskDueCardComponent], + schemas: [NO_ERRORS_SCHEMA], }) - .compileComponents(); + .overrideComponent(TaskDueCardComponent, {set: {template: ''}}) + .compileComponents(); + }); + beforeEach(() => { fixture = TestBed.createComponent(TaskDueCardComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.ts index a84d979294..4cba264c1b 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component.ts @@ -1,16 +1,15 @@ -import {Component, Input, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; import {Task} from 'src/app/api/models/task'; @Component({ selector: 'f-task-due-card', templateUrl: './task-due-card.component.html', styleUrls: ['./task-due-card.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class TaskDueCardComponent implements OnInit { +export class TaskDueCardComponent { @Input() task: Task; - constructor() {} - - ngOnInit(): void {} public get flexibleDatesEnabled(): boolean { return this.task?.unit?.allowFlexibleDates; diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.tpl.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.tpl.html index 6f14f303b2..a4ea4b1999 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.tpl.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.tpl.html @@ -1,39 +1,47 @@ -

    -
    +
    + +
    +

    Aim To Complete Soon - Due in {{task.timeUntilDueDateDescription()}}

    -
    +
    +

    - This task's due date is {{task.localDueDateString()}}. - You should aim to complete this task before then to keep your progress on track. + This task's due date is {{task.localDueDateString()}}. You should aim to + complete this task before then to keep your progress on track.

    -
    +
    +

    - This task's due date is {{task.localDueDateString()}}. - Make sure to discuss this task with your tutor as soon as possible. + This task's due date is {{task.localDueDateString()}}. Make sure to + discuss this task with your tutor as soon as possible.

    - Tasks are only considered Completed once your tutor has Discussed your work - with you. + Tasks are only considered Completed once your tutor has + Discussed your work with you.

    -
    +
    +

    Past Due Date By {{task.timePastDueDateDescription()}}

    -
    +
    +

    - You should have completed this task by {{task.localDueDateString()}}. - Try and finish it as soon as possible to avoid falling behind. As you will submit this - task after the deadline for feedback, it will not be reviewed by a tutor and it is now - your sole responsibility to ensure that this submission meets the required standard. - The task will be assessed as part of the portfolio. + You should have completed this task by {{task.localDueDateString()}}. Try + and finish it as soon as possible to avoid falling behind. As you will submit this task + after the deadline for feedback, it will not be reviewed by a tutor and it is now your + sole responsibility to ensure that this submission meets the required standard. The task + will be assessed as part of the portfolio.

    Aim to submit future tasks before the deadline to make good use of the opportunity to @@ -41,11 +49,12 @@

    Past Due Date By {{task.timePastDueDateDescription()}}

    submission meets all the requirements.

    -
    +
    +

    You should have completed this task by {{task.localDueDateString()}}. - Make sure to discuss this task with your tutor as soon as possible. If this task remains on - this state for an extended period, it will be marked as Time Exceeded. + Make sure to discuss this task with your tutor as soon as possible. If this task remains + on this state for an extended period, it will be marked as Time Exceeded.

    Tasks are only considered completed once your tutor has @@ -53,33 +62,37 @@

    Past Due Date By {{task.timePastDueDateDescription()}}

    -
    +
    +

    Passed Due Date By {{task.timePastDueDateDescription()}}

    -
    +
    +

    You should have completed this task by {{task.localDueDateString()}}. - This task is now past the deadline and will be marked as Time Exceeded when submitted. You should - consult with the unit assessment details to determine the impact of failing to complete this task within - the allocated time. + This task is now past the deadline and will be marked as Time Exceeded when + submitted. You should consult with the unit assessment details to determine the impact of + failing to complete this task within the allocated time.

    -
    +
    +

    You should have completed this task by {{task.localDueDateString()}}. Make sure to discuss this task with your tutor as soon as possible.

    - Tasks are only considered Completed once it demonstrates the required standard, and it is - discussed with your tutor. + Tasks are only considered Completed once it demonstrates the required + standard, and it is discussed with your tutor.

    -
    +
    +

    Wait for Tutor Feedback

    @@ -87,8 +100,10 @@

    Wait for Tutor Feedback

    You have submitted this task and should now wait for feedback from your tutor. - Do not re-upload new files at this time as the status will be changed to - Time Exceeded. + Do not re-upload new files at this time as the status will be changed to + Time Exceeded.

    diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-ilos-card/task-ilos-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-ilos-card/task-ilos-card.component.html index e4bf3548c2..596c316bd1 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-ilos-card/task-ilos-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-ilos-card/task-ilos-card.component.html @@ -3,19 +3,19 @@ {{ getIloContextLabel() }} Learning Outcomes - These outcomes describe the key skills and knowledge you are aiming to achieve by completing this {{ getIloContextLabel().toLowerCase() }}. @for (ilo of learningOutcomes; track ilo) { - - - - + + +
    {{ ilo.abbreviation }}{{ ilo.fullOutcomeDescription }} +
    {{ ilo.abbreviation }}{{ ilo.fullOutcomeDescription }} @for (outcome of getLinkedOutcomes(ilo); track outcome.abbreviation) { - {{ + {{ outcome.abbreviation }} } diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-ilos-card/task-ilos-card.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-ilos-card/task-ilos-card.component.ts index 3a0cf9ce54..033e5d5b6a 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-ilos-card/task-ilos-card.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-ilos-card/task-ilos-card.component.ts @@ -1,4 +1,11 @@ -import {Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Input, + OnChanges, + OnInit, + SimpleChanges, +} from '@angular/core'; import {LearningOutcome} from 'src/app/api/models/learning-outcome'; import {Project} from 'src/app/api/models/project'; import {TaskDefinition} from 'src/app/api/models/task-definition'; @@ -8,6 +15,8 @@ import {Unit} from 'src/app/api/models/unit'; selector: 'f-task-ilos-card', templateUrl: './task-ilos-card.component.html', styleUrls: ['./task-ilos-card.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TaskIlosCardComponent implements OnInit, OnChanges { @Input() iloContextType: 'Unit' | 'TaskDefinition' | 'Course' | 'Global'; diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/submission-files-modal/submission-files-modal.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/submission-files-modal/submission-files-modal.component.html index 5d1d69f412..0b5d7505e9 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/submission-files-modal/submission-files-modal.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/submission-files-modal/submission-files-modal.component.html @@ -1,4 +1,4 @@ -
    +

    @if (compareMode) { @@ -13,40 +13,38 @@

    - +
    -
    +
    @if (isLoading) { -
    +
    } @else if (errorMessage) { -
    +
    {{ errorMessage }}
    } @else if (compareMode) { @if (archiveBlob && comparedArchiveBlob) { -
    -
    +
    +
    @@ -58,17 +56,17 @@

    [ngTemplateOutletContext]="{ number: data.comparedWithNumber, isMostRecent: data.comparedWithIsMostRecent, - timestamp: data.comparedWith?.timestamp + timestamp: $safeNavigationMigration(data.comparedWith?.timestamp), }" > @if (primaryArchiveParsed) { @@ -82,42 +80,40 @@

    @if (!bothSelectionsReady) { -
    +
    Select a file in both submissions to compare.
    } @else if (canShowDiffEditor) { -
    -
    -
    +
    +
    +
    {{ primarySelectedFile?.path ?? primarySelectedFile?.name }}
    -
    +
    {{ comparedSelectedFile?.path ?? comparedSelectedFile?.name }}
    } @else { -
    +
    @@ -125,33 +121,29 @@

    } @else { -
    +
    Unable to load one or both submission archives.
    } } @else if (archiveBlob) { -
    +
    } @else { -
    +
    Unable to load submission files.
    } @@ -159,24 +151,24 @@

    -
    +
    Submission{{ number !== undefined ? ' ' + number : '' }}{{ isMostRecent ? ' (Most recent)' : '' }}: {{ timestamp | date: 'dd/MM/yyyy HH:mm' }}
    - +
    - + {{ file?.path ?? file?.name }} @if (selectedFilesMatch !== null) { @@ -191,22 +183,22 @@

    @if (isArchiveCodeOrTextFile(file)) { } @else if (isArchivePdfFile(file)) { - + } @else if (isArchiveImageFile(file)) { -
    +
    } @else { -
    +
    Preview not available for this file type.
    } diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/submission-files-modal/submission-files-modal.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/submission-files-modal/submission-files-modal.component.ts index a0068a7cde..5a1cce8f98 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/submission-files-modal/submission-files-modal.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/submission-files-modal/submission-files-modal.component.ts @@ -1,8 +1,8 @@ -import {HttpResponse} from '@angular/common/http'; -import {Component, Inject, OnDestroy, OnInit} from '@angular/core'; import * as monaco from 'monaco-editor'; +import {HttpResponse} from '@angular/common/http'; +import {ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit} from '@angular/core'; import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; -import {OverseerAssessment} from 'src/app/api/models/doubtfire-model'; +import {SubmissionArchive} from 'src/app/api/models/submission-history'; import { ArchiveFileEntry, isArchiveCodeOrTextFile, @@ -13,10 +13,10 @@ import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloa import {AlertService} from 'src/app/common/services/alert.service'; export interface SubmissionFilesModalData { - assessment: OverseerAssessment; + assessment: SubmissionArchive; assessmentNumber?: number; assessmentIsMostRecent?: boolean; - comparedWith?: OverseerAssessment; + comparedWith?: SubmissionArchive; comparedWithNumber?: number; comparedWithIsMostRecent?: boolean; } @@ -25,6 +25,8 @@ export interface SubmissionFilesModalData { selector: 'f-submission-files-modal', templateUrl: './submission-files-modal.component.html', styleUrls: ['./submission-files-modal.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class SubmissionFilesModalComponent implements OnInit, OnDestroy { private readonly diffOriginalUri = monaco.Uri.parse('inmemory://submission-compare/original'); @@ -150,7 +152,7 @@ export class SubmissionFilesModalComponent implements OnInit, OnDestroy { } } - private downloadSubmissionArchive(assessment: OverseerAssessment): Promise { + private downloadSubmissionArchive(assessment: SubmissionArchive): Promise { return new Promise((resolve, reject) => { this.fileDownloader.downloadBlob( assessment.submissionFilesUrl(), @@ -271,7 +273,7 @@ export class SubmissionFilesModalComponent implements OnInit, OnDestroy { return ''; } - const digest = await crypto.subtle.digest('SHA-256', bytes); + const digest = await crypto.subtle.digest('SHA-256', new Uint8Array(bytes).buffer); return Array.from(new Uint8Array(digest)) .map((value) => value.toString(16).padStart(2, '0')) .join(''); diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.html index 2773aed285..a1b1b889de 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.html @@ -1,55 +1,88 @@ -
    - +
    +
    - - @for (oa of overseerAssessments; track oa; let idx = $index) { - - - - - Submission {{ overseerAssessments.length - idx }}: {{ oa.timestamp | humanizedDate }} - @if (idx === 0) { - (Most recent) - } - @if (isComparisonSource(oa)) { - (Selected for comparison) - } - - @if (oa.reportReady) { -
    - {{ oa.passedSteps }} / {{ oa.totalSteps }} - @if (oa.passedSteps === oa.totalSteps) { - done - } @else { - cancel +@if (loading) { +
    + +
    +} @else { + + @for (history of histories; track history.id; let idx = $index) { + + + + @if (assessmentFor(history); as oa) { + @if (oa.taskStatus) { + + } + } +
    +
    + Submission {{ histories.length - idx }} + @if (idx === 0) { + (Most recent) + } + @if (isComparisonSource(history)) { + (Selected for comparison) + } +
    +
    + {{ history.timestamp | date: 'dd/MM/yyyy HH:mm' }} +
    +
    +
    + +
    + @if (assessmentFor(history); as oa) { + @if (oa.reportReady) { + Click to view Overseer report + {{ oa.passedSteps }} / {{ oa.totalSteps }} + @if (oa.passedSteps === oa.totalSteps) { + done + } @else { + cancel + } + } @else { + + Tests In Progress + + + } } - @if (oa.hasSubmissionFiles && currentUnitRole) { + + @if (history.hasSubmissionFiles && currentUnitRole) { -
    - - @if (hasComparisonSourceFor(oa)) { - - } @else if (isComparisonSource(oa)) { + } @else if (isComparisonSource(history)) {
    - } @else { -
    - Tests In Progress - -
    - } -
    + - - @for (result of oa.stepResultsCache.values | async; track result.id; let idx = $index) { - - - - Step {{ idx + 1 }}: {{ result.overseerStep?.displayName }} - @if (result.pass) { - done - } @else { - cancel - } - + @if (assessmentFor(history); as oa) { +
    + + @for ( + result of oa.stepResultsCache.values | async; + track result.id; + let idx = $index + ) { + + + + Step {{ idx + 1 }}: {{ result.overseerStep?.displayName }} +
    + @if (result.pass) { + done + } @else { + cancel + } +
    +
    - - + + - @if (!result.pass) { -

    - {{ result.feedbackMessage }} -

    - } + @if (!result.pass) { +

    + {{ result.feedbackMessage }} +

    + } - @if ( - result.expectedOutput && - result.expectedOutput !== result.stdout && - (result.overseerStep?.stepType === 'output_diff' || !result.overseerStep) - ) { -
    - + @if ( + result.expectedOutput && + result.expectedOutput !== result.stdout && + (result.overseerStep?.stepType === 'output_diff' || !result.overseerStep) + ) { +
    + - - + + - -
    - @if (viewOutput === 'diff' || viewOutput === 'split_diff') { - @if (result.expectedOutput !== result.stdout) { - + +
    + @if (viewOutput === 'diff' || viewOutput === 'split_diff') { + @if (result.expectedOutput !== result.stdout) { + + } + } @else if (viewOutput === 'your_output') { + + } @else if (viewOutput === 'expected_output') { + + } + } @else if (result.stdout) { +
    + } @else if (result.pass) { +
    + SUCCESS +
    +
    + (No Output) +
    + } +
    + } + @if (loadingAssessments.has(oa.id)) { +
    + +
    + } @else { + @for (skipped of oa.stepsSkipped; track skipped.id; let idx = $index) { + + + + + + Step {{ oa.stepResultsCache.currentValues.length + idx + 1 }}: + {{ skipped?.displayName ?? '-' }} + (Skipped) + + + pause + + } - } @else if (viewOutput === 'your_output') { - - } @else if (viewOutput === 'expected_output') { - } - } @else if (result.stdout) { -
    {{result.stdout}}
    - } @else if (result.pass) { -
    SUCCESS
    -
    - (No Output) -
    - } - +
    +
    } - @if (loadingAssessments.has(oa.id)) { -
    - + @if (history.hasSubmissionFiles && currentUnitRole) { +
    +
    - } @else { - @for (skipped of oa.stepsSkipped; track skipped.id; let idx = $index) { - - - - - - Step {{ oa.stepResultsCache.currentValues.length + idx + 1 }}: - {{ skipped?.displayName ?? '-' }} - (Skipped) - - - pause - - - } } - - - } @empty { -
    - subtitles_off -
    No submission reports for this task.
    -
    - } - + + } @empty { +
    + subtitles_off +
    No retained submissions for this task.
    +
    + } + +} diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.ts index e576813299..37c24bb6e0 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component.ts @@ -1,32 +1,58 @@ -import {Component, Input, OnInit} from '@angular/core'; +import Convert from 'ansi-to-html'; +import DOMPurify from 'dompurify'; +import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; import {MatDialog} from '@angular/material/dialog'; import {MatMenuTrigger} from '@angular/material/menu'; +import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; +import {forkJoin} from 'rxjs'; import {OverseerAssessment, UnitRole, UserService} from 'src/app/api/models/doubtfire-model'; +import {SubmissionHistory} from 'src/app/api/models/submission-history'; import {Task} from 'src/app/api/models/task'; import {OverseerAssessmentService} from 'src/app/api/services/overseer-assessment.service'; import {OverseerStepResultService} from 'src/app/api/services/overseer-step-result.service'; +import {SubmissionHistoryService} from 'src/app/api/services/submission-history.service'; import {AlertService} from 'src/app/common/services/alert.service'; -import {TaskSubmissionService} from 'src/app/common/services/task-submission.service'; import {SubmissionFilesModalComponent} from './submission-files-modal/submission-files-modal.component'; @Component({ selector: 'f-task-overseer-report', templateUrl: './task-overseer-report.component.html', styleUrl: './task-overseer-report.component.scss', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TaskOverseerReportComponent implements OnInit { @Input() task: Task; @Input() loadOverseerAssessmentId?: number; - public comparisonSourceAssessmentId: number | null = null; + public histories: SubmissionHistory[] = []; + public overseerAssessments: OverseerAssessment[] = []; + public comparisonSourceHistoryId: number | null = null; + public loading = false; constructor( private alerts: AlertService, - private submissions: TaskSubmissionService, + private submissionHistoryService: SubmissionHistoryService, private overseerAssessmentService: OverseerAssessmentService, private overseerStepResultsService: OverseerStepResultService, private dialog: MatDialog, + private readonly sanitizer: DomSanitizer, private userService: UserService, - ) {} + ) { + DOMPurify.addHook('uponSanitizeAttribute', (_node, data) => { + if (data.attrName !== 'style') { + return; + } + data.attrValue = data.attrValue + .split(';') + .map((rule) => rule.trim()) + .filter((rule) => + /^(color|background-color|font-weight|font-style)\s*:\s*(#[0-9a-f]{3,8}|[a-z]+|\d+)\b/i.test( + rule, + ), + ) + .join('; '); + }); + } public get currentUnitRole(): UnitRole | undefined { const currentUser = this.userService.currentUser; @@ -73,6 +99,24 @@ export class TaskOverseerReportComponent implements OnInit { lineNumbers: 'off', }; + private ansi = new Convert({ + newline: false, + escapeXML: true, + fg: '#1f2937', + bg: '#f9fafb', + }); + + protected renderOutput(output?: string | null): SafeHtml { + const html = this.ansi.toHtml(output ?? ''); + + const clean = DOMPurify.sanitize(html, { + ALLOWED_TAGS: ['span', 'br', 'i', 'b', 'strong', 'em', 'code'], + ALLOWED_ATTR: ['style'], + }); + + return this.sanitizer.bypassSecurityTrustHtml(clean); + } + diff() { this.diffEditorOptions.renderSideBySide = false; this.diffEditorOptions.compactMode = true; @@ -97,38 +141,38 @@ export class TaskOverseerReportComponent implements OnInit { this.viewOutput = 'expected_output'; } - public overseerAssessments: OverseerAssessment[] = []; - - public get comparisonSourceAssessment(): OverseerAssessment | null { - if (!this.comparisonSourceAssessmentId) { + public get comparisonSourceHistory(): SubmissionHistory | null { + if (!this.comparisonSourceHistoryId) { return null; } - return ( - this.overseerAssessments.find( - (assessment) => assessment.id === this.comparisonSourceAssessmentId, - ) ?? null - ); + return this.histories.find((history) => history.id === this.comparisonSourceHistoryId) ?? null; } ngOnInit(): void { - this.loadAssessments(); + this.loadHistory(); } - loadAssessments(isRefresh: boolean = false) { + loadHistory(isRefresh: boolean = false) { if (isRefresh) { this.loadOverseerAssessmentId = null; } - this.overseerAssessmentService.queryForTask(this.task).subscribe({ - next: (assessments) => { + this.loading = true; + + forkJoin({ + histories: this.submissionHistoryService.queryForTask(this.task), + assessments: this.overseerAssessmentService.queryForTask(this.task), + }).subscribe({ + next: ({histories, assessments}) => { + this.histories = histories; this.overseerAssessments = assessments; + if ( - this.comparisonSourceAssessmentId && - !this.overseerAssessments.some( - (assessment) => assessment.id === this.comparisonSourceAssessmentId, - ) + this.comparisonSourceHistoryId && + !this.histories.some((history) => history.id === this.comparisonSourceHistoryId) ) { - this.comparisonSourceAssessmentId = null; + this.comparisonSourceHistoryId = null; } + for (const oa of this.overseerAssessments) { for (const result of oa.stepResultsCache.currentValues) { result.overseerStep = this.task.definition.overseerStepsCache.currentValues.find( @@ -136,26 +180,39 @@ export class TaskOverseerReportComponent implements OnInit { ); } } + this.loading = false; }, error: (error) => { - this.alerts.error(`Failed to load overseer reports: ${error}`, 6000); + this.loading = false; + this.alerts.error(`Failed to load submission history: ${error}`, 6000); }, }); } - loadingAssessments = new Set(); + loadingAssessments: Set = new Set(); + + assessmentFor(history: SubmissionHistory): OverseerAssessment | undefined { + return this.overseerAssessments.find( + (assessment) => assessment.submissionHistoryId === history.id, + ); + } + + onHistoryOpen(history: SubmissionHistory) { + const overseerAssessment = this.assessmentFor(history); + if (!overseerAssessment) { + return; + } - onAssessmentOpen(overseerAssesment: OverseerAssessment) { - if (this.loadOverseerAssessmentId === overseerAssesment.id) { + if (this.loadOverseerAssessmentId === overseerAssessment.id) { setTimeout(() => { - const el = document.getElementById(`oa-panel-${overseerAssesment.id}`); + const el = document.getElementById(`history-panel-${history.id}`); el?.scrollIntoView({behavior: 'smooth', block: 'start'}); }, 250); } - this.loadingAssessments.add(overseerAssesment.id); + this.loadingAssessments.add(overseerAssessment.id); - this.overseerStepResultsService.getOverseerStepResults(overseerAssesment).subscribe({ + this.overseerStepResultsService.getOverseerStepResults(overseerAssessment).subscribe({ next: () => { for (const oa of this.overseerAssessments) { for (const result of oa.stepResultsCache.currentValues) { @@ -164,11 +221,11 @@ export class TaskOverseerReportComponent implements OnInit { ); } } - this.loadingAssessments.delete(overseerAssesment.id); + this.loadingAssessments.delete(overseerAssessment.id); }, error: (error) => { console.error(error); - this.loadingAssessments.delete(overseerAssesment.id); + this.loadingAssessments.delete(overseerAssessment.id); }, }); } @@ -177,66 +234,55 @@ export class TaskOverseerReportComponent implements OnInit { event.stopPropagation(); } - isComparisonSource(assessment: OverseerAssessment): boolean { - return this.comparisonSourceAssessmentId === assessment.id; + isComparisonSource(history: SubmissionHistory): boolean { + return this.comparisonSourceHistoryId === history.id; } - hasComparisonSourceFor(assessment: OverseerAssessment): boolean { - return ( - this.comparisonSourceAssessmentId !== null && - this.comparisonSourceAssessmentId !== assessment.id - ); + hasComparisonSourceFor(history: SubmissionHistory): boolean { + return this.comparisonSourceHistoryId !== null && this.comparisonSourceHistoryId !== history.id; } - selectComparisonSource( - assessment: OverseerAssessment, - event?: Event, - menuTrigger?: MatMenuTrigger, - ) { + selectComparisonSource(history: SubmissionHistory, event?: Event, menuTrigger?: MatMenuTrigger) { event?.stopPropagation(); - this.comparisonSourceAssessmentId = assessment.id; + this.comparisonSourceHistoryId = history.id; menuTrigger?.closeMenu(); - this.alerts.message(`Selected submission ${assessment.timestampString} for comparison.`, 3500); + this.alerts.message(`Selected submission ${history.timestampString} for comparison.`, 3500); } clearComparisonSource(event?: Event) { event?.stopPropagation(); - this.comparisonSourceAssessmentId = null; + this.comparisonSourceHistoryId = null; } - compareWithSelected(assessment: OverseerAssessment, event?: Event) { + compareWithSelected(history: SubmissionHistory, event?: Event) { event?.stopPropagation(); - const selected = this.comparisonSourceAssessment; - if (!selected || selected.id === assessment.id) { + const selected = this.comparisonSourceHistory; + if (!selected || selected.id === history.id) { return; } - this.openSubmissionFilesDialog(assessment, selected); + this.openSubmissionFilesDialog(history, selected); } - viewSubmissionFiles(assessment: OverseerAssessment, event?: Event) { + viewSubmissionFiles(history: SubmissionHistory, event?: Event) { event?.stopPropagation(); - this.openSubmissionFilesDialog(assessment); + this.openSubmissionFilesDialog(history); } - private openSubmissionFilesDialog( - assessment: OverseerAssessment, - comparedWith?: OverseerAssessment, - ) { - const assessmentIndex = this.overseerAssessments.findIndex((item) => item.id === assessment.id); + private openSubmissionFilesDialog(history: SubmissionHistory, comparedWith?: SubmissionHistory) { + const historyIndex = this.histories.findIndex((item) => item.id === history.id); const comparedWithIndex = comparedWith - ? this.overseerAssessments.findIndex((item) => item.id === comparedWith.id) + ? this.histories.findIndex((item) => item.id === comparedWith.id) : -1; this.dialog.open(SubmissionFilesModalComponent, { data: { - assessment, - assessmentNumber: - assessmentIndex >= 0 ? this.overseerAssessments.length - assessmentIndex : undefined, - assessmentIsMostRecent: assessmentIndex === 0, + assessment: history, + assessmentNumber: historyIndex >= 0 ? this.histories.length - historyIndex : undefined, + assessmentIsMostRecent: historyIndex === 0, comparedWith, comparedWithNumber: - comparedWithIndex >= 0 ? this.overseerAssessments.length - comparedWithIndex : undefined, + comparedWithIndex >= 0 ? this.histories.length - comparedWithIndex : undefined, comparedWithIsMostRecent: comparedWithIndex === 0, }, maxWidth: '95vw', diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-prerequisites-card/task-prerequisites-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-prerequisites-card/task-prerequisites-card.component.html index 686ec1d828..e5489003ec 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-prerequisites-card/task-prerequisites-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-prerequisites-card/task-prerequisites-card.component.html @@ -1,16 +1,16 @@ -
    +
    Prerequisite Tasks pending_actions
    - + You must meet the prerequisites for this task before submitting {{ taskDefinition.abbreviation }} {{ taskDefinition.name }}. diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-prerequisites-card/task-prerequisites-card.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-prerequisites-card/task-prerequisites-card.component.ts index 7bd7259574..659232c1c8 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-prerequisites-card/task-prerequisites-card.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-prerequisites-card/task-prerequisites-card.component.ts @@ -1,10 +1,13 @@ -import {Component, Input} from '@angular/core'; -import {TaskDefinition} from 'src/app/api/models/task-definition'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; import {Task} from 'src/app/api/models/task'; +import {TaskDefinition} from 'src/app/api/models/task-definition'; + @Component({ selector: 'f-task-prerequisites-card', templateUrl: './task-prerequisites-card.component.html', styleUrls: ['./task-prerequisites-card.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TaskPrerequisitesCardComponent { @Input() taskDefinition: TaskDefinition; diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html index d63cfbbd5c..f0f16f4ff5 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.html @@ -2,28 +2,28 @@ @if (this.task.latestCompletedTestAttempt.scoreScaled === 1) { - check - Knowledge Check Passed Without MistakesKnowledge Check Passed Without Mistakes } @if (this.task.latestCompletedTestAttempt.scoreScaled !== 1) { - check - Knowledge Check PassedKnowledge Check Passed } -

    +

    You have successfully completed this knowledge check. You can now proceed to submitting task files.

    diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts index 53d0618d28..c3f2cf3db3 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component.ts @@ -1,4 +1,4 @@ -import {Component, Input, OnChanges, SimpleChanges} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges} from '@angular/core'; import {Task, User, UserService} from 'src/app/api/models/doubtfire-model'; import {ScormExtensionModalService} from 'src/app/common/modals/scorm-extension-modal/scorm-extension-modal.service'; @@ -6,6 +6,8 @@ import {ScormExtensionModalService} from 'src/app/common/modals/scorm-extension- selector: 'f-task-scorm-card', templateUrl: './task-scorm-card.component.html', styleUrls: ['./task-scorm-card.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TaskScormCardComponent implements OnChanges { @Input() task: Task; @@ -25,10 +27,11 @@ export class TaskScormCardComponent implements OnChanges { this.attemptsLeft = undefined; this.isPassed = undefined; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - this.task?.fetchTestAttempts().subscribe((_) => { + this.task?.fetchTestAttempts().subscribe(() => { this.getAttemptsLeft(); - if (this.task.latestCompletedTestAttempt) this.isPassed = this.task.scormPassed; + if (this.task.latestCompletedTestAttempt) { + this.isPassed = this.task.scormPassed; + } }); } } @@ -37,7 +40,9 @@ export class TaskScormCardComponent implements OnChanges { if (this.task.definition.scormAttemptLimit != 0) { const attempts = this.task.testAttemptCache.currentValues; let count = attempts.length; - if (count > 0 && attempts[0].terminated === false) count--; + if (count > 0 && attempts[0].terminated === false) { + count--; + } this.attemptsLeft = this.task.definition.scormAttemptLimit + this.task.scormExtensions - count; } diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component.html index a7f0e0aed0..c96b1d69bc 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component.html @@ -1,6 +1,6 @@ -
    +
    - crisis_alert + crisis_alert

    Similarities

    @@ -17,76 +17,84 @@

    Similarities

    @if (!jplagOpenState) { - @for (similarity of task?.similarityCache.values | async; track similarity) { -

    - @for (part of similarity.parts; track part; let i = $index) { - - - - {{ similarity.friendlyTypeName }} - - {{ part.description }} - @if (similarity.readyForViewer) { - @if (similarity.type === 'JplagTaskSimilarity') { - - } @else { + @for ( + similarity of $safeNavigationMigration(task?.similarityCache.values) | async; + track similarity + ) { +
    + @for (part of similarity.parts; track part; let i = $index) { + + + + {{ similarity.friendlyTypeName }} + + {{ part.description }} + @if (similarity.readyForViewer) { + @if (similarity.type === 'JplagTaskSimilarity') { + + } @else if (similarity.type === 'TiiTaskSimilarity') { + + } + } + @if (i === 0) { } - } - @if (i === 0) { - - } - - @if (part.panelOpenState) { - @if (part.format) { - @if (part.format === 'html' || part.format === 'pdf') { - - } @else if (part.format === 'jplag') { - + + @if (part.panelOpenState) { + @if (part.format) { + @if (part.format === 'html' || part.format === 'pdf') { + + } @else if (part.format === 'jplag') { + + } + } @else { +

    There is no local similarity file for this.

    } - } @else { -

    There is no local similarity file for this.

    } - } -
    - } + + } +
    + } @empty { +
    + There are no similarities for this submission +
    }
    } diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component.ts index abdfcf3288..33c87996d2 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-similarity-view/task-similarity-view.component.ts @@ -1,5 +1,12 @@ import {HttpResponse} from '@angular/common/http'; -import {Component, Input, OnChanges, SimpleChanges, ViewChild} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Input, + OnChanges, + SimpleChanges, + ViewChild, +} from '@angular/core'; import {MatAccordion} from '@angular/material/expansion'; import {Task} from 'src/app/api/models/task'; import {TaskSimilarity} from 'src/app/api/models/task-similarity'; @@ -13,6 +20,8 @@ import {SelectedTaskService} from '../../../../selected-task.service'; selector: 'f-task-similarity-view', templateUrl: './task-similarity-view.component.html', styleUrls: ['./task-similarity-view.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TaskSimilarityViewComponent implements OnChanges { @Input() task: Task; diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component.html index 7b49a65daa..0594581649 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component.html @@ -1,23 +1,26 @@ @if (triggers?.length > 0) { - + - +

    {{ task?.statusLabel() }}

    @for (trigger of triggers; track trigger) { - -
    {{ trigger.label }}
    +
    + +
    {{ trigger.label }}
    +
    }
    @@ -26,7 +29,10 @@

    {{ task?.statusLabel() }}

    @if (triggers?.length < 0) { - +
    {{ task?.statusLabel() }}
    @@ -41,14 +47,16 @@
    {{ task?.statusLabel() }}
    } - -
    + +
    @@ -62,7 +70,7 @@
    {{ task?.statusLabel() }}
    -
    diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component.spec.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component.spec.ts index 10fe559f67..d377730a00 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component.spec.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component.spec.ts @@ -1,6 +1,21 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {ActivatedRoute} from '@angular/router'; +import {EMPTY} from 'rxjs'; +import {TaskService} from 'src/app/api/services/task.service'; +import {UserService} from 'src/app/api/services/user.service'; +import {ExtensionModalService} from 'src/app/common/modals/extension-modal/extension-modal.service'; +import {QrModalService} from 'src/app/common/modals/qr-modal/qr-modal.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {FeedbackAppealModalService} from 'src/app/tasks/modals/feedback-appeal-modal/feedback-appeal-modal.service'; +import {SubmissionTypeModalService} from 'src/app/tasks/modals/submission-type-modal/submission-type-modal.service'; +import {TaskStatusCardComponent} from './task-status-card.component'; -import { TaskStatusCardComponent } from './task-status-card.component'; +const taskServiceStub = { + taskStatusUpdated$: EMPTY, +}; +const emptyProvider = {}; describe('TaskStatusCardComponent', () => { let component: TaskStatusCardComponent; @@ -8,13 +23,26 @@ describe('TaskStatusCardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ TaskStatusCardComponent ] + declarations: [TaskStatusCardComponent], + providers: [ + {provide: ExtensionModalService, useValue: emptyProvider}, + {provide: TaskService, useValue: taskServiceStub}, + {provide: ActivatedRoute, useValue: emptyProvider}, + {provide: QrModalService, useValue: emptyProvider}, + {provide: DoubtfireConstants, useValue: emptyProvider}, + {provide: SubmissionTypeModalService, useValue: emptyProvider}, + {provide: UserService, useValue: emptyProvider}, + {provide: FeedbackAppealModalService, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], }) - .compileComponents(); + .overrideComponent(TaskStatusCardComponent, {set: {template: ''}}) + .compileComponents(); + }); + beforeEach(() => { fixture = TestBed.createComponent(TaskStatusCardComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component.ts index 8738a0bffd..b0ffc76283 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component.ts @@ -1,35 +1,54 @@ -import {AfterViewInit, Component, Input, OnChanges, SimpleChanges} from '@angular/core'; -import {UIRouter} from '@uirouter/core'; -import * as _ from 'lodash'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + Input, + OnChanges, + OnDestroy, + SimpleChanges, +} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {Subscription} from 'rxjs'; +import {Project} from 'src/app/api/models/project'; import {Task} from 'src/app/api/models/task'; import {TaskStatusEnum, TaskStatusUiData} from 'src/app/api/models/task-status'; +import {UnitRole} from 'src/app/api/models/unit-role'; import {TaskService} from 'src/app/api/services/task.service'; +import {UserService} from 'src/app/api/services/user.service'; import {ExtensionModalService} from 'src/app/common/modals/extension-modal/extension-modal.service'; import {QrModalService} from 'src/app/common/modals/qr-modal/qr-modal.service'; import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {FeedbackAppealModalService} from 'src/app/tasks/modals/feedback-appeal-modal/feedback-appeal-modal.service'; import {SubmissionTypeModalService} from 'src/app/tasks/modals/submission-type-modal/submission-type-modal.service'; -import {Project} from 'src/app/api/models/project'; -import {UserService} from 'src/app/api/services/user.service'; -import {FeedbackAppealModalService} from 'src/app/tasks/modals/feedback-appeal-modal/feedback-appeal-modal.service'; @Component({ selector: 'f-task-status-card', templateUrl: './task-status-card.component.html', styleUrls: ['./task-status-card.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class TaskStatusCardComponent implements OnChanges, AfterViewInit { +export class TaskStatusCardComponent implements OnChanges, AfterViewInit, OnDestroy { triggers: TaskStatusUiData[]; textCss: string; + private taskStatusSub: Subscription; + constructor( private extensions: ExtensionModalService, private taskService: TaskService, - private router: UIRouter, + private route: ActivatedRoute, private qrModalService: QrModalService, private doubtfireConstants: DoubtfireConstants, private submissionTypeModalService: SubmissionTypeModalService, private userService: UserService, private feedbackAppealService: FeedbackAppealModalService, - ) {} + ) { + this.taskStatusSub = this.taskService.taskStatusUpdated$.subscribe((task) => { + if (this.isCurrentTask(task)) { + this.reapplyTriggers(); + } + }); + } @Input() task: Task; taskStatusColor: string; @@ -52,9 +71,22 @@ export class TaskStatusCardComponent implements OnChanges, AfterViewInit { document.getElementsByTagName('style')[0].append(this.textCss); } + ngOnDestroy(): void { + this.taskStatusSub?.unsubscribe(); + } + + private isCurrentTask(task: Task): boolean { + return ( + task && + this.task && + task.project?.id === this.task.project?.id && + task.definition?.id === this.task.definition?.id + ); + } + reapplyTriggers(): void { // if tutor is in queryParam - if (this.router.globals.params.tutor != null) { + if (this.isTutor) { this.triggers = this.taskService.statusKeys .map((k) => this.taskService.statusData(k)) .filter((trigger) => { @@ -65,8 +97,7 @@ export class TaskStatusCardComponent implements OnChanges, AfterViewInit { return this.task.canMarkComplete || this.task.status === 'complete'; }); } else { - const studentTriggers = _.map( - this.taskService.switchableStates.student as TaskStatusEnum[], + const studentTriggers = (this.taskService.switchableStates.student as TaskStatusEnum[]).map( (k) => this.taskService.statusData(k), ); const filteredStudentTriggers = this.task.filterFutureStates(studentTriggers); @@ -76,7 +107,6 @@ export class TaskStatusCardComponent implements OnChanges, AfterViewInit { this.triggers.push(this.taskService.statusData(this.task.status)); } } - this.taskService.statusKeys; } public isReadyForFeedback(): boolean { @@ -129,4 +159,17 @@ export class TaskStatusCardComponent implements OnChanges, AfterViewInit { openFeedbackAppealModal(): void { this.feedbackAppealService.show(this.task); } + + public get currentUnitRole(): UnitRole | undefined { + const currentUser = this.userService.currentUser; + return this.project?.unit?.staff.find((ur) => ur.user.id === currentUser.id); + } + + public get isTutor(): boolean { + return ( + this.currentUnitRole?.role === 'Convenor' || + this.currentUnitRole?.role === 'Tutor' || + this.userService.currentUser.systemRole === 'Admin' + ); + } } diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component.html index 368d4b8343..90dd00e1d5 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component.html @@ -1,40 +1,45 @@ - + - Submission Processing + Submission Processing -

    - Your submission is being processed and will be avaliable to view soon. You will also be able to download your most - recently submitted files. +

    + Your submission is being processed and will be avaliable to view soon. You will also be able + to download your most recently submitted files.

    -

    - You can choose to download your previous +

    + You can choose to download your previous submission or the files you uploaded below.

    - @if (submission?.isUploaded && task.submissionDate) { + @if (task?.hasPdf && task.submissionDate) {

    You uploaded this submission {{ task.submissionDate | date: 'dd/MM/yyyy' }}.

    }

    - If you feel there has been an error in your submission, you can request to regenerate your submission under the - "Actions" dropdown menu. + If you feel there has been an error in your submission, you can request to regenerate your + submission under the "Actions" dropdown menu.

    -

    - If you would like to submit alternate evidence for use in your portfolio, you can upload alternate files under the - "Actions" dropdown menu. +

    + If you would like to submit alternate evidence for use in your portfolio, you can upload + alternate files under the "Actions" dropdown menu.

    - - @@ -43,17 +48,26 @@ - - - + -
    \ No newline at end of file +
    diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component.spec.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component.spec.ts index a07544b4ed..21b537a0ca 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component.spec.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component.spec.ts @@ -1,6 +1,12 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TaskService} from 'src/app/api/services/task.service'; +import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {TaskSubmissionCardComponent} from './task-submission-card.component'; -import { TaskSubmissionCardComponent } from './task-submission-card.component'; +const emptyProvider = {}; describe('TaskSubmissionCardComponent', () => { let component: TaskSubmissionCardComponent; @@ -8,13 +14,21 @@ describe('TaskSubmissionCardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ TaskSubmissionCardComponent ] + declarations: [TaskSubmissionCardComponent], + providers: [ + {provide: TaskService, useValue: emptyProvider}, + {provide: AlertService, useValue: emptyProvider}, + {provide: FileDownloaderService, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], }) - .compileComponents(); + .overrideComponent(TaskSubmissionCardComponent, {set: {template: ''}}) + .compileComponents(); + }); + beforeEach(() => { fixture = TestBed.createComponent(TaskSubmissionCardComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component.ts index 8c10c71c40..1af683d9ad 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component.ts @@ -1,27 +1,44 @@ -import { Component, Inject, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; -import { uploadSubmissionModal } from 'src/app/ajs-upgraded-providers'; -import { Task } from 'src/app/api/models/task'; -import { TaskService } from 'src/app/api/services/task.service'; -import { FileDownloaderService } from 'src/app/common/file-downloader/file-downloader.service'; -import { AlertService } from 'src/app/common/services/alert.service'; +import { + ChangeDetectionStrategy, + Component, + Input, + OnChanges, + OnInit, + SimpleChanges, +} from '@angular/core'; +import {Task} from 'src/app/api/models/task'; +import {TaskService} from 'src/app/api/services/task.service'; +import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; +import {AlertService} from 'src/app/common/services/alert.service'; @Component({ selector: 'f-task-submission-card', templateUrl: './task-submission-card.component.html', styleUrls: ['./task-submission-card.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TaskSubmissionCardComponent implements OnChanges, OnInit { @Input() task: Task; - canReuploadEvidence: boolean; - canRegeneratePdf: boolean; - submission: { isProcessing: boolean; isUploaded: boolean } = { isProcessing: false, isUploaded: false }; - urls: { pdf: string; files: string }; + + public get canRegeneratePdf(): boolean { + return ( + this.taskService.pdfRegeneratableStatuses.includes(this.task?.status) && this.task?.hasPdf + ); + } + + public get taskPdfUrl(): string { + return this.task?.submissionUrl(true); + } + + public get taskFilesUrl(): string { + return this.task?.submittedFilesUrl(); + } constructor( private taskService: TaskService, - @Inject(uploadSubmissionModal) private UploadSubmissionModal, private alerts: AlertService, - private fileDownloader: FileDownloaderService + private fileDownloader: FileDownloaderService, ) {} ngOnInit(): void { @@ -37,18 +54,7 @@ export class TaskSubmissionCardComponent implements OnChanges, OnInit { } reapplySubmissionData(): void { - this.task.getSubmissionDetails().subscribe(() => { - this.canReuploadEvidence = this.task.inSubmittedState(); - this.canRegeneratePdf = this.taskService.pdfRegeneratableStatuses.includes(this.task.status) && this.task.hasPdf; - this.submission = { - isProcessing: this.task.processingPdf, - isUploaded: this.task.hasPdf, - }; - this.urls = { - pdf: this.task.submissionUrl(true), - files: this.task.submittedFilesUrl(), - }; - }); + this.task.getSubmissionDetails().subscribe(); } uploadAlternateFiles(): void { @@ -57,28 +63,28 @@ export class TaskSubmissionCardComponent implements OnChanges, OnInit { regeneratePdf(): void { this.task.recreateSubmissionPdf().subscribe({ - next: (response: any) => { + next: (response: {result: string}) => { if (response.result === 'false') { this.alerts.error('There was an error regenerating the PDF', 6000); } else { this.task.processingPdf = true; this.alerts.success( 'The PDF is being regenerated. Please refresh the page in a few minutes.', - 6000 + 6000, ); } }, - error: (response: any) => { + error: (_response: Error) => { this.alerts.error('Request failed, cannot recreate PDF at this time.', 6000); }, }); } downloadSubmission(): void { - this.fileDownloader.downloadFile(this.urls.pdf, `${this.task.definition.abbreviation}.pdf`); + this.fileDownloader.downloadFile(this.taskPdfUrl, `${this.task.definition.abbreviation}.pdf`); } downloadSubmissionFiles(): void { - this.fileDownloader.downloadFile(this.urls.files, `${this.task.definition.abbreviation}.zip`); + this.fileDownloader.downloadFile(this.taskFilesUrl, `${this.task.definition.abbreviation}.zip`); } -} \ No newline at end of file +} diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.tpl.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.tpl.html index 44c36541dc..ec25e0f0d2 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.tpl.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.tpl.html @@ -4,11 +4,12 @@

    Submission Processing

    -
    -
    +
    + +

    - Your submission is being processed and will be avaliable to view soon. You will also - be able to download your most recently submitted files. + Your submission is being processed and will be avaliable to view soon. You will also be able + to download your most recently submitted files.

    You can choose to download your previous @@ -18,15 +19,19 @@

    You uploaded this submission {{task.submissionDate | date: 'dd/MM/yyyy'}}.

    - If you feel there has been an error in your submission, you can request to - regenerate your submission under the "Actions" dropdown menu. + If you feel there has been an error in your submission, you can request to regenerate your + submission under the "Actions" dropdown menu.

    - If you would like to submit alternate evidence for use in your portfolio, you can - upload alternate files under the "Actions" dropdown menu. + If you would like to submit alternate evidence for use in your portfolio, you can upload + alternate files under the "Actions" dropdown menu.

    -

    - + + +
    -
    -
    +
    + +
    + +
    + diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/tutor-notes-view/tutor-notes-view.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/directives/tutor-notes-view/tutor-notes-view.component.html index 71ea152187..7cfe442732 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/tutor-notes-view/tutor-notes-view.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/tutor-notes-view/tutor-notes-view.component.html @@ -1,7 +1,7 @@ -
    +
    - comment -

    Tutor Notes for {{ unitRole?.user?.name }}

    + comment +

    Moderation Notes for {{ unitRole?.user?.name }}

    @if (task) {

    diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/directives/tutor-notes-view/tutor-notes-view.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/directives/tutor-notes-view/tutor-notes-view.component.ts index 33ba1dd5a4..6b44ee689b 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/directives/tutor-notes-view/tutor-notes-view.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/directives/tutor-notes-view/tutor-notes-view.component.ts @@ -1,18 +1,27 @@ -import {Component, Input, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges} from '@angular/core'; import {UnitRole} from 'src/app/api/models/unit-role'; @Component({ selector: 'f-tutor-notes-view', templateUrl: './tutor-notes-view.component.html', styleUrls: ['./tutor-notes-view.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class TutorNotesViewComponent implements OnInit { +export class TutorNotesViewComponent implements OnChanges { @Input() task?; @Input() unitRole: UnitRole; - ngOnInit(): void { - if (this.task && !this.unitRole) { + private inferredUnitRole = false; + + ngOnChanges(changes: SimpleChanges): void { + if (changes.unitRole?.currentValue) { + this.inferredUnitRole = false; + } + + if (this.task && (!this.unitRole || this.inferredUnitRole)) { this.unitRole = this.task.tutor; + this.inferredUnitRole = true; } } } diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.coffee b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.coffee deleted file mode 100644 index a5d9df4511..0000000000 --- a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.coffee +++ /dev/null @@ -1,94 +0,0 @@ -angular.module('doubtfire.projects.states.dashboard.directives.task-dashboard', []) -# -# Dashboard of task-related info -# -.directive('taskDashboard', -> - restrict: 'E' - templateUrl: 'projects/states/dashboard/directives/task-dashboard/task-dashboard.tpl.html' - scope: - task: '=' - showFooter: '@?' - showSubmission: '@?' - controller: ($scope, $stateParams, listenerService, newTaskService, DoubtfireConstants, TaskAssessmentModal, fileDownloaderService) -> - # $scope.overseerEnabled = DoubtfireConstants.IsOverseerEnabled - - $scope.overseerEnabled = () -> - DoubtfireConstants.IsOverseerEnabled.value && $scope.task?.overseerEnabled - - $scope.urls = { - taskSheetPdfUrl: null - taskSubmissionPdfUrl: null - taskSubmissionPdfAttachmentUrl: null - taskFilesUrl: null - } - - # Is the current user a tutor? - $scope.tutor = $stateParams.tutor - # the ways in which the dashboard can be viewed - $scope.dashboardViews = ["details", "submission", "task", "similarities", "overseer"] - - # set the current dashboard view to details by default - updateCurrentView = -> - if $scope.showSubmission - $scope.currentView = $scope.dashboardViews[1] - else - $scope.currentView = $scope.dashboardViews[0] - - updateCurrentView() - - # Cleanup - listeners = listenerService.listenTo($scope) - # Required changes when task changes - listeners.push $scope.$watch('task.definition.id', -> - return unless $scope.task? - task = $scope.task - # get the url for the task sheet and the submissions - $scope.urls.taskSheetPdfUrl = task.definition.getTaskPDFUrl() - $scope.urls.taskSubmissionPdfUrl = task.submissionUrl() - $scope.urls.taskSubmissionPdfAttachmentUrl = task.submissionUrl(true) - $scope.urls.taskFilesUrl = task.submittedFilesUrl() - - if $scope.isCurrentView('task') && !task.definition.hasTaskSheet - # If the task sheet is not available, switch to details view - updateCurrentView() - else if $scope.isCurrentView('submission') && !task.hasPdf - # If the submission is not available, switch to details view - updateCurrentView() - ) - - # Set the selected dashboard view - $scope.setSelectedDashboardView = (view) -> - if view in $scope.dashboardViews - $scope.currentView = view - # Is the current view? - $scope.isCurrentView = (view) -> - return $scope.currentView == view - - $scope.showSubmissionHistoryModal = -> - TaskAssessmentModal.show($scope.task) - - # Now also load in the assessment details - if $scope.showFooter - $scope.taskStatusData = - keys: _.sortBy(newTaskService.markedStatuses, (s) -> newTaskService.statusSeq.get(s)) - help: newTaskService.helpDescriptions - icons: newTaskService.statusIcons - labels: newTaskService.statusLabels - class: newTaskService.statusClass - - # Triggers a new update to the task status - $scope.triggerTransition = (status) -> - $scope.task.updateTaskStatus(status) - - $scope.downloadSubmission = () -> - fileDownloaderService.downloadFile($scope.urls.taskSubmissionPdfAttachmentUrl) - - $scope.downloadSubmittedFiles = () -> - fileDownloaderService.downloadFile($scope.urls.taskFilesUrl) - - $scope.switchView = (view) -> - if view in $scope.dashboardViews - $scope.currentView = view - - -) diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.html b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.html index 931c569b6e..598ae8b161 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.html @@ -1,50 +1,190 @@ -

    - - - - +
    +
    + + + + + + + Your Submission + @if (task.processingPdf) { + + } + + + + @if (canAccessStaffViews) { + + + + + Similarities + @if (task.similaritiesDetected) { + warning + } + + + + + + + Student Notes + @if (task.project.staffNoteCount) { + + {{ task.project.staffNoteCount }} + + } + + + + @if (canAccessTutorNotes) { + + } + } + - - - + - - - + + + + +
    - - - - - - - - - - - - - - - - +
    + @switch (currentView) { + @case (DashboardViews.details) { +
    + + + + + + + + +
    + } + + @case (DashboardViews.task) { + @if (task && task.blockedByPrerequisiteTasks()) { +
    + warning + Warning: This task has + {{ task.definition.taskPrerequisitesCache.currentValues.length }} prerequisite{{ + task.definition.taskPrerequisitesCache.currentValues.length > 1 ? 's' : '' + }} + that you still need to complete. You won’t be able to submit this task until all + prerequisites are met. +
    + } + @if (task.definition.hasTaskSheet) { + + } @else { +
    + subtitles_off +
    + } + } + @case (DashboardViews.submission) { + @if (task.hasPdf) { + + } @else { +
    + subtitles_off +
    + } + } + @case (DashboardViews.similarity) { + @if (canAccessStaffViews) { +
    + +
    + } + } + @case (DashboardViews.submission_history) { + @if (canAccessStaffViews) { +
    + +
    + } + } + @case (DashboardViews.staff_notes) { + @if (canAccessStaffViews) { +
    + +
    + } + } + @case (DashboardViews.tutor_notes) { +
    + @if (canAccessTutorNotes) { + + } +
    + } + @case (DashboardViews.discussion_prompts) { + @if (canAccessStaffViews) { + + } + } + } +
    - - - + -
    - subtitles_off +
    + subtitles_off
    diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.scss b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.scss index d8fe31e746..2b2f7b6954 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.scss +++ b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.scss @@ -1,4 +1,4 @@ -mat-icon { +.empty-state-icon { height: 120px; width: 120px; font-size: 120px; @@ -9,3 +9,24 @@ mat-icon { font-size: 2.5rem; color: #c5c5c5; } + +:host ::ng-deep .task-dashboard-tabs { + .mat-mdc-tab-header { + justify-content: center; + max-width: 100%; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: thin; + } + + .mat-mdc-tab-label-container { + flex: 0 0 auto; + margin: 0 auto; + overflow: visible; + } + + .mat-mdc-tab-list, + .mat-mdc-tab-labels { + width: max-content; + } +} diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.spec.ts b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.spec.ts index abd4a08b1f..195716d110 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.spec.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.spec.ts @@ -1,6 +1,14 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {ActivatedRoute} from '@angular/router'; +import {TaskService} from 'src/app/api/services/task.service'; +import {UserService} from 'src/app/api/services/user.service'; +import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; +import {SelectedTaskService} from '../../selected-task.service'; +import {TaskDashboardComponent} from './task-dashboard.component'; -import { TaskDashboardComponent } from './task-dashboard.component'; +const emptyProvider = {}; describe('TaskDashboardComponent', () => { let component: TaskDashboardComponent; @@ -8,13 +16,23 @@ describe('TaskDashboardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ TaskDashboardComponent ] + declarations: [TaskDashboardComponent], + providers: [ + {provide: TaskService, useValue: emptyProvider}, + {provide: FileDownloaderService, useValue: emptyProvider}, + {provide: ActivatedRoute, useValue: emptyProvider}, + {provide: UserService, useValue: emptyProvider}, + {provide: SelectedTaskService, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], }) - .compileComponents(); + .overrideComponent(TaskDashboardComponent, {set: {template: ''}}) + .compileComponents(); + }); + beforeEach(() => { fixture = TestBed.createComponent(TaskDashboardComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.ts b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.ts index 8446b9b113..4037ceb2fa 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.ts +++ b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.component.ts @@ -1,10 +1,18 @@ -import {Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core'; -import {UIRouter} from '@uirouter/core'; +import { + ChangeDetectionStrategy, + Component, + Input, + OnChanges, + OnInit, + SimpleChanges, +} from '@angular/core'; +import {MatTabChangeEvent} from '@angular/material/tabs'; +import {ActivatedRoute} from '@angular/router'; +import {UnitRole} from 'src/app/api/models/doubtfire-model'; import {Task} from 'src/app/api/models/task'; import {TaskService} from 'src/app/api/services/task.service'; +import {UserService} from 'src/app/api/services/user.service'; import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; -import {TaskAssessmentModalService} from 'src/app/common/modals/task-assessment-modal/task-assessment-modal.service'; -import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; import {SelectedTaskService} from '../../selected-task.service'; import {DashboardViews} from '../../selected-task.service'; @@ -12,37 +20,62 @@ import {DashboardViews} from '../../selected-task.service'; selector: 'f-task-dashboard', templateUrl: './task-dashboard.component.html', styleUrls: ['./task-dashboard.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TaskDashboardComponent implements OnInit, OnChanges { @Input() task: Task; @Input() pdfUrl: string; - public DashboardViews = DashboardViews; - public taskStatusData: any; - public tutor = this.router.globals.params.tutor; + public taskStatusData: { + keys: TaskService['markedStatuses']; + help: TaskService['helpDescriptions']; + icons: TaskService['statusIcons']; + labels: TaskService['statusLabels']; + class: TaskService['statusClass']; + }; + public tutor = false; public urls: { taskSubmissionPdfAttachmentUrl: string; taskFilesUrl: string; taskSheetPdfUrl?: string; taskSubmissionPdfUrl?: string; }; - public overseerEnabledObs = this.doubtfire.IsOverseerEnabled; public currentView: DashboardViews; + public currentIndex = 0; + + private readonly tabViews: DashboardViews[] = [ + DashboardViews.details, + DashboardViews.task, + DashboardViews.submission, + DashboardViews.submission_history, + DashboardViews.similarity, + DashboardViews.staff_notes, + DashboardViews.tutor_notes, + ]; + + onTabChange(event: MatTabChangeEvent) { + const view = this.tabViews[event.index]; + if (view !== undefined) { + this.setSelectedDashboardView(view); + } + } constructor( - private doubtfire: DoubtfireConstants, private taskService: TaskService, - private taskAssessmentModal: TaskAssessmentModalService, private fileDownloader: FileDownloaderService, - private router: UIRouter, + private route: ActivatedRoute, + private userService: UserService, public selectedTaskService: SelectedTaskService, ) {} ngOnInit(): void { - this.selectedTaskService.currentView$.next(DashboardViews.submission); + this.tutor = this.currentUnitRole !== undefined; + this.setSelectedDashboardView(DashboardViews.details); this.selectedTaskService.currentView$.subscribe((view) => { - this.currentView = view; + this.currentView = this.canAccessDashboardView(view) ? view : DashboardViews.details; + this.currentIndex = this.tabIndexForView(this.currentView); }); this.taskStatusData = { @@ -64,15 +97,66 @@ export class TaskDashboardComponent implements OnInit, OnChanges { taskSubmissionPdfAttachmentUrl: changes.task.currentValue.submissionUrl(true), taskFilesUrl: changes.task.currentValue.submittedFilesUrl(), }; + this.setSelectedDashboardView(DashboardViews.details); + } + } + + setSelectedDashboardView(view: DashboardViews): void { + const nextView = this.canAccessDashboardView(view) ? view : DashboardViews.details; + this.selectedTaskService.currentView$.next(nextView); + this.currentView = nextView; + this.currentIndex = this.tabIndexForView(nextView); + } + + private tabIndexForView(view: DashboardViews): number { + const index = this.tabViews.indexOf(view); + return index >= 0 ? index : 0; + } + + private canAccessDashboardView(view: DashboardViews): boolean { + switch (view) { + case DashboardViews.similarity: + case DashboardViews.submission_history: + case DashboardViews.staff_notes: + case DashboardViews.discussion_prompts: + return this.canAccessStaffViews; + case DashboardViews.tutor_notes: + return this.canAccessTutorNotes; + default: + return true; } } - public get overseerEnabled() { - return this.doubtfire.IsOverseerEnabled.value && this.task?.overseerEnabled; + public get canAccessStaffViews(): boolean { + return this.tutor || !!this.currentUnitRole; + } + + public get currentUnitRole(): UnitRole | undefined { + const currentUser = this.userService.currentUser; + if (!currentUser) { + return undefined; + } + + return this.task?.unit?.staff?.find((ur) => ur.user?.id === currentUser.id); } - showSubmissionHistoryModal() { - this.taskAssessmentModal.show(this.task); + public get canAccessTutorNotes(): boolean { + const tutor = this.task?.tutor; + if (!tutor) { + return false; + } + + if (!this.currentUnitRole) { + return false; + } + + tutor.unit = this.task.unit; + + return ( + this.currentUnitRole.role === 'Convenor' || + this.currentUnitRole.role === 'Admin' || + (tutor.mentor && tutor.mentor.id === this.currentUnitRole.id) + ); } downloadSubmission() { diff --git a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.tpl.html b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.tpl.html index 45893297a0..b7adf8c446 100644 --- a/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.tpl.html +++ b/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.tpl.html @@ -79,9 +79,9 @@
    - + Warning: This task has {{task.definition.taskPrerequisitesCache.currentValues.length}} prerequisite{{ task.definition.taskPrerequisitesCache.currentValues.length > 1 ? 's' : '' }} that you still need to complete. You won’t be able to submit this task until all prerequisites diff --git a/src/app/projects/states/dashboard/project-dashboard/project-dashboard.component.html b/src/app/projects/states/dashboard/project-dashboard/project-dashboard.component.html new file mode 100644 index 0000000000..71f90d18ee --- /dev/null +++ b/src/app/projects/states/dashboard/project-dashboard/project-dashboard.component.html @@ -0,0 +1,203 @@ + +
    +
    +
    + + +
    +
    + +
    + @for (row of skeletonRows; track row) { +
    +
    + + +
    + +
    + } +
    +
    +
    + +@if (project$ | async; as project) { +
    +
    + @if (subs$ | async) { + @if (isProjectTaskListReady(project)) { + + } @else { +
    + +
    + } +
    +
    +
    + } + @if (selectedTaskDefinition$ | async; as selectedTaskDefinition) { +
    + +
    + @if (isCommentsNarrow && commentsCollapsed) { + + } +
    + @if (isCommentsNarrow && !commentsCollapsed) { + + } + + +
    + } @else if (!isProjectTaskListReady(project)) { +
    +
    + + + +
    +
    + } @else { +
    + +
    + } +
    +
    +} + + + + diff --git a/src/app/projects/states/dashboard/project-dashboard/project-dashboard.component.scss b/src/app/projects/states/dashboard/project-dashboard/project-dashboard.component.scss new file mode 100644 index 0000000000..133f8df83f --- /dev/null +++ b/src/app/projects/states/dashboard/project-dashboard/project-dashboard.component.scss @@ -0,0 +1,73 @@ +:host { + display: block; +} + +.comments-floating-toggle { + position: fixed; + top: 50%; + right: -20px; + z-index: 300; + opacity: 0.55; + transition: + opacity 150ms ease, + transform 150ms ease; + transform: translateY(-50%); +} + +.comments-floating-toggle mat-icon { + position: absolute; + inset: 0; + margin: auto; + font-size: 20px; + height: 20px; + width: 20px; + transition: opacity 150ms ease; +} + +.comments-floating-toggle:hover, +.comments-floating-toggle:focus-visible { + opacity: 1; + transform: translate(-20px, -50%); +} + +.comments-floating-toggle__edge-icon { + opacity: 1; + transform: translateX(-6px); +} + +.comments-floating-toggle__hover-icon { + opacity: 0; +} + +.comments-floating-toggle:hover .comments-floating-toggle__edge-icon, +.comments-floating-toggle:focus-visible .comments-floating-toggle__edge-icon { + opacity: 0; +} + +.comments-floating-toggle:hover .comments-floating-toggle__hover-icon, +.comments-floating-toggle:focus-visible .comments-floating-toggle__hover-icon { + opacity: 1; +} + +.comments-sidebar { + position: relative; +} + +.comments-sidebar--narrow { + flex-basis: 375px !important; + min-width: 375px; + width: 375px !important; + border-radius: 0 !important; + box-shadow: -2px 0 6px rgba(15, 23, 42, 0.08); + z-index: 250; +} + +.comments-sidebar__collapse { + position: absolute; + top: 50%; + left: -20px; + z-index: 260; + transform: translateY(-50%); + background: white; + box-shadow: 0 2px 8px rgba(15, 23, 42, 0.18); +} diff --git a/src/app/projects/states/dashboard/project-dashboard/project-dashboard.component.ts b/src/app/projects/states/dashboard/project-dashboard/project-dashboard.component.ts new file mode 100644 index 0000000000..2a1fd23b73 --- /dev/null +++ b/src/app/projects/states/dashboard/project-dashboard/project-dashboard.component.ts @@ -0,0 +1,220 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import {CdkDragEnd, CdkDragMove, CdkDragStart} from '@angular/cdk/drag-drop'; +import {BreakpointObserver} from '@angular/cdk/layout'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {BehaviorSubject, Observable, Subject, first, of, takeUntil} from 'rxjs'; +import {Project, TaskDefinition} from 'src/app/api/models/doubtfire-model'; +import {ProjectService} from 'src/app/api/services/project.service'; +import {UnitService} from 'src/app/api/services/unit.service'; +import {UserService} from 'src/app/api/services/user.service'; +import {GlobalStateService, ViewType} from '../../index/global-state.service'; + +@Component({ + selector: 'f-project-dashboard', + templateUrl: './project-dashboard.component.html', + styleUrl: './project-dashboard.component.scss', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class ProjectDashboardComponent implements OnInit, OnDestroy { + @Input() public project$: Observable; + @Input() public defaultTaskListCollapsed = false; + @Input() public taskSelectionUrlBase: unknown[] | null = null; + @Input() public showSubmittedGrade?: boolean = false; + @Input() public set taskListWidth(width: number | undefined) { + if (typeof width === 'number') { + this._leftWidth = width; + } + } + + @Output() public taskListWidthChange: EventEmitter = new EventEmitter(); + + /** + * The currently selected task definition - selected in the unit task list. + * This is crated here, and passed to children to interact with and share across context. + */ + public selectedTaskDefinition$: BehaviorSubject = + new BehaviorSubject(null); + + subs$: Observable = of(true); + readonly skeletonRows = Array.from({length: 10}, (_, index) => index); + private readonly projectSubject: BehaviorSubject = new BehaviorSubject(null); + + private readonly destroy$: Subject = new Subject(); + private projectReady = false; + + projectTasks = []; + + constructor( + private currentUser: UserService, + private projectService: ProjectService, + private unitService: UnitService, + private globalStateService: GlobalStateService, + private route: ActivatedRoute, + private breakpointObserver: BreakpointObserver, + ) {} + + public readonly taskListCollapsedWidth = 75; + public readonly taskListExpandedWidth = 400; + public readonly taskListCollapseThreshold = 125; + private _leftWidth = this.taskListExpandedWidth; + public lastX; + public startWidth = 0; + + public startLeftX = 0; + public isCommentsNarrow = false; + public commentsCollapsed = false; + + private readonly commentsBreakpoint = '(max-width: 999.98px)'; + + public get commentsPanelCollapsed(): boolean { + return this.isCommentsNarrow && this.commentsCollapsed; + } + + public get taskListCollapsed(): boolean { + return this.leftWidth < this.taskListCollapseThreshold; + } + + public get leftWidth(): number { + return this._leftWidth; + } + + public set leftWidth(width: number) { + this._leftWidth = width; + this.taskListWidthChange.emit(width); + } + + public isProjectTaskListReady(project: Project): boolean { + return ( + this.projectReady && + !!project?.id && + !!project.unit?.id && + project.targetGrade !== undefined && + project.targetGrade !== null + ); + } + + public taskDefinitionsForProject(project: Project): readonly TaskDefinition[] { + if (!this.isProjectTaskListReady(project)) { + return []; + } + + return project.unit.taskDefinitions; + } + + startedDragging(event: CdkDragStart, boundary: HTMLElement) { + document.body.classList.add('split-pane-resizing'); + event.source.element.nativeElement.classList.add('hovering'); + const rect = boundary.getBoundingClientRect(); + // x relative to the container + this.startLeftX = (event.event as MouseEvent).clientX - rect.left; + this.startWidth = this.leftWidth; + } + + dragging(event: CdkDragMove, boundary: HTMLElement) { + const rect = boundary.getBoundingClientRect(); + const x = (event.event as MouseEvent).clientX - rect.left; + + const delta = x - this.startLeftX; + const newWidth = this.startWidth + delta; + + this.leftWidth = Math.max(this.taskListCollapsedWidth, Math.min(500, newWidth)); + + // keep the handle visually glued to the divider + event.source.reset(); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + stoppedDragging(event: CdkDragEnd, _div: HTMLDivElement) { + document.body.classList.remove('split-pane-resizing'); + event.source.element.nativeElement.classList.remove('hovering'); + } + + ngOnInit(): void { + this.breakpointObserver + .observe(this.commentsBreakpoint) + .pipe(takeUntil(this.destroy$)) + .subscribe(({matches}) => { + this.isCommentsNarrow = matches; + this.commentsCollapsed = matches; + window.dispatchEvent(new Event('resize')); + }); + + if (this.defaultTaskListCollapsed) { + this.leftWidth = this.taskListCollapsedWidth; + } + + const initialProject$ = + this.project$ ?? of(this.route.parent?.snapshot.data.project as Project); + this.project$ = this.projectSubject.asObservable(); + initialProject$.pipe(first()).subscribe((project) => { + this.projectSubject.next(project); + this.loadProject( + project?.id ?? Number(this.route.parent?.snapshot.paramMap.get('projectId')), + ); + }); + + window.dispatchEvent(new Event('resize')); + } + + ngOnDestroy(): void { + document.body.classList.remove('split-pane-resizing'); + this.destroy$.next(); + this.destroy$.complete(); + } + + public toggleCommentsPanel(): void { + this.commentsCollapsed = !this.commentsCollapsed; + window.dispatchEvent(new Event('resize')); + } + + private loadProject(projectId: number): void { + if (!projectId) { + return; + } + + this.projectService + .get( + {id: projectId}, + { + cacheBehaviourOnGet: 'cacheQuery', + mappingCompleteCallback: (project: Project) => this.loadUnit(project), + }, + ) + .subscribe(); + } + + private loadUnit(project: Project): void { + const unitId = project.unit?.id; + if (!unitId) { + this.showLoadedProject(project); + return; + } + + this.unitService.get(unitId).subscribe({ + next: (unit) => { + project.unit = unit; + unit.studentCache.add(project); + this.showLoadedProject(project); + }, + error: () => { + this.showLoadedProject(project); + }, + }); + } + + private showLoadedProject(project: Project): void { + this.projectReady = true; + this.globalStateService.setView(ViewType.PROJECT, project); + this.projectSubject.next(project); + } +} diff --git a/src/app/projects/states/dashboard/selected-task.service.spec.ts b/src/app/projects/states/dashboard/selected-task.service.spec.ts index 7883cf970a..cecaef3b82 100644 --- a/src/app/projects/states/dashboard/selected-task.service.spec.ts +++ b/src/app/projects/states/dashboard/selected-task.service.spec.ts @@ -1,12 +1,22 @@ -import { TestBed } from '@angular/core/testing'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {TestBed} from '@angular/core/testing'; +import {TaskService} from 'src/app/api/services/task.service'; +import {GlobalStateService} from '../index/global-state.service'; +import {SelectedTaskService} from './selected-task.service'; -import { SelectedTaskService } from './selected-task.service'; +const emptyProvider = {}; describe('SelectedTaskService', () => { let service: SelectedTaskService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + providers: [ + SelectedTaskService, + {provide: TaskService, useValue: emptyProvider}, + {provide: GlobalStateService, useValue: emptyProvider}, + ], + }); service = TestBed.inject(SelectedTaskService); }); diff --git a/src/app/projects/states/dashboard/selected-task.service.ts b/src/app/projects/states/dashboard/selected-task.service.ts index 94730262da..cfcc9efd81 100644 --- a/src/app/projects/states/dashboard/selected-task.service.ts +++ b/src/app/projects/states/dashboard/selected-task.service.ts @@ -5,13 +5,14 @@ import {TaskService} from 'src/app/api/services/task.service'; import {GlobalStateService} from '../index/global-state.service'; export enum DashboardViews { + details, submission, task, similarity, staff_notes, tutor_notes, discussion_prompts, - overseer, + submission_history, } @Injectable({ @@ -23,10 +24,12 @@ export class SelectedTaskService { private globalState: GlobalStateService, ) {} - private task$ = new BehaviorSubject(null); - public currentPdfUrl$ = new BehaviorSubject(null); + private task$: BehaviorSubject = new BehaviorSubject(null); + public currentPdfUrl$: BehaviorSubject = new BehaviorSubject(null); - public currentView$ = new BehaviorSubject(DashboardViews.submission); + public currentView$: BehaviorSubject = new BehaviorSubject( + DashboardViews.submission, + ); public get hasTaskSheet(): boolean { return this.task$.value?.definition?.hasTaskSheet; @@ -50,6 +53,13 @@ export class SelectedTaskService { } else { this.task$.next(task); + if (!task) { + this.currentPdfUrl$.next(null); + this.currentView$.next(DashboardViews.submission); + this.checkFooterHeight(); + return; + } + task?.getSubmissionDetails().subscribe(); } this.checkFooterHeight(); @@ -74,7 +84,11 @@ export class SelectedTaskService { } public showOverseerReports() { - this.currentView$.next(DashboardViews.overseer); + this.currentView$.next(DashboardViews.submission_history); + } + + public showSubmissionHistory() { + this.currentView$.next(DashboardViews.submission_history); } public showDiscussionPrompts() { @@ -82,7 +96,9 @@ export class SelectedTaskService { } public showSubmission() { - if (!this.task$.value) return; + if (!this.task$.value) { + return; + } this.currentPdfUrl$.next(this.task$.value.submissionUrl(false)); this.currentView$.next(DashboardViews.submission); } diff --git a/src/app/projects/states/discussion-prompts/discussion-prompts.component.html b/src/app/projects/states/discussion-prompts/discussion-prompts.component.html index 5dcbbf84ff..e792873915 100644 --- a/src/app/projects/states/discussion-prompts/discussion-prompts.component.html +++ b/src/app/projects/states/discussion-prompts/discussion-prompts.component.html @@ -1,21 +1,23 @@ -
    +
    @for (prompt of discussionPrompts; track prompt) { - +
    {{ prompt.taskDefinition.abbreviation }}
    {{ prompt.content }}
    -
    +
    {{ prompt.priorityLabel }}
    + } @empty { +
    No discussion prompts for this task
    }
    diff --git a/src/app/projects/states/discussion-prompts/discussion-prompts.component.ts b/src/app/projects/states/discussion-prompts/discussion-prompts.component.ts index 2e384498af..cc81e95a94 100644 --- a/src/app/projects/states/discussion-prompts/discussion-prompts.component.ts +++ b/src/app/projects/states/discussion-prompts/discussion-prompts.component.ts @@ -1,4 +1,11 @@ -import {Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + ElementRef, + Input, + OnInit, + ViewChild, +} from '@angular/core'; import {DiscussionPrompt} from 'src/app/api/models/discussion-prompt'; import {Project, TaskDefinition, UserService} from 'src/app/api/models/doubtfire-model'; import {StaffNote} from 'src/app/api/models/staff-note'; @@ -10,6 +17,8 @@ import {AlertService} from 'src/app/common/services/alert.service'; selector: 'f-discussion-prompts', templateUrl: './discussion-prompts.component.html', styleUrl: './discussion-prompts.component.scss', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class DiscussionPromptsComponent implements OnInit { @ViewChild('staffNotesContainer') staffNotesContainer!: ElementRef; diff --git a/src/app/projects/states/feedback/feedback.coffee b/src/app/projects/states/feedback/feedback.coffee deleted file mode 100644 index 43e92a7e88..0000000000 --- a/src/app/projects/states/feedback/feedback.coffee +++ /dev/null @@ -1,13 +0,0 @@ -angular.module('doubtfire.projects.states.feedback', []) - -# -# Skips directly to feedback view (is a child of projects#show) -# of a specific task id -# -# This may be completely gone... -# .config(($stateProvider) -> -# projectsFeedbackStateData = -# url: ":viewing/:showTaskId" -# parent: 'projects#show' -# $stateProvider.state "projects#feedback", projectsFeedbackStateData -# ) diff --git a/src/app/projects/states/groups/groups.coffee b/src/app/projects/states/groups/groups.coffee deleted file mode 100644 index a20d2c0d8c..0000000000 --- a/src/app/projects/states/groups/groups.coffee +++ /dev/null @@ -1,21 +0,0 @@ -angular.module('doubtfire.projects.states.groups', []) - -# -# Tasks state for projects -# -.config(($stateProvider) -> - $stateProvider.state 'projects/groups', { - parent: 'projects/index' - url: '/groups' - controller: 'ProjectsGroupsStateCtrl' - templateUrl: 'projects/states/groups/groups.tpl.html' - data: - task: "Groups List" - pageTitle: "_Home_" - roleWhitelist: ['Tutor', 'Convenor', 'Admin', 'Student', 'Auditor'] - } -) - -.controller("ProjectsGroupsStateCtrl", ($scope) -> - # TODO: (@alexcu) move directive inot state -) diff --git a/src/app/projects/states/groups/groups.tpl.html b/src/app/projects/states/groups/groups.tpl.html deleted file mode 100644 index e5086ab3c6..0000000000 --- a/src/app/projects/states/groups/groups.tpl.html +++ /dev/null @@ -1,16 +0,0 @@ -
    - - -
    -
    -
    - -

    No Group Work

    -
    -
    - There is no group work enabled for this unit. -
    -
    diff --git a/src/app/projects/states/groups/project-groups-state.component.html b/src/app/projects/states/groups/project-groups-state.component.html new file mode 100644 index 0000000000..e02214e8b7 --- /dev/null +++ b/src/app/projects/states/groups/project-groups-state.component.html @@ -0,0 +1,7 @@ +@if (project) { + +} diff --git a/src/app/projects/states/groups/project-groups-state.component.scss b/src/app/projects/states/groups/project-groups-state.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/groups/project-groups-state.component.ts b/src/app/projects/states/groups/project-groups-state.component.ts new file mode 100644 index 0000000000..5d46a7c7c4 --- /dev/null +++ b/src/app/projects/states/groups/project-groups-state.component.ts @@ -0,0 +1,43 @@ +import {ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {Observable, Subscription, of} from 'rxjs'; +import {GroupSet, Project} from 'src/app/api/models/doubtfire-model'; +import {GlobalStateService} from '../index/global-state.service'; + +@Component({ + selector: 'f-project-groups-state', + templateUrl: './project-groups-state.component.html', + styleUrls: ['./project-groups-state.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class ProjectGroupsStateComponent implements OnInit, OnDestroy { + @Input() public project$: Observable; + + public project: Project; + public selectedGroupSet: GroupSet; + + private projectSub?: Subscription; + + constructor( + private globalStateService: GlobalStateService, + private route: ActivatedRoute, + ) {} + + ngOnInit(): void { + this.project$ = this.project$ ?? of(this.route.parent?.snapshot.data.project as Project); + + this.projectSub = this.project$?.subscribe((project) => { + if (!project) { + return; + } + + this.project = project; + this.selectedGroupSet = this.selectedGroupSet ?? project.unit?.groupSets?.[0]; + }); + } + + ngOnDestroy(): void { + this.projectSub?.unsubscribe(); + } +} diff --git a/src/app/projects/states/groups/project-groups/project-groups.component.html b/src/app/projects/states/groups/project-groups/project-groups.component.html new file mode 100644 index 0000000000..7eca48477b --- /dev/null +++ b/src/app/projects/states/groups/project-groups/project-groups.component.html @@ -0,0 +1,12 @@ +
    + @if (unit.hasGroupwork()) { + + + } @else { +
    + groups +

    No Group Work

    +

    There is no group work enabled for this unit.

    +
    + } +
    diff --git a/src/app/projects/states/groups/project-groups/project-groups.component.scss b/src/app/projects/states/groups/project-groups/project-groups.component.scss new file mode 100644 index 0000000000..542e68a366 --- /dev/null +++ b/src/app/projects/states/groups/project-groups/project-groups.component.scss @@ -0,0 +1,5 @@ +.mat-icon { + font-size: 75px; + width: 75px; + height: 75px; +} diff --git a/src/app/projects/states/groups/project-groups/project-groups.component.ts b/src/app/projects/states/groups/project-groups/project-groups.component.ts new file mode 100644 index 0000000000..6e64ae5f43 --- /dev/null +++ b/src/app/projects/states/groups/project-groups/project-groups.component.ts @@ -0,0 +1,17 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {GroupSet, Project} from 'src/app/api/models/doubtfire-model'; +import {Unit} from 'src/app/api/models/unit'; + +// This component is only displayed to students (projects) +@Component({ + selector: 'f-project-groups', + templateUrl: './project-groups.component.html', + styleUrl: './project-groups.component.scss', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class ProjectGroupsComponent { + @Input() unit: Unit; + @Input() project: Project; + @Input() selectedGroupSet: GroupSet; +} diff --git a/src/app/projects/states/index/global-state.service.ts b/src/app/projects/states/index/global-state.service.ts index 1837e240ea..11a335245d 100644 --- a/src/app/projects/states/index/global-state.service.ts +++ b/src/app/projects/states/index/global-state.service.ts @@ -1,8 +1,8 @@ -import {Inject, Injectable, OnDestroy} from '@angular/core'; import {MediaObserver} from 'ng-flex-layout'; -import {UIRouter} from '@uirouter/angular'; import {EntityCache} from 'ngx-entity-service'; -import {BehaviorSubject, Observable, Subject, skip, take} from 'rxjs'; +import {Injectable, OnDestroy} from '@angular/core'; +import {Router} from '@angular/router'; +import {BehaviorSubject, Observable, Subject, find} from 'rxjs'; import { CampusService, LearningOutcomeService, @@ -16,8 +16,8 @@ import { UserService, } from 'src/app/api/models/doubtfire-model'; import {AuthenticationService} from 'src/app/api/services/authentication.service'; -import {AlertService} from 'src/app/common/services/alert.service'; import {FeedbackTemplateService} from 'src/app/api/services/feedback-template.service'; +import {AlertService} from 'src/app/common/services/alert.service'; /** * The different types of views that can be shown. Used by the header to determine details to show. @@ -71,6 +71,7 @@ export class GlobalStateService implements OnDestroy { public currentUserProjects: EntityCache; private _showFooter = false; + private _isInboxState = false; private _showFooterWarning = false; /** @@ -110,7 +111,7 @@ export class GlobalStateService implements OnDestroy { private teachingPeriodService: TeachingPeriodService, private learningOutcomeService: LearningOutcomeService, private feedbackTemplateService: FeedbackTemplateService, - @Inject(UIRouter) private router: UIRouter, + private router: Router, private alerts: AlertService, private mediaObserver: MediaObserver, ) { @@ -123,14 +124,19 @@ export class GlobalStateService implements OnDestroy { // Try to login using the refresh token this.authenticationService.attemptLoginUsingRefreshToken((result: boolean) => { if (result) { - this.loadGlobals(); + if ( + this.userService.currentUser.hasRunFirstTimeSetup === false && + window.location.pathname !== '/welcome' + ) { + this.router.navigateByUrl('/welcome'); + } } else { // Loading is finshed... this.isLoadingSubject.next(false); // and if we are not going to the sign in page, then redirect to it - if (this.router.globals.current.name !== 'sign_in') { - this.router.stateService.go('sign_in'); + if (window.location.pathname !== '/sign_in') { + this.router.navigateByUrl('/sign_in'); } } }); @@ -148,7 +154,9 @@ export class GlobalStateService implements OnDestroy { setTimeout(() => { const vh = window.innerHeight * 0.01; - if (!this.mediaObserver.isActive('gt-sm') || !this._showFooter) { + if (this._isInboxState) { + document.body.style.setProperty('--vh', `${vh}px`); + } else if (!this.mediaObserver.isActive('gt-sm') || !this._showFooter) { document.body.style.setProperty('--vh', `${vh - 0.2}px`); } else { if (this._showFooter && !this._showFooterWarning) { @@ -161,13 +169,14 @@ export class GlobalStateService implements OnDestroy { } public get isInboxState(): boolean { - return this._showFooter; + return this._isInboxState; } public setInboxState() { - this._showFooter = true; - // set background color to white + this._isInboxState = true; + // set background color to inbox grey document.body.style.setProperty('background-color', '#f5f5f5'); + this.resetHeight(); } public goHome() { @@ -176,9 +185,10 @@ export class GlobalStateService implements OnDestroy { } public setNotInboxState() { - this._showFooter = false; + this._isInboxState = false; // set background color to white document.body.style.setProperty('background-color', '#fff'); + this.resetHeight(); } public showFooter(): void { @@ -194,14 +204,18 @@ export class GlobalStateService implements OnDestroy { // called when we need to set the footer to be a bit taller // to account for the alert div public showFooterWarning(): void { - if (!this._showFooter) return; + if (!this._showFooter) { + return; + } this._showFooterWarning = true; this.resetHeight(); } // called when we need to set the footer to be normal sized public hideFooterWarning(): void { - if (!this._showFooter) return; + if (!this._showFooter) { + return; + } this._showFooterWarning = false; this.resetHeight(); } @@ -222,6 +236,7 @@ export class GlobalStateService implements OnDestroy { } public loadGlobals(): void { + let loaded = 0; // Indicate we are loading data... this.isLoadingSubject.next(true); @@ -230,7 +245,7 @@ export class GlobalStateService implements OnDestroy { // Loading campuses this.campusService.query().subscribe({ next: (_response) => { - subscriber.next(true); + subscriber.next(++loaded); }, error: (_response) => { this.alerts.error('Unable to access service. Failed loading campuses.', 6000); @@ -242,7 +257,7 @@ export class GlobalStateService implements OnDestroy { .query({}, {endpointFormat: LearningOutcomeService.globalEndpoint}) .subscribe({ next: (_response) => { - subscriber.next(true); + subscriber.next(null); }, error: (_response) => { this.alerts.error('Unable to access service. Failed loading GLOs.', 6000); @@ -253,7 +268,7 @@ export class GlobalStateService implements OnDestroy { .query({}, {endpointFormat: FeedbackTemplateService.globalEndpoint}) .subscribe({ next: (_response) => { - subscriber.next(true); + subscriber.next(null); }, error: (_response) => { this.alerts.error( @@ -267,7 +282,7 @@ export class GlobalStateService implements OnDestroy { // Loading teaching periods this.teachingPeriodService.query().subscribe({ next: (_response) => { - subscriber.next(true); + subscriber.next(++loaded); }, error: (_response) => { this.alerts.error('Unable to access service. Failed loading teaching periods.', 6000); @@ -276,7 +291,7 @@ export class GlobalStateService implements OnDestroy { }); // Watch for load of campuses and teaching periods, then trigger loading of unit roles and projects - loadingObserver.pipe(skip(1), take(1)).subscribe({ + loadingObserver.pipe(find((loaded) => loaded === 2)).subscribe({ next: () => { // trigger loading of units and projects - this will end the loading when complete this.loadUnitsAndProjects(); diff --git a/src/app/projects/states/index/index.coffee b/src/app/projects/states/index/index.coffee deleted file mode 100644 index 8a0f4dfde8..0000000000 --- a/src/app/projects/states/index/index.coffee +++ /dev/null @@ -1,51 +0,0 @@ -angular.module('doubtfire.projects.states.index', []) - -# -# Root state for projects -# -.config(($stateProvider) -> - $stateProvider.state 'projects/index', { - url: "/projects/:projectId" - abstract: true - views: - main: - controller: "ProjectsIndexStateCtrl" - templateUrl: "units/states/index/index.tpl.html" # We can re-use unit's index here - data: - pageTitle: "_Home_" - roleWhitelist: ['Student', 'Tutor', 'Convenor', 'Admin', 'Auditor'] - } -) - -.controller("ProjectsIndexStateCtrl", ($scope, $rootScope, $state, $stateParams, newProjectService, listenerService, GlobalStateService) -> - # Error - required projectId is missing! - projectId = +$stateParams.projectId - return $state.go('home') unless projectId - - GlobalStateService.onLoad () -> - # Load in project - newProjectService.get(projectId, { - # Ensure that we cache queries here... so that we get any projects we are in - # even when we are also teaching that unit - cacheBehaviourOnGet: 'cacheQuery', - mappingCompleteCallback: (project)-> - # Wait for the project mapping to complete - ensuring unit details are loaded - $scope.unit = project.unit - - }).subscribe( - { - next: (project) -> - # Broadcast change in project - $scope.project = project - $scope.unit = project.unit if project.unit.taskDefinitions.length > 0 && project.tasks.length == project.unit.taskDefinitions.length - - GlobalStateService.setView('PROJECT', $scope.project) - - # Go home if no project was found - return $state.go('home') unless project? - - error: (failure) -> - $state.go('home') - } - ) -) diff --git a/src/app/projects/states/jplag/jplag-report-viewer.component.html b/src/app/projects/states/jplag/jplag-report-viewer.component.html index c5c52c229e..1b5e99f233 100644 --- a/src/app/projects/states/jplag/jplag-report-viewer.component.html +++ b/src/app/projects/states/jplag/jplag-report-viewer.component.html @@ -1,9 +1,9 @@ diff --git a/src/app/projects/states/jplag/jplag-report-viewer.component.ts b/src/app/projects/states/jplag/jplag-report-viewer.component.ts index 4021eb6d22..043f456ac7 100644 --- a/src/app/projects/states/jplag/jplag-report-viewer.component.ts +++ b/src/app/projects/states/jplag/jplag-report-viewer.component.ts @@ -1,9 +1,11 @@ -import {Component, ElementRef, Input, ViewChild} from '@angular/core'; +import {ChangeDetectionStrategy, Component, ElementRef, Input, ViewChild} from '@angular/core'; import {AlertService} from 'src/app/common/services/alert.service'; @Component({ selector: 'f-jplag-report-viewer', templateUrl: './jplag-report-viewer.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class JplagReportViewerComponent { @ViewChild('jplagIframe', {static: true}) jplagIframe!: ElementRef; @@ -33,7 +35,9 @@ export class JplagReportViewerComponent { const wrapper = doc.querySelector( '.vue-recycle-scroller__item-wrapper', ) as HTMLElement | null; - if (!wrapper) return null; + if (!wrapper) { + return null; + } let cur = wrapper.parentElement as HTMLElement | null; while (cur) { @@ -53,7 +57,9 @@ export class JplagReportViewerComponent { let elapsed = 0; const interval = setInterval(() => { const doc = iframe.contentDocument; - if (!doc) return; + if (!doc) { + return; + } const el = findLink(doc); if (el) { @@ -65,7 +71,7 @@ export class JplagReportViewerComponent { getScroller(doc)?.scrollBy(0, 600); elapsed += 50; - if (elapsed >= 5000) { + if (elapsed >= 10000) { clearInterval(interval); this.alertService.error('Could not open JPlag comparison.', 6000); } diff --git a/src/app/projects/states/outcomes/outcomes.coffee b/src/app/projects/states/outcomes/outcomes.coffee deleted file mode 100644 index d92214d086..0000000000 --- a/src/app/projects/states/outcomes/outcomes.coffee +++ /dev/null @@ -1,54 +0,0 @@ -# Component not used - -angular.module('doubtfire.projects.states.outcomes', []) - -# -# ILO outcomes visualisations -# -.config(($stateProvider) -> - $stateProvider.state 'projects/outcomes', { - parent: 'projects/index' - url: '/outcomes' - controller: 'LearningOutcomesStateCtrl' - templateUrl: 'projects/states/outcomes/outcomes.tpl.html' - data: - task: "Learning Outcomes" - pageTitle: "_Home_" - roleWhitelist: ['Tutor', 'Convenor', 'Admin', 'Student', 'Auditor'] - } -) - -.controller("LearningOutcomesStateCtrl", ($scope, $rootScope, $timeout, alertService, outcomeService, newUnitService, Visualisation) -> - $scope.poaView = { - activeTab: 'list' - } - $scope.targets = outcomeService.calculateTargets($scope.unit, $scope.unit, $scope.unit.taskStatusFactor) - $scope.currentProgress = outcomeService.calculateProgress($scope.unit, $scope.project) - - $scope.refreshCharts = Visualisation.refreshAll - - refreshAlignmentData = -> - $scope.currentProgress.length = 0 - $scope.currentProgress = _.extend $scope.currentProgress, outcomeService.calculateProgress($scope.unit, $scope.project) - - $scope.selectTab = (tab) -> - if tab is 'progress' - if !$scope.classStats? - newUnitService.loadLearningProgressClassStats($scope.unit).subscribe({ - next: (response) -> $scope.classStats = response - error: (response) -> - alertService.error( response, 6000) - $scope.classStats = {} - }) - $scope.poaView.activeTab = tab - eventName = if tab is 'progress' then "View Learning Progress Tab" else "Reflect on Learning Tab" - $scope.refreshCharts() - - # Default tab - $scope.selectTab('progress') - - $scope.$on('UpdateAlignmentChart', -> - refreshAlignmentData() - $rootScope.$broadcast('ProgressUpdated') - ) -) diff --git a/src/app/projects/states/outcomes/outcomes.tpl.html b/src/app/projects/states/outcomes/outcomes.tpl.html deleted file mode 100644 index c247911d94..0000000000 --- a/src/app/projects/states/outcomes/outcomes.tpl.html +++ /dev/null @@ -1,13 +0,0 @@ -
    - -
    -
    -
    - -

    No Learning Outcomes

    -
    -
    - There are no learning outcomes for this unit. -
    -
    - diff --git a/src/app/projects/states/plan/project-plan.component.html b/src/app/projects/states/plan/project-plan.component.html index 553a823f91..50714cdf88 100644 --- a/src/app/projects/states/plan/project-plan.component.html +++ b/src/app/projects/states/plan/project-plan.component.html @@ -1,34 +1,47 @@ -
    -

    Task Planner

    -

    - @if (unit.allowFlexibleDates) { - View and adjust the due dates for your tasks. Remember to leave time to get and respond to - feedback. - } @else { - View the task deadlines for your project, so you can organise your work and ensure you meet - all submission requirements on time. - } -

    -

    - Click on a task in the timeline to see how it connects to other tasks. This will show you which - tasks must be completed before it, and which tasks depend on it being completed first. -

    -
    +@if (project) { +
    +

    + Task Planner + @if (viewingOtherStudentProject) { + for {{ project.student.name }} + } +

    +

    + @if (unit.allowFlexibleDates) { + View and adjust the due dates for your tasks. Remember to leave time to get and respond to + feedback. + } @else { + View the task deadlines for your project, so you can organise your work and ensure you meet + all submission requirements on time. + } +

    +

    + Click on a task in the timeline to see how it connects to other tasks. This will show you + which tasks must be completed before it, and which tasks depend on it being completed first. +

    +
    + + Target Grade + + @for (grade of gradeValues; track grade) { + {{ gradeString(grade) }} + } + + +
    +

    + Subscribe to your unit calendar to be reminded about your due dates. + +

    +
    -
    -
    - - Target Grade - - @for (grade of gradeValues; track grade) { - {{ gradeString(grade) }} - } - - +
    +
    - -
    +} diff --git a/src/app/projects/states/plan/project-plan.component.ts b/src/app/projects/states/plan/project-plan.component.ts index b405fccfed..fc4965c8cc 100644 --- a/src/app/projects/states/plan/project-plan.component.ts +++ b/src/app/projects/states/plan/project-plan.component.ts @@ -1,18 +1,30 @@ -import {Component, OnInit, ViewChild} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Input, + OnDestroy, + OnInit, + ViewChild, +} from '@angular/core'; import {MatSelectChange} from '@angular/material/select'; -import {Project, ProjectService} from 'src/app/api/models/doubtfire-model'; -import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; +import {ActivatedRoute} from '@angular/router'; +import {Observable, Subscription, of} from 'rxjs'; +import {Project, ProjectService, UserService} from 'src/app/api/models/doubtfire-model'; +import {CalendarModalService} from 'src/app/common/modals/calendar-modal/calendar-modal.service'; import {AlertService} from 'src/app/common/services/alert.service'; import {GradeService} from 'src/app/common/services/grade.service'; -import {GlobalStateService} from '../index/global-state.service'; import {TaskPlannerComponent} from './task-planner/task-planner.component'; @Component({ selector: 'f-project-plan', templateUrl: 'project-plan.component.html', styleUrls: ['project-plan.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class ProjectPlanComponent implements OnInit { +export class ProjectPlanComponent implements OnInit, OnDestroy { + @Input() public project$: Observable; + public project: Project; @ViewChild(TaskPlannerComponent) planner!: TaskPlannerComponent; @@ -22,34 +34,58 @@ export class ProjectPlanComponent implements OnInit { } public get gradeValues() { - return this.gradeService.gradeValues; + return this.gradeService.gradeValuesFor(this.unit); } public get gradeAcronyms() { - return this.gradeService.gradeAcronyms; + return Object.fromEntries( + this.unit.gradeDefinitions.map((definition) => [definition.value, definition.abbreviation]), + ); } public gradeString(grade: number) { - return this.gradeService.grades[grade]; + return this.gradeService.gradeLabel(grade, this.unit); } + public selectedTargetGrade: number; + + private projectSub?: Subscription; + constructor( - private globalStateService: GlobalStateService, private gradeService: GradeService, private projectService: ProjectService, private alertService: AlertService, - ) { - this.globalStateService.currentViewAndEntitySubject$.subscribe((viewAndEntity) => { - if (viewAndEntity.viewType === 'PROJECT' && viewAndEntity.entity) { - this.project = viewAndEntity.entity as Project; + private route: ActivatedRoute, + private calendarModal: CalendarModalService, + private userService: UserService, + ) {} + + ngOnInit(): void { + this.project$ = this.project$ ?? of(this.route.parent?.snapshot.data.project as Project); + + this.projectSub = this.project$?.subscribe((project) => { + if (!project) { + return; } + + this.project = project; + this.selectedTargetGrade = project.targetGrade; }); } - public selectedTargetGrade: number; + ngOnDestroy(): void { + this.projectSub?.unsubscribe(); + } - ngOnInit(): void { - this.selectedTargetGrade = this.project.targetGrade; + openCalendar(): void { + this.calendarModal.show(null); + } + + public get viewingOtherStudentProject(): boolean { + const role = this.project?.unit?.myRole; + const currentUser = this.userService.currentUser; + + return !!role && role !== 'Student' && this.project?.student?.id !== currentUser?.id; } onTargetGradeChange(event: MatSelectChange) { diff --git a/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.html b/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.html index 5ea71774fd..8bc5faa0fc 100644 --- a/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.html +++ b/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.html @@ -1,31 +1,31 @@ - + {{ taskDefinition.abbreviation }} {{ taskDefinition.name }} - Task Description:

    {{ taskDefinition.description }}

    @if (!task.hasPrerequisiteTasks()) { -
    This task has no prerequisites.
    +
    This task has no prerequisites.
    } - + @if (dependents.length) { - + Required by - + {{ taskDefinition.abbreviation }} {{ taskDefinition.name }} is a prerequisite for the following tasks. In some cases, {{ taskDefinition.abbreviation }} {{ taskDefinition.name }} needs to @@ -34,15 +34,15 @@ - - + - - + - + - - + +
    Task + Task {{ link.taskDefinition?.abbreviation }} {{ link.taskDefinition?.name }} Submission Open + Submission Open @if (link.taskDefinition.projectTask(project).blockedByPrerequisiteTasks()) { block_outlined } @else { @@ -56,22 +56,22 @@ - Required Status + Required Status
    } @else { -
    +
    This task is not a prerequisite for any other tasks.
    } diff --git a/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.ts b/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.ts index 54f2aaadf0..7af70513b0 100644 --- a/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.ts +++ b/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.component.ts @@ -1,4 +1,4 @@ -import {Component, Inject, Input, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Inject, Input, OnInit} from '@angular/core'; import {MAT_DIALOG_DATA} from '@angular/material/dialog'; import {MatTableDataSource} from '@angular/material/table'; import {Project} from 'src/app/api/models/project'; @@ -15,13 +15,15 @@ export interface TaskPlannerPrerequisitesModalData { selector: 'f-task-planner-prerequisites-modal', templateUrl: './task-planner-prerequisites-modal.component.html', styleUrl: './task-planner-prerequisites-modal.component.scss', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TaskPlannerPrerequisitesModalComponent implements OnInit { @Input() taskDefinition: TaskDefinition; @Input() project: Project; @Input() dependents: TaskPrerequisite[]; - public dataSource = new MatTableDataSource(); + public dataSource: MatTableDataSource = new MatTableDataSource(); public displayedColumns: string[] = ['task-definition', 'current-status', 'required-status']; public get task() { diff --git a/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.service.ts b/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.service.ts index 4f12f4fd1f..eb243b1d3d 100644 --- a/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.service.ts +++ b/src/app/projects/states/plan/task-planner/task-planner-prerequisites-modal/task-planner-prerequisites-modal.service.ts @@ -1,11 +1,11 @@ import {Injectable} from '@angular/core'; import {MatDialog} from '@angular/material/dialog'; import {Project, TaskDefinition} from 'src/app/api/models/doubtfire-model'; +import {TaskPrerequisite} from 'src/app/api/models/task-prerequisite'; import { TaskPlannerPrerequisitesModalComponent, TaskPlannerPrerequisitesModalData, } from './task-planner-prerequisites-modal.component'; -import {TaskPrerequisite} from 'src/app/api/models/task-prerequisite'; @Injectable({ providedIn: 'root', diff --git a/src/app/projects/states/plan/task-planner/task-planner.component.html b/src/app/projects/states/plan/task-planner/task-planner.component.html index 025da92aa5..8e0e9894d4 100644 --- a/src/app/projects/states/plan/task-planner/task-planner.component.html +++ b/src/app/projects/states/plan/task-planner/task-planner.component.html @@ -1,14 +1,23 @@ -
    -
    +
    +
    Show Task Dates + + Hide tasks beyond target grade +
    @if (unit.allowFlexibleDates) {
    + @@ -17,37 +26,35 @@ }
    +
    -
    +
    -
    +
    {{ item.title }}
    @@ -58,19 +65,19 @@ @if (showDatesColumn) { - + {{ toDateString(item.start) }} - + {{ toDateString(item.end) }} - + {{ item.task.localDeadlineDate() ? toDateString(item.task.localDeadlineDate()) : 'N/A' }} @@ -78,17 +85,17 @@
    -
    + [ngClass]="getItemClasses(item)" + (click)="barClick(item)" + (mouseleave)="onBarLeave(item)" + (mouseover)="onBarHover(item)" + >
    +
    @if (unsavedChanges(item)) { change_circle } diff --git a/src/app/projects/states/plan/task-planner/task-planner.component.scss b/src/app/projects/states/plan/task-planner/task-planner.component.scss index ddf668e57b..df94c4a868 100644 --- a/src/app/projects/states/plan/task-planner/task-planner.component.scss +++ b/src/app/projects/states/plan/task-planner/task-planner.component.scss @@ -9,6 +9,29 @@ pointer-events: none; } +:host ::ng-deep gantt-calendar-header .secondary-text { + font-size: 11px; + line-height: 1.15; + transform: translateY(-0.45em); + white-space: pre-line; +} + +:host ::ng-deep gantt-calendar-header .today-rect { + display: flex; + height: 28px !important; + transform: translateY(-0.35rem); + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1px; + line-height: 1; +} + +:host ::ng-deep gantt-calendar-header .today-weekday { + font-size: 9px; + font-weight: 500; +} + .gantt-bar { background-color: var(--bar-bg); } diff --git a/src/app/projects/states/plan/task-planner/task-planner.component.ts b/src/app/projects/states/plan/task-planner/task-planner.component.ts index 44574d803f..d44f3c6b4a 100644 --- a/src/app/projects/states/plan/task-planner/task-planner.component.ts +++ b/src/app/projects/states/plan/task-planner/task-planner.component.ts @@ -1,15 +1,25 @@ -import {Component, Input, OnInit, ViewChild} from '@angular/core'; -import {UIRouter} from '@uirouter/core'; import { GanttBaselineItem, GanttDate, GanttItem, GanttLink, GanttLinkType, + GanttPrintService, GanttViewOptions, GanttViewType, NgxGanttComponent, } from '@worktile/gantt'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + Input, + OnDestroy, + OnInit, + ViewChild, +} from '@angular/core'; +import {ActivatedRoute, Router} from '@angular/router'; import {Project} from 'src/app/api/models/project'; import {Task} from 'src/app/api/models/task'; import {TaskDefinition} from 'src/app/api/models/task-definition'; @@ -21,6 +31,8 @@ import {GradeService} from 'src/app/common/services/grade.service'; import {TaskPlannerPrerequisitesModalService} from './task-planner-prerequisites-modal/task-planner-prerequisites-modal.service'; interface TaskGanttItem extends GanttItem { + start: number; + end: number; highlighted?: boolean; taskDefinition: TaskDefinition; task: Task; @@ -31,10 +43,15 @@ interface TaskGanttItem extends GanttItem { selector: 'f-task-planner', templateUrl: './task-planner.component.html', styleUrl: './task-planner.component.scss', + providers: [GanttPrintService], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class TaskPlannerComponent implements OnInit { +export class TaskPlannerComponent implements OnInit, AfterViewInit, OnDestroy { // Show a warning if the task's target end date is within this many days of the feedback deadline public readonly CLOSE_TO_FEEDBACK_DEADLINE_THRESHOLD = 7; + private readonly svgNamespace = 'http://www.w3.org/2000/svg'; + private ganttHeaderObserver?: MutationObserver; @Input() project: Project; @Input() targetGrade: number; @@ -54,31 +71,49 @@ export class TaskPlannerComponent implements OnInit { public animateBackground: boolean = false; public showDatesColumn: boolean = false; + public hideTasksAboveTargetGrade: boolean = false; public overlayLines: boolean = false; public get unit() { return this.project?.unit; } + private get hideTasksAboveTargetGradeStorageKey(): string { + return `ontrack.taskPlanner.${this.project?.id ?? 'unknown'}.hideTasksAboveTargetGrade`; + } + constructor( + private elementRef: ElementRef, private gradeService: GradeService, private alertService: AlertService, private confirmationModalService: ConfirmationModalService, private taskPlannerPrerequisitesModal: TaskPlannerPrerequisitesModalService, private taskPrerequisiteService: TaskPrerequisiteService, - private router: UIRouter, + private router: Router, + private route: ActivatedRoute, + private ganttPrintService: GanttPrintService, ) {} + ngAfterViewInit(): void { + setTimeout(() => this.setupGanttHeaderObserver()); + } + + ngOnDestroy(): void { + this.ganttHeaderObserver?.disconnect(); + } + public get gradeValues() { - return this.gradeService.gradeValues; + return this.gradeService.gradeValuesFor(this.unit); } public get gradeAcronyms() { - return this.gradeService.gradeAcronyms; + return Object.fromEntries( + this.unit.gradeDefinitions.map((definition) => [definition.value, definition.abbreviation]), + ); } public gradeString(grade: number) { - return this.gradeService.grades[grade]; + return this.gradeService.gradeLabel(grade, this.unit); } onBarHover(item: TaskGanttItem) { @@ -137,6 +172,12 @@ export class TaskPlannerComponent implements OnInit { this.taskPlannerPrerequisitesModal.show(this.project, td, prereqs); } + setHideTasksAboveTargetGrade(value: boolean) { + this.hideTasksAboveTargetGrade = value; + localStorage.setItem(this.hideTasksAboveTargetGradeStorageKey, JSON.stringify(value)); + this.refreshItems(false); + } + private mapPrerequisites() { for (const prerequisite of this.allTaskPrerequisites) { prerequisite.taskDefinition = this.unit.taskDefinitions.find( @@ -169,7 +210,7 @@ export class TaskPlannerComponent implements OnInit { return false; } const diff = this.normalizeDateUTC(item.end) - this.normalizeDateUTC(ganttItem.end); - const color = typeof ganttLink.color === 'string' ? ganttLink.color : ganttLink.color.default; + // const color = typeof ganttLink.color === 'string' ? ganttLink.color : ganttLink.color.default; if (diff > 0) { isAfterDependentStartDate = true; @@ -177,18 +218,18 @@ export class TaskPlannerComponent implements OnInit { continue; - if (color === '#0079D8') { - // Ready for feedback - if (diff > 0) { - isAfterDependentStartDate = true; - } - } else if (color === '#31b0d5' || color === '#5BB75B') { - // Discuss or Complete - if (diff >= -7 * 24 * 60 * 60) { - // We need to ensure this task is submitted a week earlier than its dependent so get it in a Discuss state - isAfterDependentStartDate = true; - } - } + // if (color === '#0079D8') { + // // Ready for feedback + // if (diff > 0) { + // isAfterDependentStartDate = true; + // } + // } else if (color === '#31b0d5' || color === '#5BB75B') { + // // Discuss or Complete + // if (diff >= -7 * 24 * 60 * 60) { + // // We need to ensure this task is submitted a week earlier than its dependent so get it in a Discuss state + // isAfterDependentStartDate = true; + // } + // } } return isAfterDependentStartDate; @@ -223,6 +264,8 @@ export class TaskPlannerComponent implements OnInit { } if (item.highlighted) { classes.push('[--bar-bg:#03c6fc]'); + } else if (this.isAboveTargetGrade(item)) { + classes.push('[--bar-bg:#9ca3af]', 'text-white'); } else if (this.isPastFeedbackDeadline(item)) { classes.push('[--bar-bg:#cd3704]', 'text-white'); } else if (this.isBlockedByPrerequisite(item)) { @@ -236,6 +279,10 @@ export class TaskPlannerComponent implements OnInit { return classes; } + isAboveTargetGrade(item: TaskGanttItem) { + return item.taskDefinition.targetGrade > this.targetGrade; + } + isPastFeedbackDeadline(item: TaskGanttItem) { return item.end > item.task.localDeadlineDate().getTime() / 1000; } @@ -332,6 +379,128 @@ export class TaskPlannerComponent implements OnInit { ); } + async saveImage() { + const ganttEl = this.ganttComponent.element; + const mainContainer = ganttEl.querySelector('.gantt-main-container'); + const side = ganttEl.querySelector('.gantt-side'); + + if (!mainContainer || !side) { + return; + } + + const originalStyle = { + width: ganttEl.style.width, + height: ganttEl.style.height, + overflow: ganttEl.style.overflow, + }; + const scrollElements = Array.from( + ganttEl.querySelectorAll( + '.gantt-main-container, .gantt-side-container, .gantt-virtual-scroll-viewport', + ), + ); + const scrollPositions = scrollElements.map((element) => ({ + element, + left: element.scrollLeft, + top: element.scrollTop, + })); + const windowScrollPosition = { + left: window.scrollX, + top: window.scrollY, + }; + + try { + await this.renderAllGanttBars(ganttEl); + this.resetGanttScroll(scrollElements); + window.scrollTo(windowScrollPosition.left, windowScrollPosition.top); + await this.waitForStableLayout(mainContainer); + + const fullWidth = side.offsetWidth + this.ganttComponent.view.width; + const fullHeight = + ganttEl.offsetHeight - mainContainer.offsetHeight + mainContainer.scrollHeight; + + ganttEl.style.width = `${fullWidth}px`; + ganttEl.style.height = `${fullHeight}px`; + ganttEl.style.overflow = 'visible'; + + this.resetGanttScroll(scrollElements); + await this.waitForStableLayout(mainContainer); + + const canvas = await this.ganttPrintService.html2canvas(); + this.downloadCanvas(canvas, `${this.unit.code}-Task-Plan.png`); + } catch (error) { + this.alertService.error(`Failed to download task plan: ${error}`, 6000); + } finally { + ganttEl.style.width = originalStyle.width; + ganttEl.style.height = originalStyle.height; + ganttEl.style.overflow = originalStyle.overflow; + + await this.nextAnimationFrame(); + for (const position of scrollPositions) { + position.element.scrollTo(position.left, position.top); + } + window.scrollTo(windowScrollPosition.left, windowScrollPosition.top); + } + } + + private resetGanttScroll(scrollElements: HTMLElement[]) { + for (const element of scrollElements) { + element.scrollTo(0, 0); + } + } + + private async waitForStableLayout(element: HTMLElement) { + let previousSize = ''; + let stableFrames = 0; + + for (let attempt = 0; attempt < 30 && stableFrames < 3; attempt++) { + await this.nextAnimationFrame(); + + const size = `${element.offsetWidth}:${element.offsetHeight}:${element.scrollWidth}:${element.scrollHeight}`; + if (size === previousSize) { + stableFrames++; + } else { + previousSize = size; + stableFrames = 0; + } + } + } + + private async renderAllGanttBars(ganttEl: HTMLElement) { + for (let pass = 0; pass < 4; pass++) { + if (ganttEl.querySelectorAll('[data-gantt-id]').length >= this.items.length) { + return; + } + + const placeholders = Array.from( + ganttEl.querySelectorAll('gantt-bar-placeholder'), + ); + + for (const placeholder of placeholders) { + placeholder.scrollIntoView({ + block: 'center', + inline: 'center', + }); + await this.nextAnimationFrame(); + await this.nextAnimationFrame(); + } + } + + if (ganttEl.querySelectorAll('[data-gantt-id]').length < this.items.length) { + throw new Error('Some chart items could not be rendered'); + } + } + + private nextAnimationFrame() { + return new Promise((resolve) => requestAnimationFrame(() => resolve())); + } + + private downloadCanvas(canvas: HTMLCanvasElement, filename: string) { + const link = document.createElement('a'); + link.download = filename; + link.href = canvas.toDataURL('image/png'); + link.click(); + } + // normalizeDateUTC = (ts: number) => { // const d = new GanttDate(ts * 1000); // // const utc = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 0, 0, 0, 0); @@ -371,39 +540,44 @@ export class TaskPlannerComponent implements OnInit { } public get earliestStartDate() { + const today = this.normalizeDateUTC(Date.now() / 1000); const tasks = this.taskDefs() .map((td) => this.project.findTaskForDefinition(td.id)) .filter((t) => t?.startDate); if (!tasks.length) { - return Math.floor(this.unit.startDate.getTime() / 1000); + return today; } const earliestTaskStart = Math.min(...tasks.map((t) => t.startDate.getTime() / 1000)); - return Math.floor(Math.min(this.unit.startDate.getTime() / 1000, earliestTaskStart)); + return Math.floor(Math.min(today, earliestTaskStart)); } public get latestEndDate() { + const oneWeekInSeconds = 7 * 24 * 60 * 60; const tasks = this.taskDefs() .map((td) => this.project.findTaskForDefinition(td.id)) .filter((t) => t?.localDueDate()); if (!tasks.length) { - return Math.floor(this.unit.endDate.getTime() / 1000); + return this.earliestStartDate + oneWeekInSeconds; } const latestTaskEnd = Math.max(...tasks.map((t) => t.localDueDate().getTime() / 1000)); - return Math.floor(Math.max(this.unit.endDate.getTime() / 1000, latestTaskEnd)); + return Math.floor(latestTaskEnd) + oneWeekInSeconds; } ngOnInit(): void { + this.loadHideTasksAboveTargetGradePreference(); + this.viewOptions = { - datePrecisionUnit: 'day', + precisionUnit: 'day', start: new GanttDate(this.earliestStartDate), end: new GanttDate(this.latestEndDate), - dragPreviewDateFormat: 'MMM dd', + unitWidth: 40, + dragTooltipFormat: 'MMM dd', }; this.unit.getTaskPrerequisites().subscribe({ @@ -439,6 +613,93 @@ export class TaskPlannerComponent implements OnInit { }); } + private loadHideTasksAboveTargetGradePreference(): void { + const rawPreference = localStorage.getItem(this.hideTasksAboveTargetGradeStorageKey); + try { + this.hideTasksAboveTargetGrade = rawPreference ? JSON.parse(rawPreference) === true : false; + } catch { + this.hideTasksAboveTargetGrade = false; + } + } + + private setupGanttHeaderObserver(): void { + const ganttElement = this.elementRef.nativeElement.querySelector('ngx-gantt'); + + if (!ganttElement) { + return; + } + + this.ganttHeaderObserver?.disconnect(); + this.ganttHeaderObserver = new MutationObserver(() => this.formatGanttHeaderLabels()); + this.ganttHeaderObserver.observe(ganttElement, { + childList: true, + subtree: true, + characterData: true, + }); + this.formatGanttHeaderLabels(); + } + + private formatGanttHeaderLabels(): void { + this.formatGanttDayLabels(); + this.formatGanttTodayLabel(); + } + + private formatGanttDayLabels(): void { + const dayLabels = this.elementRef.nativeElement.querySelectorAll( + 'gantt-calendar-header .secondary-text', + ); + + dayLabels.forEach((label) => { + if (label.querySelector('tspan')) { + return; + } + + const [date, day] = label.textContent?.trim().split(/\s+/) ?? []; + + if (!date || !day) { + return; + } + + const x = label.getAttribute('x') ?? '0'; + const dateLine = document.createElementNS(this.svgNamespace, 'tspan'); + dateLine.setAttribute('x', x); + dateLine.textContent = date; + + const dayLine = document.createElementNS(this.svgNamespace, 'tspan'); + dayLine.setAttribute('x', x); + dayLine.setAttribute('dy', '1.15em'); + dayLine.textContent = day; + + label.textContent = ''; + label.append(dateLine, dayLine); + }); + } + + private formatGanttTodayLabel(): void { + const todayLabel = this.elementRef.nativeElement.querySelector( + 'gantt-calendar-header .today-rect', + ); + + if (!todayLabel || todayLabel.querySelector('.today-weekday')) { + return; + } + + const today = new Date(); + const date = todayLabel.textContent?.trim() || today.getDate().toString(); + const day = new Intl.DateTimeFormat('en-US', {weekday: 'short'}).format(today); + + todayLabel.replaceChildren(); + + const dateLine = document.createElement('span'); + dateLine.textContent = date; + + const dayLine = document.createElement('span'); + dayLine.classList.add('today-weekday'); + dayLine.textContent = day; + + todayLabel.append(dateLine, dayLine); + } + refreshItems(scroll: boolean = true) { this.taskPrerequisites = this.allTaskPrerequisites.filter((pre) => this.taskDefs().find((td) => td.id === pre.taskDefinitionId), @@ -527,28 +788,14 @@ export class TaskPlannerComponent implements OnInit { _items.push(item); // Create baseline item - const baselineItem = {...item}; - - const tdTargetDate = - (this.targetGrade === 1 - ? td.cTargetDate - : this.targetGrade === 2 - ? td.dTargetDate - : this.targetGrade === 3 - ? td.hdTargetDate - : td.targetDate) ?? td.targetDate; - - const tdStartDate = - (this.targetGrade === 1 - ? td.cStartDate - : this.targetGrade === 2 - ? td.dStartDate - : this.targetGrade === 3 - ? td.hdStartDate - : td.startDate) ?? td.startDate; - - baselineItem.start = this.normalizeDateUTC(tdStartDate.getTime() / 1000); - baselineItem.end = this.normalizeDateUTC(tdTargetDate.getTime() / 1000); + const tdTargetDate = td.gradeTargetDate(this.targetGrade) ?? td.targetDate; + const tdStartDate = td.gradeStartDate(this.targetGrade) ?? td.startDate; + + const baselineItem: GanttBaselineItem = { + id: item.id, + start: this.normalizeDateUTC(tdStartDate.getTime() / 1000), + end: this.normalizeDateUTC(tdTargetDate.getTime() / 1000), + }; _baselineItems.push(baselineItem); @@ -566,8 +813,9 @@ export class TaskPlannerComponent implements OnInit { this.ganttComponent.scrollToToday(); } - if (this.router.globals.params.taskDef && scroll) { - const taskItem = this.items.find((item) => item.id === this.router.globals.params.taskDef); + const taskDef = this.route.snapshot.queryParamMap.get('taskDef'); + if (taskDef && scroll) { + const taskItem = this.items.find((item) => item.id === taskDef); if (taskItem) { this.ganttComponent.scrollToDate(taskItem.start); taskItem.highlighted = true; @@ -585,11 +833,12 @@ export class TaskPlannerComponent implements OnInit { setTimeout(() => (taskItem.highlighted = false), 1000); setTimeout(() => (this.animateBackground = false), 2000); } - this.router.stateService.go( - this.router.globals.current.name, - {taskDef: null}, - {location: 'replace', notify: false, reload: false}, - ); + this.router.navigate([], { + relativeTo: this.route, + queryParams: {taskDef: null}, + queryParamsHandling: 'merge', + replaceUrl: true, + }); } } @@ -599,7 +848,9 @@ export class TaskPlannerComponent implements OnInit { } return this.project.unit.taskDefinitions - .filter((taskDef) => taskDef.targetGrade <= this.targetGrade) + .filter( + (taskDef) => !this.hideTasksAboveTargetGrade || taskDef.targetGrade <= this.targetGrade, + ) .sort((a, b) => { const taskA = this.project.findTaskForDefinition(a.id); const taskB = this.project.findTaskForDefinition(b.id); diff --git a/src/app/projects/states/portfolio/directives/directives.coffee b/src/app/projects/states/portfolio/directives/directives.coffee deleted file mode 100644 index 661acd16e7..0000000000 --- a/src/app/projects/states/portfolio/directives/directives.coffee +++ /dev/null @@ -1,7 +0,0 @@ -angular.module('doubtfire.projects.states.portfolio.directives', [ - 'doubtfire.projects.states.portfolio.directives.portfolio-add-extra-files-step' - 'doubtfire.projects.states.portfolio.directives.portfolio-learning-summary-report-step' - 'doubtfire.projects.states.portfolio.directives.portfolio-review-step' - 'doubtfire.projects.states.portfolio.directives.portfolio-tasks-step' - 'doubtfire.projects.states.portfolio.directives.portfolio-welcome-step' -]) diff --git a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.coffee b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.coffee deleted file mode 100644 index f62984ff63..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.coffee +++ /dev/null @@ -1,25 +0,0 @@ -angular.module('doubtfire.projects.states.portfolio.directives.portfolio-add-extra-files-step', []) - -# -# Allow students to add additional files to the end of their portfolio -# They can choose any file they want to upload -# -.directive('portfolioAddExtraFilesStep', -> - restrict: 'E' - replace: true - templateUrl: 'projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.tpl.html' - controller: ($scope) -> - otherFileFileUploadData = (type) -> - type: { - file0: { name: "Other", type: type } - }, - payload: { - name: "Other" - kind: type - } - - $scope.uploadType = 'document' - $scope.$watch 'uploadType', (newType) -> - return unless newType? - $scope.uploadFileData = otherFileFileUploadData newType -) diff --git a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.html b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.html new file mode 100644 index 0000000000..315af39ff8 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.html @@ -0,0 +1,52 @@ + + + Upload Other Files + + +

    + Now is your chance to upload any extra files to include in your portfolio. They'll appear at + the very top of your portfolio, before your tasks. +

    +
    +
      + @for (file of extraFiles; track file) { +
    1. +
      + {{ icons[file.kind] }} + {{ file.name }} +
      + +
    2. + } @empty { +

      If you do not have any files to add, you can skip this step.

      + } +
    +
    +
    + + Select type of file: + + Document File + Code File + Image File + ZIP File + + +
    + + +
    + + + + +
    diff --git a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.scss b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.ts b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.ts new file mode 100644 index 0000000000..2d2316b633 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.ts @@ -0,0 +1,98 @@ +import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; +import {MatSelectChange} from '@angular/material/select'; +import {Project} from 'src/app/api/models/project'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-portfolio-add-extra-files-step', + templateUrl: 'portfolio-add-extra-files-step.component.html', + styleUrls: ['portfolio-add-extra-files-step.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class PortfolioAddExtraFilesStepComponent implements OnInit { + @Input() project: Project; + @Input() onAdvanceActiveTab?: (index: 1 | -1) => void; + + public uploadType: 'document' | 'code' | 'image' | 'zip' = 'document'; + + public isUploading: boolean; + + public uploadFileType = { + file0: { + name: 'Other', + type: 'document', + }, + }; + + public uploadFilePayload = { + name: 'Other', + kind: 'document', + }; + + constructor(private alertService: AlertService) {} + + public readonly icons = { + document: 'article_outlined', + code: 'integration_instructions_outlined', + image: 'image_outlined', + zip: 'zip_outlined', + }; + + ngOnInit(): void { + this.uploadType = 'document'; + + this.uploadFileType = { + file0: { + name: 'Other', + type: 'document', + }, + }; + + this.uploadFilePayload = { + name: 'Other', + kind: 'document', + }; + } + onTypeChange(event: MatSelectChange) { + console.log('on type change', event); + this.uploadFileType = { + file0: { + name: 'Other', + type: event.value, + }, + }; + + this.uploadFilePayload = { + name: 'Other', + kind: event.value, + }; + } + + public get extraFiles() { + // If file.idx === 0, then it's the Learning Summary Report, so we ignore it here + return this.project?.portfolioFiles.filter((file) => file.idx !== 0); + } + + deleteFileFromPortfolio(file: {idx: number; kind: string; name: string}) { + this.project.deleteFileFromPortfolio(file).subscribe({ + next: () => { + this.alertService.success('Succesfully delete file', 3000); + }, + error: (error) => { + this.alertService.error(`Failed to delete file: ${error}`, 6000); + }, + }); + } + + advanceActiveTab(index: 1 | -1) { + if (this.onAdvanceActiveTab) { + this.onAdvanceActiveTab(index); + return; + } + } + + addNewFilesToPortfolio(newFile: {kind: string; name: string; idx: number}) { + this.project.portfolioFiles.push(newFile); + } +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.scss b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.scss deleted file mode 100644 index 47c481a372..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.scss +++ /dev/null @@ -1,10 +0,0 @@ -.portfolio-add-extra-files-step { - a.clear-upload { - margin-left: 1ex; - &:hover i { - font-size: 1.15em; - color: $brand-danger; - } - display: inline-block; - } -} diff --git a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.tpl.html deleted file mode 100644 index 4de3b6f696..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.tpl.html +++ /dev/null @@ -1,58 +0,0 @@ -
    -
    -

    {{activeTab.title}}

    -
    -
    -

    - Now is your chance to upload extra files to include in your portfolio. - These files will be added at to your portfolio before your selected tasks - from the previous step. -

    -
    -
    -

    No files to add?

    -

    If you do not have any files to add you can skip this step.

    -
    -
    -

    Extra file{{extraFiles().length > 1 ? 's' : ''}} added

    -

    - {{extraFiles().length > 1 ? 'The files you add will appear in the portfolio in the order shown below.' : ''}} - If you want to delete a file, click the cross beside the file's name. -

    -
      -
    1. - {{file.name}} - - - -
    2. -
    -
    -
    -
    - -
    - -
    -
    - - -
    -
    -
    - -
    diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html index 575a9f5faf..008b8109a3 100644 --- a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html +++ b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html @@ -1,5 +1,5 @@ -
    - +
    + @@ -8,22 +8,22 @@

    Select Grade

    -

    +

    In preparing your portfolio, you need to undertake a self-assessment. Use the unit's assessment criteria to determine the grade your portfolio should be awarded.

    - + - + warning Read the assessment criteria -

    +

    Make sure that you have reviewed the Assessment Criteria for the grade you are applying for. Each grade will have a list of criteria that you can use to determine if you meet the requirements to achieve that grade. @@ -40,15 +40,15 @@

    Select Grade

    @if (agreedToAssessmentCriteria) { - + - + Grade Application -

    +

    Select the grade you are applying for {{ unit.code }} {{ unit.name }} below.

    @@ -61,16 +61,16 @@

    Select Grade

    > @for (grade of gradeValues; track grade) { - + } -

    +

    Make sure your Learning Summary Report justifies how your portfolio demonstrates you have met all unit learning outcomes to a {{ targetGrade }} level @@ -82,10 +82,10 @@

    Select Grade

    - + +
    + } +
    +
    + warning + + Remember to provide a justification for why you believe you have achieved a + {{ targetGradeLabel }} in {{ unit.code }} {{ unit.name }}. +
    + +
    + + + +
    + @if (!projectHasDraftLearningSummaryReport) { +
    + You're missing a Learning Summary Report. Upload one to continue. +
    + } + +
    +
    +
    diff --git a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.scss b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.scss new file mode 100644 index 0000000000..fde6acc603 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.scss @@ -0,0 +1,5 @@ +.submitted .mat-icon { + font-size: 50px; + width: 50px; + height: 50px; +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.ts b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.ts new file mode 100644 index 0000000000..7dfb134cbb --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.ts @@ -0,0 +1,72 @@ +import {ChangeDetectionStrategy, Component, Injector, Input} from '@angular/core'; +import {Project} from 'src/app/api/models/project'; +import {Unit} from 'src/app/api/models/unit'; +import {GradeService} from 'src/app/common/services/grade.service'; + +@Component({ + selector: 'f-portfolio-learning-summary-report-step', + templateUrl: 'portfolio-learning-summary-report-step.component.html', + styleUrls: ['portfolio-learning-summary-report-step.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class PortfolioLearningSummaryReportStepComponent { + @Input() unit: Unit; + @Input() project: Project; + @Input() onAdvanceActiveTab?: (index: 1 | -1) => void; + + public learningSummaryReportFileUploadData = { + type: { + file0: {name: 'Learning Summary Report', type: 'document'}, + }, + payload: { + name: 'LearningSummaryReport', // DO NOT MODIFY - case sensitive on API + kind: 'document', + }, + }; + + public forceLSRSubmit: boolean = false; + public acceptUploadNewLearningSummary: boolean = false; + + constructor( + private injector: Injector, + private gradeService: GradeService, + ) {} + + public get projectHasDraftLearningSummaryReport() { + return ( + this.project?.usesDraftLearningSummary || + this.project?.portfolioFiles.find((f) => f.idx === 0) + ); + } + + public get targetGradeLabel(): string { + return this.gradeService.gradeLabel(this.project.targetGrade, this.unit); + } + + advanceActiveTab(index: 1 | -1) { + if (this.onAdvanceActiveTab) { + this.onAdvanceActiveTab(index); + return; + } + } + + addNewFile(newFile: {kind: string; name: string; idx: number}) { + this.project.portfolioFiles.push(newFile); + this.acceptUploadNewLearningSummary = false; + this.forceLSRSubmit = false; + } + + draftTaskDefinitionWasUsed(): boolean { + const draftTaskDef = this.unit.draftTaskDefinition; + if (draftTaskDef) { + const task = this.project.findTaskForDefinition(draftTaskDef.id); + if (task && task.inSubmittedState()) { + return true; + } + } + return false; + } + + // downloadLearningSummaryReport(){} +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.tpl.html deleted file mode 100644 index 960c0014ab..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.tpl.html +++ /dev/null @@ -1,73 +0,0 @@ -
    -
    -

    - {{activeTab.title}} -

    -
    -
    -

    - Upload the Learning Summary Report, the primary porfolio document which - justifies your desired grade. -

    -

    - Your Learning Summary Report is a summary of what you have learnt in this unit. - It consists of two sections: -

      -
    1. a self-assessment, and
    2. -
    3. your reflections on the unit.
    4. -
    -

    -

    - The self-assessment indicates how your portfolio aligns - with the assessment criteria, and which grade you are applying for. -

    -

    - Your reflections are a personal comment on what you have - learnt in the unit, and how your knowledge and skills have developed. -

    -
    -
    -

    - Before you submit your portfolio... -

    - Your draft learning summary has already been copied over, - it is advised you upload a revised copy. -
    -
    - - -
    -
    - -
    -
    -

    - Learning Summary Report Submitted -

    - Click here to re-upload a new Learning Summary Report -
    - -
    -

    - Before you submit the Learning Summary Report... -

    - Remember to provide a justification for why you believe - you have achieved a {{targetGrade}} in {{unit.name}}. -
    -
    - -
    -
    -
    - -
    diff --git a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.html b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.html index 7444d7176c..3944af9a70 100644 --- a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.html +++ b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.html @@ -1,20 +1,20 @@ -@if (loading) { -
    +@if (loadingIncludedTasks) { +
    Loading tasks...
    } @else { @if (tasksInPortfolio.length === 0) { -
    +
    assignment_late -

    No tasks found

    +

    No tasks found

    } @else {
      @for (task of tasksInPortfolio; track task) { -
    1. +
    2. -
      +
      {{ task.definition.abbreviation }} — {{ task.definition.name }}
      @@ -23,4 +23,33 @@ }
    } + @if (tasksStillProcessing.length > 0) { +
    +
    + hourglass_top +
    +

    Tasks still processing

    +

    + Please wait until these tasks have finished processing before creating your portfolio to + ensure they are included. +

    +
    +
    +
      + @for (task of tasksStillProcessing; track task) { +
    1. +
      + {{ task.definition.abbreviation }} — {{ task.definition.name }} +
      +
      + +
      +
    2. + } +
    +

    + This list will automatically refresh as tasks finish processing. +

    +
    + } } diff --git a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.ts b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.ts index e425d93976..3be9d74844 100644 --- a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.ts +++ b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-included-tasks/portfolio-included-tasks.component.ts @@ -1,4 +1,13 @@ -import {Component, Input, OnInit} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import {Subscription, interval} from 'rxjs'; import {Project} from 'src/app/api/models/project'; import {Task} from 'src/app/api/models/task'; import {AlertService} from 'src/app/common/services/alert.service'; @@ -7,30 +16,96 @@ import {AlertService} from 'src/app/common/services/alert.service'; selector: 'f-portfolio-included-tasks', templateUrl: 'portfolio-included-tasks.component.html', styleUrls: ['portfolio-included-tasks.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class PortfolioIncludedTasksComponent implements OnInit { +export class PortfolioIncludedTasksComponent implements OnInit, OnDestroy { @Input() project: Project; + @Output() canCreatePortfolioChange: EventEmitter = new EventEmitter(); constructor(private alertService: AlertService) {} - loading: boolean = false; + loadingIncludedTasks: boolean = false; + loadingProcessingTasks: boolean = false; + hasTasksStillProcessing: boolean = false; tasksInPortfolio: Task[] = []; + tasksStillProcessing: Task[] = []; + + private processingTasksPoll?: Subscription; + ngOnInit() { - this.loading = true; + this.getTasksIncludedInPortfolio(); + this.getProcessingTasks(); + } + + ngOnDestroy(): void { + this.processingTasksPoll?.unsubscribe(); + } + + public getTasksIncludedInPortfolio() { + this.loadingIncludedTasks = true; + this.canCreatePortfolioChange.emit(false); this.project.getTasksIncludedInPortfolio().subscribe({ next: (tasks) => { - for (const taskId of tasks) { - const task = this.project.tasks.find((t) => t.id === taskId); - if (task) { - this.tasksInPortfolio.push(task); - } + this.tasksInPortfolio = this.getProjectTasks(tasks); + + this.loadingIncludedTasks = false; + this.updateCanCreatePortfolio(); + }, + error: (error) => { + this.alertService.error(`Failed to get tasks for portfolio: ${error}`, 6000); + }, + }); + } + + public getProcessingTasks(refreshIncludedTasksOnCountChange: boolean = false) { + this.loadingProcessingTasks = true; + this.canCreatePortfolioChange.emit(false); + this.project.getTasksStillProcessing().subscribe({ + next: (tasks) => { + const processingTaskCountChanged = tasks.length !== this.tasksStillProcessing.length; + + this.hasTasksStillProcessing = tasks.length > 0; + this.tasksStillProcessing = this.getProjectTasks(tasks); + + if (refreshIncludedTasksOnCountChange && processingTaskCountChanged) { + this.getTasksIncludedInPortfolio(); } - this.loading = false; + + this.updateProcessingTasksPoll(); + this.loadingProcessingTasks = false; + this.updateCanCreatePortfolio(); }, error: (error) => { this.alertService.error(`Failed to get tasks for portfolio: ${error}`, 6000); }, }); } + + private getProjectTasks(taskIds: number[]): Task[] { + return taskIds + .map((taskId) => this.project.tasks.find((task) => task.id === taskId)) + .filter((task): task is Task => task !== undefined); + } + + private updateProcessingTasksPoll(): void { + if (this.hasTasksStillProcessing && !this.processingTasksPoll) { + this.processingTasksPoll = interval(30_000).subscribe(() => { + this.getProcessingTasks(true); + }); + return; + } + + if (!this.hasTasksStillProcessing) { + this.processingTasksPoll?.unsubscribe(); + this.processingTasksPoll = undefined; + } + } + + private updateCanCreatePortfolio(): void { + this.canCreatePortfolioChange.emit( + !this.loadingIncludedTasks && !this.loadingProcessingTasks && !this.hasTasksStillProcessing, + ); + } } diff --git a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.coffee b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.coffee deleted file mode 100644 index 4c8ca4ac9d..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.coffee +++ /dev/null @@ -1,49 +0,0 @@ -angular.module('doubtfire.projects.states.portfolio.directives.portfolio-review-step', []) - -# -# Step for students to view their portfolio and optionally delete it -# -.directive('portfolioReviewStep', -> - restrict: 'E' - replace: true - templateUrl: 'projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.tpl.html' - controller: ($scope, alertService, newProjectService, DoubtfireConstants, ConfirmationModal, fileDownloaderService) -> - - # Get the confugurable, external name of Doubtfire - $scope.externalName = DoubtfireConstants.ExternalName - - # Watch when portfolio value is changed to reassess - $scope.$watch 'project.portfolioAvailable', -> - $scope.hasLSR = $scope.projectHasLearningSummaryReport() - $scope.hasTasksSelected = $scope.selectedTasks().length > 0 - $scope.portfolioIsCompiling = $scope.project.compilePortfolio - $scope.canCompilePortfolio = (not $scope.portfolioIsCompiling) and $scope.hasTasksSelected and $scope.hasLSR and not $scope.project.portfolioAvailable - - # - # Compile portfolio - # - $scope.toggleCompileProject = -> - $scope.project.compilePortfolio = not $scope.project.compilePortfolio - - newProjectService.update($scope.project).subscribe( - (response) -> - $scope.portfolioIsCompiling = true - $scope.canCompilePortfolio = false - $scope.project.portfolioStatus = 0.5 - ) - # - # PDF Local Funcs - # - $scope.deletePortfolio = -> - doDelete = -> - $scope.project.deletePortfolio().subscribe( (response) -> - $scope.project.portfolioAvailable = false - $scope.project.portfolioStatus = 0 - alertService.message( "Portfolio has been deleted!", 5000) - ) - ConfirmationModal.show("Delete Portfolio?", 'Are you sure you want to delete your portfolio? You will need to recreate your porfolio again if you do so.', doDelete) - - # Download the pdf - $scope.downloadPortfolio = -> - fileDownloaderService.downloadFile($scope.project.portfolioUrl(true), "#{$scope.project.student.username}-portfolio.pdf") -) diff --git a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.component.html b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.component.html new file mode 100644 index 0000000000..889d52931e --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.component.html @@ -0,0 +1,160 @@ + + + Review Portfolio + + + + @if (!hasLearningSummaryReport || !hasTasksSelected) { + + + + warning + There are issues with your portfolio + + + + @if (!hasLearningSummaryReport) { +

    + Your portfolio must include a learning summary report. Upload this + before you schedule your portfolio to be compiled. +

    + } + + @if (!hasTasksSelected) { + @if (unitHasILOs) { +

    + Your portfolio must include tasks aligned to the unit's + {{ unit?.ilos?.length }} intended learning outcomes. Please + indicate which tasks you think align to the unit's learning outcomes before you + schedule your portfolio to be compiled. +

    + } @else { +

    + Your portfolio must include tasks that you have completed this + teaching period. Please indicate which tasks you would like to include in your + portfolio before you schedule your portfolio to be compiled. +

    + } + } +
    +
    + } + + @if (portfolioIsCompiling) { + + + Portfolio Processing + + +

    + Your portfolio compilation is scheduled with {{ externalName }}. This process will take + some time, check back soon to see if your portfolio is available. +

    +
    +
    + } + + @if (canCompilePortfolio) { +

    + {{ externalName }} will create your portfolio once you are happy with your submissions so + far. +

    + + @if (extraFiles.length > 0) { +

    + You have attached + {{ extraFiles.length }} extra file{{ extraFiles.length > 1 ? 's' : '' }} + to your portfolio. You may add more files or remove + {{ extraFiles.length > 1 ? 'these' : 'this' }} + file{{ extraFiles.length > 1 ? 's' : '' }} in the previous step. +

    + +
      + @for (file of extraFiles; track file) { +
    1. + {{ getIcon(file.kind) }} + {{ file.name }} +
    2. + } +
    + } + +

    + Only submitted tasks will be included in your portfolio. If a task is missing from the list, + ensure that you have submitted it before compiling your portfolio. All feedback and comments + for each task will appear in the final portfolio, so you can add any additional comments now + if there's something you'd like to address. +

    + +

    The following tasks will be included automatically in this order:

    + + + + + Create Your Portfolio + + +

    + Once you click Create Portfolio, the construction of your portfolio will be scheduled + with {{ externalName }}. This process will take some time, and you will be emailed when + your portfolio is ready to review if you have enabled portfolio notifications. +

    +

    + + If you would like to make any further changes after you have created your portfolio, + you will need to delete and recreate your portfolio. + +

    +
    + + + +
    + } + + @if (project?.portfolioAvailable) { + + + Download Portfolio + + +

    + You're done! There's nothing left for you to do. Your portfolio has + been submitted. +

    +

    + This is the exact same document your assessor will see. +

    +

    + If you need to update any tasks or upload additional files, delete your portfolio and + compile it again before the submission deadline. +

    +
    + + + + +
    + } +
    + + + + +
    diff --git a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.component.scss b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.component.ts b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.component.ts new file mode 100644 index 0000000000..865e9449c0 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.component.ts @@ -0,0 +1,137 @@ +import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; +import {Project} from 'src/app/api/models/project'; +import {Task} from 'src/app/api/models/task'; +import {Unit} from 'src/app/api/models/unit'; +import {ProjectService} from 'src/app/api/services/project.service'; +import {TaskService} from 'src/app/api/services/task.service'; +import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; + +@Component({ + selector: 'f-portfolio-review-step', + templateUrl: 'portfolio-review-step.component.html', + styleUrls: ['portfolio-review-step.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class PortfolioReviewStepComponent implements OnInit { + @Input() project: Project; + @Input() unit: Unit; + @Input() onAdvanceActiveTab?: (index: 1 | -1) => void; + + public externalName: string = 'OnTrack'; + public canCreatePortfolio: boolean = false; + + public readonly icons: Record = { + document: 'article_outlined', + code: 'integration_instructions_outlined', + image: 'image_outlined', + zip: 'zip_outlined', + }; + + constructor( + private constants: DoubtfireConstants, + private projectService: ProjectService, + private taskService: TaskService, + private alertService: AlertService, + private confirmationModal: ConfirmationModalService, + private fileDownloaderService: FileDownloaderService, + ) {} + + ngOnInit(): void { + this.constants.ExternalName.subscribe((name) => { + this.externalName = name; + }); + } + + public get hasLearningSummaryReport(): boolean { + return (this.project?.portfolioFiles ?? []).some((file) => file.idx === 0); + } + + public get hasTasksSelected(): boolean { + return this.selectedTasks.length > 0; + } + + public get portfolioIsCompiling(): boolean { + return Boolean(this.project?.compilePortfolio); + } + + public get canCompilePortfolio(): boolean { + return ( + !this.portfolioIsCompiling && + this.hasTasksSelected && + this.hasLearningSummaryReport && + !this.project?.portfolioAvailable + ); + } + + public get unitHasILOs(): boolean { + return (this.unit?.ilos?.length ?? 0) > 0; + } + + public get extraFiles(): {kind: string; name: string; idx: number}[] { + return (this.project?.portfolioFiles ?? []).filter((file) => file.idx !== 0); + } + + public get selectedTasks(): Task[] { + const toBeWorkedOn = this.taskService?.toBeWorkedOn ?? []; + return [...(this.project?.tasks ?? [])] + .filter((task) => !toBeWorkedOn.includes(task.status)) + .sort((a, b) => a.definition.seq - b.definition.seq); + } + + public getIcon(kind: string): string { + return this.icons[kind] ?? 'insert_drive_file'; + } + + public createPortfolio(): void { + this.project.compilePortfolio = !this.project.compilePortfolio; + + this.projectService.update(this.project).subscribe({ + next: () => { + this.project.compilePortfolio = true; + this.project.portfolioStatus = 0.5; + }, + error: (error) => { + this.project.compilePortfolio = false; + this.alertService.error(`Could not create portfolio: ${error}`, 6000); + }, + }); + } + + public deletePortfolio(): void { + this.confirmationModal.show( + 'Delete Portfolio?', + 'Are you sure you want to delete your portfolio? You will need to recreate your portfolio again if you do so.', + () => { + this.project.deletePortfolio().subscribe({ + next: () => { + this.project.portfolioAvailable = false; + this.project.portfolioStatus = 0; + this.alertService.message('Portfolio has been deleted!', 5000); + }, + error: (error) => { + this.alertService.error(`Could not delete portfolio: ${error}`, 6000); + }, + }); + }, + ); + } + + public downloadPortfolio(): void { + const username = this.project?.student?.username ?? 'student'; + this.fileDownloaderService.downloadFile( + this.project.portfolioUrl(true), + `${username}-portfolio.pdf`, + ); + } + + goBack() { + if (this.onAdvanceActiveTab) { + this.onAdvanceActiveTab(-1); + return; + } + } +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.scss b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.scss deleted file mode 100644 index 1a35a9a0c3..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.scss +++ /dev/null @@ -1,19 +0,0 @@ -.project-portfolio-wizard .panel-body .portfolio-review-step { - &.expanded { - // override with !important to override mixins in project-portfolio-wizard.scss - margin: 0 auto !important; - width: 100% !important; - float: none !important; - padding: 0 !important; - } - - .pdf-viewer-panel { - border-top: 1px solid $brand-primary; - border-bottom: 1px solid $brand-primary; - margin-top: 2em; - } - - .portfolio-tool-buttons { - margin-top: 2em; - } -} diff --git a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.tpl.html deleted file mode 100644 index 4a9a098324..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.tpl.html +++ /dev/null @@ -1,113 +0,0 @@ -
    -
    -

    {{activeTab.title}}

    -
    -
    -
    -
    -

    There are issues with your portfolio

    -
    -
    -

    - Your portfolio must include a learning summary report. Upload this before you schedule your - portfolio to be compiled. -

    -

    -

    - Your portfolio must include tasks aligned to the unit's - {{unit.ilos.length}} intended learning outcomes. Please indicate which tasks you think align - to the unit's learning outcomes before you schedule your portfolio to be compiled. -

    -

    - Your portfolio must include tasks that you have completed this teaching period. Please - indicate which tasks you you would like to include in your portfolio before you schedule your portfolio to be - compiled. -

    -
    -
    - -
    -
    -

    Portfolio Processing

    -
    -
    -

    - Your portfolio compilation is scheduled with {{externalName.value}}. This process will take some time, check - back soon to see if your portfolio is available. -

    -
    -
    - -
    -

    {{externalName.value}} will create your portfolio once you are happy with your submissions so far.

    -
    -

    - You have attached {{extraFiles().length}} extra file{{extraFiles().length > 1 ? 's' : ''}} - to your portfolio. You may add more files or remove {{extraFiles().length > 1 ? 'these' : 'this'}} - file{{extraFiles().length > 1 ? 's' : ''}} in the previous step. Each file is listed in order of attachment - below: -

    -
      -
    1. {{file.name}}
    2. -
    -
    -

    - Only submitted tasks will be included in your portfolio. If a task is missing from the list, - ensure that you have submitted it before compiling your portfolio. All feedback and comments - for each task will appear in the final portfolio, so you can add any additional comments now - if there's something you'd like to address. -

    The following tasks will be included - automatically in this order:

    -

    - -
    -
    -
    -

    Create Your Portfolio

    -
    -
    -

    - Once you click Create Portfolio, the construction of your portfolio will be scheduled with - {{externalName.value}}. This process will take some time, and you will be emailed when your portfolio is ready - to review if you have enabled portfolio notifications. -

    -

    - If you would like to make any further changes after you have created your portfolio, you will need to delete - and recreate your portfolio. -

    -
    - -
    - -
    -
    -

    Download Portfolio

    -
    -
    -

    - This is the exact same document your assessor will see. -

    -

    - You're done! There's nothing left for you to do. Your portfolio has been submitted. -

    -
    - -
    -
    - -
    diff --git a/src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.coffee b/src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.coffee deleted file mode 100644 index bae7d0abd7..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.coffee +++ /dev/null @@ -1,16 +0,0 @@ -# Component not used - -angular.module('doubtfire.projects.states.portfolio.directives.portfolio-tasks-step', []) - -# -# Allows students to select which tasks they have completed can -# be included in their portfolio -# -.directive('portfolioTasksStep', -> - restrict: 'E' - replace: true - templateUrl: 'projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.tpl.html' - controller: ($scope) -> - $scope.noTasksSelected = -> - $scope.selectedTasks().length is 0 -) diff --git a/src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.scss b/src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.scss deleted file mode 100644 index 0195fe8ba4..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.scss +++ /dev/null @@ -1,6 +0,0 @@ -.project-portfolio-wizard .panel-body .portfolio-tasks-step { - // override with !important to override mixins in project-portfolio-wizard.scss - margin: 0 auto !important; - width: 90% !important; - float: none !important; -} diff --git a/src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.tpl.html deleted file mode 100644 index 1b4267f5cd..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.tpl.html +++ /dev/null @@ -1,33 +0,0 @@ -
    -
    -

    Relate ILOs to Tasks

    -
    -
    -

    Select tasks to include

    -
    -
    -

    - For each task, please indicate whether you think this task is related to one - or more of the unit's {{unit.ilos.length}} learning outcomes. - You may provide a rationale for each task selected, which will be noted in - your portfolio. -

    -

    - Please indicate which tasks you would like to include in your portfolio. -

    -
    - - -
    diff --git a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.coffee b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.coffee deleted file mode 100644 index c34f31b354..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.coffee +++ /dev/null @@ -1,10 +0,0 @@ -angular.module('doubtfire.projects.states.portfolio.directives.portfolio-welcome-step', []) - -# -# Welcome introductory step -# -.directive('portfolioWelcomeStep', -> - restrict: 'E' - replace: true - templateUrl: 'projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html' -) diff --git a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.html b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.html new file mode 100644 index 0000000000..26ec79f3a3 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.html @@ -0,0 +1,23 @@ + + + Portfolio Preparation + + +

    Preparing your portfolio involves 5 steps:

    +
      +
    1. Select your Grade you are applying for
    2. +
    3. Upload your Learning Summary Report
    4. +
    5. Upload any Other Resources you want to add
    6. +
    7. Compile your resources into your portfolio and review
    8. +
    +

    + Once you have completed all of these steps, your portfolio will be prepared by + {{ externalName }} and you will be notified when it is ready. You can then check your work, + and if you want to make any corrections repeat these steps to create a new version of your + portfolio. +

    +
    + + + +
    diff --git a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.scss b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.ts b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.ts new file mode 100644 index 0000000000..e2c9bda66c --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.ts @@ -0,0 +1,30 @@ +import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; + +@Component({ + selector: 'f-portfolio-welcome-step', + templateUrl: 'portfolio-welcome-step.component.html', + styleUrls: ['portfolio-welcome-step.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class PortfolioWelcomeStepComponent implements OnInit { + @Input() onAdvanceActiveTab?: (index: 1 | -1) => void; + + public externalName: string = 'OnTrack'; + + constructor(private constants: DoubtfireConstants) {} + + ngOnInit(): void { + this.constants.ExternalName.subscribe((name) => { + this.externalName = name; + }); + } + + goNextStep() { + if (this.onAdvanceActiveTab) { + this.onAdvanceActiveTab(1); + return; + } + } +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html deleted file mode 100644 index cb062284f1..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html +++ /dev/null @@ -1,20 +0,0 @@ -
    -
    -

    Portfolio Preparation

    -
    -
    -

    Preparing your portfolio involves 5 steps:

    -
      -
    1. Select your Grade you are applying for
    2. -
    3. Upload your Learning Summary Report
    4. -
    5. Upload any Other Resources you want to add
    6. -
    7. Compile your resources into your portfolio and review
    8. -
    -

    - Once you have completed all of these steps, your portfolio will be prepared by {{externalName.value}} and you will be notified when it is ready. You can then check your work, and if you want to make any corrections repeat these steps to create a new version of your portfolio. -

    -
    - -
    diff --git a/src/app/projects/states/portfolio/portfolio-state.component.html b/src/app/projects/states/portfolio/portfolio-state.component.html new file mode 100644 index 0000000000..e464e313b9 --- /dev/null +++ b/src/app/projects/states/portfolio/portfolio-state.component.html @@ -0,0 +1,49 @@ +@if (project) { +
    + + @for (tab of orderedTabs; track tab.seq) { + + } + + @if (activeTab === tabs.welcomeStep) { + + } + @if (activeTab === tabs.gradeStep) { + + } + @if (activeTab === tabs.summaryStep) { + + } + @if (activeTab === tabs.otherFilesStep) { + + } + @if (activeTab === tabs.reviewStep) { + + } +
    +} diff --git a/src/app/projects/states/portfolio/portfolio-state.component.scss b/src/app/projects/states/portfolio/portfolio-state.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/portfolio/portfolio-state.component.ts b/src/app/projects/states/portfolio/portfolio-state.component.ts new file mode 100644 index 0000000000..e96f5d2eec --- /dev/null +++ b/src/app/projects/states/portfolio/portfolio-state.component.ts @@ -0,0 +1,175 @@ +import {ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {Observable, Subscription, of} from 'rxjs'; +import {Project} from 'src/app/api/models/project'; +import {GlobalStateService} from '../index/global-state.service'; + +interface PortfolioStepTab { + title: string; + seq: number; + active?: boolean; +} + +@Component({ + selector: 'f-portfolio-state', + templateUrl: './portfolio-state.component.html', + styleUrls: ['./portfolio-state.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class PortfolioStateComponent implements OnInit, OnDestroy { + @Input() public project$: Observable; + + public project: Project; + + public readonly tabs: { + welcomeStep: PortfolioStepTab; + gradeStep: PortfolioStepTab; + summaryStep: PortfolioStepTab; + otherFilesStep: PortfolioStepTab; + reviewStep: PortfolioStepTab; + } = { + welcomeStep: { + title: 'Portfolio Preparation', + seq: 1, + }, + gradeStep: { + title: 'Select Grade', + seq: 2, + }, + summaryStep: { + title: 'Learning Summary Report', + seq: 3, + }, + otherFilesStep: { + title: 'Upload Other Files', + seq: 4, + }, + reviewStep: { + title: 'Review Portfolio', + seq: 5, + }, + }; + + public readonly orderedTabs = Object.values(this.tabs).sort((a, b) => a.seq - b.seq); + public activeTab: PortfolioStepTab = this.tabs.welcomeStep; + + private projectSub?: Subscription; + + constructor( + private globalStateService: GlobalStateService, + private route: ActivatedRoute, + ) {} + + public get selectedTabIndex(): number { + return Math.max(0, (this.activeTab?.seq ?? 1) - 1); + } + + public get hasSubmittedGrade(): boolean { + return this.project?.submittedGrade !== null && this.project?.submittedGrade !== undefined; + } + + public get hasLearningSummaryReport(): boolean { + return ( + Boolean(this.project?.usesDraftLearningSummary) || + (this.project?.portfolioFiles ?? []).some((file) => file.idx === 0) + ); + } + + ngOnInit(): void { + this.project$ = this.project$ ?? of(this.route.parent?.snapshot.data.project as Project); + + this.projectSub = this.project$?.subscribe((project) => { + if (!project) { + return; + } + + this.project = project; + this.setInitialActiveTab(); + }); + } + + ngOnDestroy(): void { + this.projectSub?.unsubscribe(); + } + + public onSelectedTabIndexChange(index: number): void { + const targetTab = this.orderedTabs[index]; + if (!targetTab || this.isTabDisabled(targetTab)) { + return; + } + + this.setActiveTab(targetTab); + } + + public isTabDisabled(tab: PortfolioStepTab): boolean { + if (!tab || !this.project) { + return true; + } + + // Keep the current tab selectable even when other steps are locked. + if (tab.seq === this.activeTab?.seq) { + return false; + } + + // Portfolio is compiling or ready; review step only. + if (this.project.portfolioAvailable || this.project.compilePortfolio) { + return tab.seq !== this.tabs.reviewStep.seq; + } + + // No submitted grade: allow steps 1-2. + if (!this.hasSubmittedGrade) { + return tab.seq > this.tabs.gradeStep.seq; + } + + // No learning summary report: allow steps 1-3. + if (!this.hasLearningSummaryReport) { + return tab.seq > this.tabs.summaryStep.seq; + } + + // Once grade + learning summary requirements are met, allow review before compilation. + return false; + } + + public setActiveTab(tab: PortfolioStepTab): void { + if (!tab) { + return; + } + + if (this.activeTab === tab) { + return; + } + + this.activeTab = tab; + this.orderedTabs.forEach((currentTab) => { + currentTab.active = currentTab === tab; + }); + } + + public advanceActiveTab(advanceBy: 1 | -1): void { + const newSeq = (this.activeTab?.seq ?? 1) + advanceBy; + const nextTab = this.orderedTabs.find((tab) => tab.seq === newSeq); + + if (nextTab) { + this.setActiveTab(nextTab); + } + } + + private projectHasLearningSummaryReportFile(): boolean { + return (this.project?.portfolioFiles ?? []).some((file) => file.idx === 0); + } + + private setInitialActiveTab(): void { + if (this.project.portfolioAvailable || this.project.compilePortfolio) { + this.setActiveTab(this.tabs.reviewStep); + } else if (!this.hasSubmittedGrade) { + this.setActiveTab(this.tabs.welcomeStep); + } else if (this.project.usesDraftLearningSummary) { + this.setActiveTab(this.tabs.summaryStep); + } else if (this.projectHasLearningSummaryReportFile()) { + this.setActiveTab(this.tabs.otherFilesStep); + } else { + this.setActiveTab(this.tabs.welcomeStep); + } + } +} diff --git a/src/app/projects/states/portfolio/portfolio.coffee b/src/app/projects/states/portfolio/portfolio.coffee deleted file mode 100644 index 0224ae100d..0000000000 --- a/src/app/projects/states/portfolio/portfolio.coffee +++ /dev/null @@ -1,107 +0,0 @@ -angular.module('doubtfire.projects.states.portfolio', [ - 'doubtfire.projects.states.portfolio.directives' -]) - -# -# Tasks state for projects -# -.config(($stateProvider) -> - $stateProvider.state 'projects/portfolio', { - parent: 'projects/index' - url: '/portfolio' - controller: 'ProjectsPortfolioStateCtrl' - templateUrl: 'projects/states/portfolio/portfolio.tpl.html' - data: - task: "Portfolio Creation" - pageTitle: "_Home_" - roleWhitelist: ['Tutor', 'Convenor', 'Admin', 'Student', 'Auditor'] - } -) - -.controller("ProjectsPortfolioStateCtrl", ($scope, alertService, DoubtfireConstants, newTaskService, gradeService, analyticsService) -> - # - # Active task tab group - # - $scope.tabs = - welcomeStep: - title: "Portfolio Preparation" - seq: 1 - gradeStep: - title: "Select Grade" - seq: 2 - summaryStep: - title: "Learning Summary Report" - seq: 3 - otherFilesStep: - title: "Upload Other Files" - seq: 4 - reviewStep: - title: "Review Portfolio" - seq: 5 - $scope.setActiveTab = (tab) -> - $scope.activeTab = tab - $scope.activeTab.active = true - analyticsService.event 'Portfolio Wizard', 'Switched to Step', "#{tab.title} Step" - $scope.advanceActiveTab = (advanceBy) -> - newSeq = $scope.activeTab.seq + advanceBy - $scope.setActiveTab (tab for tabKey, tab of $scope.tabs when tab.seq is newSeq)[0] - - $scope.projectHasLearningSummaryReport = -> - _.filter($scope.project.portfolioFiles, { idx: 0 }).length > 0 - - # Determine whether project is using draft learning summary - $scope.projectHasDraftLearningSummaryReport = $scope.project.usesDraftLearningSummary - - # Called whenever a new file is added to the portfolio - $scope.addNewFilesToPortfolio = (newFile) -> - $scope.project.portfolioFiles.push newFile - - # Delete file from the portfolio - $scope.deleteFileFromPortfolio = (file) -> - $scope.project.deleteFileFromPortfolio(file).subscribe({ - next: (response) -> - alertService.success( "File removed.", 2000) - $scope.project.portfolioFiles.splice - error: (response) -> - alertService.error "Error removing file - #{response}" - }) - - # Update targetGrade value on change - $scope.$watch 'project.targetGrade', (newValue) -> - $scope.targetGrade = gradeService.grades[newValue] - - # Get the confugurable, external name of Doubtfire - $scope.externalName = DoubtfireConstants.ExternalName - - # Get only extra files submitted - $scope.extraFiles = -> - _.filter $scope.project.portfolioFiles, (f) -> - # when f.idx is 0 it's the LSR - f.idx isnt 0 - - # Gets selected tasks in the task selector - $scope.selectedTasks = -> - # Filter by included in portfolio - tasks = $scope.project.tasks - tasks = _.filter tasks, (t) -> !_.includes(newTaskService.toBeWorkedOn, t.status) - _.sortBy tasks, (t) -> t.definition.seq - - # Jump to a step - if $scope.project.portfolioAvailable or $scope.project.compilePortfolio - $scope.setActiveTab $scope.tabs.reviewStep - else if not $scope.project.submittedGrade? - $scope.setActiveTab $scope.tabs.welcomeStep - else if $scope.projectHasDraftLearningSummaryReport - $scope.setActiveTab $scope.tabs.summaryStep - else if $scope.projectHasLearningSummaryReport() - $scope.setActiveTab $scope.tabs.otherFilesStep - else - $scope.setActiveTab $scope.tabs.welcomeStep - - # - # Functions from newTaskService to get data - # - $scope.statusText = newTaskService.statusText - $scope.statusData = newTaskService.statusData - $scope.statusClass = newTaskService.statusClass -) diff --git a/src/app/projects/states/portfolio/portfolio.scss b/src/app/projects/states/portfolio/portfolio.scss deleted file mode 100644 index 464a6b4838..0000000000 --- a/src/app/projects/states/portfolio/portfolio.scss +++ /dev/null @@ -1,10 +0,0 @@ -#portfolio-state > .panel { - width: 75%; - margin: 0 auto; - .card { - margin-top: 1.5em; - margin-left: auto; - margin-right: auto; - width: 90%; - } -} diff --git a/src/app/projects/states/portfolio/portfolio.tpl.html b/src/app/projects/states/portfolio/portfolio.tpl.html deleted file mode 100644 index 934f90e76f..0000000000 --- a/src/app/projects/states/portfolio/portfolio.tpl.html +++ /dev/null @@ -1,18 +0,0 @@ -
    - - - - Step {{tab.seq}}: {{tab.title}} - - - - - - - - - -
    diff --git a/src/app/projects/states/project-root-state.component.css b/src/app/projects/states/project-root-state.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/project-root-state.component.html b/src/app/projects/states/project-root-state.component.html new file mode 100644 index 0000000000..795a7e5baf --- /dev/null +++ b/src/app/projects/states/project-root-state.component.html @@ -0,0 +1 @@ + diff --git a/src/app/projects/states/project-root-state.component.ts b/src/app/projects/states/project-root-state.component.ts new file mode 100644 index 0000000000..a9d436dde3 --- /dev/null +++ b/src/app/projects/states/project-root-state.component.ts @@ -0,0 +1,53 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import {ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {Observable, Subscription, of} from 'rxjs'; +import {Project} from 'src/app/api/models/doubtfire-model'; + +interface ProjectRouteChild { + project$?: Observable; + taskListWidth?: number; + taskListWidthChange?: EventEmitter; +} + +@Component({ + selector: 'f-project-root-state', + templateUrl: './project-root-state.component.html', + styleUrl: './project-root-state.component.css', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class ProjectRootStateComponent implements OnDestroy { + @Input() public project$: Observable; + + private readonly taskListExpandedWidth = 400; + private taskListWidth = this.taskListExpandedWidth; + private taskListWidthSub?: Subscription; + + constructor(private activatedRoute: ActivatedRoute) { + const project = this.activatedRoute.snapshot.data.project as Project; + this.project$ = this.project$ ?? (project ? of(project) : undefined); + } + + onActivate(component: ProjectRouteChild): void { + this.taskListWidthSub?.unsubscribe(); + + if ('project$' in component) { + component.project$ = this.project$; + } + + if ('taskListWidth' in component) { + component.taskListWidth = this.taskListWidth; + } + + if (component.taskListWidthChange) { + this.taskListWidthSub = component.taskListWidthChange.subscribe((width) => { + this.taskListWidth = width; + }); + } + } + + ngOnDestroy(): void { + this.taskListWidthSub?.unsubscribe(); + } +} diff --git a/src/app/projects/states/staff-notes/staff-notes.component.html b/src/app/projects/states/staff-notes/staff-notes.component.html index 9c89b3a391..13b472e7cb 100644 --- a/src/app/projects/states/staff-notes/staff-notes.component.html +++ b/src/app/projects/states/staff-notes/staff-notes.component.html @@ -1,43 +1,43 @@ -
    +
    @if (!loadingStaffNotes && project?.staffNoteCount === 0) { -
    +
    No staff notes for {{ project.student.preferredName }} {{ project.student.lastName }}
    } -
    +
    @if (!loadingStaffNotes) { @for (note of project?.staffNoteCache?.currentValues; track note) { @if (note.replyToId) {
    - reply + reply
    @if (note.replyTo) { Replying to {{ note.replyTo.user.preferredName }} {{ note.replyTo.user.lastName }} ({{ note.replyTo.user.nickname }}) - {{ note.replyTo.note }} + {{ note.replyTo.note }} } @else { - Replying to: Deleted note + Replying to: Deleted note }
    } -
    +
    @if (note.authorIsMe) { edit } @@ -46,33 +46,33 @@
    - + {{ note.user?.firstName }} {{ note.user?.lastName }}
    {{ note.createdAt | humanizedDate }}
    - + @if (editingNote && editingNote.id === note.id) { - + Update Note -
    - - +
    @@ -92,35 +92,35 @@ }
    -
    +
    @if (replyingToNote) {
    - reply + reply
    Replying to {{ replyingToNote.user.firstName }} {{ replyingToNote.user.lastName }} ({{ replyingToNote.user.nickname }}) - {{ replyingToNote.note }} + {{ replyingToNote.note }}
    close
    } - + Staff Note
    - +
    diff --git a/src/app/projects/states/staff-notes/staff-notes.component.spec.ts b/src/app/projects/states/staff-notes/staff-notes.component.spec.ts index fcd374cbfe..270c10d536 100644 --- a/src/app/projects/states/staff-notes/staff-notes.component.spec.ts +++ b/src/app/projects/states/staff-notes/staff-notes.component.spec.ts @@ -1,19 +1,36 @@ +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; - +import {UserService} from 'src/app/api/models/doubtfire-model'; +import {StaffNoteService} from 'src/app/api/services/staff-note.service'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; +import {AlertService} from 'src/app/common/services/alert.service'; import {StaffNotesComponent} from './staff-notes.component'; +const emptyProvider = {}; + describe('StaffNotesComponent', () => { let component: StaffNotesComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [StaffNotesComponent], - }).compileComponents(); + declarations: [StaffNotesComponent], + providers: [ + {provide: UserService, useValue: emptyProvider}, + {provide: StaffNoteService, useValue: emptyProvider}, + {provide: AlertService, useValue: emptyProvider}, + {provide: ConfirmationModalService, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(StaffNotesComponent, {set: {template: ''}}) + .compileComponents(); + }); + beforeEach(() => { fixture = TestBed.createComponent(StaffNotesComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/projects/states/staff-notes/staff-notes.component.ts b/src/app/projects/states/staff-notes/staff-notes.component.ts index 78821277a5..6ca071e75e 100644 --- a/src/app/projects/states/staff-notes/staff-notes.component.ts +++ b/src/app/projects/states/staff-notes/staff-notes.component.ts @@ -1,4 +1,11 @@ -import {Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + ElementRef, + Input, + OnInit, + ViewChild, +} from '@angular/core'; import {Project, UserService} from 'src/app/api/models/doubtfire-model'; import {StaffNote} from 'src/app/api/models/staff-note'; import {StaffNoteService} from 'src/app/api/services/staff-note.service'; @@ -9,6 +16,8 @@ import {AlertService} from 'src/app/common/services/alert.service'; selector: 'f-staff-notes', templateUrl: './staff-notes.component.html', styleUrl: './staff-notes.component.scss', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class StaffNotesComponent implements OnInit { @ViewChild('staffNotesContainer') staffNotesContainer!: ElementRef; @@ -35,7 +44,7 @@ export class StaffNotesComponent implements OnInit { ) {} ngOnInit(): void { this.loadingStaffNotes = true; - this.staffNoteService.loadStaffNotes(this.project).subscribe((notes) => { + this.staffNoteService.loadStaffNotes(this.project).subscribe((_notes) => { this.loadingStaffNotes = false; this.staffNoteService.updateStaffNoteReplies(this.project?.staffNoteCache.currentValues); this.scrollDown(); @@ -62,7 +71,7 @@ export class StaffNotesComponent implements OnInit { this.noteText = ''; this.staffNoteService.addNote(this.project, noteText, this.replyingToNote).subscribe({ - next: (note) => { + next: (_note) => { this.alertService.success('Succesfully submitted note', 4000); this.scrollDown(); this.project.staffNoteCount++; @@ -83,7 +92,7 @@ export class StaffNotesComponent implements OnInit { } this.staffNoteService.updateNote(this.project, this.editingNote, noteText).subscribe({ - next: (note) => { + next: (_note) => { this.alertService.success('Succesfully updated note', 4000); this.editingNote = null; this.editingNoteText = ''; @@ -132,7 +141,6 @@ export class StaffNotesComponent implements OnInit { public autoResizeStaffNoteEditor() { const el = this.staffNoteEditor.nativeElement; el.style.height = 'auto'; - el.offsetHeight; el.style.height = el.scrollHeight + 'px'; } diff --git a/src/app/projects/states/states.coffee b/src/app/projects/states/states.coffee deleted file mode 100644 index 01b9d42dd4..0000000000 --- a/src/app/projects/states/states.coffee +++ /dev/null @@ -1,8 +0,0 @@ -angular.module('doubtfire.projects.states', [ - 'doubtfire.projects.states.index' - 'doubtfire.projects.states.dashboard' - 'doubtfire.projects.states.tutorials' - 'doubtfire.projects.states.portfolio' - 'doubtfire.projects.states.groups' - 'doubtfire.projects.states.outcomes' -]) diff --git a/src/app/projects/states/tutor-discussion/tutor-discussion.component.html b/src/app/projects/states/tutor-discussion/tutor-discussion.component.html index 97c5d30809..78089a4f36 100644 --- a/src/app/projects/states/tutor-discussion/tutor-discussion.component.html +++ b/src/app/projects/states/tutor-discussion/tutor-discussion.component.html @@ -1,57 +1,68 @@
    @if (loadingStudentData) { }
    -
    +
    Scan a student's QR code to instantly load their project and mark submissions.
    -
    +
    @if (attendance && unit) { -
    - +
    + - @for (td of unit?.taskDefinitionCache.values | async; track td) { + @for ( + td of $safeNavigationMigration(unit?.taskDefinitionCache.values) | async; + track td + ) { {{ td.abbreviation }} - {{ td.name }} } @if (selectedTaskDefinition) { - + }
    } -
    -
    +
    +
    @if (project && project?.student) { -
    -
    + +
    {{ project?.student?.firstName }} {{ project?.student?.lastName }} @if (project?.student.studentId) { @@ -59,53 +70,58 @@ }
    - Target Grade: {{ getTargetTradeString(project?.targetGrade) }} + Target Grade: {{ getTargetTradeString($safeNavigationMigration(project?.targetGrade)) }}
    } @else {
    -
    Click the QR code to open the scanner..
    +
    Click the QR code to open the scanner..
    } -
    @if (project && !filteredTasks.length) { -
    No tasks to discuss.
    +
    No tasks to discuss.
    } - + @for (task of filteredTasks; track task) { @if (task) {
    - -
    -

    {{ task.definition.name }}

    - + + +
    +

    {{ task.definition.name }}

    + {{ task.definition.abbreviation }} - {{ getTargetTradeString(task.definition.targetGrade) }} Task @@ -118,17 +134,17 @@

    {{ task.definition.name }}

    } } @else { - } @@ -163,13 +179,13 @@

    {{ task.definition.name }}

    @if (project && filteredTasks.length) { -
    -
    +
    +
    @if (attendance) { } @else {

    group + @if (selectedTasksIncludeDiscuss) { + + } }
    @@ -216,24 +243,25 @@

    {{ task.definition.name }}

    @if (footerTabView === TutorDiscussionTabView.SHOW_COMMENTS) {
    - +
    diff --git a/src/app/projects/states/tutor-discussion/tutor-discussion.component.scss b/src/app/projects/states/tutor-discussion/tutor-discussion.component.scss index 37fe08fe84..b2d80373f0 100644 --- a/src/app/projects/states/tutor-discussion/tutor-discussion.component.scss +++ b/src/app/projects/states/tutor-discussion/tutor-discussion.component.scss @@ -2,11 +2,11 @@ @use '@angular/material' as mat; // $my-palette: mat.define-palette(mat.$indigo-palette); -@import '../../../../theme.scss'; -@import '../../../../styles/mixins/task-list.scss'; -@import '../../../../styles/mixins/scrollable.scss'; +@use 'theme' as *; +@use 'styles/mixins/task-list' as *; +@use 'styles/mixins/scrollable' as *; -$my-palette: mat.define-palette($md-formatif); +// $my-palette: mat.define-palette($md-formatif); :host { --background-gray: rgba(0, 0, 0, 0.04); @@ -39,7 +39,8 @@ user-icon { background-color: transparent; &.active { - background-color: mat.get-color-from-palette($my-palette, 500); + // background-color: mat.get-color-from-palette($my-palette, 500); + background-color: #3939ff; } &.active.similarities { @@ -96,17 +97,40 @@ user-icon { #html5-qrcode-button-camera-stop, #html5-qrcode-button-camera-start { - @apply bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded shadow w-full uppercase; + width: 100%; max-width: 1000px; height: 45px; + padding: 0.25rem 0.625rem; margin-top: 15px; + border-radius: 0.25rem; + box-shadow: + 0 1px 3px 0 rgb(0 0 0 / 10%), + 0 1px 2px -1px rgb(0 0 0 / 10%); + font-weight: 500; + color: white; + text-transform: uppercase; + background-color: #2563eb; +} + +#html5-qrcode-button-camera-stop:hover, +#html5-qrcode-button-camera-start:hover, +#html5-qrcode-button-camera-permission:hover { + background-color: #1d4ed8; } #html5-qrcode-button-camera-permission { - @apply bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded shadow w-full uppercase; width: 100%; max-width: 250px; height: 60px; + padding: 0.25rem 0.625rem; + border-radius: 0.25rem; + box-shadow: + 0 1px 3px 0 rgb(0 0 0 / 10%), + 0 1px 2px -1px rgb(0 0 0 / 10%); + font-weight: 500; + color: white; + text-transform: uppercase; + background-color: #2563eb; } .comment-user-icon-wrapper.ng-star-inserted { diff --git a/src/app/projects/states/tutor-discussion/tutor-discussion.component.spec.ts b/src/app/projects/states/tutor-discussion/tutor-discussion.component.spec.ts index 25f6e0711e..75306dc282 100644 --- a/src/app/projects/states/tutor-discussion/tutor-discussion.component.spec.ts +++ b/src/app/projects/states/tutor-discussion/tutor-discussion.component.spec.ts @@ -1,19 +1,55 @@ +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; - +import {MatDialog} from '@angular/material/dialog'; +import {ActivatedRoute, Router} from '@angular/router'; +import { + AuthenticationService, + ProjectService, + TaskCommentService, + TaskService, + UnitService, + UserService, +} from 'src/app/api/models/doubtfire-model'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; +import {DiscussedInClassReasonModalService} from 'src/app/common/modals/discussed-in-class-reason-modal/discussed-in-class-reason-modal.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {GradeService} from 'src/app/common/services/grade.service'; import {TutorDiscussionComponent} from './tutor-discussion.component'; +const emptyProvider = {}; + describe('TutorDiscussionComponent', () => { let component: TutorDiscussionComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TutorDiscussionComponent], - }).compileComponents(); + declarations: [TutorDiscussionComponent], + providers: [ + {provide: UnitService, useValue: emptyProvider}, + {provide: AuthenticationService, useValue: emptyProvider}, + {provide: UserService, useValue: emptyProvider}, + {provide: ProjectService, useValue: emptyProvider}, + {provide: GradeService, useValue: emptyProvider}, + {provide: Router, useValue: emptyProvider}, + {provide: ActivatedRoute, useValue: emptyProvider}, + {provide: AlertService, useValue: emptyProvider}, + {provide: ConfirmationModalService, useValue: emptyProvider}, + {provide: DiscussedInClassReasonModalService, useValue: emptyProvider}, + {provide: TaskCommentService, useValue: emptyProvider}, + {provide: TaskService, useValue: emptyProvider}, + {provide: MatDialog, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(TutorDiscussionComponent, {set: {template: ''}}) + .compileComponents(); + }); + beforeEach(() => { fixture = TestBed.createComponent(TutorDiscussionComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts b/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts index c9e43c890a..8845bfecd1 100644 --- a/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts +++ b/src/app/projects/states/tutor-discussion/tutor-discussion.component.ts @@ -1,8 +1,19 @@ -import {AfterViewInit, Component, Input, ViewChild, ViewEncapsulation} from '@angular/core'; +import {Html5QrcodeScanner, Html5QrcodeScannerState} from 'html5-qrcode'; +import {DOCUMENT} from '@angular/common'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + Inject, + Input, + OnDestroy, + ViewChild, + ViewEncapsulation, +} from '@angular/core'; +import {MatDialog} from '@angular/material/dialog'; import {MatSelectionList} from '@angular/material/list'; import {MatTabChangeEvent} from '@angular/material/tabs'; -import {StateService, UIRouter} from '@uirouter/core'; -import {Html5QrcodeScanner, Html5QrcodeScannerState} from 'html5-qrcode'; +import {ActivatedRoute, Router} from '@angular/router'; import { AuthenticationService, Project, @@ -21,6 +32,7 @@ import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal import {DiscussedInClassReasonModalService} from 'src/app/common/modals/discussed-in-class-reason-modal/discussed-in-class-reason-modal.service'; import {AlertService} from 'src/app/common/services/alert.service'; import {GradeService} from 'src/app/common/services/grade.service'; +import {AddEngagementDialogComponent} from '../dashboard/directives/progress-dashboard/engagement-passport-card/add-engagement-dialog/add-engagement-dialog.component'; enum TutorDiscussionTabView { SHOW_COMMENTS, @@ -31,10 +43,14 @@ enum TutorDiscussionTabView { selector: 'f-tutor-discussion', templateUrl: './tutor-discussion.component.html', styleUrl: './tutor-discussion.component.scss', - encapsulation: ViewEncapsulation.None, // enables custom material-ui css + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class TutorDiscussionComponent implements AfterViewInit { +export class TutorDiscussionComponent implements AfterViewInit, OnDestroy { private readonly discussedInClassNotePrefix = `I'm manually marking this discussed in class because...`; + private readonly mobileDiscussionViewportContent = + 'width=device-width, initial-scale=0.8, maximum-scale=5'; @Input() unitId: number; @Input() username: string; @@ -50,11 +66,15 @@ export class TutorDiscussionComponent implements AfterViewInit { public project: Project | null; public selectedTask: Task | null; + public allowHover = true; + public isNarrow = false; public scanningQr: boolean = false; public loadingStudentData: boolean = false; - private html5QrcodeScanner: Html5QrcodeScanner; + private html5QrcodeScanner?: Html5QrcodeScanner; + private originalViewportContent: string | null = null; + private mobileDiscussionZoomApplied = false; private _unitId: number; private _username: string; @@ -63,20 +83,27 @@ export class TutorDiscussionComponent implements AfterViewInit { public footerTabView: TutorDiscussionTabView = TutorDiscussionTabView.SHOW_COMMENTS; constructor( + @Inject(DOCUMENT) private document: Document, private unitService: UnitService, private authService: AuthenticationService, private userService: UserService, private projectService: ProjectService, private gradeService: GradeService, - private state: StateService, + private router: Router, + private activatedRoute: ActivatedRoute, private alertService: AlertService, private confirmationModalService: ConfirmationModalService, private discussedInClassReasonModal: DiscussedInClassReasonModalService, - private route: UIRouter, private taskCommentService: TaskCommentService, private taskService: TaskService, + private dialog: MatDialog, ) {} + public ngOnDestroy(): void { + this.stopQrScanner(); + this.restoreViewportZoom(); + } + public currentUserTutorsInStream(tutorialStream: TutorialStream): boolean { const user = this.userService.currentUser; const tutorials = this.unit.tutorials.filter( @@ -113,9 +140,21 @@ export class TutorDiscussionComponent implements AfterViewInit { } public ngAfterViewInit(): void { + this.unitId = + this.unitId ?? + Number( + this.activatedRoute.parent?.snapshot.paramMap.get('unitId') ?? + this.activatedRoute.snapshot.queryParamMap.get('unitId'), + ); + this.username = this.username ?? this.activatedRoute.snapshot.queryParamMap.get('username'); + this.attendance = + this.attendance ?? + this.activatedRoute.snapshot.data.attendance ?? + this.activatedRoute.snapshot.queryParamMap.get('attendance') === 'true'; + this.authService.afterAuthCall((result) => { if (!result) { - return this.state.go('sign_in'); + return this.router.navigateByUrl('/sign_in'); } else { if (this.userService.currentUser.systemRole === 'Student') { // Avoid prompting students for camera permissions before redirecting to unauthorised state @@ -129,7 +168,7 @@ export class TutorDiscussionComponent implements AfterViewInit { this._username = this.username; this.getStudentTasks(); } else { - this.scanQrCode(); + setTimeout(() => this.scanQrCode()); } } else { this.getUnit().then((u) => { @@ -170,21 +209,21 @@ export class TutorDiscussionComponent implements AfterViewInit { public closeQrReader(): void { if (!this.project) { // Exiting the route entirely + this.stopQrScanner(); if (this.unitId) { - this.route.stateService.go('units/tasks/inbox', { - unitId: this.unitId, - }); + this.router.navigate(['/units', this.unitId, 'tasks', 'inbox']); } else { - this.route.stateService.go('home'); + this.router.navigateByUrl('/home'); } } else { // Close the camera view this.scanningQr = false; + this.stopQrScanner(); } } private changeProject() { - this.html5QrcodeScanner.pause(true); + this.html5QrcodeScanner?.pause(true); this.loadingStudentData = true; setTimeout(() => { try { @@ -194,14 +233,112 @@ export class TutorDiscussionComponent implements AfterViewInit { this.loadingStudentData = false; setTimeout(() => { - this.html5QrcodeScanner.resume(); + this.html5QrcodeScanner?.resume(); }, 2000); } }); } + private applyMobileDiscussionZoom(): void { + if (!window.matchMedia('(max-width: 768px)').matches) { + return; + } + + const viewport = this.document.querySelector('meta[name="viewport"]'); + if (!viewport) { + return; + } + + this.originalViewportContent ??= viewport.getAttribute('content'); + viewport.setAttribute('content', this.mobileDiscussionViewportContent); + this.mobileDiscussionZoomApplied = true; + } + + private restoreViewportZoom(): void { + if (!this.mobileDiscussionZoomApplied) { + return; + } + + const viewport = this.document.querySelector('meta[name="viewport"]'); + if (viewport && this.originalViewportContent) { + viewport.setAttribute('content', this.originalViewportContent); + } + + this.mobileDiscussionZoomApplied = false; + } + hideQrScannerBloat: boolean = true; + private async stopQrScanner(): Promise { + if (!this.html5QrcodeScanner) { + return; + } + + try { + await this.html5QrcodeScanner.clear(); + } catch (_e) { + // The scanner may already be stopped by its own controls. + } finally { + this.html5QrcodeScanner = undefined; + } + } + + private async getCameraPermissionState(): Promise { + if (!navigator.permissions?.query) { + return null; + } + + try { + const permissionStatus = await navigator.permissions.query({ + name: 'camera' as PermissionName, + }); + return permissionStatus.state; + } catch (_e) { + return null; + } + } + + private async prepareQrScannerCamera(): Promise { + const cachedScannerData = localStorage.getItem('HTML5_QRCODE_DATA'); + const cameraPermissionState = await this.getCameraPermissionState(); + if (cachedScannerData) { + try { + const html5QrcodeData = JSON.parse(cachedScannerData); + if (html5QrcodeData?.hasPermission && cameraPermissionState === 'granted') { + this.hideQrScannerBloat = html5QrcodeData.lastUsedCameraId ? true : false; + return; + } + } catch (_e) { + localStorage.removeItem('HTML5_QRCODE_DATA'); + } + } + + // Trigger video permissions once so device labels are available for back camera selection. + // Stopping these tracks releases the camera; the browser keeps the permission grant. + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + + // Find the deviceId of the back camera + const backCameras = devices.filter( + (d) => d.kind === 'videoinput' && d.label.toLowerCase().includes('back camera'), + ); + + const html5QrcodeData = { + hasPermission: true, + lastUsedCameraId: backCameras[0]?.deviceId ?? null, + }; + localStorage.setItem('HTML5_QRCODE_DATA', JSON.stringify(html5QrcodeData)); + + // Hide most of the UI if we found and set the back camera + // Otherwise, we need to reveal the UI so that the user can select which camera to use + this.hideQrScannerBloat = html5QrcodeData.lastUsedCameraId ? true : false; + } finally { + stream.getTracks().forEach((track) => track.stop()); + } + } + public scanQrCode() { if (this.attendance && !this.selectedTaskDefinition) { this.alertService.error('You must select a task first', 3000); @@ -211,39 +348,13 @@ export class TutorDiscussionComponent implements AfterViewInit { this.scanningQr = true; this.loadingStudentData = false; - if ( - this.html5QrcodeScanner && - this.html5QrcodeScanner.getState() === Html5QrcodeScannerState.PAUSED - ) { + if (this.html5QrcodeScanner?.getState() === Html5QrcodeScannerState.PAUSED) { this.html5QrcodeScanner.resume(); } else { - this.html5QrcodeScanner?.clear(); - - // Trigger video permissions - // If we call getUserMedia when html5QrcodeScanner is already active, the scanner will break on iOS - navigator.mediaDevices - .getUserMedia({video: true}) + this.stopQrScanner() + .then(() => this.prepareQrScannerCamera()) .then(() => { - return navigator.mediaDevices.enumerateDevices(); - }) - .then((devices) => { - // Find the deviceId of the back camera - const backCameras = devices.filter( - (d) => d.kind === 'videoinput' && d.label.toLowerCase().includes('back camera'), - ); - - const html5QrcodeData = { - hasPermission: true, - lastUsedCameraId: backCameras[0]?.deviceId ?? null, - }; - localStorage.setItem('HTML5_QRCODE_DATA', JSON.stringify(html5QrcodeData)); - - // Hide most of the UI if we found and set the back camera - // Otherwise, we need to reveal the UI so that the user can select which camera to use - this.hideQrScannerBloat = html5QrcodeData.lastUsedCameraId ? true : false; - setTimeout(() => { - // Only init the scanner once and let it run in the background this.html5QrcodeScanner = new Html5QrcodeScanner( 'qr-reader', // id of the div in the html {fps: 10, qrbox: 250}, @@ -259,10 +370,27 @@ export class TutorDiscussionComponent implements AfterViewInit { }, ); }); + }) + .catch((_e) => { + this.scanningQr = false; + this.alertService.error('Camera permission is required to scan QR codes', 3000); }); } } + public openAddEngagementDialog(): void { + if (!this.project) { + return; + } + + this.dialog.open(AddEngagementDialogComponent, { + data: {project: this.project}, + width: 'calc(100vw - 32px)', + maxWidth: '640px', + autoFocus: false, + }); + } + public loadTaskComments(event: MouseEvent, task: Task) { event.stopPropagation(); this.selectedTask = task; @@ -354,6 +482,14 @@ export class TutorDiscussionComponent implements AfterViewInit { }); } + public get selectedTasksIncludeDiscuss(): boolean { + const selectedTasks = this.tasksList?.selectedOptions?.selected ?? []; + return selectedTasks.some((taskOption) => { + const task = taskOption.value as Task; + return task.status === 'discuss'; + }); + } + public markSelectedTasksDicussed() { const selectedTasks = this.tasksList.selectedOptions.selected; if (!this.unit?.enforceFeedbackBeforeDiscussedInClass) { @@ -444,7 +580,7 @@ export class TutorDiscussionComponent implements AfterViewInit { } public getTargetTradeString(grade: number) { - return this.gradeService.grades[grade]; + return this.gradeService.gradeLabel(grade, this.project?.unit); } public refresh() { @@ -460,6 +596,7 @@ export class TutorDiscussionComponent implements AfterViewInit { // 'complete', 'fix_and_resubmit', 'redo', + 'rediscuss', ]; public viewAllSubmittedTasks() { @@ -523,6 +660,8 @@ export class TutorDiscussionComponent implements AfterViewInit { this.project = project; this.scanningQr = false; this.loadingStudentData = false; + this.stopQrScanner(); + this.applyMobileDiscussionZoom(); }) .catch((e) => { console.error(e); diff --git a/src/app/projects/states/tutor-notes/tutor-notes.component.html b/src/app/projects/states/tutor-notes/tutor-notes.component.html index 5d65d88936..ec2c4581d4 100644 --- a/src/app/projects/states/tutor-notes/tutor-notes.component.html +++ b/src/app/projects/states/tutor-notes/tutor-notes.component.html @@ -1,44 +1,44 @@ -
    +
    -
    +
    @if (!loadingTutorNotes) { @for (note of filteredNotes; track note) { @if (note.replyToId) {
    - reply + reply
    @if (note.replyTo) { Replying to {{ note.replyTo.user.preferredName }} {{ note.replyTo.user.lastName }} ({{ note.replyTo.user.nickname }}) - {{ note.replyTo.note }} + {{ note.replyTo.note }} } @else { - Replying to: Deleted note + Replying to: Deleted note }
    } -
    -
    +
    +
    @if (note.authorIsMe) { edit } @@ -47,31 +47,31 @@
    @if (!note.readByUnitRole) { @if (note.noteIsForMe) { - } } @else { -
    Read by tutor
    +
    Read by tutor
    }
    - + {{ note.user?.firstName }} {{ note.user?.lastName }}
    {{ note.createdAt | humanizedDate }}
    -
    +
    @if (note.taskDefinition) { {{ note.taskDefinition?.abbreviation }} {{ note.taskDefinition.name }} - + @if (editingNote && editingNote.id === note.id) { - + Update Note -
    - - +
    @@ -122,8 +122,8 @@ }
    -
    - +
    + @for (option of taskDefinitionFilters; track option) { {{ option }} @@ -143,32 +143,32 @@ @if (replyingToNote) {
    - reply + reply
    Replying to {{ replyingToNote.user.firstName }} {{ replyingToNote.user.lastName }} ({{ replyingToNote.user.nickname }}) - {{ replyingToNote.note }} + {{ replyingToNote.note }}
    close
    } - + Tutor note
    - +
    diff --git a/src/app/projects/states/tutor-notes/tutor-notes.component.ts b/src/app/projects/states/tutor-notes/tutor-notes.component.ts index 2b1543c247..94f258a5ae 100644 --- a/src/app/projects/states/tutor-notes/tutor-notes.component.ts +++ b/src/app/projects/states/tutor-notes/tutor-notes.component.ts @@ -1,4 +1,11 @@ -import {Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + ElementRef, + Input, + OnInit, + ViewChild, +} from '@angular/core'; import {Task, UnitRole, UserService} from 'src/app/api/models/doubtfire-model'; import {TutorNote} from 'src/app/api/models/tutor-note'; import {TutorNoteService} from 'src/app/api/services/tutor-note.service'; @@ -9,6 +16,8 @@ import {AlertService} from 'src/app/common/services/alert.service'; selector: 'f-tutor-notes', templateUrl: './tutor-notes.component.html', styleUrl: './tutor-notes.component.scss', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TutorNotesComponent implements OnInit { @ViewChild('tutorNotesContainer') tutorNotesContainer!: ElementRef; @@ -40,7 +49,7 @@ export class TutorNotesComponent implements OnInit { } this.loadingTutorNotes = true; - this.tutorNoteService.loadTutorNotes(this.unitRole).subscribe((notes) => { + this.tutorNoteService.loadTutorNotes(this.unitRole).subscribe((_notes) => { this.loadingTutorNotes = false; this.tutorNoteService.updateTutorNoteReplies(this.unitRole?.tutorNotesCache.currentValues); this.scrollDown(); @@ -160,7 +169,6 @@ export class TutorNotesComponent implements OnInit { public autoResizeTutorNoteEditor() { const el = this.tutorNoteEditor.nativeElement; el.style.height = 'auto'; - el.offsetHeight; el.style.height = el.scrollHeight + 'px'; } @@ -183,7 +191,9 @@ export class TutorNotesComponent implements OnInit { this.unitRole?.tutorNotesCache?.currentValues?.filter((note) => { const abbr = note.taskDefinition?.abbreviation; // if (!abbr) return false; // skip notes without taskDefinition - if (allSelected) return true; + if (allSelected) { + return true; + } return selected.get(abbr); }) ?? [] ); diff --git a/src/app/projects/states/tutorials/tutorials.coffee b/src/app/projects/states/tutorials/tutorials.coffee deleted file mode 100644 index 06c8d3d531..0000000000 --- a/src/app/projects/states/tutorials/tutorials.coffee +++ /dev/null @@ -1,24 +0,0 @@ -angular.module('doubtfire.projects.states.tutorials', []) - -# -# Tasks state for projects -# -.config(($stateProvider) -> - $stateProvider.state 'projects/tutorials', { - parent: 'projects/index' - url: '/tutorials' - controller: 'ProjectsTutorialsStateCtrl' - templateUrl: 'projects/states/tutorials/tutorials.tpl.html' - data: - task: "Tutorial List" - pageTitle: "_Home_" - roleWhitelist: ['Tutor', 'Convenor', 'Admin', 'Student', 'Auditor'] - } -) - -.controller("ProjectsTutorialsStateCtrl", ($scope) -> - if $scope.unit.tutorialStreamsCache.size > 0 - $scope.sortOrder = 'tutorialStream.name' - else - $scope.sortOrder = 'abbreviation' -) diff --git a/src/app/projects/states/tutorials/tutorials.component.html b/src/app/projects/states/tutorials/tutorials.component.html new file mode 100644 index 0000000000..b953b21592 --- /dev/null +++ b/src/app/projects/states/tutorials/tutorials.component.html @@ -0,0 +1,108 @@ +@if (project && unit) { +
    +
    +

    Tutorials

    +

    + View available tutorials and manage your enrolment. Note that availability is subject to + capacity. If you are unable to enrol in a tutorial, please contact your unit coordinator. +

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Stream + @if (unit.tutorialStreamsCache.size > 0) { +
    {{ tutorial.tutorialStream?.name || 'All' }}
    + } @else { +
    N/A
    + } +
    Campus + {{ tutorial.campus?.name || 'All' }} + Code + {{ tutorial.abbreviation }} + Day + {{ tutorial.meetingDay }} + Time + {{ shortTime(tutorial.meetingTime) }} + Room + {{ tutorial.meetingLocation }} + Tutor + {{ tutorial.tutorName }} + Actions + @if (project.isEnrolledIn(tutorial)) { + @if (unit.allowStudentChangeTutorial) { + + } @else { +
    + Enrolled +
    + } + } @else if (unit.allowStudentChangeTutorial) { + + } @else { +
    + + } +
    +
    +} diff --git a/src/app/projects/states/tutorials/tutorials.component.scss b/src/app/projects/states/tutorials/tutorials.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/tutorials/tutorials.component.ts b/src/app/projects/states/tutorials/tutorials.component.ts new file mode 100644 index 0000000000..07f4879f06 --- /dev/null +++ b/src/app/projects/states/tutorials/tutorials.component.ts @@ -0,0 +1,158 @@ +import {ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {Sort} from '@angular/material/sort'; +import {MatTableDataSource} from '@angular/material/table'; +import {ActivatedRoute} from '@angular/router'; +import {Observable, Subscription, of} from 'rxjs'; +import {Project, Tutorial, Unit} from 'src/app/api/models/doubtfire-model'; + +@Component({ + selector: 'f-tutorials', + templateUrl: './tutorials.component.html', + styleUrls: ['./tutorials.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class TutorialsComponent implements OnInit, OnDestroy { + @Input() public project$: Observable; + + filteredTutorials: Tutorial[] = []; + + project: Project; + unit: Unit; + + displayedColumns: string[] = [ + 'stream', + 'campus', + 'code', + 'day', + 'time', + 'room', + 'tutor', + 'actions', + ]; + + dataSource: MatTableDataSource = new MatTableDataSource([]); + + private projectSub?: Subscription; + + constructor(private route: ActivatedRoute) {} + + ngOnInit(): void { + this.project$ = this.project$ ?? of(this.route.parent?.snapshot.data.project as Project); + + this.projectSub = this.project$?.subscribe((project) => { + if (!project || !project.unit) { + return; + } + + this.project = project; + this.unit = project.unit; + this.filteredTutorials = this.tutorialCampusFilter([...(this.unit.tutorials ?? [])], project); + this.dataSource.data = this.filteredTutorials; + }); + } + + ngOnDestroy(): void { + this.projectSub?.unsubscribe(); + } + + /** + * Switches to the passed-in tutorial. + * + * @param tutorial + * + * @returns void + */ + switchToTutorial(tutorial: Tutorial): void { + this.project.switchToTutorial(tutorial); + } + + /** + * Filters a collection of passed-in tutorials based on the campus_id of the passed-in project. + * + * @param tutorials + * @param project + * + * @returns Tutorial[] + */ + tutorialCampusFilter(tutorials: Tutorial[], project: Project): Tutorial[] { + if (!project) { + return tutorials; + } + return tutorials.filter((tutorial) => { + return ( + !project.campus?.id || + !tutorial.campus || + tutorial.campus.id === project.campus.id || + project.isEnrolledIn(tutorial) + ); + }); + } + + /** + * Formats the passed-in time string to the format of: HH:mm + * Todo: Add date validation + * @param meetingTime + * + * @returns string + */ + shortTime(meetingTime: string): string { + const [hours, minutes] = meetingTime.split(':'); + const formattedHours = hours.padStart(2, '0'); + const formattedMinutes = minutes.padStart(2, '0'); + + return `${formattedHours}:${formattedMinutes}`; + } + + private sortCompare( + aValue: number | string | undefined, + bValue: number | string | undefined, + isAsc: boolean, + ) { + const left = aValue ?? ''; + const right = bValue ?? ''; + + if (left === right) { + return 0; + } + + return (left < right ? -1 : 1) * (isAsc ? 1 : -1); + } + + sortTableData(sort: Sort) { + if (!sort.active || sort.direction === '') { + return; + } + this.dataSource.data = this.dataSource.data.sort((a, b) => { + switch (sort.active) { + case 'stream': + return this.sortCompare( + a.tutorialStream?.name, + b.tutorialStream?.name, + sort.direction === 'asc', + ); + case 'campus': + return this.sortCompare(a.campus?.name, b.campus?.name, sort.direction === 'asc'); + case 'code': + return this.sortCompare(a.abbreviation, b.abbreviation, sort.direction === 'asc'); + case 'day': { + return this.sortCompare(a.meetingDay, b.meetingDay, sort.direction === 'asc'); + } + case 'time': { + return this.sortCompare( + this.shortTime(a.meetingTime), + this.shortTime(b.meetingTime), + sort.direction === 'asc', + ); + } + case 'room': { + return this.sortCompare(a.meetingLocation, b.meetingLocation, sort.direction === 'asc'); + } + case 'tutor': + return this.sortCompare(a.tutorName, b.tutorName, sort.direction === 'asc'); + default: + return 0; + } + }); + } +} diff --git a/src/app/projects/states/tutorials/tutorials.scss b/src/app/projects/states/tutorials/tutorials.scss deleted file mode 100644 index d402eae6a6..0000000000 --- a/src/app/projects/states/tutorials/tutorials.scss +++ /dev/null @@ -1,10 +0,0 @@ -#tutorials-state table { - th.stream { width: 10%; } - th.campus { width: 20%; } - th.code { width: 10%; } - th.day { width: 10%; } - th.time { width: 10%; } - th.room { width: 10%; } - th.tutor { width: 15%; } - th.actions { width: 15%; } -} diff --git a/src/app/projects/states/tutorials/tutorials.tpl.html b/src/app/projects/states/tutorials/tutorials.tpl.html deleted file mode 100644 index fae91d6ae3..0000000000 --- a/src/app/projects/states/tutorials/tutorials.tpl.html +++ /dev/null @@ -1,71 +0,0 @@ -
    -
    -
    -

    Select a Tutorial

    -
    -
    -

    - Click the plus on the specific tutorial to enrol in that tutorial, or click the minus icon to withdraw from your - current tutorial. -

    -
    - - - - - - - - - - - - - - - - - - - - - - - - - -
    - Stream - - Campus - - Code - - Day - - Time - - Room - - Tutor - Actions
    {{tutorial.tutorialStream.name || 'All'}}{{tutorial.campus ? tutorial.campus.name : 'All'}}{{tutorial.abbreviation}}{{tutorial.meetingDay}}{{tutorial.meetingTime | date: 'shortTime'}}{{tutorial.meetingLocation}}{{tutorial.tutorName}} - - -
    -
    -
    diff --git a/src/app/sessions/auth/http-auth-injector.coffee b/src/app/sessions/auth/http-auth-injector.coffee deleted file mode 100644 index 56ef98961e..0000000000 --- a/src/app/sessions/auth/http-auth-injector.coffee +++ /dev/null @@ -1,37 +0,0 @@ -angular.module("doubtfire.sessions.auth.http-auth-injector", []) -# -# This module is responsible for injecting the auth credentials to -# all -# -.config(($httpProvider) -> - $httpProvider.interceptors.push ($q, $rootScope, DoubtfireConstants, newUserService) -> - # - # Inject authentication token for requests - # - injectAuthForRequest = (request) -> - # Intercept API requests and inject the auth token. - if _.startsWith(request.url, DoubtfireConstants.API_URL) and newUserService.currentUser.authenticationToken? - request.headers = {} unless _.has(request, "headers") - request.headers.Auth_Token = newUserService.currentUser.authenticationToken - request.headers.Username = newUserService.currentUser.username - request or $q.when request - - # - # Inject handlers for 419 and 401 response errors - # - injectAuthForResponseWithError = (response) -> - # Intercept unauthorised API responses and fire an event. - if response.config && response.config.url and _.startsWith(response.config.url, DoubtfireConstants.API_URL) - # Timeout? - if response.status is 419 - $rootScope.$broadcast "tokenTimeout" - # Unauthorised? - else if response.status is 401 - $rootScope.$broadcast "unauthorisedRequestIntercepted" - $q.reject response - - { - request: injectAuthForRequest - responseError: injectAuthForResponseWithError - } -) diff --git a/src/app/sessions/service-worker-updater/check-for-update.service.ts b/src/app/sessions/service-worker-updater/check-for-update.service.ts index 409e444719..302af93ef1 100644 --- a/src/app/sessions/service-worker-updater/check-for-update.service.ts +++ b/src/app/sessions/service-worker-updater/check-for-update.service.ts @@ -1,13 +1,14 @@ -import { ApplicationRef, Injectable } from '@angular/core'; -import { SwUpdate } from '@angular/service-worker'; -import { interval } from 'rxjs'; -import { MatSnackBar } from '@angular/material/snack-bar'; -import { delay } from 'rxjs/operators'; -import { concat } from 'rxjs'; +import {ApplicationRef, Injectable} from '@angular/core'; +import {MatSnackBar} from '@angular/material/snack-bar'; +import {SwUpdate} from '@angular/service-worker'; @Injectable() export class CheckForUpdateService { - constructor(appRef: ApplicationRef, private updates: SwUpdate, private _snackBar: MatSnackBar) { + constructor( + appRef: ApplicationRef, + private updates: SwUpdate, + private _snackBar: MatSnackBar, + ) { // Allow the app to stabilize first, before starting polling for updates with `interval()`. // const appIsStable$ = appRef.isStable.pipe(delay(10000)); @@ -22,15 +23,15 @@ export class CheckForUpdateService { if (updateEvent.type === 'VERSION_READY') { const snackBarRef = _snackBar.open( 'An update to the app has been found, would you like to refresh now?', - 'refresh' + 'refresh', ); - snackBarRef.onAction().subscribe((result) => { + snackBarRef.onAction().subscribe((_result) => { updates.activateUpdate().then(() => document.location.reload()); }); } }); - this.updates.unrecoverable.subscribe((event) => { + this.updates.unrecoverable.subscribe((_event) => { _snackBar.open('An error occurred during update, please refresh the page'); }); } diff --git a/src/app/sessions/sessions.coffee b/src/app/sessions/sessions.coffee deleted file mode 100644 index ccf6e6b5f1..0000000000 --- a/src/app/sessions/sessions.coffee +++ /dev/null @@ -1,3 +0,0 @@ -angular.module('doubtfire.sessions', [ - "doubtfire.sessions.auth.http-auth-injector" -]) diff --git a/src/app/sessions/states/sign-in/sign-in.component.html b/src/app/sessions/states/sign-in/sign-in.component.html index 29b7743d41..2c57ca1db0 100644 --- a/src/app/sessions/states/sign-in/sign-in.component.html +++ b/src/app/sessions/states/sign-in/sign-in.component.html @@ -1,76 +1,77 @@ -
    - -
    +
    + +
    @if (!isLoading) { -
    -
    -
    - Homepage Logo -

    {{ externalName.value }}

    -
    -

    - Welcome to {{ externalName.value }} -

    - + @if (showCredentials) { + + Username + + + } + @if (showCredentials) { + + Password + + + } + @if (!showCredentials) { + + Automatically redirect + + } + + Stay logged in + + + +
    -
    + } } @else if (authMethodFailed) { -
    +
    } diff --git a/src/app/sessions/states/sign-in/sign-in.component.scss b/src/app/sessions/states/sign-in/sign-in.component.scss index 1594f3adc7..d7b57ef6e9 100644 --- a/src/app/sessions/states/sign-in/sign-in.component.scss +++ b/src/app/sessions/states/sign-in/sign-in.component.scss @@ -1,4 +1,4 @@ -@import '../../../../styles/common/hero-sidebar-layout.scss'; +@use 'styles/common/hero-sidebar-layout' as *; .sign-in-form { section { @@ -9,7 +9,7 @@ } } -.subcontainer { +.content-panel { overflow-y: hidden; } diff --git a/src/app/sessions/states/sign-in/sign-in.component.spec.ts b/src/app/sessions/states/sign-in/sign-in.component.spec.ts index e0d4c2f0d0..f80e7c1961 100644 --- a/src/app/sessions/states/sign-in/sign-in.component.spec.ts +++ b/src/app/sessions/states/sign-in/sign-in.component.spec.ts @@ -1,6 +1,16 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {HttpClient} from '@angular/common/http'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {ActivatedRoute, Router} from '@angular/router'; +import {AuthenticationService} from 'src/app/api/services/authentication.service'; +import {UserService} from 'src/app/api/services/user.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {GlobalStateService} from 'src/app/projects/states/index/global-state.service'; +import {SignInComponent} from './sign-in.component'; -import { SignInComponent } from './sign-in.component'; +const emptyProvider = {}; describe('SignInComponent', () => { let component: SignInComponent; @@ -8,15 +18,26 @@ describe('SignInComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ SignInComponent ] + declarations: [SignInComponent], + providers: [ + {provide: AuthenticationService, useValue: emptyProvider}, + {provide: UserService, useValue: emptyProvider}, + {provide: Router, useValue: emptyProvider}, + {provide: ActivatedRoute, useValue: emptyProvider}, + {provide: DoubtfireConstants, useValue: emptyProvider}, + {provide: HttpClient, useValue: emptyProvider}, + {provide: GlobalStateService, useValue: emptyProvider}, + {provide: AlertService, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], }) - .compileComponents(); + .overrideComponent(SignInComponent, {set: {template: ''}}) + .compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(SignInComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { diff --git a/src/app/sessions/states/sign-in/sign-in.component.ts b/src/app/sessions/states/sign-in/sign-in.component.ts index 2196ee83c9..4e1905a484 100644 --- a/src/app/sessions/states/sign-in/sign-in.component.ts +++ b/src/app/sessions/states/sign-in/sign-in.component.ts @@ -1,6 +1,6 @@ import {HttpClient} from '@angular/common/http'; -import {Component, Input, OnInit} from '@angular/core'; -import {StateService, Transition} from '@uirouter/core'; +import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; +import {ActivatedRoute, Router} from '@angular/router'; import {BehaviorSubject} from 'rxjs'; import {AuthenticationService} from 'src/app/api/services/authentication.service'; import {UserService} from 'src/app/api/services/user.service'; @@ -9,9 +9,7 @@ import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; import {GlobalStateService} from 'src/app/projects/states/index/global-state.service'; // Add fallback to check url for query parameters -interface IParams { - [key: string]: string; -} +type IParams = Record; const paramReducer = (params: IParams, pair: string): IParams => { const [key, value] = `${pair}=`.split('=').map(decodeURIComponent); @@ -41,6 +39,8 @@ type signInData = selector: 'f-sign-in', templateUrl: './sign-in.component.html', styleUrls: ['./sign-in.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class SignInComponent implements OnInit { public signingIn: boolean; @@ -64,12 +64,12 @@ export class SignInComponent implements OnInit { @Input() isLtiLogin: boolean; constructor( - private authService: AuthenticationService, + public authService: AuthenticationService, private userService: UserService, - private state: StateService, + private router: Router, + private route: ActivatedRoute, private constants: DoubtfireConstants, private http: HttpClient, - private transition: Transition, private globalState: GlobalStateService, private alerts: AlertService, ) {} @@ -83,12 +83,12 @@ export class SignInComponent implements OnInit { if (params.isLtiLogin && params.ltik) { this.globalState.hideHeader(); this.userService.currentUser.ltik = params.ltik; - return this.state.go('lti', { - ltik: params.ltik, - }); + return this.router.navigate(['/lti'], {queryParams: {ltik: params.ltik}}); + } else if (this.userService.currentUser.hasRunFirstTimeSetup === false) { + return this.router.navigateByUrl('/welcome'); } else { this.globalState.goHome(); - return this.state.go('welcome'); + return this.router.navigateByUrl('/home'); } } this.isLoading = true; @@ -114,25 +114,26 @@ export class SignInComponent implements OnInit { this.api = this.constants.API_URL; this.externalName = this.constants.ExternalName; - // HACK: Workaround the fact that query params do not work in Safari with ui-router + const queryParams = this.route.snapshot.queryParams; const params = getUrlParams(document.location.href); if (!this.username) { - this.username = this.transition.params().username || params.username; - this.authToken = this.transition.params().authToken || params.authToken; + this.username = queryParams.username || params.username; + this.authToken = queryParams.authToken || params.authToken; } - this.ltiToken = params.ltiToken ?? undefined; - this.ltik = params.ltik ?? undefined; - this.isLtiLogin = params.isLtiLogin?.toLowerCase() === 'true' ? true : false; + this.ltiToken = queryParams.ltiToken || params.ltiToken || undefined; + this.ltik = queryParams.ltik || params.ltik || undefined; + this.isLtiLogin = + (queryParams.isLtiLogin || params.isLtiLogin)?.toLowerCase() === 'true' ? true : false; // wait 2 seconds with rxjs const wait = new Promise((resolve) => setTimeout(resolve, 3000)); this.http.get(`${this.constants.API_URL}/auth/method`).subscribe({ - next: (response: any) => { + next: (response: {redirect_to?: string}) => { this.isLoading = false; // if there is a string in response.data.redirect_to - this.SSOLoginUrl = response.redirect_to || false; + this.SSOLoginUrl = response.redirect_to || ''; if (this.authToken) { // We have an auth token - so attempt to convert to access token @@ -187,7 +188,7 @@ export class SignInComponent implements OnInit { return wait.then(); } }, - error: (err) => { + error: (_err) => { this.authMethodFailed = true; // this.error = err; @@ -211,8 +212,9 @@ export class SignInComponent implements OnInit { * Perform the actions needed when the user successfully signs in. */ private actionSignInSuccess(): void { - this.globalState.loadGlobals(); - this.state.go('welcome'); + this.router.navigateByUrl( + this.userService.currentUser.hasRunFirstTimeSetup === false ? '/welcome' : '/home', + ); } /** @@ -251,11 +253,8 @@ export class SignInComponent implements OnInit { this.authService.signIn(signInCredentials).subscribe({ next: () => { if (this.isLtiLogin) { - this.globalState.loadGlobals(); const params = getUrlParams(document.location.href); - this.state.go('lti', { - ltik: params.ltik, - }); + this.router.navigate(['/lti'], {queryParams: {ltik: params.ltik}}); } else { this.actionSignInSuccess(); } diff --git a/src/app/sessions/transition-hooks.service.spec.ts b/src/app/sessions/transition-hooks.service.spec.ts deleted file mode 100644 index 90d821953b..0000000000 --- a/src/app/sessions/transition-hooks.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { TransitionHooksService } from './transition-hooks.service'; - -describe('TransitionHooksService', () => { - let service: TransitionHooksService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(TransitionHooksService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/src/app/sessions/transition-hooks.service.ts b/src/app/sessions/transition-hooks.service.ts deleted file mode 100644 index 949e312f31..0000000000 --- a/src/app/sessions/transition-hooks.service.ts +++ /dev/null @@ -1,122 +0,0 @@ -import {Injectable} from '@angular/core'; -import {TransitionService} from '@uirouter/angular'; -import {UserService} from '../api/services/user.service'; -import {DoubtfireAngularModule} from '../doubtfire-angular.module'; -import {GlobalStateService} from '../projects/states/index/global-state.service'; -import {DoubtfireConstants} from '../config/constants/doubtfire-constants'; -import {AuthenticationService} from '../api/services/authentication.service'; - -/** - * The TransitionHooksService is responsible for intercepting transitions between states. - * This is used to update the global state, and enforce certain routing rules - such as redirecting - * to the welcome page if the user has not completed first time setup, and accepting the eula. - */ -@Injectable({ - providedIn: DoubtfireAngularModule, -}) -export class TransitionHooksService { - private tiiEnabled = false; - - constructor( - private userService: UserService, - private transitions: TransitionService, - private globalState: GlobalStateService, - private constants: DoubtfireConstants, - private authenticationService: AuthenticationService, - ) { - // Get the tii settings... - this.constants.IsTiiEnabled.subscribe((enabled) => { - this.tiiEnabled = enabled; - }); - - // Hook into "onBefore" to check transitions before they occur - this.transitions.onBefore({}, (transition) => { - // log all possible states - // console.log(transition.router.stateRegistry.get()) - - // Where is the transition coming from and going to? - const toState = transition.to().name; - const toStateData = transition.to().data; - // const fromState = transition.from().name; - - // Setup the global state - if (this.isInboxState(toState)) { - this.globalState.setInboxState(); - } else { - this.globalState.setNotInboxState(); - } - - // Adjust settings such as headers - switch (toState) { - case 'timeout': - case 'success-close': - return true; - case 'sign_in': - this.globalState.hideHeader(); - break; - case 'welcome': - if ( - authenticationService.isAuthenticated() && - userService.currentUser.hasRunFirstTimeSetup - ) { - return transition.router.stateService.target('home'); - } - - this.globalState.hideHeader(); - break; - case 'home': - this.globalState.goHome(); - break; - default: - break; - } - - // After auth... check the following - this.authenticationService.afterAuthCall(() => { - // Check authorization whitelist - if ( - toStateData.roleWhitelist && - !this.authenticationService.isAuthorised(toStateData.roleWhitelist) - ) { - if (authenticationService.isAuthenticated()) { - return transition.router.stateService.go('unauthorised'); - } else if (toState !== 'sign_in') { - return transition.router.stateService.go('sign_in'); - } - } - // Redirect to welcome if user has not run first time setup - if ( - !this.userService.isAnonymousUser() && - !userService.currentUser.hasRunFirstTimeSetup && - toState !== 'welcome' - ) { - return transition.router.stateService.go('welcome'); - } - - // Block access to welcome after account setup - if (userService.currentUser.hasRunFirstTimeSetup && toState === 'welcome') { - return transition.router.stateService.go('home'); - } - - // Redirect to eula if user has not accepted eula - // they are loged in, have run first time setup, - // but not accepted eula - if ( - this.tiiEnabled && - !this.userService.isAnonymousUser() && - userService.currentUser.hasRunFirstTimeSetup && - !userService.currentUser.acceptedTiiEula && - toState !== 'eula' - ) { - return transition.router.stateService.go('eula'); - } - }); - }); - } - - // function to return true if navigating to inbox or task definition - private isInboxState(toState: string): boolean { - // return toState.startsWith('units/tasks/inbox') || toState.endsWith('tasks/definition'); - return toState.startsWith('units/tasks') || toState.endsWith('tasks/definition'); - } -} diff --git a/src/app/tasks/modals/feedback-appeal-modal/feedback-appeal-modal.component.html b/src/app/tasks/modals/feedback-appeal-modal/feedback-appeal-modal.component.html index 131bc86541..676ed612b4 100644 --- a/src/app/tasks/modals/feedback-appeal-modal/feedback-appeal-modal.component.html +++ b/src/app/tasks/modals/feedback-appeal-modal/feedback-appeal-modal.component.html @@ -17,14 +17,14 @@

    Request Feedback Review

    request will not be counted against your remaining total.

    - + Reason for review request
    {{ reviewComment?.length || 0 }}/1000 @@ -35,10 +35,10 @@

    Request Feedback Review

    diff --git a/src/app/tasks/modals/feedback-appeal-modal/feedback-appeal-modal.component.ts b/src/app/tasks/modals/feedback-appeal-modal/feedback-appeal-modal.component.ts index d44c1853ca..271630413a 100644 --- a/src/app/tasks/modals/feedback-appeal-modal/feedback-appeal-modal.component.ts +++ b/src/app/tasks/modals/feedback-appeal-modal/feedback-appeal-modal.component.ts @@ -1,14 +1,16 @@ -import {Component, Inject, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Inject, OnInit} from '@angular/core'; import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; import {Task} from 'src/app/api/models/task'; +import {TaskService} from 'src/app/api/services/task.service'; import {AlertService} from 'src/app/common/services/alert.service'; import {FeedbackAppealModalData} from './feedback-appeal-modal.service'; -import {TaskService} from 'src/app/api/services/task.service'; @Component({ selector: 'f-feedback-appeal-modal', templateUrl: './feedback-appeal-modal.component.html', styleUrl: './feedback-appeal-modal.component.scss', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class FeedbackAppealModalComponent implements OnInit { task: Task; diff --git a/src/app/tasks/modals/grade-task-modal/grade-task-modal.coffee b/src/app/tasks/modals/grade-task-modal/grade-task-modal.coffee deleted file mode 100644 index 057dffee92..0000000000 --- a/src/app/tasks/modals/grade-task-modal/grade-task-modal.coffee +++ /dev/null @@ -1,41 +0,0 @@ -angular.module('doubtfire.tasks.modals.grade-task-modal', []) - -# -# A modal to grade a graded task -# -.factory('GradeTaskModal', ($modal) -> - GradeTaskModal = {} - - # - # Open a grade task modal with the provided task - # - GradeTaskModal.show = (task) -> - $modal.open - templateUrl: 'tasks/modals/grade-task-modal/grade-task-modal.tpl.html' - controller: 'GradeTaskModal' - resolve: - task: -> task - - GradeTaskModal -) -.controller('GradeTaskModal', ($scope, $modalInstance, gradeService, task) -> - $scope.task = task - $scope.data = { desiredGrade: task.grade, rating: task.qualityPts || 1, overStar: 0, confRating: 0 } - $scope.gradeValues = gradeService.allGradeValues - $scope.grades = gradeService.grades - $scope.dismiss = $modalInstance.dismiss - $scope.numStars = task.definition.maxQualityPts || 5 - $scope.close = -> - $modalInstance.close { qualityPts: $scope.data.rating, selectedGrade: $scope.data.desiredGrade} - - $scope.hoveringOver = (value) -> - $scope.data.overStar = value - - $scope.checkClearRating = -> - if $scope.data.confRating == 1 && $scope.data.rating == 1 && $scope.data.overStar == 1 - $scope.data.rating = 0 - else if $scope.data.confRating == 1 && $scope.data.overStar == 1 && $scope.data.rating == 0 - $scope.data.rating = 1 - - $scope.data.confRating = $scope.data.rating -) diff --git a/src/app/tasks/modals/grade-task-modal/grade-task-modal.component.html b/src/app/tasks/modals/grade-task-modal/grade-task-modal.component.html new file mode 100644 index 0000000000..2b8072a9dd --- /dev/null +++ b/src/app/tasks/modals/grade-task-modal/grade-task-modal.component.html @@ -0,0 +1,75 @@ + + + Assess Task Quality + + + + @if (task.definition.isGraded) { +
    +

    + Please provide a grade for task: + + {{ task.definition.abbreviation }} + +

    +
    + + @for (idx of gradeValues; track idx) { + + + + } + +
    +
    + } + + @if (task.definition.maxQualityPts > 0) { +
    +

    + Please provide a quality rating for task: + + {{ task.definition.abbreviation }} + +

    + + + +
    +

    Rating: {{ rating }} / {{ totalRating }}

    +
    +
    + } +
    + +
    + + +
    +
    +
    diff --git a/src/app/tasks/modals/grade-task-modal/grade-task-modal.component.scss b/src/app/tasks/modals/grade-task-modal/grade-task-modal.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/tasks/modals/grade-task-modal/grade-task-modal.component.spec.ts b/src/app/tasks/modals/grade-task-modal/grade-task-modal.component.spec.ts new file mode 100644 index 0000000000..ba20795fb0 --- /dev/null +++ b/src/app/tasks/modals/grade-task-modal/grade-task-modal.component.spec.ts @@ -0,0 +1,196 @@ +import {beforeEach, describe, expect, it, vi} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {GradeService} from 'src/app/common/services/grade.service'; +import {GradeTaskModalComponent} from './grade-task-modal.component'; + +describe('GradeTaskModalComponent', () => { + let component: GradeTaskModalComponent; + let fixture: ComponentFixture; + let gradeServiceStub: GradeService; + let dialogRefMock: {close: () => void}; + let dialogDataStub: { + task: { + grade?: number; + qualityPts?: number; + definition: {maxQualityPts?: number}; + }; + }; + + beforeEach(async () => { + gradeServiceStub = new GradeService(); + + dialogDataStub = { + task: { + grade: undefined, + qualityPts: undefined, + definition: { + maxQualityPts: undefined, + }, + }, + }; + + dialogRefMock = { + close: () => { + /* empty */ + }, + }; + + await TestBed.configureTestingModule({ + declarations: [GradeTaskModalComponent], + providers: [ + {provide: GradeService, useValue: gradeServiceStub}, + {provide: MatDialogRef, useValue: dialogRefMock}, + {provide: MAT_DIALOG_DATA, useValue: dialogDataStub}, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(GradeTaskModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should return rating & grade when closed', () => { + vi.spyOn(component.dialogRef, 'close'); + + component.rating = 5; + component.selectedGrade = 2; + component.close(); + + expect(component.dialogRef.close).toHaveBeenCalledWith({ + qualityPts: 5, + selectedGrade: 2, + }); + }); + + it('should dismiss', () => { + vi.spyOn(component.dialogRef, 'close'); + component.dismiss(); + expect(component.dialogRef.close).toHaveBeenCalled(); + }); + + /** + * For Rating tasks + */ + it('should accept a new task object', () => { + const newRatingTask = { + grade: undefined, + qualityPts: 5, + definition: { + maxQualityPts: 10, + }, + }; + dialogDataStub.task = newRatingTask; + + component.ngOnInit(); + expect(component.task).toEqual(newRatingTask); + expect(component.rating).toEqual(newRatingTask.qualityPts); + expect(component.selectedGrade).toEqual(0); + expect(component.totalRating).toEqual(newRatingTask.definition.maxQualityPts); + }); + + it('should treat an unrated quality task as unselected in the modal', () => { + dialogDataStub.task = { + grade: undefined, + qualityPts: -1, + definition: { + maxQualityPts: 5, + }, + }; + + component.ngOnInit(); + + expect(component.rating).toEqual(0); + expect(component.ratingLabel).toEqual('0 / 5'); + expect(component.qualityRatingSelected).toBe(false); + expect(component.isValid()).toBe(false); + }); + + it('should allow 0 as a selected quality rating', () => { + dialogDataStub.task = { + grade: undefined, + qualityPts: -1, + definition: { + maxQualityPts: 5, + }, + }; + + component.ngOnInit(); + component.updateRating(0); + + expect(component.rating).toEqual(0); + expect(component.qualityRatingSelected).toBe(true); + expect(component.isValid()).toBe(true); + }); + + it('should not allow a rating higher than the max rating', () => { + component.ngOnInit(); + component.rating = 1; + component.totalRating = 10; + component.updateRating(20); + + expect(component.rating).toEqual(1); + expect(component.totalRating).toEqual(10); + }); + + it('should not allow a rating lower than 0', () => { + component.ngOnInit(); + component.totalRating = 10; + component.updateRating(-10); + + expect(component.rating).toEqual(0); + expect(component.totalRating).toEqual(10); + }); + + it('should accept a new valid rating', () => { + component.ngOnInit(); + component.totalRating = 10; + component.updateRating(9); + + expect(component.rating).toEqual(9); + expect(component.totalRating).toEqual(10); + }); + + it('should reflect the rating in the rating label', () => { + component.ngOnInit(); + expect(component.ratingLabel).toEqual('0 / 5'); + + component.updateRating(-1); + expect(component.ratingLabel).toEqual('0 / 5'); + + component.updateRating(12); + expect(component.ratingLabel).toEqual('0 / 5'); + + component.updateRating(2); + expect(component.ratingLabel).toEqual('2 / 5'); + + component.updateRating(5); + expect(component.ratingLabel).toEqual('5 / 5'); + }); + + /** + * For Graded Tasks + */ + it('should accept a new valid grade', () => { + component.ngOnInit(); + component.updateGrade(3); + expect(component.selectedGrade).toEqual(3); + }); + + it('should not accept a new invalid grade', () => { + component.ngOnInit(); + component.updateGrade(10); + expect(component.selectedGrade).toEqual(0); + + component.updateGrade(-10); + expect(component.selectedGrade).toEqual(0); + }); +}); diff --git a/src/app/tasks/modals/grade-task-modal/grade-task-modal.component.ts b/src/app/tasks/modals/grade-task-modal/grade-task-modal.component.ts new file mode 100644 index 0000000000..b6a8adb053 --- /dev/null +++ b/src/app/tasks/modals/grade-task-modal/grade-task-modal.component.ts @@ -0,0 +1,82 @@ +import {ChangeDetectionStrategy, Component, Inject, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {GradeService, Task} from 'src/app/api/models/doubtfire-model'; + +@Component({ + selector: 'grade-task-modal', + templateUrl: './grade-task-modal.component.html', + styleUrls: ['./grade-task-modal.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class GradeTaskModalComponent implements OnInit { + task: Task; + gradeValues: number[]; + + // Task Rating + totalRating: number; + rating: number; + ratingLabel: string; + qualityRatingSelected: boolean; + + // Grade Select + selectedGrade: number; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public dialogData: {task: Task}, + private gradeService: GradeService, + ) {} + + ngOnInit(): void { + this.task = this.dialogData.task; + this.rating = this.task.qualityPts > 0 ? this.task.qualityPts : 0; + this.qualityRatingSelected = this.task.qualityPts >= 0; + this.selectedGrade = this.task.grade || 0; + this.totalRating = this.task.definition.maxQualityPts || 5; + this.gradeValues = this.gradeService.allGradeValuesFor(this.task.unit); + this.updateRatingLabel(); + } + + gradeName(grade: number): string { + return this.gradeService.gradeLabel(grade, this.task.unit); + } + + dismiss(): void { + this.dialogRef.close(); + } + + close(): void { + // Pass values back to service + this.dialogRef.close({ + qualityPts: this.rating, + selectedGrade: this.selectedGrade, + }); + } + + isValid() { + return ( + (this.task.definition.isGraded && this.selectedGrade) || + (this.task.definition.maxQualityPts > 0 && this.qualityRatingSelected) + ); + } + + updateRating(value: number): void { + if (value >= 0 && value <= this.totalRating) { + this.rating = value; + this.qualityRatingSelected = true; + this.updateRatingLabel(); + } + } + + updateRatingLabel(): void { + this.ratingLabel = `${this.rating} / ${this.totalRating}`; + } + + updateGrade(grade: number | string): void { + const gradeValue = Number(grade); + if (this.gradeValues.includes(gradeValue)) { + this.selectedGrade = gradeValue; + } + } +} diff --git a/src/app/tasks/modals/grade-task-modal/grade-task-modal.scss b/src/app/tasks/modals/grade-task-modal/grade-task-modal.scss deleted file mode 100644 index 29e7941885..0000000000 --- a/src/app/tasks/modals/grade-task-modal/grade-task-modal.scss +++ /dev/null @@ -1,19 +0,0 @@ -.grade-task-modal { - .task-quality-rating { - &:focus { - outline: none; - } - i { - font-size: 2em; - cursor: pointer; - } - .icon-colorful { - color: rgb(255, 247, 141); - -webkit-text-stroke-width: 1px; - -webkit-text-stroke-color: orange; - } - .icon-disable { - color: #ccc; - } - } -} diff --git a/src/app/tasks/modals/grade-task-modal/grade-task-modal.service.ts b/src/app/tasks/modals/grade-task-modal/grade-task-modal.service.ts new file mode 100644 index 0000000000..aa24a9b56a --- /dev/null +++ b/src/app/tasks/modals/grade-task-modal/grade-task-modal.service.ts @@ -0,0 +1,32 @@ +import {Injectable} from '@angular/core'; +import {MatDialog} from '@angular/material/dialog'; +import {Task} from 'src/app/api/models/doubtfire-model'; +import {GradeTaskModalComponent} from './grade-task-modal.component'; + +@Injectable({ + providedIn: 'root', +}) +export class GradeTaskModalService { + constructor(public dialog: MatDialog) {} + + public show( + task: Task, + successCallback: (response: {grade: number; qualityPts: number}) => void, + errorCallback: () => void, + ): void { + this.dialog + .open(GradeTaskModalComponent, { + data: { + task: task, + }, + }) + .afterClosed() + .subscribe((result) => { + if (result) { + successCallback(result); + } else { + errorCallback(); + } + }); + } +} diff --git a/src/app/tasks/modals/grade-task-modal/grade-task-modal.tpl.html b/src/app/tasks/modals/grade-task-modal/grade-task-modal.tpl.html deleted file mode 100644 index 7fa8e7055e..0000000000 --- a/src/app/tasks/modals/grade-task-modal/grade-task-modal.tpl.html +++ /dev/null @@ -1,27 +0,0 @@ -
    - - - -
    diff --git a/src/app/tasks/modals/modals.coffee b/src/app/tasks/modals/modals.coffee deleted file mode 100644 index 9dcd33874f..0000000000 --- a/src/app/tasks/modals/modals.coffee +++ /dev/null @@ -1,4 +0,0 @@ -angular.module('doubtfire.tasks.modals', [ - 'doubtfire.tasks.modals.grade-task-modal' - 'doubtfire.tasks.modals.upload-submission-modal' -]) diff --git a/src/app/tasks/modals/submission-type-modal/submission-type-modal.component.html b/src/app/tasks/modals/submission-type-modal/submission-type-modal.component.html index fbd3164016..f4eff4dfd0 100644 --- a/src/app/tasks/modals/submission-type-modal/submission-type-modal.component.html +++ b/src/app/tasks/modals/submission-type-modal/submission-type-modal.component.html @@ -4,25 +4,28 @@

    Select submission type

    This task won't be marked as complete by your tutor. It will be assessed as part of your final portfolio.

    -

    - You can submit it for feedback before the deadline, but you'll need to resubmit it later for - portfolio assessment. -

    -
    + @if (!isPastFeedbackDeadline) { +

    + You can submit it for feedback before the deadline, but you'll need to resubmit it later for + portfolio assessment. +

    + } +
    - @if (selectedTransition === 'ready_for_feedback') { + @if (isPastFeedbackDeadline) { + + } @else if (selectedTransition === 'ready_for_feedback') {

    You've made progress on this task and would like feedback, clarification, or to discuss questions with your tutor. @@ -55,8 +71,8 @@

    Select submission type

    + + @if (showPlagiarism) { +
    + {{ privacyPolicy.plagiarism }} +
    + } +
    + + + } +
    + } +
    + + +@if (!uploadStarted) { + + + + @if (isGroupStage) { + + } + + @if (isDetailsStage && showCommentsSection) { + + } + + @if (isDetailsStage && showGroupSection) { + + } + + @if (isCommentsStage) { + + } + + @if ((!showCommentsSection && isDetailsStage) || isCommentsStage) { + + } + +} diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.component.scss b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.component.ts b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.component.ts new file mode 100644 index 0000000000..5107f3f584 --- /dev/null +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.component.ts @@ -0,0 +1,357 @@ +import {ChangeDetectionStrategy, Component, Inject, OnInit, ViewChild} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {MemberContribution} from 'src/app/api/models/groups/group'; +import {Task} from 'src/app/api/models/task'; +import {TaskStatusEnum} from 'src/app/api/models/task-status'; +import {ProjectService} from 'src/app/api/services/project.service'; +import {TaskService} from 'src/app/api/services/task.service'; +import {FileUploaderComponent} from 'src/app/common/file-uploader/file-uploader.component'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {EmojiService} from 'src/app/common/services/emoji.service'; +import {PrivacyPolicy} from 'src/app/config/privacy-policy/privacy-policy'; + +type UploadStage = 'group' | 'details' | 'comments'; +type UploadSubmissionType = TaskStatusEnum | 'reupload_evidence' | 'test_submission'; + +interface UploadSubmissionTypeOption { + id: UploadSubmissionType; + label: string; +} + +interface UploadSubmissionFileSpec { + name: string; + type: string; +} + +type UploadSubmissionFileMap = Record; + +interface TeamData { + memberContributions: MemberContribution[]; +} + +interface UploadSubmissionResponse { + id: number; + project_id: number; + status: TaskStatusEnum; + [key: string]: unknown; +} + +export interface UploadSubmissionModalData { + task: Task; + reuploadEvidence: boolean; + isTestSubmission: boolean; +} + +export interface UploadSubmissionModalCloseResult { + value: Task; +} + +export interface UploadSubmissionModalDismissResult { + dismissed: true; +} + +export type UploadSubmissionModalResult = + | UploadSubmissionModalCloseResult + | UploadSubmissionModalDismissResult; + +@Component({ + selector: 'f-upload-submission-modal', + templateUrl: './upload-submission-modal.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class UploadSubmissionModalComponent implements OnInit { + @ViewChild(FileUploaderComponent) private fileUploader?: FileUploaderComponent; + + public readonly minCommentLength = 25; + public readonly task = this.data.task; + public readonly privacyPolicy = this.privacyPolicyService; + public readonly fileRequirements: UploadSubmissionFileMap = + this.task.definition.uploadRequirements.reduce((files, file) => { + files[file.key] = { + name: file.name, + type: file.type, + }; + return files; + }, {} as UploadSubmissionFileMap); + public readonly uploadUrl = this.data.isTestSubmission + ? this.task.testSubmissionUrl() + : this.task.submissionUrl(); + + public submissionTypeOptions: UploadSubmissionTypeOption[] = []; + public submissionType: UploadSubmissionType = 'ready_for_feedback'; + public currentStage: UploadStage = 'details'; + public payload: Record = {}; + public team: TeamData = {memberContributions: []}; + public comment = ''; + public showPlagiarism = false; + public isUploaderReady = false; + public uploadStarted = false; + public uploadSubmitLocked = false; + + private uploadResponse: UploadSubmissionResponse | null = null; + private startUpload?: () => void; + + constructor( + @Inject(MAT_DIALOG_DATA) public data: UploadSubmissionModalData, + private dialogRef: MatDialogRef, + private taskService: TaskService, + private projectService: ProjectService, + private privacyPolicyService: PrivacyPolicy, + private alertService: AlertService, + private emojiService: EmojiService, + ) {} + + ngOnInit(): void { + this.submissionTypeOptions = this.buildSubmissionTypeOptions(); + this.submissionType = this.data.isTestSubmission + ? 'test_submission' + : this.data.reuploadEvidence + ? 'reupload_evidence' + : this.task.status; + + this.resetUploadState(); + } + + public get isUploading(): boolean { + return this.fileUploader?.isUploading ?? false; + } + + public get showGroupSection(): boolean { + return this.submissionType === 'ready_for_feedback' && this.task.isGroupTask(); + } + + public get showCommentsSection(): boolean { + return this.submissionType !== 'test_submission'; + } + + public get isDetailsStage(): boolean { + return this.currentStage === 'details'; + } + + public get isGroupStage(): boolean { + return this.currentStage === 'group' && this.showGroupSection; + } + + public get isCommentsStage(): boolean { + return this.currentStage === 'comments' && this.showCommentsSection; + } + + public get requiresComment(): boolean { + return ( + this.submissionType === 'need_help' || + ((this.submissionType === 'ready_for_feedback' || + this.submissionType === 'reupload_evidence') && + this.task.definition.assessInPortfolioOnly) + ); + } + + public get submitTooltip(): string { + return this.requiresComment && this.comment.trim().length < this.minCommentLength + ? 'This submission requires a comment' + : ''; + } + + public get commentPlaceholder(): string { + if (this.submissionType === 'need_help') { + return 'I need help with...'; + } + + if ( + this.submissionType === 'ready_for_feedback' && + this.task.definition.assessInPortfolioOnly + ) { + return 'I would like feedback with...'; + } + + return 'Make a comment...'; + } + + public get hasRatedTeamMember(): boolean { + return this.team.memberContributions.some((member) => !!member.rating); + } + + public shouldDisableNext(): boolean { + if (this.isGroupStage) { + return !this.hasRatedTeamMember; + } + + if (this.isDetailsStage) { + return !this.isUploaderReady; + } + + return false; + } + + public shouldDisableSubmit(): boolean { + return ( + this.uploadSubmitLocked || + (this.showGroupSection && !this.hasRatedTeamMember) || + !this.isUploaderReady || + (this.requiresComment && this.comment.trim().length < this.minCommentLength) + ); + } + + public onSubmissionTypeChange(newType: UploadSubmissionType): void { + if (newType !== 'reupload_evidence' && newType !== 'test_submission') { + this.task.status = newType as TaskStatusEnum; + } + + this.submissionType = newType; + this.resetUploadState(); + } + + public goToCommentsStage(): void { + if (this.showCommentsSection && !this.shouldDisableNext()) { + this.currentStage = 'comments'; + } + } + + public goToGroupStage(): void { + if (this.showGroupSection) { + this.currentStage = 'group'; + } + } + + public goToDetailsStage(): void { + this.currentStage = 'details'; + } + + public cancel = (): void => { + this.uploadSubmitLocked = false; + this.dialogRef.close({dismissed: true}); + }; + + public onReadyChange(isReady: boolean): void { + this.isUploaderReady = isReady; + } + + public onUploaderReady(startUpload: () => void): void { + this.startUpload = startUpload; + } + + public onBeforeUpload = (): void => { + Object.keys(this.payload).forEach((key) => delete this.payload[key]); + + if (this.showGroupSection) { + this.payload['contributions'] = this.mapTeamToPayload(); + } + + if (this.submissionType === 'need_help') { + this.payload['trigger'] = 'need_help'; + } + + if ( + this.submissionType === 'assess_in_portfolio' || + this.task.status === 'assess_in_portfolio' + ) { + this.payload['trigger'] = 'assess_in_portfolio'; + } + + const trimmedComment = this.comment.trim(); + if (trimmedComment !== '') { + this.payload['comment'] = this.emojiService.nativeEmojiToColons(trimmedComment); + } + }; + + public onUploadSuccess = (response: unknown): void => { + if (this.isValidUploadResponse(response)) { + this.uploadResponse = response; + + if (this.data.isTestSubmission) { + this.projectService.loadProject(response.project_id, this.task.unit).subscribe({ + next: (project) => { + this.task.project = project; + }, + }); + } + + return; + } + + console.error('Invalid response', response); + this.dialogRef.close({value: this.task}); + this.alertService.error( + 'Upload failed. Please try again, or contact your tutor if the issue continues.', + 8000, + ); + }; + + public onUploadComplete = (): void => { + this.uploadSubmitLocked = false; + + if (!this.uploadResponse?.id) { + return; + } + + const response = this.uploadResponse; + this.dialogRef.close({value: this.task}); + + window.setTimeout(() => { + if (this.data.isTestSubmission) { + return; + } + + const expectedStatus = + this.submissionType === 'need_help' || this.submissionType === 'ready_for_feedback' + ? this.submissionType + : response.status; + + this.task.updateFromJson(response, this.taskService.mapping); + this.task.processTaskStatusChange(expectedStatus as TaskStatusEnum, this.alertService); + }, 1500); + }; + + public uploadButtonClicked(): void { + if (this.uploadSubmitLocked || this.isUploading) { + return; + } + + this.uploadSubmitLocked = true; + this.uploadStarted = true; + this.currentStage = 'details'; + this.startUpload?.(); + } + + private buildSubmissionTypeOptions(): UploadSubmissionTypeOption[] { + if (this.data.isTestSubmission) { + return [{id: 'test_submission', label: 'Test Submission'}]; + } + + const options: UploadSubmissionTypeOption[] = this.taskService.submittableStatuses.map( + (status) => ({ + id: status, + label: this.taskService.statusLabels.get(status) ?? status, + }), + ); + + if (this.task.inSubmittedState()) { + options.push({id: 'reupload_evidence', label: 'New Evidence'}); + } + + return options; + } + + private resetUploadState(): void { + this.uploadStarted = false; + this.uploadSubmitLocked = false; + this.uploadResponse = null; + this.currentStage = this.showGroupSection ? 'group' : 'details'; + } + + private mapTeamToPayload(): {project_id: number; pct: string; pts: number}[] { + const total = this.task.group?.contributionSum(this.team.memberContributions) ?? 0; + + return this.team.memberContributions.map((member) => ({ + project_id: member.project.id, + pct: total > 0 ? ((100 * member.rating) / total).toFixed(0) : '0', + pts: member.rating, + })); + } + + private isValidUploadResponse(response: unknown): response is UploadSubmissionResponse { + const candidate = response as Partial | null; + + return !!candidate && typeof candidate === 'object' && !!candidate.id && !!candidate.project_id; + } +} diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.scss b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.scss deleted file mode 100644 index 335602a147..0000000000 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.scss +++ /dev/null @@ -1,72 +0,0 @@ -.upload-submission-modal { - .modal-header { - padding-left: 12px; - padding-top: 5px; - padding-bottom: 5px; - } - select.submission-type { - width: auto !important; - display: inline-block; - margin-left: 0.6ex; - font-size: 20px; - } - .modal-body { - min-height: 420px; - overflow-x: hidden; - overflow-y: scroll; - } - .state { - position: absolute; - left: $panel-body-padding; - right: $panel-body-padding; - opacity: 1; - transition: 0.25s ease-in-out left, 0.25s ease-in-out right, 0.25s linear opacity; - &.state-hidden-left { - left: -100%; - right: 100%; - opacity: 0; - } - &.state-hidden-right { - left: 100%; - right: -100%; - opacity: 0; - } - } - .state.state-files { - .file-uploader { - margin: 0; - .well { - padding: 10px; - } - .error-message-area { - font-size: $font-size-base; - } - } - &.state-files-uploading { - top: 40%; - width: 50%; - margin: 0 auto; - font-size: 2em; - } - .card-body { - padding: 8px; - } - .card-heading { - padding: 8px ; - } - } - .state.state-alignment { - table th { - width: 50%; - } - table td { - border-top-style: none; - } - table td .panel { - margin-top: 1em; - } - .task-ilo-alignment-rater { - height: 55px; - } - } -} diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.service.ts b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.service.ts new file mode 100644 index 0000000000..051fab0bbf --- /dev/null +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.service.ts @@ -0,0 +1,70 @@ +import {Injectable} from '@angular/core'; +import {MatDialog} from '@angular/material/dialog'; +import {Task} from 'src/app/api/models/task'; +import {AlertService} from 'src/app/common/services/alert.service'; +import { + UploadSubmissionModalCloseResult, + UploadSubmissionModalComponent, + UploadSubmissionModalData, + UploadSubmissionModalDismissResult, + UploadSubmissionModalResult, +} from './upload-submission-modal.component'; + +export interface UploadSubmissionModalHandle { + result: Promise; +} + +@Injectable({ + providedIn: 'root', +}) +export class UploadSubmissionModalService { + constructor( + private dialog: MatDialog, + private alertService: AlertService, + ) {} + + public show( + task: Task, + reuploadEvidence: boolean, + isTestSubmission: boolean = false, + ): UploadSubmissionModalHandle | null { + if (!isTestSubmission && task.isGroupTask() && !task.group) { + this.alertService.error( + `This is a group task. Join a ${task.definition.groupSet.name} group to submit this task.`, + 8000, + ); + return null; + } + + const dialogRef = this.dialog.open< + UploadSubmissionModalComponent, + UploadSubmissionModalData, + UploadSubmissionModalResult + >(UploadSubmissionModalComponent, { + autoFocus: false, + disableClose: true, + position: {top: '2.5%'}, + width: '100%', + maxWidth: '960px', + maxHeight: '95vh', + data: { + task, + reuploadEvidence, + isTestSubmission, + }, + }); + + return { + result: new Promise((resolve, reject) => { + dialogRef.afterClosed().subscribe((result) => { + if ((result as UploadSubmissionModalDismissResult | undefined)?.dismissed) { + reject(result); + return; + } + + resolve((result as UploadSubmissionModalCloseResult | undefined)?.value ?? task); + }); + }), + }; + } +} diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html deleted file mode 100644 index bfda995467..0000000000 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html +++ /dev/null @@ -1,149 +0,0 @@ -
    - - -
    - - -
    diff --git a/src/app/tasks/project-tasks-list/project-tasks-list.coffee b/src/app/tasks/project-tasks-list/project-tasks-list.coffee deleted file mode 100644 index 5e1dc4fd56..0000000000 --- a/src/app/tasks/project-tasks-list/project-tasks-list.coffee +++ /dev/null @@ -1,59 +0,0 @@ -angular.module('doubtfire.tasks.project-tasks-list', []) - -# -# Displays the tasks associated with a student's project which -# when a task is clicked will automatically jump to the task viewer -# of the task that was clicked -# -.directive('projectTasksList', -> - replace: true - restrict: 'E' - templateUrl: 'tasks/project-tasks-list/project-tasks-list.tpl.html' - scope: - unit: "=" - project: "=" - onSelect: "=" - inMenu: '@' - - controller: ($scope, $modal, newTaskService, analyticsService, gradeService) -> - analyticsService.event 'Student Project View', "Showed Task Button List" - - $scope.groupTasks = [] - - $scope.groupTasks.push.apply $scope.groupTasks, $scope.unit.groupSets.map (gs) -> - { - groupSet: gs, - name: gs.name - } - - $scope.groupTasks.push {groupSet: null, name: 'Individual Work'} - - # functions from task service - $scope.statusClass = newTaskService.statusClass - $scope.statusText = newTaskService.statusText - - $scope.taskDisabled = (task) -> - task.definition.targetGrade > $scope.project.targetGrade - - $scope.groupSetName = (id) -> - $scope.unit.groupSetsCache.get(id)?.name || "Individual Work" - - $scope.hideGroupSetName = $scope.unit.groupSets.length is 0 - - $scope.taskText = (task) -> - result = task.definition.abbreviation - - if task.definition.isGraded - if task.grade? - result += " (" + gradeService.gradeAcronyms[task.grade] + ")" - else - result += " (?)" - - if task.definition.maxQualityPts > 0 - if task.qualityPts? - result += " (" + task.qualityPts + "/" + task.definition.maxQualityPts + ")" - else - result += " (?/" + task.definition.maxQualityPts + ")" - - result -) diff --git a/src/app/tasks/project-tasks-list/project-tasks-list.component.html b/src/app/tasks/project-tasks-list/project-tasks-list.component.html new file mode 100644 index 0000000000..ed091e545e --- /dev/null +++ b/src/app/tasks/project-tasks-list/project-tasks-list.component.html @@ -0,0 +1,37 @@ +
      + @for (grouping of groupTasks; track grouping) { +
      + @if (!hideGroupSetName) { +
      {{ grouping.name }}
      + } +
      + @for ( + task of project.tasks + | tasksForGroupset: grouping.groupSet + | orderBy: ['definition.targetGrade', 'definition.seq']; + track task; + let i = $index + ) { + + @if (task.similarityFlag) { + + } + {{ + taskText(task) + }} + } +
      +
      + } +
    diff --git a/src/app/tasks/project-tasks-list/project-tasks-list.component.scss b/src/app/tasks/project-tasks-list/project-tasks-list.component.scss new file mode 100644 index 0000000000..36bd44ade0 --- /dev/null +++ b/src/app/tasks/project-tasks-list/project-tasks-list.component.scss @@ -0,0 +1,25 @@ +.project-tasks-list { + .chip { + border-radius: 4px; + ::ng-deep .mdc-evolution-chip__text-label { + color: inherit !important; + } + } + + .group-set-name { + color: #777; + text-align: center; + font-size: 1em; + font-weight: bold; + } + + .mat-chip-clicked { + outline: #007bff auto 1px !important; + } + + .task-status { + ::ng-deep .mdc-evolution-chip__cell { + justify-content: center !important; + } + } +} diff --git a/src/app/tasks/project-tasks-list/project-tasks-list.component.ts b/src/app/tasks/project-tasks-list/project-tasks-list.component.ts new file mode 100644 index 0000000000..e5bacbc2f6 --- /dev/null +++ b/src/app/tasks/project-tasks-list/project-tasks-list.component.ts @@ -0,0 +1,82 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; +import { + GradeService, + Project, + Task, + TaskService, + TaskStatusEnum, + Unit, +} from 'src/app/api/models/doubtfire-model'; + +@Component({ + selector: 'f-project-tasks-list', + templateUrl: './project-tasks-list.component.html', + styleUrls: ['./project-tasks-list.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class ProjectTasksListComponent implements OnInit { + @Input() unit?: Unit; + @Input() project?: Project; + @Output() selectTask = new EventEmitter(); + selectedTask: Task | null = null; + + groupTasks = []; + + constructor( + public newTaskService: TaskService, + public gradeService: GradeService, + ) {} + + ngOnInit(): void { + this.groupTasks.push( + ...this.unit.groupSets.map((gs) => ({ + groupSet: gs, + name: gs.name, + })), + ); + this.groupTasks.push({groupSet: null, name: 'Individual Work'}); + } + + statusClass(status: TaskStatusEnum): string { + return this.newTaskService.statusClass(status); + } + + statusText(status: TaskStatusEnum): string { + return this.newTaskService.statusText(status); + } + + get hideGroupSetName(): boolean { + return this.unit.groupSets.length === 0; + } + + taskText(task: Task): string { + let result = task.definition.abbreviation; + if (task.definition.isGraded) { + if (task.grade !== undefined && task.grade !== null) { + result += ` (${this.gradeService.gradeAbbreviation(task.grade, this.unit)})`; + } else { + result += ' (?)'; + } + } + if (task.definition.maxQualityPts > 0) { + if (task.qualityPts >= 0) { + result += ` (${task.qualityPts}/${task.definition.maxQualityPts})`; + } else { + result += ` (?/${task.definition.maxQualityPts})`; + } + } + return result; + } + + selectChip(task: Task): void { + this.selectedTask = task; + } +} diff --git a/src/app/tasks/project-tasks-list/project-tasks-list.scss b/src/app/tasks/project-tasks-list/project-tasks-list.scss deleted file mode 100644 index 4f94364fad..0000000000 --- a/src/app/tasks/project-tasks-list/project-tasks-list.scss +++ /dev/null @@ -1,26 +0,0 @@ -.project-tasks-list { - @include remove-list-padding; - text-align: center; - - .groupset-name { - color: #777; - text-align: center; - font-size: 1em; - font-weight: bold; - } - - li { - display: inline; - padding: 2px; - } - - .groupset-tasks:first-child .groupset-name { - margin-top: 0; - } - - // As a dropdown menu - &.dropdown-menu { - padding: 15px; - width: 400px; - } -} diff --git a/src/app/tasks/project-tasks-list/project-tasks-list.tpl.html b/src/app/tasks/project-tasks-list/project-tasks-list.tpl.html deleted file mode 100644 index ee9b845aed..0000000000 --- a/src/app/tasks/project-tasks-list/project-tasks-list.tpl.html +++ /dev/null @@ -1,10 +0,0 @@ -
      -
      -
      {{grouping.name}}
      -
    • - -
    • -
      -
    diff --git a/src/app/tasks/task-comment-composer/attachment-confirmation-dialog/attachment-confirmation-dialog.component.html b/src/app/tasks/task-comment-composer/attachment-confirmation-dialog/attachment-confirmation-dialog.component.html new file mode 100644 index 0000000000..d74eebfffc --- /dev/null +++ b/src/app/tasks/task-comment-composer/attachment-confirmation-dialog/attachment-confirmation-dialog.component.html @@ -0,0 +1,59 @@ +
    +

    Post Attachment?

    + +

    This attachment is ready to post as a task comment.

    + +
    + @if (isImage) { + + } @else if (isPdf) { +
    + picture_as_pdf +
    + {{ file.name }} + PDF document +
    +
    + } @else if (isAudio) { +
    +
    + audio_file +
    + {{ file.name }} + {{ file.type || 'Audio file' }} +
    +
    + +
    + } @else { +
    + attach_file +
    + {{ file.name }} + {{ file.type || 'Attachment' }} +
    +
    + } +
    + +

    + {{ file.name }} + @if (file.size) { + ({{ formatFileSize(file.size) }}) + } +

    +
    + + + + + +
    diff --git a/src/app/tasks/task-comment-composer/attachment-confirmation-dialog/attachment-confirmation-dialog.component.ts b/src/app/tasks/task-comment-composer/attachment-confirmation-dialog/attachment-confirmation-dialog.component.ts new file mode 100644 index 0000000000..644281e04e --- /dev/null +++ b/src/app/tasks/task-comment-composer/attachment-confirmation-dialog/attachment-confirmation-dialog.component.ts @@ -0,0 +1,61 @@ +import {ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; + +export interface AttachmentConfirmationDialogData { + file: File; +} + +@Component({ + selector: 'f-attachment-confirmation-dialog', + templateUrl: './attachment-confirmation-dialog.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class AttachmentConfirmationDialogComponent implements OnInit, OnDestroy { + public file: File; + public previewUrl: string | null = null; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: AttachmentConfirmationDialogData, + ) {} + + ngOnInit() { + this.file = this.data.file; + this.previewUrl = URL.createObjectURL(this.file); + } + + ngOnDestroy() { + if (this.previewUrl) { + URL.revokeObjectURL(this.previewUrl); + } + } + + get isImage(): boolean { + return this.file?.type?.startsWith('image/') ?? false; + } + + get isPdf(): boolean { + return this.file?.type === 'application/pdf' || this.file?.name?.toLowerCase().endsWith('.pdf'); + } + + get isAudio(): boolean { + return this.file?.type?.startsWith('audio/') ?? false; + } + + dismiss(confirmed: boolean) { + this.dialogRef.close(confirmed); + } + + formatFileSize(size: number): string { + if (size < 1024) { + return `${size} B`; + } + + if (size < 1024 * 1024) { + return `${(size / 1024).toFixed(1)} KB`; + } + + return `${(size / (1024 * 1024)).toFixed(1)} MB`; + } +} diff --git a/src/app/tasks/task-comment-composer/discussion-prompt-composer-dialog.html b/src/app/tasks/task-comment-composer/discussion-prompt-composer-dialog.html index 19e37cbcbc..07649e2788 100644 --- a/src/app/tasks/task-comment-composer/discussion-prompt-composer-dialog.html +++ b/src/app/tasks/task-comment-composer/discussion-prompt-composer-dialog.html @@ -1,28 +1,47 @@ - - + Introduction -
    - Discussion Splash Image -

    - Discussions are a great way for both you and your students to gauge student's understanding of concepts and is a - great learning opportunity for them. -

    +

    + Discussion Splash Image +

    + Discussions are a great way for both you and your students to gauge student's understanding + of concepts and is a great learning opportunity for them. +
    +
    - Discussion comments allow you to present up to three discussion prompts to a students on a task. These - prompts are presented to the student, at which point their spoken reply will be recorded in real time. This - means you are receiving their unfiltered and immediate response, just like an informal chat in the classroom. -

    + Discussion comments allow you to present up to three discussion prompts to a students on a + task. These prompts are presented to the student, at which point their spoken reply will be + recorded in real time. This means you are receiving their unfiltered and immediate response, + just like an informal chat in the classroom. +
    +
    - These discussions are designed to be casual and informal and will require a working microphone and speakers. + These discussions are designed to be casual and informal and will require a working + microphone and speakers.

    - +
    @@ -31,7 +50,9 @@
    - +
    diff --git a/src/app/tasks/task-comment-composer/discussion-prompt-composer/discussion-prompt-composer.component.html b/src/app/tasks/task-comment-composer/discussion-prompt-composer/discussion-prompt-composer.component.html index cae98595e2..6f0dc1f062 100644 --- a/src/app/tasks/task-comment-composer/discussion-prompt-composer/discussion-prompt-composer.component.html +++ b/src/app/tasks/task-comment-composer/discussion-prompt-composer/discussion-prompt-composer.component.html @@ -1,47 +1,117 @@ -

    Step 1. Record and add up to 3 prompts.

    -
    -
    - -
    - -
    -

    Audio recording
    only supported in modern versions of Chrome, Firefox and Safari.

    - -
    - +
    +
    +
    +

    Create discussion prompts

    +

    + Record up to three short prompts. Students will hear them one at a time and respond out + loud. +

    - + + {{ recordings.length }} / 3 prompts +
    -
    - + +
    + +
    +
    + + +
    + } @else { +
    + You have recorded the maximum of three prompts. +
    + } + } + + + + +

    Discussion Prompts

    + @if (recordings.length === 0) { +
    No prompts recorded yet.
    + } + + @for (recording of recordings; track recording; let i = $index) { + + + {{ isRecordingPlaying(i) ? 'stop_circle' : 'play_arrow_rounded' }} + + Prompt {{ i + 1 }} + Click to {{ isRecordingPlaying(i) ? 'stop' : 'play' }} recording + + + } +
    + +
    +
    - -

    Step 2. Optionally play back prompts.

    - -

    Discussion Prompts

    - @for (recording of recordings; track recording; let i = $index) { - -} -
    - -

    Step 3. Send.

    -
    - -
    - - diff --git a/src/app/tasks/task-comment-composer/discussion-prompt-composer/discussion-prompt-composer.component.scss b/src/app/tasks/task-comment-composer/discussion-prompt-composer/discussion-prompt-composer.component.scss index 80ff2d1450..e69de29bb2 100644 --- a/src/app/tasks/task-comment-composer/discussion-prompt-composer/discussion-prompt-composer.component.scss +++ b/src/app/tasks/task-comment-composer/discussion-prompt-composer/discussion-prompt-composer.component.scss @@ -1,101 +0,0 @@ -#discussion-recordings-list { - flex-grow: 1; - max-height: 130px; - margin-top: -10px; -} - -#dialogCloseButton { - float: right; - margin-bottom: 20px; - color: red; -} - -mat-action-list { - height: 100%; -} - -#btnRecordPrompt { - background-color: #f03d25; - color: white; -} - -.discussion-prompt-audio-visualiser { - // max-width: 100px; -} - -.btn-circle-small { - user-select: none; - background-color: #f03d25; - border-radius: 72px; - color: #fff; - height: 40px; - transition: width 0.1s, height 0.1s; - width: 40px; - border: none; - outline: none; -} - -.btn-circle-small:disabled { - background-color: #c42a15; -} - -.btn-circle-small i { - font-size: 14px; -} - -#recordingList { - margin-bottom: 2em; -} - -#audio-recorder-visualiser.discussion-audio-visualiser { - height: 60px; - width: 115px; - bottom: 10px; - left: 65px; -} - -:host .mat-mdc-list-item-icon { - color: black; -} - -:host .mat-mdc-dialog-container { - max-width: 800px !important; -} - -:host #intelligentDiscussionStepper { - margin-top: 20px; -} - -:host img#discussion-splash-image { - float: left; - margin: auto; - max-width: 800px; - max-height: 300px; - vertical-align: middle; -} - -:host .introduction-text { - margin-top: 4em; - display: inline-flex; -} - -:host .introduction-text p { - font-family: "Helvetica Neue", "Segoe UI", "Helvetica", "Arial", "sans-serif"; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - line-height: 1.1; - font-size: 1.1em; - text-align: justify; - margin-left: 3em; - margin-top: 0; - float: right; -} - -:host #discussionRecorderContainer { - line-height: 1.2em; - font-family: "Helvetica Neue", "Segoe UI", "Helvetica", "Arial", "sans-serif"; - - p { - font-size: 1.2em; - } -} diff --git a/src/app/tasks/task-comment-composer/discussion-prompt-composer/discussion-prompt-composer.component.spec.ts b/src/app/tasks/task-comment-composer/discussion-prompt-composer/discussion-prompt-composer.component.spec.ts new file mode 100644 index 0000000000..b65604ea97 --- /dev/null +++ b/src/app/tasks/task-comment-composer/discussion-prompt-composer/discussion-prompt-composer.component.spec.ts @@ -0,0 +1,122 @@ +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; +import {of} from 'rxjs'; +import {Task, TaskComment, TaskCommentService} from 'src/app/api/models/doubtfire-model'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {MediaRecorderService} from 'src/app/common/services/recorder-service'; +import {DiscussionPromptComposerComponent} from './discussion-prompt-composer.component'; + +class TestableDiscussionPromptComposerComponent extends DiscussionPromptComposerComponent { + set testAudio(audio: HTMLAudioElement) { + this.audio = audio; + } + + get testAudio(): HTMLAudioElement { + return this.audio; + } + + set testBlob(blob: Blob) { + this.blob = blob; + } + + get testBlob(): Blob { + return this.blob; + } +} + +describe('DiscussionPromptComposerComponent', () => { + let component: TestableDiscussionPromptComposerComponent; + let taskCommentService: {addComment: ReturnType}; + let createObjectURL: ReturnType; + let revokeObjectURL: ReturnType; + let originalCreateObjectURL: typeof URL.createObjectURL; + let originalRevokeObjectURL: typeof URL.revokeObjectURL; + + beforeEach(() => { + originalCreateObjectURL = URL.createObjectURL; + originalRevokeObjectURL = URL.revokeObjectURL; + createObjectURL = vi.fn((blob: Blob) => `blob:${blob.size}:${blob.type}`); + revokeObjectURL = vi.fn(); + Object.defineProperty(URL, 'createObjectURL', {configurable: true, value: createObjectURL}); + Object.defineProperty(URL, 'revokeObjectURL', {configurable: true, value: revokeObjectURL}); + + taskCommentService = { + addComment: vi.fn(() => of({} as TaskComment)), + }; + component = new TestableDiscussionPromptComposerComponent( + {} as MediaRecorderService, + taskCommentService as unknown as TaskCommentService, + {error: vi.fn()} as unknown as AlertService, + ); + component.task = {id: 1} as Task; + component.testAudio = { + src: '', + pause: vi.fn(), + removeAttribute: vi.fn(function (this: {src: string}, attribute: string) { + if (attribute === 'src') { + this.src = ''; + } + }), + load: vi.fn(), + play: vi.fn(), + } as unknown as HTMLAudioElement; + }); + + afterEach(() => { + Object.defineProperty(URL, 'createObjectURL', { + configurable: true, + value: originalCreateObjectURL, + }); + Object.defineProperty(URL, 'revokeObjectURL', { + configurable: true, + value: originalRevokeObjectURL, + }); + }); + + it('saves a recording as a reusable object URL and clears the active blob', () => { + const recording = new Blob(['prompt one'], {type: 'audio/webm'}); + component.testBlob = recording; + component.recordingAvailable = true; + + component.saveRecording(); + + expect(component.recordings).toEqual([{blob: recording, url: 'blob:10:audio/webm'}]); + expect(createObjectURL).toHaveBeenCalledWith(recording); + expect(component.testBlob.size).toBe(0); + expect(component.recordingAvailable).toBe(false); + }); + + it('deletes a saved prompt and revokes its object URL', () => { + const recording = {blob: new Blob(['prompt one']), url: 'blob:prompt-one'}; + component.recordings = [recording]; + component.testAudio.src = recording.url; + component.playingRecordingIndex = 0; + + component.deleteRecording(0); + + expect(component.recordings).toEqual([]); + expect(component.testAudio.pause).toHaveBeenCalled(); + expect(component.testAudio.removeAttribute).toHaveBeenCalledWith('src'); + expect(component.testAudio.load).toHaveBeenCalled(); + expect(component.playingRecordingIndex).toBeNull(); + expect(revokeObjectURL).toHaveBeenCalledWith(recording.url); + }); + + it('uploads only the remaining saved prompt blobs', () => { + const first = {blob: new Blob(['prompt one']), url: 'blob:prompt-one'}; + const second = {blob: new Blob(['prompt two']), url: 'blob:prompt-two'}; + component.recordings = [first, second]; + component.deleteRecording(0); + + component.sendRecording(); + + expect(taskCommentService.addComment).toHaveBeenCalledWith( + component.task, + undefined, + 'discussion', + undefined, + [second.blob], + ); + expect(component.isSending).toBe(false); + expect(component.recordingAvailable).toBe(false); + }); +}); diff --git a/src/app/tasks/task-comment-composer/discussion-prompt-composer/discussion-prompt-composer.component.ts b/src/app/tasks/task-comment-composer/discussion-prompt-composer/discussion-prompt-composer.component.ts index a589b8dc6d..171938e4dc 100644 --- a/src/app/tasks/task-comment-composer/discussion-prompt-composer/discussion-prompt-composer.component.ts +++ b/src/app/tasks/task-comment-composer/discussion-prompt-composer/discussion-prompt-composer.component.ts @@ -1,23 +1,39 @@ -import {Component, Inject, Input, ViewChild, ElementRef} from '@angular/core'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + Inject, + Input, + OnDestroy, + ViewChild, +} from '@angular/core'; +import {Task, TaskComment, TaskCommentService} from 'src/app/api/models/doubtfire-model'; import {BaseAudioRecorderComponent} from 'src/app/common/audio-recorder/audio/base-audio-recorder'; -import {audioRecorderService} from 'src/app/ajs-upgraded-providers'; -import {TaskComment, TaskCommentService, Task} from 'src/app/api/models/doubtfire-model'; import {AlertService} from 'src/app/common/services/alert.service'; +import {MediaRecorderService} from 'src/app/common/services/recorder-service'; @Component({ selector: 'discussion-prompt-composer', templateUrl: './discussion-prompt-composer.component.html', styleUrls: ['./discussion-prompt-composer.component.scss'], + providers: [MediaRecorderService], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class DiscussionPromptComposerComponent extends BaseAudioRecorderComponent { +export class DiscussionPromptComposerComponent + extends BaseAudioRecorderComponent + implements AfterViewInit, OnDestroy +{ @Input() task: Task; - @ViewChild('discussionPromptComposerCanvas', {static: true}) canvasRef: ElementRef; - @ViewChild('discussionPromptComposerAudio', {static: true}) audioRef: ElementRef; - recordings: Blob[] = new Array(); + @ViewChild('discussionPromptComposerCanvas') canvasRef: ElementRef; + @ViewChild('discussionPromptComposerAudio') audioRef: ElementRef; + recordings: {blob: Blob; url: string}[] = []; canvas: HTMLCanvasElement; canvasCtx: CanvasRenderingContext2D; isSending: boolean = false; + playingRecordingIndex: number = null; get canAddRecording(): boolean { return this.recordings.length < 3; @@ -28,7 +44,7 @@ export class DiscussionPromptComposerComponent extends BaseAudioRecorderComponen } constructor( - @Inject(audioRecorderService) mediaRecorderService: any, + private mediaRecorderService: MediaRecorderService, @Inject(TaskCommentService) private taskCommentService: TaskCommentService, private alerts: AlertService, ) { @@ -43,27 +59,65 @@ export class DiscussionPromptComposerComponent extends BaseAudioRecorderComponen } } + ngOnDestroy(): void { + this.recordings.forEach((recording) => URL.revokeObjectURL(recording.url)); + } + init(): void { super.init(); this.audio = this.audioRef.nativeElement; + this.audio.onended = () => { + this.playingRecordingIndex = null; + }; this.canvas = this.canvasRef.nativeElement; this.canvasCtx = this.canvas.getContext('2d'); } - getUrl(b: Blob) { - return URL.createObjectURL(b); + isRecordingPlaying(index: number): boolean { + return this.playingRecordingIndex === index; } - playRecording(url: string) { - this.audio.src = url; + playRecording(recording: {blob: Blob; url: string}, index: number) { + if (this.isRecordingPlaying(index)) { + this.audio.pause(); + this.audio.currentTime = 0; + this.playingRecordingIndex = null; + return; + } + + this.playingRecordingIndex = index; + this.audio.src = recording.url; this.audio.load(); this.audio.play(); } + deleteRecording(index: number): void { + const [recording] = this.recordings.splice(index, 1); + if (!recording) { + return; + } + + if (this.audio.src === recording.url) { + this.audio.pause(); + this.audio.removeAttribute('src'); + this.audio.load(); + this.playingRecordingIndex = null; + } + + URL.revokeObjectURL(recording.url); + } + saveRecording(): void { if (this.blob && this.blob.size > 0) { if (this.canAddRecording) { - this.recordings.push(this.blob); + this.audio.pause(); + this.audio.removeAttribute('src'); + this.audio.load(); + this.playingRecordingIndex = null; + this.recordings.push({ + blob: this.blob, + url: URL.createObjectURL(this.blob), + }); } this.blob = new Blob(); this.recordingAvailable = false; @@ -71,18 +125,22 @@ export class DiscussionPromptComposerComponent extends BaseAudioRecorderComponen } sendRecording(): void { + this.isSending = true; this.taskCommentService - .addComment(this.task, undefined, 'discussion', undefined, this.recordings) + .addComment( + this.task, + undefined, + 'discussion', + undefined, + this.recordings.map((recording) => recording.blob), + ) .subscribe( - (tc: TaskComment) => { + (_tc: TaskComment) => { this.isSending = false; }, - (failure: any) => { - this.alerts.error( - `Failed to create discussion comment. ${ - failure.data != null ? failure.data.error : failure - }`, - ); + (failure: {data?: {error?: string}} | string) => { + const message = typeof failure === 'string' ? failure : failure.data?.error || failure; + this.alerts.error(`Failed to create discussion comment. ${String(message)}`); this.isSending = false; }, ); @@ -105,6 +163,7 @@ export class DiscussionPromptComposerComponent extends BaseAudioRecorderComponen const bar_x = i * 8; const bar_y = HEIGHT / 2; const bar_height = -(dataArray[i] / 4) + 1; + this.canvasCtx.fillStyle = '#2563eb'; this.canvasCtx.fillRect(bar_x, bar_y, bar_width, bar_height); this.canvasCtx.fillRect(bar_x, bar_y - bar_height, bar_width, bar_height); i++; diff --git a/src/app/tasks/task-comment-composer/task-comment-composer.component.html b/src/app/tasks/task-comment-composer/task-comment-composer.component.html index c589c6f17e..8455a0b330 100644 --- a/src/app/tasks/task-comment-composer/task-comment-composer.component.html +++ b/src/app/tasks/task-comment-composer/task-comment-composer.component.html @@ -1,11 +1,11 @@ @@ -13,10 +13,10 @@
    @@ -27,15 +27,32 @@
    } +@if (task) { +
    +
    + + Editing your comment +
    +

    Changes can only be saved within 10 minutes of posting.

    +
    +} + @for (emoji of emojiSearchResults; track emoji) {
    - +
    {{ emoji.name }}
    {{ emoji.colons }}
    @@ -46,34 +63,34 @@
    @if (task) { } -
    +
    @if ($userIsTyping | async) { @@ -94,53 +111,59 @@ --> @if (isStaff) { + @if (!isEditing) { + + } + } + + @if (!isEditing) { } - - - + @if (!isEditing) { + + }
    @if (recording) { }
    + @if (isEditing) { + + check + } emoji_emotions
    - - task + @if (!isEditing) { + + task + }
    diff --git a/src/app/tasks/task-comment-composer/task-comment-composer.component.scss b/src/app/tasks/task-comment-composer/task-comment-composer.component.scss index d83ccd18a0..a1eddf543b 100644 --- a/src/app/tasks/task-comment-composer/task-comment-composer.component.scss +++ b/src/app/tasks/task-comment-composer/task-comment-composer.component.scss @@ -1,7 +1,6 @@ -$emoji-color: lighten( - $color: black, - $amount: 25, -); +@use 'sass:color'; + +$emoji-color: color.adjust(black, $lightness: 25%); .composer-container { padding-bottom: 0; @@ -46,7 +45,6 @@ $emoji-color: lighten( word-break: break-word; text-wrap: wrap; user-select: text; - } #textField[placeholder]:empty::before { @@ -64,14 +62,19 @@ $emoji-color: lighten( } } - #textField.draft-loaded { animation: draftFadeIn 0.3s ease-in; } @keyframes draftFadeIn { - from { opacity: 0.5; background-color: rgba(0, 0, 255, 0.05); } - to { opacity: 1; background-color: transparent; } + from { + opacity: 0.5; + background-color: rgba(0, 0, 255, 0.05); + } + to { + opacity: 1; + background-color: transparent; + } } .draft-loaded { @@ -122,7 +125,7 @@ $emoji-color: lighten( border-radius: 12px; border-style: solid; border-width: 1px; - border-color: darken($color: white, $amount: 10); + border-color: color.adjust($color: white, $lightness: -10%); } #replyContainer { @@ -133,7 +136,7 @@ $emoji-color: lighten( color: rgba(0, 0, 0, 0.5); font-size: 12px; max-lines: 2; - max-height: 30px; + max-height: 35px; line-clamp: 2; line-height: 15.36px; overflow-x: hidden; diff --git a/src/app/tasks/task-comment-composer/task-comment-composer.component.ts b/src/app/tasks/task-comment-composer/task-comment-composer.component.ts index 294f305292..112e601253 100644 --- a/src/app/tasks/task-comment-composer/task-comment-composer.component.ts +++ b/src/app/tasks/task-comment-composer/task-comment-composer.component.ts @@ -1,6 +1,9 @@ +import {EmojiSearch} from '@ctrl/ngx-emoji-mart'; +import {EmojiData} from '@ctrl/ngx-emoji-mart/ngx-emoji'; import {animate, style, transition, trigger} from '@angular/animations'; import { AfterViewInit, + ChangeDetectionStrategy, ChangeDetectorRef, Component, DoCheck, @@ -10,17 +13,13 @@ import { KeyValueDiffer, KeyValueDiffers, OnChanges, - OnInit, QueryList, SimpleChanges, ViewChild, ViewChildren, } from '@angular/core'; import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog'; -import {EmojiSearch} from '@ctrl/ngx-emoji-mart'; -import {EmojiData} from '@ctrl/ngx-emoji-mart/ngx-emoji/'; import {BehaviorSubject, Subscription} from 'rxjs'; -import {analyticsService} from 'src/app/ajs-upgraded-providers'; import { FeedbackTemplate, Task, @@ -30,6 +29,7 @@ import { import {AlertService} from 'src/app/common/services/alert.service'; import {EmojiService} from 'src/app/common/services/emoji.service'; import {TaskCommentsViewerComponent} from '../task-comments-viewer/task-comments-viewer.component'; +import {AttachmentConfirmationDialogComponent} from './attachment-confirmation-dialog/attachment-confirmation-dialog.component'; interface ApiError { error?: string; @@ -43,7 +43,9 @@ interface ApiError { */ export interface TaskCommentComposerData { + [key: string]: TaskComment; originalComment: TaskComment; + editingComment: TaskComment; } const ACCEPTED_FILE_TYPES = [ @@ -75,18 +77,21 @@ const ACCEPTED_FILE_TYPES = [ transition('false => true', [style({width: 80}), animate('150ms 0ms ease-in-out')]), ]), ], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class TaskCommentComposerComponent implements OnInit, AfterViewInit, DoCheck, OnChanges { +export class TaskCommentComposerComponent implements AfterViewInit, DoCheck, OnChanges { @Input() task: Task; @Input() sharedData: TaskCommentComposerData; - public $userIsTyping = new BehaviorSubject(false); + public $userIsTyping: BehaviorSubject = new BehaviorSubject(false); private draftSaveSubscription = new Subscription(); private readonly DRAFT_KEY_PREFIX = 'task_comment_draft_'; public isDraftLoaded = false; private submittedTaskIds: Set = new Set(); public isSending: boolean = false; + private draftBeforeEdit: string = ''; comment = { text: '', @@ -97,9 +102,10 @@ export class TaskCommentComposerComponent implements OnInit, AfterViewInit, DoCh @ViewChildren('cag') cag: QueryList; @ViewChild('uploader') uploader: ElementRef; - differ: KeyValueDiffer; + differ: KeyValueDiffer; showEmojiPicker = false; emojiSearchMode = false; + // eslint-disable-next-line no-useless-escape emojiRegex: RegExp = /(?:\:)(.*?)(?=\:|$)/; emojiSearchResults: EmojiData[] = []; emojiMatch: string; @@ -113,7 +119,6 @@ export class TaskCommentComposerComponent implements OnInit, AfterViewInit, DoCh private emojiSearch: EmojiSearch, private emojiService: EmojiService, private commentsViewer: TaskCommentsViewerComponent, - @Inject(analyticsService) private analytics, private alerts: AlertService, @Inject(TaskCommentService) private taskCommentService: TaskCommentService, private cdRef: ChangeDetectorRef, @@ -130,8 +135,6 @@ export class TaskCommentComposerComponent implements OnInit, AfterViewInit, DoCh } } - ngOnInit() {} - ngOnChanges(changes: SimpleChanges) { this.showFeedbackTemplatePicker = false; @@ -139,6 +142,7 @@ export class TaskCommentComposerComponent implements OnInit, AfterViewInit, DoCh const newTask = changes.task.currentValue as Task; // Check if the task has changed + this.cancelEdit(); this.cancelReply(); this.clearInput(); @@ -174,9 +178,13 @@ export class TaskCommentComposerComponent implements OnInit, AfterViewInit, DoCh // Update onInputChange to reset submitted status onInputChange(event: Event) { + if (this.isEditing) { + return; + } + const target = event.target as HTMLElement; const text = target.innerText; - const raw = target.innerText; + const _raw = target.innerText; // If user is typing something new after submission, reset the submitted status if (this.task) { @@ -200,7 +208,7 @@ export class TaskCommentComposerComponent implements OnInit, AfterViewInit, DoCh } } - const draftKey = this.getDraftKey(this.task); + const _draftKey = this.getDraftKey(this.task); // this.taskDraftContents.set(draftKey, raw); } @@ -226,7 +234,7 @@ export class TaskCommentComposerComponent implements OnInit, AfterViewInit, DoCh } // Update saveDraftForTask to use the taskDraftContents map - private saveDraftForTask(task: Task, rawFromDom?: string): void { + private saveDraftForTask(task: Task, _rawFromDom?: string): void { if (!task) { return; } @@ -299,7 +307,9 @@ export class TaskCommentComposerComponent implements OnInit, AfterViewInit, DoCh }; retryWithTimeout(); - } catch (error) {} + } catch (error) { + console.error(error); + } } private clearInput() { @@ -310,7 +320,9 @@ export class TaskCommentComposerComponent implements OnInit, AfterViewInit, DoCh } private saveCurrentDraft() { - if (!this.task) return; + if (!this.task) { + return; + } this.saveDraftForTask(this.task); } @@ -321,11 +333,7 @@ export class TaskCommentComposerComponent implements OnInit, AfterViewInit, DoCh change.forEachChangedItem((item) => { // If it has changed to be an actual comment if (item != null) { - // Set the input field as focused, so the user can start typing - // timeout is required - setTimeout(() => { - this.input.first.nativeElement.focus(); - }); + this.syncComposerState(); } }); } @@ -335,6 +343,14 @@ export class TaskCommentComposerComponent implements OnInit, AfterViewInit, DoCh return this.sharedData.originalComment; } + get editingComment(): TaskComment { + return this.sharedData.editingComment; + } + + get isEditing(): boolean { + return this.editingComment != null; + } + get isStaff() { return this.task?.unit?.currentUserIsStaff; } @@ -343,6 +359,11 @@ export class TaskCommentComposerComponent implements OnInit, AfterViewInit, DoCh this.sharedData.originalComment = null; } + cancelEdit() { + this.sharedData.editingComment = null; + this.restoreDraftAfterEdit(); + } + contentEditableValue() { const UA = navigator.userAgent; const isWebkit = /WebKit/.test(UA) && !/Edge/.test(UA); @@ -372,7 +393,11 @@ export class TaskCommentComposerComponent implements OnInit, AfterViewInit, DoCh this.emojiSearchMode = false; this.showEmojiPicker = false; if (this.input.first.nativeElement.innerText.trim() !== '') { - this.addComment(); + if (this.isEditing) { + this.saveEditedComment(); + } else { + this.addComment(); + } } } @@ -534,9 +559,31 @@ export class TaskCommentComposerComponent implements OnInit, AfterViewInit, DoCh }); } + saveEditedComment() { + if (this.isSending || !this.editingComment) { + return; + } + + this.isSending = true; + const text = this.emojiService.nativeEmojiToColons(this.input.first.nativeElement.innerText); + + this.taskCommentService.editComment(this.editingComment, text).subscribe({ + next: (_tc: TaskComment) => { + this.isSending = false; + this.sharedData.editingComment = null; + this.draftBeforeEdit = ''; + this.clearInput(); + }, + error: (error: ApiError) => { + this.isSending = false; + this.alerts.error(error.error || error.message || `Failed to edit comment: ${error}`, 6000); + }, + }); + } + addCommentWithType(comment: string, type: string) { this.taskCommentService.addComment(this.task, comment, type).subscribe({ - next: (success: TaskComment) => { + next: (_success: TaskComment) => { this.comment.text = ''; this.commentsViewer.scrollDown(); console.log('implement - check map comments'); @@ -550,36 +597,196 @@ export class TaskCommentComposerComponent implements OnInit, AfterViewInit, DoCh this.uploader.nativeElement.click(); } - uploadFiles(event) { - [...event].forEach((file) => { + handlePaste(event: ClipboardEvent) { + const files = this.getClipboardFiles(event); + + if (files.length === 0) { + return; + } + + const existingText = this.input?.first?.nativeElement?.innerText ?? ''; + event.preventDefault(); + this.clearPastedPlaceholderContent(existingText); + this.uploadFiles(files); + } + + handleBeforeInput(event: InputEvent) { + if (event.inputType !== 'insertFromPaste') { + return; + } + + const files = Array.from(event.dataTransfer?.files ?? []); + + if (files.length === 0) { + return; + } + + const existingText = this.input?.first?.nativeElement?.innerText ?? ''; + event.preventDefault(); + this.clearPastedPlaceholderContent(existingText); + this.uploadFiles(files); + } + + uploadFiles(files: ArrayLike) { + const acceptedFiles: File[] = []; + + Array.from(files).forEach((file) => { if ( ACCEPTED_FILE_TYPES.includes(file.type) || file.type.startsWith('audio/') || file.type.startsWith('image/') ) { - this.postAttachmentComment(file); + acceptedFiles.push(file); } else { this.alerts.error('Cannot upload that file - only images, audio, and PDFs.', 4000); } }); + + this.confirmAttachmentsSequentially(acceptedFiles); + this.resetUploader(); + } + + private getClipboardFiles(event: ClipboardEvent): File[] { + const clipboardData = event.clipboardData; + + if (!clipboardData) { + return []; + } + + const directFiles = Array.from(clipboardData.files ?? []); + if (directFiles.length > 0) { + return directFiles; + } + + return Array.from(clipboardData.items ?? []) + .filter((item) => item.kind === 'file') + .map((item) => item.getAsFile()) + .filter((file): file is File => file != null); + } + + private clearPastedPlaceholderContent(existingText: string) { + if (!this.input?.first?.nativeElement) { + return; + } + + // Let the browser finish the paste event lifecycle, then restore the pre-paste text + // so clipboard attachment placeholders do not replace an in-progress draft. + setTimeout(() => { + this.input.first.nativeElement.innerText = existingText; + this.saveCurrentDraft(); + this.cdRef.detectChanges(); + }); } // # Upload image files as comments to a given task postAttachmentComment(file) { this.taskCommentService.addComment(this.task, file, 'file', null).subscribe( - (tc: TaskComment) => { + (_tc: TaskComment) => { this.commentsViewer.scrollDown(); }, - (error: any) => { - this.alerts.error(error || error?.message, 2000); + (error: Error) => { + this.alerts.error(error.message, 2000); }, ); } + private confirmAttachmentsSequentially(files: File[], index: number = 0) { + if (index >= files.length) { + return; + } + + const dialogRef = this.dialog.open(AttachmentConfirmationDialogComponent, { + data: { + file: files[index], + }, + maxWidth: '720px', + width: 'min(92vw, 720px)', + }); + + dialogRef.afterClosed().subscribe((confirmed: boolean) => { + if (confirmed) { + this.postAttachmentComment(files[index]); + } + + this.confirmAttachmentsSequentially(files, index + 1); + }); + } + + private resetUploader() { + if (this.uploader?.nativeElement) { + this.uploader.nativeElement.value = ''; + } + } + showFeedbackPicker() { this.showFeedbackTemplatePicker = !this.showFeedbackTemplatePicker; this.commentsViewer.scrollDown(); } + + private syncComposerState() { + if (this.isEditing) { + this.beginEditingComment(); + return; + } + + setTimeout(() => { + this.input.first.nativeElement.focus(); + }); + } + + private beginEditingComment() { + const currentText = this.input?.first?.nativeElement?.innerText ?? ''; + const nextText = this.editingComment?.text ?? ''; + + if (this.sharedData.originalComment != null) { + this.sharedData.originalComment = null; + } + + if (currentText !== nextText) { + this.draftBeforeEdit = currentText; + this.setComposerText(nextText); + } + + setTimeout(() => { + this.focusComposerAtEnd(); + }); + } + + private restoreDraftAfterEdit() { + const draft = this.draftBeforeEdit; + this.draftBeforeEdit = ''; + this.setComposerText(draft); + } + + private setComposerText(text: string) { + if (!this.input?.first?.nativeElement) { + return; + } + + this.input.first.nativeElement.innerText = text; + this.cdRef.detectChanges(); + } + + private focusComposerAtEnd() { + const element = this.input?.first?.nativeElement; + if (!element) { + return; + } + + element.focus(); + + const selection = window.getSelection(); + if (!selection) { + return; + } + + const range = document.createRange(); + range.selectNodeContents(element); + range.collapse(false); + + selection.removeAllRanges(); + selection.addRange(range); + } } // The discussion prompt composer dialog Component @@ -588,12 +795,12 @@ export class TaskCommentComposerComponent implements OnInit, AfterViewInit, DoCh selector: 'discussion-prompt-composer-dialog.html', templateUrl: 'discussion-prompt-composer-dialog.html', styleUrls: ['./discussion-prompt-composer/discussion-prompt-composer.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class DiscussionComposerDialog implements OnInit { +export class DiscussionComposerDialog { constructor( public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: {task: Task}, ) {} - - ngOnInit() {} } diff --git a/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.html b/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.html index 323a2e0eed..02c21111bc 100644 --- a/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.html +++ b/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.html @@ -1,11 +1,13 @@ - - + + @for (category of categories; track category) { + + } - + search -
    -
    -
    - - check_circle - {{ template.chipText }} - -
    -
    - -
    -
    -
    - {{ item.outcome.abbreviation }} - {{ item.outcome.shortDescription }} -
    -
    +
    +
    +
    + @for (template of genTemplates$ | async; track template) { - check_circle - - folder - chevron_right + @if (isTemplateSelected(template)) { + check_circle + } {{ template.chipText }} -
    + }
    -
    -
    -
    - {{ item.outcome.abbreviation }} - {{ item.outcome.shortDescription }} -
    -
    - - check_circle - - folder - chevron_right - {{ template.chipText }} - +
    + @for (item of tlos$ | async; track item) { +
    +
    + {{ item.outcome.abbreviation }} - {{ item.outcome.shortDescription }} +
    +
    + @for (template of item.templates; track template) { + + @if (isTemplateSelected(template)) { + check_circle + } + @if (!isTemplateSelected(template) && template.taskStatus) { + {{ + taskService.statusData(template.taskStatus).materialIcon + }} + } + @if (template.type === 'group' && !isGroupExpanded(template)) { + folder + } + @if (template.type === 'group' && isGroupExpanded(template)) { + chevron_right + } + {{ template.chipText }} + + } +
    -
    + }
    -
    -
    -
    - {{ item.outcome.abbreviation }} - {{ item.outcome.shortDescription }} -
    -
    - - check_circle - - folder - chevron_right - {{ template.chipText }} - +
    + @for (item of ulos$ | async; track item) { +
    +
    + {{ item.outcome.abbreviation }} - {{ item.outcome.shortDescription }} +
    +
    + @for (template of item.templates; track template) { + + @if (isTemplateSelected(template)) { + check_circle + } + @if (!isTemplateSelected(template) && template.taskStatus) { + {{ + taskService.statusData(template.taskStatus).materialIcon + }} + } + @if (template.type === 'group' && !isGroupExpanded(template)) { + folder + } + @if (template.type === 'group' && isGroupExpanded(template)) { + chevron_right + } + {{ template.chipText }} + + } +
    -
    + } +
    + +
    + @for (item of glos$ | async; track item) { +
    +
    + {{ item.outcome.abbreviation }} - {{ item.outcome.shortDescription }} +
    +
    + @for (template of item.templates; track template) { + + @if (isTemplateSelected(template)) { + check_circle + } + @if (!isTemplateSelected(template) && template.taskStatus) { + {{ + taskService.statusData(template.taskStatus).materialIcon + }} + } + @if (template.type === 'group' && !isGroupExpanded(template)) { + folder + } + @if (template.type === 'group' && isGroupExpanded(template)) { + chevron_right + } + {{ template.chipText }} + + } +
    +
    + }
    -
    - {{ +
    + {{ hoveredTemplate?.type === 'group' ? 'folder' : 'description' }}
    -

    +

    {{ hoveredTemplate?.chipText || 'Select a feedback chip' }}

    -

    +

    {{ hoveredTemplate?.description || 'This will populate the comment area with feedback.' }}

    diff --git a/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.scss b/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.scss index 7158f04fb8..ebab784fe9 100644 --- a/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.scss +++ b/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.scss @@ -1,12 +1,13 @@ .category-tab-group .mat-mdc-tab-header { - --mdc-secondary-navigation-tab-container-height: 40px; + --mat-tab-container-height: 40px; } .template-search .mat-mdc-form-field-subscript-wrapper.mat-mdc-form-field-bottom-align { height: 0px; } -.template-search .mat-mdc-text-field-wrapper, .template-search .mat-mdc-form-field { +.template-search .mat-mdc-text-field-wrapper, +.template-search .mat-mdc-form-field { height: 40px; } diff --git a/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.ts b/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.ts index 696e5ecca9..816f4c6e2d 100644 --- a/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.ts +++ b/src/app/tasks/task-comment-composer/task-feedback-templates/task-feedback-templates.component.ts @@ -1,22 +1,24 @@ import { + ChangeDetectionStrategy, Component, ElementRef, + EventEmitter, Input, OnChanges, + OnInit, Output, SimpleChanges, ViewChild, ViewEncapsulation, - EventEmitter, - OnInit, } from '@angular/core'; -import {BehaviorSubject, combineLatest, map, Observable} from 'rxjs'; +import {MatTabChangeEvent} from '@angular/material/tabs'; +import {BehaviorSubject, Observable, combineLatest, map} from 'rxjs'; import { - LearningOutcome, FeedbackTemplate, - Task, FeedbackTemplateService, + LearningOutcome, LearningOutcomeService, + Task, TaskService, } from 'src/app/api/models/doubtfire-model'; @@ -25,24 +27,26 @@ import { styleUrl: './task-feedback-templates.component.scss', templateUrl: './task-feedback-templates.component.html', encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TaskFeedbackTemplatesComponent implements OnInit, OnChanges { @Input() task: Task; - @Output() templateSelected = new EventEmitter(); + @Output() templateSelected: EventEmitter = new EventEmitter(); categories = ['TLO', 'ULO', 'GLO']; selectedTemplates: FeedbackTemplate[] = []; hoveredTemplate: FeedbackTemplate; - private generalTemplatesSubject = new BehaviorSubject([]); + private generalTemplatesSubject: BehaviorSubject = new BehaviorSubject([]); generalTemplates$ = this.generalTemplatesSubject.asObservable(); - private navigationStackSubject = new BehaviorSubject>( + private navigationStackSubject: BehaviorSubject> = new BehaviorSubject( new Map(), ); navigationStack$ = this.navigationStackSubject.asObservable(); - private searchTermSubject = new BehaviorSubject(''); + private searchTermSubject: BehaviorSubject = new BehaviorSubject(''); searchTerm$ = this.searchTermSubject.asObservable(); public genTemplates$ = combineLatest([this.generalTemplates$, this.searchTerm$]).pipe( @@ -74,14 +78,14 @@ export class TaskFeedbackTemplatesComponent implements OnInit, OnChanges { constructor( private learningOutcomeService: LearningOutcomeService, private feedbackTemplateService: FeedbackTemplateService, - private taskService: TaskService, + public taskService: TaskService, ) {} ngOnInit(): void { const greetingTemplate = new FeedbackTemplate(); greetingTemplate.type = 'template'; greetingTemplate.chipText = 'Greeting'; - greetingTemplate.description = 'Insert a greeting with the student\'s name.'; + greetingTemplate.description = "Insert a greeting with the student's name."; const summaryTemplate = new FeedbackTemplate(); summaryTemplate.type = 'template'; @@ -91,7 +95,7 @@ export class TaskFeedbackTemplatesComponent implements OnInit, OnChanges { this.generalTemplatesSubject.next([greetingTemplate, summaryTemplate]); } - ngOnChanges(changes: SimpleChanges): void { + ngOnChanges(_changes: SimpleChanges): void { this.selectedTemplates = []; this.navigationStackSubject.next(new Map()); this.searchTermSubject.next(''); @@ -158,7 +162,9 @@ export class TaskFeedbackTemplatesComponent implements OnInit, OnChanges { templatesToDisplay.push(allTemplates.find((template) => template.id === groupId)); allTemplates.forEach((template) => { - if (template.parentChipId === groupId) templatesToDisplay.push(template); + if (template.parentChipId === groupId) { + templatesToDisplay.push(template); + } }); } else { templatesToDisplay = allTemplates.filter((template) => !template.parentChipId); @@ -179,7 +185,7 @@ export class TaskFeedbackTemplatesComponent implements OnInit, OnChanges { @ViewChild('uloSection') uloSection!: ElementRef; @ViewChild('gloSection') gloSection!: ElementRef; - scrollToSection(event: any) { + scrollToSection(event: MatTabChangeEvent) { const sections = [this.tloSection, this.uloSection, this.gloSection]; const selectedSection = sections[event.index]; @@ -193,10 +199,14 @@ export class TaskFeedbackTemplatesComponent implements OnInit, OnChanges { if (template.chipText === 'Greeting') { template.commentText = `Hi ${this.task.project.student.preferredName}. `; } else if (template.chipText === 'Summarise feedback') { - if (!this.selectedTemplates || this.selectedTemplates.length < 1) return; + if (!this.selectedTemplates || this.selectedTemplates.length < 1) { + return; + } template.commentText = 'Summary of the given feedback:'; this.selectedTemplates.forEach((t) => { - if (!t.summaryText) return; + if (!t.summaryText) { + return; + } template.commentText += '\n- ' + t.summaryText; }); } else { @@ -208,8 +218,11 @@ export class TaskFeedbackTemplatesComponent implements OnInit, OnChanges { const updatedStack = new Map(this.navigationStackSubject.getValue()); const outcomeStack = updatedStack.get(template.learningOutcomeId) || []; const index = outcomeStack.indexOf(template.id); - if (index === -1) outcomeStack.push(template.id); - else outcomeStack.splice(index, 1); + if (index === -1) { + outcomeStack.push(template.id); + } else { + outcomeStack.splice(index, 1); + } updatedStack.set(template.learningOutcomeId, outcomeStack); this.navigationStackSubject.next(updatedStack); } @@ -223,8 +236,11 @@ export class TaskFeedbackTemplatesComponent implements OnInit, OnChanges { const stack = this.navigationStackSubject.getValue(); const outcomeStack = stack.get(template.learningOutcomeId) || []; const index = outcomeStack.indexOf(template.id); - if (index === -1) return false; - else return true; + if (index === -1) { + return false; + } else { + return true; + } } onHoverTemplate(template: FeedbackTemplate) { @@ -235,7 +251,9 @@ export class TaskFeedbackTemplatesComponent implements OnInit, OnChanges { if (template.taskStatus && this.task.suggestedTaskStatus) { const currentSeq = this.taskService.statusSeq.get(this.task.suggestedTaskStatus); const templateSeq = this.taskService.statusSeq.get(template.taskStatus); - if (templateSeq < currentSeq) this.task.suggestedTaskStatus = template.taskStatus; + if (templateSeq < currentSeq) { + this.task.suggestedTaskStatus = template.taskStatus; + } } else { this.task.suggestedTaskStatus = template.taskStatus; } diff --git a/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.html b/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.html index fa43ccb7ee..f3b9b5c748 100644 --- a/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.html +++ b/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.html @@ -1,24 +1,34 @@ -
    +
    reply + edit + delete diff --git a/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.scss b/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.scss index 5ff063b4e3..d14ba0b3bb 100644 --- a/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.scss +++ b/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.scss @@ -6,7 +6,7 @@ cursor: pointer; vertical-align: middle; text-align: center; - margin-left: 0.3em + margin-left: 0.3em; } .mat-icon:hover { diff --git a/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.spec.ts b/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.spec.ts index 5bb9a879fa..7e7508a7d2 100644 --- a/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.spec.ts +++ b/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.spec.ts @@ -1,33 +1,31 @@ -// import { async, ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -// import { TaskComment } from 'src/app/api/models/doubtfire-model'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; +import {CommentBubbleActionComponent} from './comment-bubble-action.component'; -// import { CommentBubbleActionComponent } from './comment-bubble-action.component'; +const emptyProvider = {}; -// describe('CommentBubbleActionComponent', () => { -// let component: CommentBubbleActionComponent; -// let fixture: ComponentFixture; -// let taskComment: TaskComment; +describe('CommentBubbleActionComponent', () => { + let component: CommentBubbleActionComponent; + let fixture: ComponentFixture; -// beforeEach( -// waitForAsync(() => { -// TestBed.configureTestingModule({ -// declarations: [CommentBubbleActionComponent], -// }).compileComponents(); -// }) -// ); + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CommentBubbleActionComponent], + providers: [{provide: ConfirmationModalService, useValue: emptyProvider}], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(CommentBubbleActionComponent, {set: {template: ''}}) + .compileComponents(); + }); -// beforeEach(() => { -// fixture = TestBed.createComponent(CommentBubbleActionComponent); -// component = fixture.componentInstance; + beforeEach(() => { + fixture = TestBed.createComponent(CommentBubbleActionComponent); + component = fixture.componentInstance; + }); -// taskComment = jasmine.createSpyObj('TaskComment', ['currentUserCanEdit']); -// taskComment.currentUserCanEdit.and.returnValue(false); -// component.comment = taskComment; - -// fixture.detectChanges(); -// }); - -// it('should create', () => { -// expect(component).toBeTruthy(); -// }); -// }); + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.ts b/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.ts index 5b62675292..dc68053e56 100644 --- a/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.ts +++ b/src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.ts @@ -1,24 +1,31 @@ -import {Component, OnInit, Input} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; import {TaskComment} from 'src/app/api/models/doubtfire-model'; -import {TaskCommentComposerData} from '../../task-comment-composer/task-comment-composer.component'; import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; +import {TaskCommentComposerData} from '../../task-comment-composer/task-comment-composer.component'; @Component({ selector: 'comment-bubble-action', templateUrl: './comment-bubble-action.component.html', styleUrls: ['./comment-bubble-action.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class CommentBubbleActionComponent implements OnInit { +export class CommentBubbleActionComponent { @Input() comment: TaskComment; @Input() sharedData: TaskCommentComposerData; constructor(private confirmationModalService: ConfirmationModalService) {} - ngOnInit() {} reply() { + this.sharedData.editingComment = null; this.sharedData.originalComment = this.comment; } + edit() { + this.sharedData.originalComment = null; + this.sharedData.editingComment = this.comment; + } + delete() { this.confirmationModalService.show( `Delete comment`, diff --git a/src/app/tasks/task-comments-viewer/extension-comment/extension-comment.component.html b/src/app/tasks/task-comments-viewer/extension-comment/extension-comment.component.html index 927ee7e260..00af98e249 100644 --- a/src/app/tasks/task-comments-viewer/extension-comment/extension-comment.component.html +++ b/src/app/tasks/task-comments-viewer/extension-comment/extension-comment.component.html @@ -1,27 +1,32 @@
    @if (comment.assessed) { -
    -
    -

    reason: {{ comment.text }}

    -
    -} +
    +
    +

    reason: {{ comment.text }}

    +
    + } @if (!comment.assessed) { -
    -
    -

    - {{ message }}
    - reason: {{ comment.text }} -

    - @if (isNotStudent) { -
    - - +
    +
    +

    + {{ message }}
    + reason: {{ comment.text }} +

    + @if (isNotStudent) { +
    + + +
    + } +
    -} -
    -
    -} + }
    diff --git a/src/app/tasks/task-comments-viewer/extension-comment/extension-comment.component.scss b/src/app/tasks/task-comments-viewer/extension-comment/extension-comment.component.scss index c2917f902e..1a5626b320 100644 --- a/src/app/tasks/task-comments-viewer/extension-comment/extension-comment.component.scss +++ b/src/app/tasks/task-comments-viewer/extension-comment/extension-comment.component.scss @@ -32,7 +32,7 @@ hr { height: 1.5em; opacity: 0.8; &:before { - content: ""; + content: ''; background: linear-gradient(to right, transparent, #9696969d, transparent); position: absolute; left: 0; diff --git a/src/app/tasks/task-comments-viewer/extension-comment/extension-comment.component.ts b/src/app/tasks/task-comments-viewer/extension-comment/extension-comment.component.ts index 27d437d928..4db00f9864 100644 --- a/src/app/tasks/task-comments-viewer/extension-comment/extension-comment.component.ts +++ b/src/app/tasks/task-comments-viewer/extension-comment/extension-comment.component.ts @@ -1,5 +1,5 @@ -import {Component, OnInit, Input, Inject} from '@angular/core'; -import {TaskComment, Task} from 'src/app/api/models/doubtfire-model'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {Task, TaskComment} from 'src/app/api/models/doubtfire-model'; import {ExtensionComment} from 'src/app/api/models/task-comment/extension-comment'; import {AlertService} from 'src/app/common/services/alert.service'; @@ -7,19 +7,19 @@ import {AlertService} from 'src/app/common/services/alert.service'; selector: 'extension-comment', templateUrl: './extension-comment.component.html', styleUrls: ['./extension-comment.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class ExtensionCommentComponent implements OnInit { +export class ExtensionCommentComponent { @Input() comment: ExtensionComment; @Input() task: Task; constructor(private alerts: AlertService) {} - private handleError(error: any) { + private handleError(error: {data: {error: string}}) { this.alerts.error('Error: ' + error.data.error, 6000); } - ngOnInit() {} - get message() { const studentName = this.comment.author.name; if (this.comment.assessed) { @@ -42,7 +42,7 @@ export class ExtensionCommentComponent implements OnInit { denyExtension() { this.comment.deny().subscribe({ - next: (tc: TaskComment) => { + next: (_tc: TaskComment) => { this.alerts.success('Extension updated', 2000); }, error: (response) => { @@ -53,7 +53,7 @@ export class ExtensionCommentComponent implements OnInit { grantExtension() { this.comment.grant().subscribe({ - next: (tc: TaskComment) => { + next: (_tc: TaskComment) => { this.alerts.success('Extension updated', 2000); }, error: (response) => { diff --git a/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-dialog.html b/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-dialog.html index 8ba35a616f..78ae16364b 100644 --- a/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-dialog.html +++ b/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-dialog.html @@ -1,75 +1,155 @@ - - + + Introduction -
    - Discussion Splash Image -

    - Your tutor would like to discuss some topics with you regarding this task - - Discussions are a great way for you and your tutor to gauge your understanding of concepts and are an important - aspect of your portfolio. - - These discussions are designed to be casual and informal, and will require a working microphone and - speakers. - These are timed discussions, you will have a few minutes from when you hear your tutor's discussion - prompt to reply.



    - - You should only proceed once you are in a suitably quiet environment. -

    +
    + Discussion Splash Image +
    +

    + Your tutor would like to discuss this task +

    +

    + You will hear one or more short prompts, then respond out loud. The discussion is casual, + but it is recorded as part of your task conversation. +

    +

    + You will need a working microphone and speakers. Start only when you are somewhere quiet. +

    +
    -
    - + +
    +
    - Test everything is working + Microphone check + - -
    - Ready to go! - - + +
    + + +
    + Ready to go + +
    Discussion -
    - - -
    -

    {{guide.text}}

    - +
    +
    +
    +

    + Prompt {{ activePromptId + 1 }} of {{ numberOfPrompts }} +

    +

    {{ discussionStatusTitle }}

    +

    {{ discussionStatusHint }}

    +
    + +
    +
    + + {{ promptLoading ? 'hourglass_empty' : promptPlaying ? 'volume_up' : 'hearing' }} + +

    Listen

    +

    The tutor prompt will play first.

    +
    + +
    + + {{ responseRecording ? 'record_voice_over' : 'chat_bubble_outline' }} + +

    Respond

    +

    Answer after the tone.

    +
    +
    + +
    -
    - - -
    -

    Prompt {{activePromptId+1}} of {{numberOfPrompts}}

    - -
    -
    - - +
    + + +
    + @if (!startedDiscussion) { + + } @else if (!discussionComplete) { + + } @else { + + } +
    +
    Done - Thank you for completing the discussion. +
    + Thank you for completing the discussion. +
    diff --git a/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.html b/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.html index c000036835..8f80d2763b 100644 --- a/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.html +++ b/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.html @@ -1,71 +1,73 @@ -
    - +
    + +
    -

    - Discussion {{ discussion.status }} - question_answer -

    - -
    - @if (discussion.status === 'opened' && isNotStudent) { -
    - Warning: The student has opened the discussion prompts without sending a response. -
    -} +
    +

    + + question_answer + + Discussion {{ discussion.status }} +

    + + + {{ discussion.numberOfPrompts }} prompt{{ discussion.numberOfPrompts === 1 ? '' : 's' }} + +
    + + + + @if (discussion.status === 'opened' && isNotStudent) {
    - - - - P1 - - @if (discussion.numberOfPrompts > 1) { - - P2 - -} - @if (discussion.numberOfPrompts > 2) { - - P3 - -} - - Response - - + The student has opened the discussion prompts without sending a response.
    -
    + } -
    -

    Your tutor would like to discuss this task with you.

    - -
    + @if (isNotStudent || responseAvailable) { + + + +

    Discussion audio

    + + @for (promptNumber of promptNumbers; track promptNumber) { + + + {{ + isTrackPlaying(promptTrackKey(promptNumber)) ? 'stop_circle' : 'play_arrow_rounded' + }} + + Prompt {{ promptNumber + 1 }} + + } + + @if (responseAvailable) { + + + {{ isTrackPlaying('response') ? 'stop_circle' : 'record_voice_over' }} + + Student response + + Recorded response to the prompt sequence + + + } +
    + } + + @if (!responseAvailable && !isNotStudent) { +
    +

    + Your tutor would like to discuss this task with you. +

    + +
    + }
    diff --git a/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.scss b/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.scss index 58ff4d0db0..e69de29bb2 100644 --- a/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.scss +++ b/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.scss @@ -1,93 +0,0 @@ -:host mat-divider { - margin-bottom: 20px; -} - -#discussion-title { - -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; -} - -mat-divider { - background-color: white; -} - -#startDiscussionBtn { - margin: 20px; - float: right; -} - -/* TODO(mdc-migration): The following rule targets internal classes of button that may no longer apply for the MDC version. */ -/* TODO(mdc-migration): The following rule targets internal classes of button that may no longer apply for the MDC version. */ -.mat-button-toggle-group { - border-radius: 12px; -} - -.mat-mdc-progress-bar { - width: 100%; - margin-bottom: 1em; -} - -audio-player { - width: 100%; -} - -microphone-tester .btn-circle-xl { - position: inherit; -} - -:host .mat-mdc-dialog-container { - max-width: 800px !important; -} - -:host #intelligentDiscussionStepper { - margin-top: 20px; -} - -:host .introduction-text { - margin-top: 4em; - display: inline-flex; -} - -:host img#discussion-splash-image { - float: left; - margin: auto; - max-width: 800px; - max-height: 300px; - vertical-align: middle; -} - -:host .introduction-text p { - font-family: "Helvetica Neue", "Segoe UI", "Helvetica", "Arial", "sans-serif"; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - line-height: 1.1; - font-size: 1.1em; - text-align: justify; - margin-left: 3em; - margin-top: 0; - float: right; -} - -:host #dialogCloseButton { - float: right; - margin-bottom: 20px; - color: red; -} - -:host #discussionReadout { - overflow: hidden; -} - -:host #discussionRecorderContainer { - line-height: 1.2em; - font-family: "Helvetica Neue", "Segoe UI", "Helvetica", "Arial", "sans-serif"; - - p { - font-size: 1.2em; - } - - p#discussionCountdown { - font-size: 3em; - font-weight: bold; - } -} diff --git a/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.spec.ts b/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.spec.ts new file mode 100644 index 0000000000..070a96755f --- /dev/null +++ b/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.spec.ts @@ -0,0 +1,98 @@ +import {beforeEach, describe, expect, it, vi} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatDialog} from '@angular/material/dialog'; +import {DiscussionComment, Task, TaskCommentService} from 'src/app/api/models/doubtfire-model'; +import {AudioPlayerComponent} from 'src/app/common/audio-player/audio-player.component'; +import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {IntelligentDiscussionPlayerComponent} from './intelligent-discussion-player.component'; + +describe('IntelligentDiscussionPlayerComponent', () => { + let component: IntelligentDiscussionPlayerComponent; + let fixture: ComponentFixture; + let fileDownloader: {downloadBlob: ReturnType}; + let audioPlayer: { + setSrc: ReturnType; + play: ReturnType; + stop: ReturnType; + }; + + beforeEach(async () => { + fileDownloader = { + downloadBlob: vi.fn((url: string, onSuccess: (blobUrl: string) => void) => { + onSuccess(`blob:${url}`); + }), + }; + audioPlayer = { + setSrc: vi.fn(), + play: vi.fn(), + stop: vi.fn(), + }; + + await TestBed.configureTestingModule({ + declarations: [IntelligentDiscussionPlayerComponent], + providers: [ + {provide: MatDialog, useValue: {open: vi.fn()}}, + {provide: TaskCommentService, useValue: {}}, + {provide: FileDownloaderService, useValue: fileDownloader}, + {provide: AlertService, useValue: {error: vi.fn()}}, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(IntelligentDiscussionPlayerComponent, {set: {template: ''}}) + .compileComponents(); + + fixture = TestBed.createComponent(IntelligentDiscussionPlayerComponent); + component = fixture.componentInstance; + component.discussion = { + id: 69, + numberOfPrompts: 2, + status: 'complete', + responseUrl: '/discussion/response', + generateDiscussionPromptUrl: (promptNumber: number) => `/discussion/prompt/${promptNumber}`, + } as unknown as DiscussionComment; + component.task = {unit: {currentUserIsStaff: true}} as Task; + component.audioPlayer = audioPlayer as unknown as AudioPlayerComponent; + }); + + it('downloads and plays a selected prompt', () => { + component.togglePromptTrack(1); + + expect(fileDownloader.downloadBlob).toHaveBeenCalledWith( + '/discussion/prompt/1', + expect.any(Function), + expect.any(Function), + ); + expect(audioPlayer.setSrc).toHaveBeenCalledWith('blob:/discussion/prompt/1'); + expect(audioPlayer.play).toHaveBeenCalled(); + expect(component.selectedTrackLabel).toEqual('Prompt 2'); + expect(component.selectedTrackKey).toEqual('prompt-1'); + }); + + it('stops the current prompt instead of downloading it again', () => { + component.selectedTrackKey = 'prompt-1'; + component.audioPlaying = true; + + component.togglePromptTrack(1); + + expect(audioPlayer.stop).toHaveBeenCalled(); + expect(fileDownloader.downloadBlob).not.toHaveBeenCalled(); + }); + + it('only downloads and plays the response when requested', () => { + expect(fileDownloader.downloadBlob).not.toHaveBeenCalled(); + + component.toggleResponseTrack(); + + expect(fileDownloader.downloadBlob).toHaveBeenCalledWith( + '/discussion/response', + expect.any(Function), + expect.any(Function), + ); + expect(audioPlayer.setSrc).toHaveBeenCalledWith('blob:/discussion/response'); + expect(audioPlayer.play).toHaveBeenCalled(); + expect(component.selectedTrackLabel).toEqual('Response'); + expect(component.selectedTrackKey).toEqual('response'); + }); +}); diff --git a/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.ts b/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.ts index a862e70dd8..cbdb8469d9 100644 --- a/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.ts +++ b/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.ts @@ -1,30 +1,46 @@ -import { Component, Inject, OnInit, ViewChild, Input, AfterViewInit } from '@angular/core'; -import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog'; -import { timer, Subscription } from 'rxjs'; -import { IntelligentDiscussionPlayerService } from './intelligent-discussion-player.service'; import moment from 'moment'; -import { MicrophoneTesterComponent } from 'src/app/common/audio-recorder/audio/microphone-tester/microphone-tester.component'; -import { IntelligentDiscussionRecorderComponent } from './intelligent-discussion-recorder/intelligent-discussion-recorder.component'; -import { AudioPlayerComponent } from 'src/app/common/audio-player/audio-player.component'; -import { Task, DiscussionComment } from 'src/app/api/models/doubtfire-model'; +import { + ChangeDetectionStrategy, + Component, + Inject, + Input, + OnDestroy, + ViewChild, +} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog'; +import {Subscription, timer} from 'rxjs'; +import {DiscussionComment, Task} from 'src/app/api/models/doubtfire-model'; +import {AudioPlayerComponent} from 'src/app/common/audio-player/audio-player.component'; +import {MicrophoneTesterComponent} from 'src/app/common/audio-recorder/audio/microphone-tester/microphone-tester.component'; +import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {IntelligentDiscussionPlayerService} from './intelligent-discussion-player.service'; +import {IntelligentDiscussionRecorderComponent} from './intelligent-discussion-recorder/intelligent-discussion-recorder.component'; + @Component({ selector: 'intelligent-discussion-player', templateUrl: './intelligent-discussion-player.component.html', styleUrls: ['./intelligent-discussion-player.component.scss'], providers: [IntelligentDiscussionPlayerService], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class IntelligentDiscussionPlayerComponent implements AfterViewInit { +export class IntelligentDiscussionPlayerComponent { @Input() discussion: DiscussionComment; @Input() task: Task; @ViewChild('player') audioPlayer: AudioPlayerComponent; loading: boolean = false; audioProgress: number = 0; + selectedTrackLabel = 'Response'; + selectedTrackKey = 'response'; + audioPlaying = false; - constructor(public dialog: MatDialog, private discussionService: IntelligentDiscussionPlayerService) {} - - ngAfterViewInit() { - this.setPromptTrack('response'); - } + constructor( + public dialog: MatDialog, + private discussionService: IntelligentDiscussionPlayerService, + private fileDownloader: FileDownloaderService, + private alerts: AlertService, + ) {} get responseAvailable() { return this.discussion.status === 'complete'; @@ -34,33 +50,82 @@ export class IntelligentDiscussionPlayerComponent implements AfterViewInit { return this.task.unit.currentUserIsStaff; } + get promptNumbers(): number[] { + return Array.from({length: this.discussion.numberOfPrompts}, (_, index) => index); + } + + promptTrackKey(promptNumber: number): string { + return `prompt-${promptNumber}`; + } + + isTrackPlaying(trackKey: string): boolean { + return this.selectedTrackKey === trackKey && this.audioPlaying; + } + + togglePromptTrack(promptNumber: number): void { + const trackKey = this.promptTrackKey(promptNumber); + if (this.isTrackPlaying(trackKey)) { + this.audioPlayer?.stop(); + return; + } + + this.setPromptTrack('prompt', promptNumber); + } + + toggleResponseTrack(): void { + if (this.isTrackPlaying('response')) { + this.audioPlayer?.stop(); + return; + } + + this.setPromptTrack('response'); + } + setPromptTrack(track: string, promptNumber?: number) { - let url: string = ''; + let url: string; if (track === 'prompt') { url = this.discussion.generateDiscussionPromptUrl(promptNumber); + this.selectedTrackLabel = `Prompt ${promptNumber + 1}`; + this.selectedTrackKey = this.promptTrackKey(promptNumber); } else { url = this.discussion.responseUrl; + this.selectedTrackLabel = 'Response'; + this.selectedTrackKey = 'response'; } - this.audioPlayer.setSrc(url); + this.fileDownloader.downloadBlob( + url, + (blobUrl) => { + if (!this.audioPlayer) { + return; + } + + this.audioPlayer.setSrc(blobUrl); + this.audioPlayer.play(); + }, + (error) => { + this.alerts.error(`Error loading discussion audio. ${error}`, 6000); + }, + ); } beginDiscussion(): void { - let dialogRef: MatDialogRef; - - dialogRef = this.dialog.open(IntelligentDiscussionDialog, { - data: { - dc: this.discussion, - task: this.task, - audioRef: this.audioPlayer.audio, + const dialogRef: MatDialogRef = this.dialog.open( + IntelligentDiscussionDialog, + { + data: { + dc: this.discussion, + task: this.task, + audioRef: new Audio(), + }, + maxWidth: '800px', + disableClose: true, }, - maxWidth: '800px', - disableClose: true, - }); + ); - dialogRef.afterOpened().subscribe((result: any) => {}); + dialogRef.afterOpened().subscribe(); - dialogRef.afterClosed().subscribe((result: any) => {}); + dialogRef.afterClosed().subscribe(); } } @@ -71,33 +136,50 @@ export class IntelligentDiscussionPlayerComponent implements AfterViewInit { templateUrl: 'intelligent-discussion-dialog.html', styleUrls: ['./intelligent-discussion-player.component.scss'], providers: [IntelligentDiscussionPlayerService], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class IntelligentDiscussionDialog implements OnInit { +export class IntelligentDiscussionDialog implements OnDestroy { confirmed = false; timerText: string = '15m:00s'; ticks: number = 0; startedDiscussion = false; inDiscussion = false; discussionComplete: boolean = false; + promptLoading = false; + promptPlaying = false; + responseRecording = false; + countdownValue: number = null; count: number = 3 * 60 * 1000; // 3 minutes activePromptId: number = 0; counter: Subscription; - guide = { text: 'Click start to begin' }; + private countdownTimer: ReturnType; + guide = {text: 'Click start to begin'}; + private promptBlobUrl: string; - @ViewChild('testRecorder', { static: true }) testRecorder: MicrophoneTesterComponent; - @ViewChild('discussionRecorder', { static: true }) discussionRecorder: IntelligentDiscussionRecorderComponent; + @ViewChild('testRecorder', {static: true}) testRecorder: MicrophoneTesterComponent; + @ViewChild('discussionRecorder', {static: true}) + discussionRecorder: IntelligentDiscussionRecorderComponent; constructor( public dialogRef: MatDialogRef, private discussionService: IntelligentDiscussionPlayerService, - @Inject(MAT_DIALOG_DATA) public data: { + private fileDownloader: FileDownloaderService, + private alerts: AlertService, + @Inject(MAT_DIALOG_DATA) + public data: { dc: DiscussionComment; task: Task; audioRef: HTMLAudioElement; - } + }, ) {} - ngOnInit() {} + ngOnDestroy(): void { + this.counter?.unsubscribe(); + this.clearCountdown(); + this.data.audioRef.pause(); + this.releasePromptBlob(); + } disableTester() { this.testRecorder.stopRecording(); @@ -111,68 +193,163 @@ export class IntelligentDiscussionDialog implements OnInit { return this.data.dc.numberOfPrompts; } + get canAdvancePrompt(): boolean { + return this.inDiscussion && this.responseRecording; + } + + get discussionStatusTitle(): string { + if (this.countdownValue) { + return 'Starting discussion'; + } + if (!this.startedDiscussion) { + return 'Ready when you are'; + } + if (this.promptLoading) { + return 'Loading prompt'; + } + if (this.promptPlaying) { + return `Listening to prompt ${this.activePromptId + 1}`; + } + if (this.responseRecording) { + return 'Respond now'; + } + if (this.discussionComplete) { + return 'Discussion complete'; + } + return 'Discussion in progress'; + } + + get discussionStatusHint(): string { + if (this.countdownValue) { + return 'Get ready. Recording will begin when the countdown finishes.'; + } + if (!this.startedDiscussion) { + return 'When you start, your microphone will begin recording and the first prompt will play.'; + } + if (this.promptLoading) { + return 'Getting the next tutor prompt ready.'; + } + if (this.promptPlaying) { + return 'Listen carefully. Wait for the tone before responding.'; + } + if (this.responseRecording) { + return 'Speak your answer now. When you are ready, move to the next prompt or finish the discussion.'; + } + if (this.discussionComplete) { + return 'Your response has been recorded. Select Complete to close out the discussion.'; + } + return ''; + } + finishDiscussion() { this.discussionComplete = true; this.inDiscussion = false; - this.guide = { text: '' }; + this.promptLoading = false; + this.promptPlaying = false; + this.responseRecording = false; + this.clearCountdown(); + this.guide = {text: ''}; this.discussionRecorder.stopRecording(); this.data.audioRef.pause(); this.data.audioRef.currentTime = 0; - this.counter.unsubscribe(); + this.counter?.unsubscribe(); this.data.dc.status = 'complete'; } startDiscussion() { if (!this.startedDiscussion) { - this.setPrompt(); - - // start recording - this.discussionRecorder.startRecording(); - - // start the discussion this.startedDiscussion = true; this.inDiscussion = true; + this.startCountdown(); + } + } - // get the cutoff date from the server - // For now this is stubbed as 15 minutes from now. - const discussionCutoff = moment().add(15, 'minutes'); + private beginRecordingAndFirstPrompt(): void { + // start recording + this.discussionRecorder.startRecording(); - this.counter = timer(0, 1000).subscribe((val) => { - let difference = discussionCutoff.diff(moment()); - if (difference <= 0) { - difference = 0; - } - this.timerText = moment.utc(difference).format('mm[m]:ss[s]'); - this.ticks = val; + this.setPrompt(); - if (difference === 0) { - this.inDiscussion = false; - this.counter.unsubscribe(); - } - }); + // get the cutoff date from the server + // For now this is stubbed as 15 minutes from now. + const discussionCutoff = moment().add(15, 'minutes'); + + this.counter = timer(0, 1000).subscribe((val) => { + let difference = discussionCutoff.diff(moment()); + if (difference <= 0) { + difference = 0; + } + this.timerText = moment.utc(difference).format('mm[m]:ss[s]'); + this.ticks = val; + + if (difference === 0) { + this.inDiscussion = false; + this.counter.unsubscribe(); + } + }); + } + + private startCountdown(): void { + this.countdownValue = 3; + this.guide.text = 'Starting discussion'; + + this.countdownTimer = setInterval(() => { + this.countdownValue--; + + if (this.countdownValue <= 0) { + this.clearCountdown(); + this.beginRecordingAndFirstPrompt(); + } + }, 1000); + } + + private clearCountdown(): void { + if (this.countdownTimer) { + clearInterval(this.countdownTimer); + this.countdownTimer = undefined; } + this.countdownValue = null; } setPrompt() { - this.data.audioRef.src = this.data.dc.generateDiscussionPromptUrl( - this.activePromptId + this.promptLoading = true; + this.promptPlaying = false; + this.responseRecording = false; + this.guide.text = 'Loading prompt'; + this.data.audioRef.pause(); + this.releasePromptBlob(); + + this.fileDownloader.downloadBlob( + this.data.dc.generateDiscussionPromptUrl(this.activePromptId), + (blobUrl) => { + this.promptBlobUrl = blobUrl; + this.data.audioRef.src = blobUrl; + this.guide.text = 'Listening to prompt'; + this.promptLoading = false; + this.promptPlaying = true; + this.data.audioRef.load(); + this.data.audioRef.play(); + this.data.audioRef.onended = () => { + this.promptPlaying = false; + const audio = new Audio(); + audio.src = '/assets/sounds/discussion-start-signal.wav'; + audio.load(); + audio.play(); + this.guide.text = 'Start responding'; + this.responseRecording = true; + }; + }, + (error) => { + this.promptLoading = false; + this.promptPlaying = false; + this.responseRecording = false; + this.guide.text = 'Unable to load prompt'; + this.alerts.error(`Error loading discussion prompt. ${error}`, 6000); + }, ); - this.guide.text = 'Listening to prompt'; - this.data.audioRef.load(); - this.data.audioRef.play(); - const _this = this; - this.data.audioRef.addEventListener('ended', () => { - setTimeout(() => { - const audio = new Audio(); - audio.src = '/assets/sounds/discussion-start-signal.wav'; - audio.load(); - audio.play(); - _this.guide.text = 'Start responding'; - }, 400); - }); } - responseConfirmed(e: any) { + responseConfirmed(_event: Event) { if (this.activePromptId !== this.numberOfPrompts - 1) { this.activePromptId++; this.setPrompt(); @@ -180,4 +357,11 @@ export class IntelligentDiscussionDialog implements OnInit { this.finishDiscussion(); } } + + private releasePromptBlob(): void { + if (this.promptBlobUrl) { + this.fileDownloader.releaseBlob(this.promptBlobUrl); + this.promptBlobUrl = undefined; + } + } } diff --git a/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-recorder/intelligent-discussion-recorder.component.css b/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-recorder/intelligent-discussion-recorder.component.css index 7d53ba95fc..e69de29bb2 100644 --- a/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-recorder/intelligent-discussion-recorder.component.css +++ b/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-recorder/intelligent-discussion-recorder.component.css @@ -1,4 +0,0 @@ -:host #mainDiscussionRecorderVisualiser { - height: 100px; - width: 300px; -} diff --git a/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-recorder/intelligent-discussion-recorder.component.html b/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-recorder/intelligent-discussion-recorder.component.html index cdd5382198..a7c0bc6331 100644 --- a/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-recorder/intelligent-discussion-recorder.component.html +++ b/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-recorder/intelligent-discussion-recorder.component.html @@ -1,7 +1,37 @@ -
    -

    Audio recording
    only supported in modern versions of Chrome, Firefox and Safari.

    - -
    - +
    +

    + Audio recording is only supported in modern versions of Chrome, Firefox and Safari. +

    +
    + +
    +
    + {{ countdownText }} +
    +
    + +
    -
    \ No newline at end of file +
    diff --git a/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-recorder/intelligent-discussion-recorder.component.ts b/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-recorder/intelligent-discussion-recorder.component.ts index 0e22045160..0450b050a7 100644 --- a/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-recorder/intelligent-discussion-recorder.component.ts +++ b/src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-recorder/intelligent-discussion-recorder.component.ts @@ -1,30 +1,47 @@ -import { Component, Inject, AfterViewInit, Input } from '@angular/core'; -import { BaseAudioRecorderComponent } from 'src/app/common/audio-recorder/audio/base-audio-recorder'; -import { IntelligentDiscussionPlayerService } from '../intelligent-discussion-player.service'; -import { audioRecorderService } from 'src/app/ajs-upgraded-providers'; -import { DiscussionComment, Task } from 'src/app/api/models/doubtfire-model'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + Input, + ViewChild, +} from '@angular/core'; +import {DiscussionComment, Task} from 'src/app/api/models/doubtfire-model'; +import {TaskCommentService} from 'src/app/api/models/doubtfire-model'; +import { + BaseAudioRecorderComponent, + RecordingEvent, +} from 'src/app/common/audio-recorder/audio/base-audio-recorder'; +import {MediaRecorderService} from 'src/app/common/services/recorder-service'; @Component({ selector: 'intelligent-discussion-recorder', templateUrl: './intelligent-discussion-recorder.component.html', styleUrls: ['./intelligent-discussion-recorder.component.css'], + providers: [MediaRecorderService], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class IntelligentDiscussionRecorderComponent extends BaseAudioRecorderComponent implements AfterViewInit { +export class IntelligentDiscussionRecorderComponent + extends BaseAudioRecorderComponent + implements AfterViewInit +{ + @Input() countdownText: number; @Input() discussion: DiscussionComment; + @Input() promptActive = false; @Input() task: Task; + @ViewChild('mainDiscussionRecorderVisualiser') canvasRef: ElementRef; canvas: HTMLCanvasElement; canvasCtx: CanvasRenderingContext2D; - isSending: boolean; + isSending: boolean = false; constructor( - @Inject(audioRecorderService) mediaRecorderService: any, - @Inject(IntelligentDiscussionPlayerService) private dps: any + private mediaRecorderService: MediaRecorderService, + private taskCommentService: TaskCommentService, ) { super(mediaRecorderService); } - ngOnInit() {} - ngAfterViewInit() { if (this.canRecord) { this.init(); @@ -33,11 +50,12 @@ export class IntelligentDiscussionRecorderComponent extends BaseAudioRecorderCom init(): void { super.init(); - this.canvas = document.getElementById('mainDiscussionRecorderVisualiser') as HTMLCanvasElement; + this.canvas = this.canvasRef.nativeElement; this.canvasCtx = this.canvas.getContext('2d'); + this.clearWaveform(); } - onNewRecording(evt: any): void { + onNewRecording(evt: RecordingEvent): void { this.blob = evt.detail.recording.blob; this.recordingAvailable = true; this.sendRecording(); @@ -48,23 +66,78 @@ export class IntelligentDiscussionRecorderComponent extends BaseAudioRecorderCom if (this.isRecording) { this.mediaRecorder.stopRecording(); this.isRecording = false; + this.clearWaveform(); } } sendRecording() { if (this.blob && this.blob.size > 0) { - this.dps.addDiscussionReply( - this.task, - this.discussion.id, - this.blob, - () => { + this.isSending = true; + this.taskCommentService.postDiscussionReply(this.discussion, this.blob).subscribe({ + next: () => { this.isSending = false; }, - (failure: { data: { error: any } }) => { + error: (failure: {data: {error: string}}) => { console.error(failure); - } - ); + this.isSending = false; + }, + }); this.blob = {} as Blob; } } + + protected visualise(): void { + const draw = () => { + let WIDTH: number; + let HEIGHT: number; + + this.canvas.width = 1; + this.canvas.height = 1; + + this.canvas.width = WIDTH = this.canvas.clientWidth; + this.canvas.height = HEIGHT = this.canvas.clientHeight; + requestAnimationFrame(draw); + analyser.getByteTimeDomainData(dataArray); + analyser.getByteFrequencyData(dataArray); + + this.canvasCtx.clearRect(0, 0, WIDTH, HEIGHT); + + const barWidth = 2; + const barGap = 2; + + for (let i = 0; i < WIDTH; i++) { + const barX = i * (barWidth + barGap); + const barY = HEIGHT / 2; + const barHeight = -(dataArray[i] / 8) + 1; + this.canvasCtx.fillStyle = this.waveformColour; + this.canvasCtx.fillRect(barX, barY, barWidth, barHeight); + this.canvasCtx.fillRect(barX, barY - barHeight, barWidth, barHeight); + } + }; + + const analyser = this.mediaRecorder.analyserNode; + analyser.fftSize = 2048; + const bufferLength = analyser.frequencyBinCount; + const dataArray = new Uint8Array(bufferLength); + draw(); + } + + private get waveformColour(): string { + if (!this.isRecording) { + return '#2563eb'; + } + + return this.promptActive ? '#b91c1c66' : '#dc2626'; + } + + private clearWaveform(): void { + if (!this.canvas || !this.canvasCtx) { + return; + } + + this.canvas.width = this.canvas.clientWidth; + this.canvas.height = this.canvas.clientHeight; + + this.canvasCtx.clearRect(0, 0, this.canvas.width, this.canvas.height); + } } diff --git a/src/app/tasks/task-comments-viewer/pdf-image-comment/pdf-image-comment.component.html b/src/app/tasks/task-comments-viewer/pdf-image-comment/pdf-image-comment.component.html index 729b8016e3..9f759eed40 100644 --- a/src/app/tasks/task-comments-viewer/pdf-image-comment/pdf-image-comment.component.html +++ b/src/app/tasks/task-comments-viewer/pdf-image-comment/pdf-image-comment.component.html @@ -1,8 +1,10 @@ @if (comment.commentType === 'image' && resourceUrl) { - -} + Image attachment preview + } @if (comment.commentType === 'pdf') { -

    view pdf

    -} +
    + picture_as_pdf view pdf +
    + }
    diff --git a/src/app/tasks/task-comments-viewer/pdf-image-comment/pdf-image-comment.component.ts b/src/app/tasks/task-comments-viewer/pdf-image-comment/pdf-image-comment.component.ts index 37631facda..62b41df3a1 100644 --- a/src/app/tasks/task-comments-viewer/pdf-image-comment/pdf-image-comment.component.ts +++ b/src/app/tasks/task-comments-viewer/pdf-image-comment/pdf-image-comment.component.ts @@ -1,6 +1,5 @@ -import {Component, OnInit, Input, Inject, OnDestroy} from '@angular/core'; -import {commentsModal} from 'src/app/ajs-upgraded-providers'; -import {Project, TaskComment, Task} from 'src/app/api/models/doubtfire-model'; +import {ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {Project, Task, TaskComment} from 'src/app/api/models/doubtfire-model'; import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; import {CommentsModalService} from 'src/app/common/modals/comments-modal/comments-modal.service'; import {AlertService} from 'src/app/common/services/alert.service'; @@ -9,6 +8,8 @@ import {AlertService} from 'src/app/common/services/alert.service'; selector: 'pdf-image-comment', templateUrl: './pdf-image-comment.component.html', styleUrls: [], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class PdfImageCommentComponent implements OnInit, OnDestroy { @Input() comment: TaskComment; @@ -19,12 +20,14 @@ export class PdfImageCommentComponent implements OnInit, OnDestroy { constructor( private alerts: AlertService, - @Inject(commentsModal) private commentsModalRef: CommentsModalService, + private commentsModalRef: CommentsModalService, private fileDownloaderService: FileDownloaderService, ) {} ngOnInit() { - if (this.comment.commentType === 'image') this.downloadCommentResource(); + if (this.comment.commentType === 'image') { + this.downloadCommentResource(); + } } ngOnDestroy(): void { @@ -39,11 +42,13 @@ export class PdfImageCommentComponent implements OnInit, OnDestroy { this.fileDownloaderService.downloadBlob( url, - ((blobUrl, response) => { + ((blobUrl, _response) => { this.resourceUrl = blobUrl; - if (fn) fn(blobUrl); + if (fn) { + fn(blobUrl); + } }).bind(this), - ((error) => this.alerts.error(`Unable to download image comment. ${error}`, 6000)).bind(this) + ((error) => this.alerts.error(`Unable to download image comment. ${error}`, 6000)).bind(this), ); } diff --git a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html index 0c44be728c..c9c39db6a6 100644 --- a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html +++ b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.html @@ -1,36 +1,37 @@ -
    -
    - +
    +
    @if (!user.isStaff && !task.definition.scormAllowReview) { -
    +
    } @else { -
    +
    } - - +
    - - + + + + + +
    -
    diff --git a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.scss b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.scss index 7bc5f74d91..da3e3ff584 100644 --- a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.scss +++ b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.scss @@ -24,7 +24,7 @@ hr { height: 1.5em; opacity: 0.8; &:before { - content: ""; + content: ''; background: linear-gradient(to right, transparent, #9696969d, transparent); position: absolute; left: 0; diff --git a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts index 869b79aa51..5cf6e6845c 100644 --- a/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts +++ b/src/app/tasks/task-comments-viewer/scorm-comment/scorm-comment.component.ts @@ -1,17 +1,19 @@ -import {Component, Input, Inject} from '@angular/core'; -import {confirmationModal} from 'src/app/ajs-upgraded-providers'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; import { - Task, ScormComment, + Task, + TestAttemptService, User, UserService, - TestAttemptService, } from 'src/app/api/models/doubtfire-model'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; @Component({ selector: 'f-scorm-comment', templateUrl: './scorm-comment.component.html', styleUrls: ['./scorm-comment.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class ScormCommentComponent { @Input() task: Task; @@ -22,7 +24,7 @@ export class ScormCommentComponent { constructor( private userService: UserService, private testAttemptService: TestAttemptService, - @Inject(confirmationModal) private confirmationModal: any, + private confirmationModal: ConfirmationModalService, ) { this.user = this.userService.currentUser; } diff --git a/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.html b/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.html index b0a74a991e..ae9073e8bb 100644 --- a/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.html +++ b/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.html @@ -1,29 +1,34 @@ -
    +
    @if (comment.assessed) { -
    -
    -

    reason: {{ comment.text }}

    +
    +
    + + {{ message }} + +
    +

    + Reason: {{ comment.text }} +

    } @if (!comment.assessed) { -
    -
    -

    - {{ message }}
    - reason: {{ comment.text }} +

    + +

    + {{ message }} + Reason: {{ comment.text }}

    @if (isNotStudent) { - } -
    +
    }
    diff --git a/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.scss b/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.scss index c2917f902e..1a5626b320 100644 --- a/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.scss +++ b/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.scss @@ -32,7 +32,7 @@ hr { height: 1.5em; opacity: 0.8; &:before { - content: ""; + content: ''; background: linear-gradient(to right, transparent, #9696969d, transparent); position: absolute; left: 0; diff --git a/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.ts b/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.ts index 7585e8d7c9..ea963d9b00 100644 --- a/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.ts +++ b/src/app/tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component.ts @@ -1,24 +1,24 @@ -import {Component, OnInit, Input} from '@angular/core'; -import {ScormExtensionComment, TaskComment, Task} from 'src/app/api/models/doubtfire-model'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {ScormExtensionComment, Task, TaskComment} from 'src/app/api/models/doubtfire-model'; import {AlertService} from 'src/app/common/services/alert.service'; @Component({ selector: 'f-scorm-extension-comment', templateUrl: './scorm-extension-comment.component.html', styleUrls: ['./scorm-extension-comment.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class ScormExtensionCommentComponent implements OnInit { +export class ScormExtensionCommentComponent { @Input() comment: ScormExtensionComment; @Input() task: Task; constructor(private alerts: AlertService) {} - private handleError(error: any) { + private handleError(error: {data: {error: string}}) { this.alerts.error('Error: ' + error.data.error, 6000); } - ngOnInit() {} - get message() { const studentName = this.comment.author.name; if (this.comment.assessed && this.comment.granted) { @@ -41,7 +41,7 @@ export class ScormExtensionCommentComponent implements OnInit { grantExtension() { this.comment.grant().subscribe({ - next: (tc: TaskComment) => { + next: (_tc: TaskComment) => { this.alerts.success('Attempt request granted', 2000); }, error: (response) => { diff --git a/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.html b/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.html index 4feda189a2..18c1508a16 100644 --- a/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.html +++ b/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.html @@ -1,11 +1,11 @@ -
    +
    -
    +
    -
    +
    @if (comment.overseerStatus === 'pre_queued') { Tests In Progress } @else if (comment.overseerStatus === 'passed') { @@ -16,32 +16,32 @@
    @if (comment.overseerStatus === 'passed') { check_circle } @else if (comment.overseerStatus === 'failed') { highlight_off_outline } @else if (comment.overseerStatus === 'pre_queued') { - + } @if (comment.overseerStatus !== 'pre_queued') { diff --git a/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.ts b/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.ts index a57f866649..2c4e842f98 100644 --- a/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.ts +++ b/src/app/tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component.ts @@ -1,11 +1,11 @@ -import {Component, OnInit, Input, Inject} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Inject, Input} from '@angular/core'; +import {Task} from 'src/app/api/models/doubtfire-model'; +import {TaskAssessmentModalService} from 'src/app/common/modals/task-assessment-modal/task-assessment-modal.service'; +import {AlertService} from 'src/app/common/services/alert.service'; import { - TaskSubmissionService, TaskAssessmentResult, + TaskSubmissionService, } from 'src/app/common/services/task-submission.service'; -import {TaskAssessmentModalService} from 'src/app/common/modals/task-assessment-modal/task-assessment-modal.service'; -import {Task} from 'src/app/api/models/doubtfire-model'; -import {AlertService} from 'src/app/common/services/alert.service'; export interface User { id: number; @@ -37,8 +37,10 @@ export interface TaskAssessmentComment { selector: 'app-task-assessment-comment', templateUrl: './task-assessment-comment.component.html', styleUrls: ['./task-assessment-comment.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class TaskAssessmentCommentComponent implements OnInit { +export class TaskAssessmentCommentComponent { @Input() task: Task; @Input() comment: TaskAssessmentComment; @@ -53,10 +55,6 @@ export class TaskAssessmentCommentComponent implements OnInit { this.alerts.error('Error: ' + error, 6000); } - ngOnInit() { - // this.update(); - } - get message() { return this.comment.assessment_result.assessment_output; } diff --git a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html index 924ec54829..4b029a8639 100644 --- a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html +++ b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.html @@ -1,262 +1,315 @@
    -
    - -
    - -
    - forum +
    +
    -
    - -
    -

    - {{ comment.author.preferredName }} {{ comment.author.lastName }} - {{ comment.createdAt | humanizedDate }} -

    -
    -
    - - -
    + @if (!task || task.comments.length === 0) { +
    + forum +
    + } + @if (task) { +
    + + @for (comment of task.comments; track comment) {
    + @if (comment.shouldShowTimestamp) { +

    + {{ comment.author.preferredName }} {{ comment.author.lastName }} + {{ comment.createdAt | humanizedDate }} +

    + }
    -
    -
    + @if (!comment.authorIsMe && shouldShowAuthorIcon(comment.commentType)) { +
    + @if (comment.shouldShowAvatar && shouldShowAuthorIcon(comment.commentType)) { + + + } +
    + } -
    - - - group - +
    + @switch (comment.commentType) { + @case ('status') { +
    +
    +
    + } - - {{ comment.text }} - - -
    + @case ('discussed_in_class') { +
    + + + group + -
    - - - comment - + + {{ comment.text }} + + +
    + } - - {{ comment.text }} - -
    -
    + @case ('feedback_review_request') { +
    + + + comment + -
    - - - how_to_reg - + + {{ comment.text }} + + +
    + } - - {{ comment.text }} - -
    -
    + @case ('checked_in') { +
    + + + how_to_reg + -
    -
    -
    + + {{ comment.text }} + +
    +
    + } -
    - -
    + @case ('plan') { +
    +
    +
    + } -
    - -
    + @case ('assessment') { +
    + @if ( + overseerEnabled && asAssessmentComment(comment); + as assessmentComment + ) { + + } +
    + } -
    - -

    - - {{ comment.originalComment ? comment.originalComment.text : 'comment removed' }} -

    -
    -
    - -
    + @case ('scorm') { +
    + @if (scormEnabled && asScormComment(comment); as scormComment) { + + } +
    + } + } -
    - - -
    +
    + @if (!!comment.replyToId) { + +

    + reply + {{ + comment.originalComment + ? comment.originalComment.text + : 'comment removed' + }} +

    +
    + } -
    -
    -
    + @switch (comment.commentType) { + @case ('extension') { +
    + @if (asExtensionComment(comment); as extensionComment) { + + + } +
    + } -
    - -
    + @case ('scorm_extension') { +
    + @if (asScormExtensionComment(comment); as scormExtensionComment) { + + + } +
    + } -
    - -
    + @case ('text') { +
    +
    +
    + } -
    - -
    + @case ('audio') { +
    + +
    + } -
    - + @case ('discussion') { +
    + @if (asDiscussionComment(comment); as discussionComment) { + + } +
    + } + + @case ('image') { +
    + +
    + } + + @case ('pdf') { +
    + +
    + } + } +
    + @if (comment.isBubbleComment) { +
    + +
    + }
    -
    - -
    + @if (comment.lastRead && shouldShowReadReceipt()) { +
    + + +
    + }
    -
    -
    - - -
    -
    - -
    + } + +
    + }
    diff --git a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.scss b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.scss index 5f78a88a4c..a09a92c66e 100644 --- a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.scss +++ b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.scss @@ -1,4 +1,5 @@ -@import '../../../styles/mixins/scrollable.scss'; +@use 'sass:color'; +@use 'styles/mixins/scrollable' as *; :host { min-width: 230px; @@ -9,9 +10,12 @@ } $comment-bubble-color: #3939ff; -$comment-bubble-color-darker: darken($comment-bubble-color, 10%); +$comment-bubble-color-darker: color.adjust($comment-bubble-color, $lightness: -10%); $comment-bubble-color-other-user: rgb(241, 240, 240); -$comment-bubble-color-other-user-darker: darken($comment-bubble-color-other-user, 10%); +$comment-bubble-color-other-user-darker: color.adjust( + $comment-bubble-color-other-user, + $lightness: -10% +); $comment-author-bubble-size: 38px; $comment-text-padding: 12px; @@ -78,7 +82,7 @@ $comment-inner-border-radius: 4px; // padding-left: 40px; // &.own { - // display: none; + // display: none; // } } @@ -140,11 +144,13 @@ $comment-inner-border-radius: 4px; } } - .comment-container .comment-extension, .comment-container .comment-scorm-extension { + .comment-container .comment-extension, + .comment-container .comment-scorm-extension { width: 100%; } - .comment-container .comment-assessment, .comment-container .comment-scorm { + .comment-container .comment-assessment, + .comment-container .comment-scorm { width: 100%; } @@ -222,7 +228,7 @@ $comment-inner-border-radius: 4px; .reply-text { margin: 0 0.4em 1em 0; max-lines: 2; - max-height: 30px; + max-height: 35px; line-clamp: 2; line-height: 15.36px; margin-left: 0.8em; @@ -250,7 +256,11 @@ $comment-inner-border-radius: 4px; .comment.comment-by-other-user user-icon .user-icon-initials { box-shadow: 0 0 0 1px $comment-bubble-color-other-user-darker; background: $comment-bubble-color-other-user; - background: linear-gradient(to left, $comment-bubble-color-other-user, $comment-bubble-color-other-user-darker); + background: linear-gradient( + to left, + $comment-bubble-color-other-user, + $comment-bubble-color-other-user-darker + ); // color: $text-color; color: black; } @@ -332,7 +342,6 @@ $comment-inner-border-radius: 4px; .comment-text, .comment-pdf, - .comment-discussion, .markdown-to-html p { padding-right: 6px; padding-left: 4px; @@ -354,7 +363,8 @@ $comment-inner-border-radius: 4px; } } - .comment .extension-bubble, .comment .scorm_extension-bubble { + .comment .extension-bubble, + .comment .scorm_extension-bubble { width: 100%; background-color: transparent; } @@ -418,7 +428,7 @@ $comment-inner-border-radius: 4px; } // Disucssion comments - .comment .discussion-bubble { + /* .comment .discussion-bubble { @include base-bubble; max-width: 300px; @@ -427,7 +437,7 @@ $comment-inner-border-radius: 4px; } background: $discussion-comment-bubble-color; - } + } */ .comment.comment-by-other-user .text-bubble { background: $comment-bubble-color-other-user; diff --git a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.spec.ts b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.spec.ts index d28e56ff6b..908c92e587 100644 --- a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.spec.ts +++ b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.spec.ts @@ -1,47 +1,50 @@ -// import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -// import { EventEmitter } from '@angular/core'; -// import { alertService, commentsModal } from 'src/app/ajs-upgraded-providers'; -// import { TaskComment, TaskCommentService } from 'src/app/api/models/doubtfire-model'; -// import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {EMPTY} from 'rxjs'; +import {TaskCommentService, TaskService, UserService} from 'src/app/api/models/doubtfire-model'; +import {FeedbackTemplateService} from 'src/app/api/services/feedback-template.service'; +import {CommentsModalService} from 'src/app/common/modals/comments-modal/comments-modal.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {TaskCommentsViewerComponent} from './task-comments-viewer.component'; -// import { TaskCommentsViewerComponent } from './task-comments-viewer.component'; +const taskCommentServiceStub = { + commentAdded$: EMPTY, +}; +const taskServiceStub = { + taskStatusUpdated$: EMPTY, +}; +const emptyProvider = {}; -// describe('TaskCommentsViewerComponent', () => { -// let component: TaskCommentsViewerComponent; -// let fixture: ComponentFixture; -// let taskCommentServiceStub: Partial; -// let doubtfireConstantsStub: Partial; -// let commentsModalStub: jasmine.SpyObj; -// let taskStub: jasmine.SpyObj; -// let alertServiceStub: jasmine.SpyObj; +describe('TaskCommentsViewerComponent', () => { + let component: TaskCommentsViewerComponent; + let fixture: ComponentFixture; -// beforeEach( -// waitForAsync(() => { -// const commentAdded: EventEmitter = new EventEmitter(); -// taskCommentServiceStub = { -// commentAdded$: commentAdded, -// }; + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [TaskCommentsViewerComponent], + providers: [ + {provide: TaskCommentService, useValue: taskCommentServiceStub}, + {provide: FeedbackTemplateService, useValue: emptyProvider}, + {provide: UserService, useValue: emptyProvider}, + {provide: TaskService, useValue: taskServiceStub}, + {provide: DoubtfireConstants, useValue: emptyProvider}, + {provide: CommentsModalService, useValue: emptyProvider}, + {provide: AlertService, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(TaskCommentsViewerComponent, {set: {template: ''}}) + .compileComponents(); + }); -// TestBed.configureTestingModule({ -// declarations: [TaskCommentsViewerComponent], -// providers: [ -// { provide: TaskCommentService, useValue: taskCommentServiceStub }, -// { provide: DoubtfireConstants, useValue: doubtfireConstantsStub }, -// { provide: commentsModal, useValue: commentsModalStub }, -// { provide: Task, useValue: taskStub }, -// { provide: alertService, useValue: alertServiceStub }, -// ], -// }).compileComponents(); -// }) -// ); + beforeEach(() => { + fixture = TestBed.createComponent(TaskCommentsViewerComponent); + component = fixture.componentInstance; + }); -// beforeEach(() => { -// fixture = TestBed.createComponent(TaskCommentsViewerComponent); -// component = fixture.componentInstance; -// fixture.detectChanges(); -// }); - -// it('should create', () => { -// expect(component).toBeTruthy(); -// }); -// }); + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.ts b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.ts index d7366dac8d..40b1fe4522 100644 --- a/src/app/tasks/task-comments-viewer/task-comments-viewer.component.ts +++ b/src/app/tasks/task-comments-viewer/task-comments-viewer.component.ts @@ -1,45 +1,51 @@ import { + ChangeDetectionStrategy, Component, - OnInit, + ElementRef, Input, - Inject, OnChanges, + OnDestroy, SimpleChanges, ViewChild, - ElementRef, - OnDestroy, } from '@angular/core'; -import {commentsModal} from 'src/app/ajs-upgraded-providers'; +import {Subscription} from 'rxjs'; import { - Task, + DiscussionComment, Project, + ScormComment, + ScormExtensionComment, + Task, TaskComment, TaskCommentService, - UserService, TaskService, + UserService, } from 'src/app/api/models/doubtfire-model'; -import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; -import {TaskCommentComposerData} from '../task-comment-composer/task-comment-composer.component'; -import {AlertService} from 'src/app/common/services/alert.service'; +import {ExtensionComment} from 'src/app/api/models/task-comment/extension-comment'; import {FeedbackTemplateService} from 'src/app/api/services/feedback-template.service'; import {CommentsModalService} from 'src/app/common/modals/comments-modal/comments-modal.service'; -import {Subscription} from 'rxjs'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; +import {TaskCommentComposerData} from '../task-comment-composer/task-comment-composer.component'; +import {TaskAssessmentComment} from './task-assessment-comment/task-assessment-comment.component'; @Component({ selector: 'task-comments-viewer', templateUrl: './task-comments-viewer.component.html', styleUrls: ['./task-comments-viewer.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class TaskCommentsViewerComponent implements OnChanges, OnInit, OnDestroy { +export class TaskCommentsViewerComponent implements OnChanges, OnDestroy { // Get the comments body from the HTML template @ViewChild('commentsBody') commentsBody: ElementRef; lastComment: TaskComment; - project: Project; + @Input() project: Project; loading: boolean = true; sharedCommentComposerData: TaskCommentComposerData = { originalComment: null, + editingComment: null, }; @Input() comment?: TaskComment; @@ -55,12 +61,11 @@ export class TaskCommentsViewerComponent implements OnChanges, OnInit, OnDestroy private userService: UserService, private taskService: TaskService, private constants: DoubtfireConstants, - @Inject(commentsModal) private commentsModalRef: CommentsModalService, + private commentsModalRef: CommentsModalService, private alerts: AlertService, ) { - const self = this; - this.commentAddedSub = this.taskCommentService.commentAdded$.subscribe((tc: TaskComment) => { - self.scrollDown(); + this.commentAddedSub = this.taskCommentService.commentAdded$.subscribe((_tc: TaskComment) => { + this.scrollDown(); }); this.taskStatusSub = this.taskService.taskStatusUpdated$.subscribe((task) => { @@ -70,13 +75,33 @@ export class TaskCommentsViewerComponent implements OnChanges, OnInit, OnDestroy }); } - ngOnInit(): void {} - ngOnDestroy(): void { this.taskStatusSub?.unsubscribe(); this.commentAddedSub?.unsubscribe(); } + public asAssessmentComment(comment: TaskComment): TaskAssessmentComment | null { + return comment.commentType === 'assessment' + ? (comment as unknown as TaskAssessmentComment) + : null; + } + + public asScormComment(comment: TaskComment): ScormComment | null { + return comment.commentType === 'scorm' ? (comment as ScormComment) : null; + } + + public asExtensionComment(comment: TaskComment): ExtensionComment | null { + return comment.commentType === 'extension' ? (comment as ExtensionComment) : null; + } + + public asScormExtensionComment(comment: TaskComment): ScormExtensionComment | null { + return comment.commentType === 'scorm_extension' ? (comment as ScormExtensionComment) : null; + } + + public asDiscussionComment(comment: TaskComment): DiscussionComment | null { + return comment.commentType === 'discussion' ? (comment as DiscussionComment) : null; + } + ngOnChanges(changes: SimpleChanges) { // Must have project for task to be mapped if (changes.task?.currentValue?.project != null) { @@ -172,9 +197,8 @@ export class TaskCommentsViewerComponent implements OnChanges, OnInit, OnDestroy } scrollDown() { - const component: TaskCommentsViewerComponent = this; setTimeout(() => { - const element = component.commentsBody.nativeElement; + const element = this.commentsBody.nativeElement; element.scrollTop = element.scrollHeight; }, 50); } @@ -223,14 +247,11 @@ export class TaskCommentsViewerComponent implements OnChanges, OnInit, OnDestroy // # Upload image files as comments to a given task postAttachmentComment(file) { - const self: TaskCommentsViewerComponent = this; - - this.taskCommentService.addComment(this.task, file, 'file', null).subscribe( - (tc: TaskComment) => {}, - (error: any) => { + this.taskCommentService.addComment(this.task, file, 'file', null).subscribe({ + error: (error) => { this.alerts.error(error || error?.message, 2000); }, - ); + }); } scrollToComment(commentID) { diff --git a/src/app/tasks/task-ilo-alignment/modals/task-ilo-alignment-modal/task-ilo-alignment-modal.coffee b/src/app/tasks/task-ilo-alignment/modals/task-ilo-alignment-modal/task-ilo-alignment-modal.coffee deleted file mode 100644 index b40980c706..0000000000 --- a/src/app/tasks/task-ilo-alignment/modals/task-ilo-alignment-modal/task-ilo-alignment-modal.coffee +++ /dev/null @@ -1,95 +0,0 @@ -# Component not used - -angular.module('doubtfire.tasks.task-ilo-alignment.modals.task-ilo-alignment-modal', []) - -# -# Shows a modal where users can align tasks to ILOs -# -.factory('TaskILOAlignmentModal', ($modal) -> - TaskILOAlignmentModal = {} - - TaskILOAlignmentModal.show = (task, ilo, alignment, unit, project, source) -> - $modal.open - controller: 'TaskILOAlignmentModalCtrl' - templateUrl: 'tasks/task-ilo-alignment/modals/task-ilo-alignment-modal/task-ilo-alignment-modal.tpl.html' - resolve: - task: -> task - ilo: -> ilo - alignment: -> alignment - unit: -> unit - project: -> project - source: -> source - - TaskILOAlignmentModal -) - -.controller('TaskILOAlignmentModalCtrl', ($scope, $rootScope, $modalInstance, alertService, newTaskOutcomeAlignmentService, task, ilo, alignment, unit, project, source) -> - $scope.source = source - $scope.unit = unit - $scope.task = task - $scope.ilo = ilo - $scope.alignment = alignment - $scope.project = project - - if !$scope.alignment - $scope.alignment = newTaskOutcomeAlignmentService.createInstanceFrom({}, $scope.source) - $scope.alignment.learningOutcome = $scope.ilo - $scope.alignment.taskDefinition = task.definition - $scope.alignment.rating = 0 - $scope.alignment.description = "" - - $scope.editingRationale = false - - $scope.toggleEditRationale = -> - if $scope.editingRationale - updateAlignment() - $scope.editingRationale = !$scope.editingRationale - - $scope.removeAlignmentItem = -> - if $scope.project? - params = { - project_id: $scope.project.id - } - newTaskOutcomeAlignmentService.delete($scope.alignment, {cache: $scope.alignment.within.taskOutcomeAlignmentsCache, params: params}).subscribe({ - next: (response) -> - alertService.success( "Task - Outcome alignment rating removed", 2000) - $rootScope.$broadcast('UpdateAlignmentChart') - $modalInstance.close $scope.alignment - error: (message) -> alertService.error( message, 6000) - }) - - updateAlignment = -> - if $scope.project? - params = { - project_id: $scope.project.id - } - newTaskOutcomeAlignmentService.update($scope.alignment, {cache: $scope.alignment.within.taskOutcomeAlignmentsCache, params: params}).subscribe({ - next: (response) -> - alertService.success( "Task - Outcome alignment rating saved", 2000) - $rootScope.$broadcast('UpdateAlignmentChart') - error: (message) -> alertService.error( message, 6000) - }) - - addAlignment = -> - if $scope.project? - params = { - project_id: $scope.project.id - } - newTaskOutcomeAlignmentService.store($scope.alignment, {cache: $scope.source.taskOutcomeAlignmentsCache, constructorParams: $scope.source, params: params}).subscribe({ - next: (response) -> - $scope.alignment = response - $rootScope.$broadcast('UpdateAlignmentChart') - error: (message) -> alertService.error( message, 6000) - }) - - $scope.updateRating = (alignment) -> - unless $scope.alignment.id? - addAlignment alignment - else - updateAlignment alignment - - $scope.closeModal = -> - if $scope.editingRationale - $scope.updateRating $scope.alignment - $modalInstance.close $scope.alignment -) diff --git a/src/app/tasks/task-ilo-alignment/modals/task-ilo-alignment-modal/task-ilo-alignment-modal.tpl.html b/src/app/tasks/task-ilo-alignment/modals/task-ilo-alignment-modal/task-ilo-alignment-modal.tpl.html deleted file mode 100644 index 91fdabc350..0000000000 --- a/src/app/tasks/task-ilo-alignment/modals/task-ilo-alignment-modal/task-ilo-alignment-modal.tpl.html +++ /dev/null @@ -1,40 +0,0 @@ -
    - - - -
    diff --git a/src/app/tasks/task-ilo-alignment/modals/task-ilo-alignment.coffee b/src/app/tasks/task-ilo-alignment/modals/task-ilo-alignment.coffee deleted file mode 100644 index 8217d7b43f..0000000000 --- a/src/app/tasks/task-ilo-alignment/modals/task-ilo-alignment.coffee +++ /dev/null @@ -1,3 +0,0 @@ -angular.module('doubtfire.tasks.task-ilo-alignment.modals', [ - 'doubtfire.tasks.task-ilo-alignment.modals.task-ilo-alignment-modal' -]) diff --git a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.coffee b/src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.coffee deleted file mode 100644 index 8381daf763..0000000000 --- a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.coffee +++ /dev/null @@ -1,103 +0,0 @@ -# Component not used - -angular.module('doubtfire.tasks.task-ilo-alignment.task-ilo-alignment-editor',[]) - -.directive('taskIloAlignmentEditor', -> - replace: true - restrict: 'E' - templateUrl: 'tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.tpl.html' - scope: - unit: "=" - project: "=?" - showCsv: "=" - hidePanel: '=?' - # select tasks to include in portfolio - showIncludeTasks: '=?' - controller: ($scope, $modal, $rootScope, $filter, alertService, gradeService, Visualisation, fileDownloaderService, CsvResultModal, outcomeService, TaskILOAlignmentModal, newTaskService, newTaskOutcomeAlignmentService) -> - $scope.showTaskName = $scope.unit.ilos.length < 5 - $scope.showGraph = false - $scope.closeGraph = -> - $scope.showGraph = false - # Set source - if $scope.project? - $scope.source = $scope.project - $scope.tasks = $scope.project.tasks - $scope.taskStatusFactor = $scope.project.taskStatusFactor.bind($scope.project) - else - $scope.source = $scope.unit - #TODO unsubscribe on destroy - $scope.unit.taskDefinitionCache.values.subscribe( - (taskDefs) -> - $scope.tasks = _.map taskDefs, (td) -> - { definition: td } - ) - - $scope.taskStatusFactor = $scope.unit.taskStatusFactor - - alignments = [] - $scope.$watch 'source.taskOutcomeAlignments.length', -> - return unless $scope.source.taskOutcomeAlignments? - alignments = - _ .chain($scope.source.taskOutcomeAlignments) - .filter( (d) -> d.rating > 0 ) - .groupBy('taskDefinition.id') - .map (d, i) -> - d = _ .chain(d) - .groupBy('learningOutcome.id') - .map( (d, i) -> [i, d[0]] ) - .fromPairs() - .value() - [i, d] - .fromPairs() - .value() - alignments - - $scope.showAlignmentModal = (task, ilo, alignment) -> - TaskILOAlignmentModal.show task, ilo, alignment, $scope.unit, $scope.project, $scope.source - - $scope.alignmentForTaskAndIlo = (task, ilo) -> - if task.definition - result = alignments[task.definition.id]?[ilo.id] - td = task.definition - else - result = alignments[task.id]?[ilo.id] - td = task - - result - - $scope.disableInclude = (task) -> - # if there are no ILOs, you can always include tasks - if $scope.unit.ilos.length > 0 - alignments[task.definition.id] is undefined - else - false - - $scope.includeTaskInPorfolio = (task) -> - task.includeInPortfolio = !task.includeInPortfolio - newTaskService.update(task).subscribe({ - next: (success) -> alertService.success( "Task updated", 2000) - error: (message) -> alertService.error( message, 6000) - }) - - - # CSV stuff - $scope.csvImportResponse = {} - $scope.taskAlignmentCSV = { file: { name: 'Task Outcome Link CSV', type: 'csv' } } - - $scope.isTaskCSVUploading = null - $scope.onTaskAlignmentCSVSuccess = (response) -> - CsvResultModal.show 'Task CSV upload results.', response - $rootScope.$broadcast('UpdateAlignmentChart', response, { batch: true }) - if $scope.project? - $scope.project.refresh($scope.unit) - else - $scope.unit.refresh() - $scope.onTaskAlignmentCSVComplete = -> - $scope.isTaskCSVUploading = null - - $scope.downloadTaskAlignmentCSV = -> - if $scope.project? - fileDownloaderService.downloadFile($scope.project.taskAlignmentCSVUploadUrl, "#{$scope.project.student.name}-alignments.csv") - else - fileDownloaderService.downloadFile($scope.unit.taskAlignmentCSVUploadUrl, "#{$scope.unit.code}-alignments.csv") -) diff --git a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.scss b/src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.scss deleted file mode 100644 index 9cf6bd10fa..0000000000 --- a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.scss +++ /dev/null @@ -1,45 +0,0 @@ -.task-ilo-alignment-editor { - .alignment-panels .panel-body { - overflow: scroll; - } - - table.table-task-alignment { - td.task-abbreviation { width: 5ex; } - td.task-status-icon { width: 6em; } - td.task-name { width: 10ex; } - td.include-in-portfolio { width: 10ex; } - td.task-abbreviation label { - display: inline-block; - width: 100%; - padding: 0.55em; // to match height of status - } - td.include-in-portfolio { - button, .button-wrapper { - width: 100%; - height: 3em; - } - } - td.ilo-alignment { - .btn.btn-alignment { - width: 6em; - padding: 0.25em; - margin: 0 auto; - } - } - } - - .visualisation { - position: fixed; - bottom: 0; - right: 0; - left: 0; - z-index: 3; - opacity: 0.6; - margin: 0; - border-radius: 0; - &:hover { - cursor: pointer; - opacity: 1; - } - } -} diff --git a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.tpl.html b/src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.tpl.html deleted file mode 100644 index e95245bd46..0000000000 --- a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.tpl.html +++ /dev/null @@ -1,126 +0,0 @@ -
    -
    -
    -
    -

    - {{showIncludeTasks ? 'Select Tasks' : 'Intended Learning Outcome Alignment'}} -

    -
    -
    - Select tasks to include and showcase in your portfolio that demonstrates your - understanding of each Learning Outcome. -
    -
    - Align tasks to an Intended Learning Outcome by selecting the red circle and choosing a rating, - providing an optional rational to justify why that task is related to the outcome selected. -
    -
    -
    - -
    -
    - -
    -
    - -

    No tasks in this unit

    -
    -
    - -

    No learning outcomes in this unit

    -
    - - - - - - - - - - - - - - - - - -
    Task - - Include
    - - - - - {{task.definition.name}} - - {{alignment = alignmentForTaskAndIlo(task, ilo); ''}} - - -
    - - -
    -
    -
    -
    -
    -
    -

    Visualisation

    - -
    -
    - -
    - -
    -
    -
    -
    -
    -

    Import Task Outcome Alignments

    - Import links between tasks and outcomes from a CSV containing: unit_code, learning_outcome, task_abbr, rating. -
    -
    - -
    -
    -

    Export Task Outcome Alignments

    - Download a CSV of task outcome alignment details. -
    -
    - -
    -
    -
    -
    diff --git a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.coffee b/src/app/tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.coffee deleted file mode 100644 index 9179c4c6bd..0000000000 --- a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.coffee +++ /dev/null @@ -1,63 +0,0 @@ -# Component not used - -angular.module('doubtfire.tasks.task-ilo-alignment.task-ilo-alignment-rater',[]) - -# -# A star-based rater where the strength of an alignment between a -# task and an ILO can be specified, along with a provided rationale -# for that strength -# -.directive('taskIloAlignmentRater', -> - replace: true - restrict: 'E' - templateUrl: 'tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.tpl.html' - scope: - readonly: '=?' - # pass in whole align object - ngModel: '=' - unit: '=' - # Function to call when rating is changed - onRatingChanged: '=?' - # Show tooltip when hovering (defaults to true) - tooltips: '=?' - # Is in colour? (defaults to true) - colorful: '=?' - # Show static tooltip for selected rating (defaults to true) - selectedTooltip: '=?' - # Show as tooltips instead (false) - showTooltips: '=?' - # Hide labels - hideLabels: '=?' - # Compact version - compact: '=?' - # Expose label outwards - label: '=?' - # Show the zero label - showZeroRating: '=?' - controller: ($scope, outcomeService) -> - $scope.max = 5 - - $scope.hideLabels ?= false - - $scope.showZeroRating ?= false - - $scope.readonly = true if $scope.compact - - $scope.tooltips = outcomeService.alignmentLabels - - $scope.setHoverValue = (value) -> - return $scope.ngModel if $scope.readonly and not $scope.showTooltips - $scope.hoveringOver = value - $scope.label = $scope.tooltips[value] - - # Set defaults - for property in ['tooltips', 'colorful', 'selectedTooltip'] - $scope[property] = if $scope[property]? then $scope[property] else true - - $scope.showTooltips = if $scope.showTooltips? then $scope.showTooltips else false - - if $scope.onRatingChanged? - $scope.$watch 'ngModel.rating', (newValue, oldValue) -> - if newValue? and newValue isnt oldValue - $scope.onRatingChanged($scope.ngModel) -) diff --git a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.scss b/src/app/tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.scss deleted file mode 100644 index dc25163248..0000000000 --- a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.scss +++ /dev/null @@ -1,41 +0,0 @@ -.task-ilo-alignment-rater { - &.colorful { - $base-color: hsl(219, 74%, 83%); - - // Generates colors for 0..5 stars - @for $i from 1 through 5 { - $color: darken($base-color, $i * 10%); - .rating-static-tooltip .badge.rating-#{$i}, .compact-rating.rating-#{$i} { - background-color: $color !important; - color: white; - } - .rating-area { - i:nth-of-type(#{$i}) { color: $color; } - } - } - } - .compact-rating { - border-radius: 2em; - font-size: 2em; - text-align: center; - width: 1.5em; - cursor: pointer; - margin: 0 auto; - } - .rating-area { - & + *:not(.ng-hide) { - margin-top: 10px; - } - width: 100%; - text-align: center; - display: flex; - flex-wrap: nowrap; - &:focus { - outline: none; - } - i { - cursor: pointer; - padding: 0 10px; - } - } -} diff --git a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.tpl.html b/src/app/tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.tpl.html deleted file mode 100644 index cde629302d..0000000000 --- a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.tpl.html +++ /dev/null @@ -1,10 +0,0 @@ -
    - -
    - {{tooltips[(hoveringOver || ngModel.rating)]}} - This task is not related to this outcome at all -
    -
    - {{ngModel.rating}} -
    -
    diff --git a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-viewer/task-ilo-alignment-viewer.coffee b/src/app/tasks/task-ilo-alignment/task-ilo-alignment-viewer/task-ilo-alignment-viewer.coffee deleted file mode 100644 index e95cbfdc5d..0000000000 --- a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-viewer/task-ilo-alignment-viewer.coffee +++ /dev/null @@ -1,37 +0,0 @@ -# Component not used - -angular.module('doubtfire.tasks.task-ilo-alignment.task-ilo-alignment-viewer', []) - -# -# Views the alignment between a task and an ILO, with descriptive -# text of the alignment and ILO -# -.directive('taskIloAlignmentViewer', -> - restrict: 'E' - replace: true - templateUrl: 'tasks/task-ilo-alignment/task-ilo-alignment-viewer/task-ilo-alignment-viewer.tpl.html' - scope: - currentProgress: '=?' - classStats: '=?' - project: '=?' - task: '=?' - unit: '=' - alignments: '=?' - summaryOnly: '=?' - hideVisualisation: '=?' - controller: ($scope, Visualisation, outcomeService) -> - $scope.hideVisualisation = if $scope.hideVisualisation? then $scope.hideVisualisation else false - $scope.targets = outcomeService.calculateTargets($scope.unit, $scope.unit, $scope.unit.taskStatusFactor) - - $scope.toggleExpanded = (align) -> - align.expanded = !align.expanded - if align.expanded - Visualisation.refreshAll() - - $scope.alignments = $scope.unit.ilos unless $scope.alignments? - - if $scope.project? and $scope.task? - $scope.classStats = outcomeService.calculateTaskPotentialContribution($scope.unit, $scope.project, $scope.task) - $scope.currentProgress = outcomeService.calculateTaskContribution($scope.unit, $scope.project, $scope.task) - -) diff --git a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-viewer/task-ilo-alignment-viewer.scss b/src/app/tasks/task-ilo-alignment/task-ilo-alignment-viewer/task-ilo-alignment-viewer.scss deleted file mode 100644 index 918893519a..0000000000 --- a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-viewer/task-ilo-alignment-viewer.scss +++ /dev/null @@ -1,71 +0,0 @@ -.task-ilo-alignment-viewer { - .related-outcome-tasks { - max-height: 200px; - overflow: scroll; - background-color: #ffffff; - border: 1px solid #dddddd; - border-radius: 4px; - .list-group li { - border-left: none; - border-right: none; - &:first-child { - border-top: none; - } - &:last-child { - border-bottom: none; - } - } - .task-def-alignment-item { - font-size: 1.5em; - label { - margin-top: 10px; - } - } - } - .alignment-list { - margin: 0; - & > li { - border-radius: 0; - border: 0; - padding: 3em 1.5em; - cursor: pointer; - &:hover { - background-color: #eee; - } - &.expanded { - background-color: #f9f9f9; - } - header { - display: flex; - .header-item { - display: flex; - align-items: center; - } - .header-item.pull-right { - justify-content: flex-end; - } - .expand-icon { - margin-left: 2em; - } - } - .alignment-content { - h5 { - margin-top: 2em; - color: #666; - font-size: 1.2em; - font-weight: bold; - text-transform: uppercase; - } - .markdown-to-html { - font-size: 1.5em; - } - } - } - svg.nvd3-svg { - margin: initial; - } - & > li:not(:first-child) { - border-top: 1px solid #dddddd; - } - } -} diff --git a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-viewer/task-ilo-alignment-viewer.tpl.html b/src/app/tasks/task-ilo-alignment/task-ilo-alignment-viewer/task-ilo-alignment-viewer.tpl.html deleted file mode 100644 index 2032d7bf1d..0000000000 --- a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-viewer/task-ilo-alignment-viewer.tpl.html +++ /dev/null @@ -1,85 +0,0 @@ -
    -
    -
      -
    • -
      -
      - {{ilo = summaryOnly ? align : unit.outcome(align.learningOutcome.id); ''}} -

      - {{ilo.abbreviation}} - {{ilo.name}} -

      -
      -
      - -
      - -
      -
      - -
      -
      -
      -
      -
      -
      Description
      -
      -
      -
      -
      Visualisation
      - - -
      -
      -
      Rationale
      -
      -
      -
      -
      Related Tasks
      - -
      -
      -
    • -
    -
    -
    - -

    No alignments

    -
    -
    diff --git a/src/app/tasks/task-ilo-alignment/task-ilo-alignment.coffee b/src/app/tasks/task-ilo-alignment/task-ilo-alignment.coffee deleted file mode 100644 index 3de80debf5..0000000000 --- a/src/app/tasks/task-ilo-alignment/task-ilo-alignment.coffee +++ /dev/null @@ -1,6 +0,0 @@ -angular.module('doubtfire.tasks.task-ilo-alignment', [ - 'doubtfire.tasks.task-ilo-alignment.modals' - 'doubtfire.tasks.task-ilo-alignment.task-ilo-alignment-editor' - 'doubtfire.tasks.task-ilo-alignment.task-ilo-alignment-rater' - 'doubtfire.tasks.task-ilo-alignment.task-ilo-alignment-viewer' -]) diff --git a/src/app/tasks/task-submission-history/task-submission-history.component.html b/src/app/tasks/task-submission-history/task-submission-history.component.html deleted file mode 100644 index c561635816..0000000000 --- a/src/app/tasks/task-submission-history/task-submission-history.component.html +++ /dev/null @@ -1,43 +0,0 @@ -
    -
    -
    -
    Submissions
    - - @for (tab of tabs; track tab) { - -
    -
    - {{ tab.timestamp | humanizedDate }} -
    - @if (tab.status === 'pre_queued') { - schedule - } @else { - - } -
    -
    - } -
    -
    -
    - - @for (selTab of selectedTab.content; track selTab) { - -
    {{ selTab.result }} 
    - -
    - } -
    -
    -
    -
    diff --git a/src/app/tasks/task-submission-history/task-submission-history.component.scss b/src/app/tasks/task-submission-history/task-submission-history.component.scss deleted file mode 100644 index 892d10e0c4..0000000000 --- a/src/app/tasks/task-submission-history/task-submission-history.component.scss +++ /dev/null @@ -1,184 +0,0 @@ -// @import "src/styles/mixins/flex-center"; -//== Media queries breakpoints -// -//## Define the breakpoints at which your layout will change, adapting to different screen sizes. - -// Extra small screen / phone -//** Deprecated `$screen-xs` as of v3.0.1 -// $screen-xs: 480px !default; -// //** Deprecated `$screen-xs-min` as of v3.2.0 -// $screen-xs-min: $screen-xs !default; -// //** Deprecated `$screen-phone` as of v3.0.1 -// $screen-phone: $screen-xs-min !default; - -// // Small screen / tablet -// //** Deprecated `$screen-sm` as of v3.0.1 -// $screen-sm: 768px !default; -// $screen-sm-min: $screen-sm !default; -// //** Deprecated `$screen-tablet` as of v3.0.1 -// $screen-tablet: $screen-sm-min !default; - -// // Medium screen / desktop -// //** Deprecated `$screen-md` as of v3.0.1 -// $screen-md: 992px !default; -// $screen-md-min: $screen-md !default; -// //** Deprecated `$screen-desktop` as of v3.0.1 -// $screen-desktop: $screen-md-min !default; - -// // Large screen / wide desktop -// //** Deprecated `$screen-lg` as of v3.0.1 -// $screen-lg: 1200px !default; -// $screen-lg-min: $screen-lg !default; -// //** Deprecated `$screen-lg-desktop` as of v3.0.1 -// $screen-lg-desktop: $screen-lg-min !default; - -// // So media queries don't overlap when required, provide a maximum -// $screen-xs-max: ($screen-sm-min - 1) !default; -// $screen-sm-max: ($screen-md-min - 1) !default; -// $screen-md-max: ($screen-lg-min - 1) !default; - -.submission-wrap { - height: 100%; - display: flex; -} - -.submission-main { - flex: 1; - display: flex; - width: 100%; -} - -@media (max-width: 992px) { - .submission-main { - flex-direction: column; - } -} - -.submission-sidenav, .submisson-result { - overflow-y: scroll; - padding: 1em 1em 0 1em; -} - -/*TODO(mdc-migration): The following rule targets internal classes of list that may no longer apply for the MDC version.*/ -/*TODO(mdc-migration): The following rule targets internal classes of list that may no longer apply for the MDC version.*/ -mat-list.list-group { - overflow-y: scroll; -} - -.terminal-output { - padding: 10px; - margin: 1em 0 0 0; - // margin-bottom: 0; -} - -.submission-sidenav { - flex: 1; - width: 100%; - padding: 0; - display: inline-block; - line-height: 1; -} - -.submission-sidenav.panel.panel-primary{ - margin-bottom: 0; -} - -@media (max-width: 992px) { - .submission-sidenav.panel.panel-primary{ - margin-bottom: 2em; - } -} - -.submission-sidenav .panel-heading { - line-height: 2; -} - -.submisson-result { - flex: 3; - height: 100%; - padding-top: 0; - - padding-right: 0; -} -@media (max-width: 992px) { - .submisson-result { - padding-left: 0; - } -} - -.panel-heading.panel-title.submission-heading { - position: sticky; - top: 0; - z-index: 2; -} - -pre{ - white-space: pre-wrap; -} - -/*TODO(mdc-migration): The following rule targets internal classes of list that may no longer apply for the MDC version.*/ -/*TODO(mdc-migration): The following rule targets internal classes of list that may no longer apply for the MDC version.*/ -mat-list, mat-nav-list{ - padding-top: 0; - border-bottom-width: 1px; - padding-bottom: 1px; -} - -/*TODO(mdc-migration): The following rule targets internal classes of tabs that may no longer apply for the MDC version.*/ -/*TODO(mdc-migration): The following rule targets internal classes of tabs that may no longer apply for the MDC version.*/ -mat-tab-header.mat-mdc-tab-header { - margin-bottom: 5px; - position: sticky; - top: 0; - z-index: 2; - background: white; -} - - -/*TODO(mdc-migration): The following rule targets internal classes of tabs that may no longer apply for the MDC version.*/ -/*TODO(mdc-migration): The following rule targets internal classes of tabs that may no longer apply for the MDC version.*/ -mat-tab-body.mat-mdc-tab-body.mat-tab-body-active { - z-index: 1; -} - -.submission-heading { - border-radius: 0; - background-color: #337ab7; - color: white; -} - -@mixin custom-box-shadow($color) { - box-shadow: -15px 0 $color inset; -} - -/*TODO(mdc-migration): The following rule targets internal classes of list that may no longer apply for the MDC version.*/ -/*TODO(mdc-migration): The following rule targets internal classes of list that may no longer apply for the MDC version.*/ -mat-list-item { - border-bottom-width: 1px !important; - border-color: #f5f5f5; - transition: all 200ms ease-out; - transition-property: box-shadow, padding-right; - display: flex; - &:hover, &:focus { - cursor: pointer; - text-decoration: none; - background-color: #F5F5F5; - @include custom-box-shadow(lighten(#0079D8, 15%)); - } - &.selected { - background-color: rgb(231, 231, 231); - @include custom-box-shadow(#0079D8); - } -} - -.panel > .list-group:last-child .list-group-item:last-child, -.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { - border-color: #f5f5f5; - border-style: solid; - border-radius: 0; -} - -.panel .panel-heading { - margin-bottom: 0; - padding: 6px 15px; -} diff --git a/src/app/tasks/task-submission-history/task-submission-history.component.ts b/src/app/tasks/task-submission-history/task-submission-history.component.ts deleted file mode 100644 index 916c6cd043..0000000000 --- a/src/app/tasks/task-submission-history/task-submission-history.component.ts +++ /dev/null @@ -1,109 +0,0 @@ -import {Component, OnInit, Inject, Input, Output, EventEmitter} from '@angular/core'; -import {TaskSubmissionService} from 'src/app/common/services/task-submission.service'; -import {Subject} from 'rxjs'; -import {OverseerAssessmentService, Task} from 'src/app/api/models/doubtfire-model'; -import {OverseerAssessment} from 'src/app/api/models/doubtfire-model'; -import {AlertService} from 'src/app/common/services/alert.service'; - -@Component({ - selector: 'task-submission-history', - templateUrl: './task-submission-history.component.html', - styleUrls: ['./task-submission-history.component.scss'], -}) -export class TaskSubmissionHistoryComponent implements OnInit { - @Input() task: Task; - @Output() hasNoData = new EventEmitter(); - tabs: OverseerAssessment[]; - // timestamps: string[]; - selectedTab: OverseerAssessment = new OverseerAssessment(); - @Input() refreshTrigger: Subject; - - constructor( - private alerts: AlertService, - private submissions: TaskSubmissionService, - private overseerAssessmentService: OverseerAssessmentService, - ) {} - - ngOnInit() { - this.fillTabs(); - - this.refreshTrigger.subscribe(() => { - this.fillTabs(); - }); - } - - private handleError(error: any) { - this.alerts.error('Error: ' + error, 6000); - } - - fillTabs(): void { - // this.submissions.getLatestSubmissionsTimestamps(this.task); - // let transformedData = this.overseerAssessmentService.queryForTask(this.task).pipe( - // map(data => { - // return data.map((ts: any) => { - // let result = new SubmissionTab(); - // timestamp: new Date(ts.submission_timestamp * 1000), - // content: '', - // timestampString: ts.submission_timestamp, - // taskStatus: ts.result_task_status, - // submissionStatus: ts.status, - // createdAt: ts.created_at, - // updatedAt: ts.updated_at, - // taskId: ts.task_id}; - // }); - // }) - // ); - - this.overseerAssessmentService.queryForTask(this.task).subscribe( - (tabs) => { - if (tabs.length === 0) { - this.tabs = [new OverseerAssessment()]; - this.selectedTab.content = [ - {label: 'No Data', result: 'There are no submissions for this task at the moment.'}, - ]; - } else { - this.tabs = tabs; - } - // if (this.selectedTab.timestampString) { - // this.openSubmission(tabs.filter(x => x.timestampString === this.selectedTab.timestampString)[0]); - // } else { - // this.openSubmission(tabs[0]); - // } - }, - (error) => { - this.handleError(error); - }, - ); - } - - triggerOverseer(tab: OverseerAssessment) { - this.overseerAssessmentService.triggerOverseer(tab).subscribe( - (response: OverseerAssessment) => { - this.alerts.success('Overseer assessment will be run again.', 2000); - }, - (response: any) => { - this.alerts.error('Error requesting overseer assessment.', 6000); - }, - ); - } - - openSubmission(tab: OverseerAssessment) { - this.selectedTab = tab; - // this.selectedTab.timestamp = tab.timestamp; - // this.selectedTab.timestampString = tab.timestampString; - // this.selectedTab.taskStatus = tab.taskStatus; - // this.selectedTab.submissionStatus = tab.submissionStatus; - - this.submissions.getSubmissionByTimestamp(this.task, tab.timestampString).subscribe( - (sub) => { - this.selectedTab.content = sub; - this.hasNoData.emit(false); - }, - (error) => { - // TODO: make error handling more readable... - this.selectedTab.content = [{label: 'Error', result: error?.error?.error}]; - this.hasNoData.emit(true); - }, - ); - } -} diff --git a/src/app/tasks/tasks.coffee b/src/app/tasks/tasks.coffee deleted file mode 100644 index 5144dc6667..0000000000 --- a/src/app/tasks/tasks.coffee +++ /dev/null @@ -1,5 +0,0 @@ -angular.module('doubtfire.tasks', [ - 'doubtfire.tasks.modals' - 'doubtfire.tasks.task-ilo-alignment' - 'doubtfire.tasks.project-tasks-list' -]) diff --git a/src/app/test.service.spec.ts b/src/app/test.service.spec.ts index 5ec9e41130..6c5f5a280c 100644 --- a/src/app/test.service.spec.ts +++ b/src/app/test.service.spec.ts @@ -1,6 +1,6 @@ -import { TestBed } from '@angular/core/testing'; - -import { TestService } from './test.service'; +import {beforeEach, describe, expect, it} from 'vitest'; +import {TestBed} from '@angular/core/testing'; +import {TestService} from './test.service'; describe('TestService', () => { beforeEach(() => TestBed.configureTestingModule({})); diff --git a/src/app/test.service.ts b/src/app/test.service.ts index 4069839fc0..92fe881ec0 100644 --- a/src/app/test.service.ts +++ b/src/app/test.service.ts @@ -1,9 +1,6 @@ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) -export class TestService { - - constructor() { } -} +export class TestService {} diff --git a/src/app/units/modals/modals.coffee b/src/app/units/modals/modals.coffee deleted file mode 100644 index fb091e725e..0000000000 --- a/src/app/units/modals/modals.coffee +++ /dev/null @@ -1,4 +0,0 @@ -angular.module('doubtfire.units.modals', [ - 'doubtfire.units.modals.unit-ilo-edit-modal' - 'doubtfire.units.modals.unit-student-enrolment-modal' -]) diff --git a/src/app/units/modals/unit-ilo-edit-modal/unit-ilo-edit-modal.coffee b/src/app/units/modals/unit-ilo-edit-modal/unit-ilo-edit-modal.coffee deleted file mode 100644 index 1aef6ca1b3..0000000000 --- a/src/app/units/modals/unit-ilo-edit-modal/unit-ilo-edit-modal.coffee +++ /dev/null @@ -1,56 +0,0 @@ -# Component not used - -angular.module('doubtfire.units.modals.unit-ilo-edit-modal', []) -# -# Modal to edit or create a new ILO -# -.factory('UnitILOEditModal', ($modal) -> - UnitILOEditModalCtrl = {} - - # - # Provide unit, and optionally a ILO. If no ILO is provided - # it will assume you want to make a new ILO - # - UnitILOEditModalCtrl.show = (unit, ilo) -> - $modal.open - controller: 'UnitILOEditModalCtrl' - templateUrl: 'units/modals/unit-ilo-edit-modal/unit-ilo-edit-modal.tpl.html' - resolve: { - ilo: -> ilo - unit: -> unit - } - - UnitILOEditModalCtrl -) -.controller('UnitILOEditModalCtrl', ($scope, $modalInstance, alertService, ilo, unit, newLearningOutcomeService) -> - prototypeIlo = { name: null, description: null, abbreviation: null } - $scope.ilo = ilo or prototypeIlo - $scope.isNew = !ilo? - - $scope.saveILO = -> - if $scope.isNew - newLearningOutcomeService.create({ - unitId: unit.id - }, { - body: { - name: $scope.ilo.name - description: $scope.ilo.description - abbreviation: $scope.ilo.abbreviation - }, - cache: unit.learningOutcomesCache - }).subscribe({ - next: (response) -> - $modalInstance.close(response) - alertService.success( "Intended Learning Outcome Added", 2000) - error: (response) -> - alertService.error( response, 6000) - }) - else - newLearningOutcomeService.update( {unitId: unit.id, id: ilo.id}, {entity: ilo}).subscribe({ - next: (response) -> - $modalInstance.close(response) - alertService.success( "Intended Learning Outcome Updated", 2000) - error: (response) -> - alertService.error( response, 6000) - }) -) diff --git a/src/app/units/modals/unit-ilo-edit-modal/unit-ilo-edit-modal.tpl.html b/src/app/units/modals/unit-ilo-edit-modal/unit-ilo-edit-modal.tpl.html deleted file mode 100644 index 94c09e2b96..0000000000 --- a/src/app/units/modals/unit-ilo-edit-modal/unit-ilo-edit-modal.tpl.html +++ /dev/null @@ -1,32 +0,0 @@ -
    - - - - - -
    diff --git a/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.coffee b/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.coffee deleted file mode 100644 index 162889b6a6..0000000000 --- a/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.coffee +++ /dev/null @@ -1,50 +0,0 @@ -angular.module('doubtfire.units.modals.unit-student-enrolment-modal', []) -# -# Modal to enrol a student in the given tutorial -# -.factory('UnitStudentEnrolmentModal', ($modal) -> - UnitStudentEnrolmentModal = {} - - # Must provide unit - UnitStudentEnrolmentModal.show = (unit) -> - $modal.open - controller: 'UnitStudentEnrolmentModalCtrl' - templateUrl: 'units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.tpl.html' - resolve: { - unit: -> unit - } - - UnitStudentEnrolmentModal -) -.controller('UnitStudentEnrolmentModalCtrl', ($scope, $modalInstance, alertService, newUserService, unit, campusService, newProjectService) -> - $scope.unit = unit - $scope.projects = unit.students - $scope.campuses = [] - $scope.data = { campusId: 1 } # need in object for observing - - campusService.query().subscribe( (campuses) -> - $scope.campuses = campuses - $scope.data.campusId = campuses[0].id - ) - - $scope.enrolStudent = (studentId, campusId) -> - if ! campusId? - alertService.error( 'Campus missing. Please indicate student campus', 5000) - return - - newProjectService.create( - {}, { - cache: unit.studentCache - body: { - unit_id: unit.id, - student_num: studentId, - campus_id: campusId - } - constructorParams: unit - }).subscribe({ - next: (project) -> - alertService.success( "Student enrolled", 2000) - $modalInstance.close() - error: (message) -> alertService.error( "Error enrolling student: #{message}", 6000) - }) -) diff --git a/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component.html b/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component.html new file mode 100644 index 0000000000..13afa33ac6 --- /dev/null +++ b/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component.html @@ -0,0 +1,41 @@ + + + Enrol Student + + + + + + + + + + diff --git a/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component.scss b/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component.scss new file mode 100644 index 0000000000..4ffc18b0ce --- /dev/null +++ b/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component.scss @@ -0,0 +1,27 @@ +.modal-complete { + width: 500px; +} + +.modal-heading-container { + margin-bottom: 16px; +} + +.modal-heading { + font-size: 24px; +} + +.modal-content-container { + margin-top: 16px; +} + +.modal-container { + display: flex; + justify-content: space-around; + align-items: center; +} + +.card-actions { + margin-top: 8px; + display: flex; + justify-content: flex-end; +} diff --git a/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component.ts b/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component.ts new file mode 100644 index 0000000000..21460b7484 --- /dev/null +++ b/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component.ts @@ -0,0 +1,49 @@ +import {ChangeDetectionStrategy, Component, Inject, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {Campus, Project, Unit} from 'src/app/api/models/doubtfire-model'; +import {CampusService} from 'src/app/api/services/campus.service'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-unit-student-enrolment-modal', + templateUrl: 'unit-student-enrolment-modal.component.html', + styleUrls: ['unit-student-enrolment-modal.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class UnitStudentEnrolmentModalComponent implements OnInit { + unit: Unit; + campuses: Campus[]; + studentIdOrEmail: string; + selectedCampus: Campus; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: {unit: Unit}, + public alertService: AlertService, + public campusService: CampusService, + ) {} + + ngOnInit() { + this.unit = this.data.unit; + this.campusService.query().subscribe((campuses: Campus[]) => { + this.campuses = campuses; + }); + } + + enrolStudent(studentIdOrEmail: string, campus: Campus) { + if (!campus) { + this.alertService.error('Campus missing. Please indicate student campus', 5000); + return; + } + this.unit.enrolStudent(studentIdOrEmail, campus).subscribe({ + next: (_: Project) => { + this.alertService.success('Student enrolled', 2000); + this.dialogRef.close(); + }, + error: (response: string) => { + this.alertService.error(`Error enrolling student: ${response}`, 6000); + }, + }); + } +} diff --git a/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service.ts b/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service.ts new file mode 100644 index 0000000000..d3b4ee10f8 --- /dev/null +++ b/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service.ts @@ -0,0 +1,19 @@ +import {Injectable} from '@angular/core'; +import {MatDialog} from '@angular/material/dialog'; +import {Unit} from 'src/app/api/models/doubtfire-model'; +import {UnitStudentEnrolmentModalComponent} from './unit-student-enrolment-modal.component'; + +@Injectable({ + providedIn: 'root', +}) +export class UnitStudentEnrolmentModalService { + constructor(public dialog: MatDialog) {} + + public show(unit: Unit) { + this.dialog.open(UnitStudentEnrolmentModalComponent, { + data: { + unit: unit, + }, + }); + } +} diff --git a/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.spec.ts b/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.spec.ts new file mode 100644 index 0000000000..c6869756c4 --- /dev/null +++ b/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.spec.ts @@ -0,0 +1,38 @@ +import {beforeEach, describe, expect, it} from 'vitest'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {CampusService} from 'src/app/api/services/campus.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {UnitStudentEnrolmentModalComponent} from './unit-student-enrolment-modal.component'; + +const emptyProvider = {}; + +describe('UnitStudentEnrolmentModalComponent', () => { + let component: UnitStudentEnrolmentModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [UnitStudentEnrolmentModalComponent], + providers: [ + {provide: MatDialogRef, useValue: emptyProvider}, + {provide: MAT_DIALOG_DATA, useValue: emptyProvider}, + {provide: AlertService, useValue: emptyProvider}, + {provide: CampusService, useValue: emptyProvider}, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(UnitStudentEnrolmentModalComponent, {set: {template: ''}}) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UnitStudentEnrolmentModalComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.tpl.html b/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.tpl.html deleted file mode 100644 index b522107ffb..0000000000 --- a/src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.tpl.html +++ /dev/null @@ -1,26 +0,0 @@ -
    - - - -
    diff --git a/src/app/units/states/analytics/analytics.coffee b/src/app/units/states/analytics/analytics.coffee deleted file mode 100644 index 8346a095b7..0000000000 --- a/src/app/units/states/analytics/analytics.coffee +++ /dev/null @@ -1,62 +0,0 @@ -angular.module('doubtfire.units.states.analytics', []) -# -# State for unit analytics -# -.config(($stateProvider) -> - $stateProvider.state 'units/analytics', { - parent: 'units/index' - url: '/analytics' - templateUrl: "units/states/analytics/analytics.tpl.html" - controller: "UnitAnalyticsStateCtrl" - data: - task: "Unit Analytics" - pageTitle: "_Home_" - roleWhitelist: ['Tutor', 'Convenor', 'Admin', 'Auditor'] - } -) -.controller("UnitAnalyticsStateCtrl", ($scope) -> - # TODO: (@alexcu) Refactor directives into sub states - - # - # Active task tab group - # - $scope.tabs = - csvStatDownload: - title: "Unit Statistics" - subtitle: "Download details related to student progress within the unit." - seq: 0 - taskStatusStats: - title: "Task Status Statistics" - subtitle: "View distribution of tasks by their current status either unit-wide or broken down into a specific tutorial or task" - seq: 1 - taskCompletionStats: - title: "Task Completion Statistics" - subtitle: "View how tasks have been marked as completed as a box plot" - seq: 2 - targetGradeStats: - title: "Target Grade Statistics" - subtitle: "View distribution of student target grades either unit-wide or broken down into a specific tutorial" - seq: 3 - achievementStats: - title: "ILO Achievement Statistics" - subtitle: "View how ILOs have been achieved by students to their associated tasks as a box plot" - seq: 4 - - # - # Sets the active tab - # - $scope.setActiveTab = (tab) -> - # Do nothing if we're switching to the same tab - return if tab is $scope.activeTab - $scope.activeTab?.active = false - $scope.activeTab = tab - $scope.activeTab.active = true - - $scope.setActiveTab($scope.tabs.csvStatDownload) - - # - # Checks if tab is the active tab - # - $scope.isActiveTab = (tab) -> - tab is $scope.activeTab -) diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html index dbd2f4302a..d02e990a5e 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.html +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.html @@ -1,17 +1,17 @@ - +

    Tutor Times Session Summary

    -
    +
    @if (role === 'Convenor') { @@ -19,10 +19,10 @@

    Tutor Times Session Summary

    @@ -30,14 +30,14 @@

    Tutor Times Session Summary

    -
    - - +
    -
    +
    Choose a date Tutor Times Session Summary
    -
    +
    Tutor Times Session Summary
    -
    +
    {{ event.tutorName }} ({{ event.duration }} minutes) {{ event.duringTutorial ? 'T' : '' }}Tutor Times Session Summary
    @if (isLoading) { - - +
    } + + +
    + {{ weekEvent.event.title }} +
    +
    diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.scss b/src/app/units/states/analytics/directives/analytics-tutor-times.component.scss index 2ce45fc98d..298a078abc 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.scss +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.scss @@ -14,3 +14,18 @@ width: 100%; min-height: 35px; } + +.analytics-loading-spinner { + width: 48px; + height: 48px; + border: 4px solid rgb(0 0 0 / 12%); + border-top-color: var(--mat-sys-primary, #3f51b5); + border-radius: 50%; + animation: analytics-spinner-rotate 800ms linear infinite; +} + +@keyframes analytics-spinner-rotate { + to { + transform: rotate(360deg); + } +} diff --git a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts index a3451a78de..af819add7f 100644 --- a/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts +++ b/src/app/units/states/analytics/directives/analytics-tutor-times.component.ts @@ -1,4 +1,5 @@ -import {Component, Input, OnInit, ViewEncapsulation} from '@angular/core'; +import {CalendarEvent} from 'angular-calendar'; +import {ChangeDetectionStrategy, Component, Input, OnInit, ViewEncapsulation} from '@angular/core'; import {MatDatepickerInputEvent} from '@angular/material/datepicker'; import {Observable} from 'rxjs'; import {SidekiqJob} from 'src/app/api/models/sidekiq-job'; @@ -8,16 +9,9 @@ import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloa import {SidekiqProgressModalService} from 'src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.service'; import {AlertService} from 'src/app/common/services/alert.service'; -interface SessionEvent { - start: Date; - end: Date; +interface SessionEvent extends CalendarEvent { startHour: string; endHour: string; - title: string; - color: { - primary: string; - secondary: string; - }; userId: number; commentsAdded: number; assessments: number; @@ -32,6 +26,8 @@ interface SessionEvent { templateUrl: 'analytics-tutor-times.component.html', styleUrls: ['analytics-tutor-times.component.scss'], encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class AnalyticsTutorTimesComponent implements OnInit { @Input() unit: Unit; @@ -46,7 +42,7 @@ export class AnalyticsTutorTimesComponent implements OnInit { viewDate = new Date(); events: SessionEvent[] = []; - filteredEvents = []; + filteredEvents: SessionEvent[] = []; tutorTimeSummaryStartDate: Date; tutorTimeSummaryEndDate: Date; @@ -256,7 +252,11 @@ export class AnalyticsTutorTimesComponent implements OnInit { }); } - eventClicked({event}: {event: SessionEvent}): void { + eventClicked({event}: {event: CalendarEvent; sourceEvent?: MouseEvent | KeyboardEvent}): void { + if (!this.isSessionEvent(event)) { + return; + } + if (event.userId !== undefined) { if (this.selectedUserId === null) { this.selectedUserId = Number(event.userId); @@ -267,6 +267,21 @@ export class AnalyticsTutorTimesComponent implements OnInit { } } + private isSessionEvent(event: CalendarEvent): event is SessionEvent { + return 'userId' in event && 'duration' in event; + } + + sessionEventTitle(event: SessionEvent): string { + return [ + `${event.tutorName} (${event.duration} minutes)${event.duringTutorial ? ' T' : ''}`, + `${event.startHour} - ${event.endHour}`, + `Assessments: ${event.assessments || 0}`, + `Comments: ${event.commentsAdded || 0}`, + `Submissions opened: ${event.submissionsOpened || 0}`, + `During Tutorial?: ${event.duringTutorial ? 'yes' : 'no'}`, + ].join('\n'); + } + private stringToHexColor( name: string, opts?: {hue?: [number, number]; sat?: [number, number]; lit?: [number, number]}, diff --git a/src/app/units/states/analytics/unit-analytics-route.component.html b/src/app/units/states/analytics/unit-analytics-route.component.html index 40017c59e7..c5e7f03cb3 100644 --- a/src/app/units/states/analytics/unit-analytics-route.component.html +++ b/src/app/units/states/analytics/unit-analytics-route.component.html @@ -1,22 +1,41 @@ -

    Unit Statistics

    +
    +

    Unit Statistics

    -
    - - - - - - - - +
    + + + + + + + @if (role === 'Convenor' || isAdmin) { + + } + + - + +
    + diff --git a/src/app/units/states/analytics/unit-analytics-route.component.ts b/src/app/units/states/analytics/unit-analytics-route.component.ts index 4c8e068722..edf666e6cb 100644 --- a/src/app/units/states/analytics/unit-analytics-route.component.ts +++ b/src/app/units/states/analytics/unit-analytics-route.component.ts @@ -1,7 +1,7 @@ -import {Component, Input, OnInit} from '@angular/core'; -import {MatDatepickerInputEvent} from '@angular/material/datepicker'; -import {CalendarEvent} from 'angular-calendar'; -import {Observable} from 'rxjs'; +import {formatDate} from '@angular/common'; +import {ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {Observable, first, of} from 'rxjs'; import {SidekiqJob} from 'src/app/api/models/sidekiq-job'; import {Unit} from 'src/app/api/models/unit'; import {UserService} from 'src/app/api/services/user.service'; @@ -13,9 +13,13 @@ import {AlertService} from 'src/app/common/services/alert.service'; selector: 'f-unit-analytics', templateUrl: 'unit-analytics-route.component.html', styleUrls: ['unit-analytics-route.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class UnitAnalyticsComponent { - @Input() unit: Unit; +export class UnitAnalyticsComponent implements OnInit { + @Input() public unit$: Observable; + + public unit: Unit; constructor( private sidekiqProgressModalService: SidekiqProgressModalService, @@ -23,10 +27,23 @@ export class UnitAnalyticsComponent { private fileDownloaderService: FileDownloaderService, private userService: UserService, private alertService: AlertService, + private route: ActivatedRoute, + @Inject(LOCALE_ID) private locale: string, ) {} + ngOnInit(): void { + this.unit$ = this.unit$ ?? of(this.route.parent.snapshot.data.unit); + this.unit$?.pipe(first()).subscribe((unit) => { + this.unit = unit; + }); + } + get role() { - return this.unit.staff.find((s) => s.user.id === this.userService.currentUser.id)?.role; + return this.unit?.staff.find((s) => s.user.id === this.userService.currentUser.id)?.role; + } + + get isAdmin() { + return this.userService.currentUser?.systemRole === 'Admin'; } public getTaskCompletionCsv() { @@ -61,6 +78,16 @@ export class UnitAnalyticsComponent { ); } + public getOverflowTaskClaimsCsv() { + const timestamp = formatDate(new Date(), 'd-MMMM-y-HHmm', this.locale).toLowerCase(); + + this.downloadCsv( + this.unit.downloadOverflowTaskClaimsCsv(), + 'Overflow Task Claims CSV', + `${this.unit.code}-overflow-task-claims-${timestamp}.csv`, + ); + } + public downloadCsv(newJob: Observable, title: string, filename: string) { newJob.subscribe({ next: (job) => { @@ -74,8 +101,8 @@ export class UnitAnalyticsComponent { this.fileDownloaderService.downloadBlobToFile(url, filename); }); }, - error: (_error) => { - this.alertsService.error(`Could not download ${title}`, 6000); + error: (error) => { + this.alertsService.error(`Could not download ${title}: ${error}`, 6000); }, }); } diff --git a/src/app/units/states/edit/directives/directives.coffee b/src/app/units/states/edit/directives/directives.coffee deleted file mode 100644 index 4da41cacfb..0000000000 --- a/src/app/units/states/edit/directives/directives.coffee +++ /dev/null @@ -1,4 +0,0 @@ -angular.module('doubtfire.units.states.edit.directives', [ - 'doubtfire.units.states.edit.directives.unit-group-set-editor' - 'doubtfire.units.states.edit.directives.unit-ilo-editor' -]) diff --git a/src/app/units/states/edit/directives/unit-communications-editor/actions/change-target-grade-action/change-target-grade-action.component.html b/src/app/units/states/edit/directives/unit-communications-editor/actions/change-target-grade-action/change-target-grade-action.component.html new file mode 100644 index 0000000000..205949ae2f --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/actions/change-target-grade-action/change-target-grade-action.component.html @@ -0,0 +1,23 @@ +@if (mode === 'edit') { + + Target grade + + @for (grade of editor.targetGrades; track grade.value) { + + {{ grade.label }} + + } + + +} @else { + + Target grade + + @for (grade of editor.targetGrades; track grade.value) { + + {{ grade.label }} + + } + + +} diff --git a/src/app/units/states/edit/directives/unit-communications-editor/actions/change-target-grade-action/change-target-grade-action.component.ts b/src/app/units/states/edit/directives/unit-communications-editor/actions/change-target-grade-action/change-target-grade-action.component.ts new file mode 100644 index 0000000000..5be3b69259 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/actions/change-target-grade-action/change-target-grade-action.component.ts @@ -0,0 +1,16 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {CommunicationRule} from 'src/app/api/models/doubtfire-model'; +import type {UnitCommunicationsEditorComponent} from '../../unit-communications-editor.component'; + +@Component({ + selector: 'f-change-target-grade-action', + standalone: false, + templateUrl: './change-target-grade-action.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + host: {class: 'flex w-full flex-col items-center'}, +}) +export class ChangeTargetGradeActionComponent { + @Input({required: true}) editor: UnitCommunicationsEditorComponent; + @Input({required: true}) rule: CommunicationRule; + @Input({required: true}) mode: 'add' | 'edit'; +} diff --git a/src/app/units/states/edit/directives/unit-communications-editor/actions/communication-actions.component.html b/src/app/units/states/edit/directives/unit-communications-editor/actions/communication-actions.component.html new file mode 100644 index 0000000000..b1697b45d3 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/actions/communication-actions.component.html @@ -0,0 +1,194 @@ +
    + + @for (action of rule.actions; track action.id) { + + @if (editor.editingActionId[rule.id] === action.id) { +
    + + Action + + @for (type of editor.actionTypes; track type) { + + {{ editor.actionTypeLabel(type) }} + + } + + + @switch (editor.actionFor(rule).type) { + @case ('EmailStudentAction') { + + } + @case ('EmailStaffAction') { + + } + @case ('TaskCommentAction') { + + } + @case ('ChangeTargetGradeAction') { + + } + } +
    + + +
    +
    + } @else { +
    +
    +
    + {{ editor.actionTypeLabel(action.type) }} +
    +
    + @switch (action.type) { + @case ('ChangeTargetGradeAction') { + Change student's target grade to + + {{ editor.targetGradeName(action.target_grade) }} + + } + @case ('EmailStudentAction') { + @if (action.subject || action.body) { +
    + @if (action.subject) { +
    +
    Subject
    +
    +
    + } + @if (action.body) { +
    +
    Body
    +
    +
    + } +
    + } + } + @case ('EmailStaffAction') { + Send email to + + {{ editor.staffAudienceLabel(action) }} + + @if (action.subject || action.body) { +
    + @if (action.subject) { +
    +
    Subject
    +
    +
    + } + @if (action.body) { +
    +
    Body
    +
    +
    + } +
    + } + } + @case ('TaskCommentAction') { + Add comment to + + {{ editor.taskDefinitionLabel(action.task_definition_id) }} + + @if (action.body) { +
    +
    +
    Comment
    +
    +
    +
    + } + } + @default { + {{ editor.actionSummary(action) }} + } + } +
    +
    +
    + + +
    +
    + } +
    + } +
    + + @if (editor.actionFormOpen[rule.id] && !editor.editingActionId[rule.id]) { +
    + + Action + + @for (type of editor.actionTypes; track type) { + + {{ editor.actionTypeLabel(type) }} + + } + + + + @switch (editor.actionFor(rule).type) { + @case ('EmailStudentAction') { + + } + + @case ('EmailStaffAction') { + + } + + @case ('TaskCommentAction') { + + } + + @case ('ChangeTargetGradeAction') { + + } + } + + + + +
    + } @else { +
    + +
    + } +
    diff --git a/src/app/units/states/edit/directives/unit-communications-editor/actions/communication-actions.component.ts b/src/app/units/states/edit/directives/unit-communications-editor/actions/communication-actions.component.ts new file mode 100644 index 0000000000..254e5ccf50 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/actions/communication-actions.component.ts @@ -0,0 +1,14 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {CommunicationRule} from 'src/app/api/models/doubtfire-model'; +import type {UnitCommunicationsEditorComponent} from '../unit-communications-editor.component'; + +@Component({ + selector: 'f-communication-actions', + standalone: false, + changeDetection: ChangeDetectionStrategy.Eager, + templateUrl: './communication-actions.component.html', +}) +export class CommunicationActionsComponent { + @Input({required: true}) editor: UnitCommunicationsEditorComponent; + @Input({required: true}) rule: CommunicationRule; +} diff --git a/src/app/units/states/edit/directives/unit-communications-editor/actions/email-staff-action/email-staff-action.component.html b/src/app/units/states/edit/directives/unit-communications-editor/actions/email-staff-action/email-staff-action.component.html new file mode 100644 index 0000000000..f9b756c6fc --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/actions/email-staff-action/email-staff-action.component.html @@ -0,0 +1,122 @@ +@if (mode === 'edit') { +
    + + Subject + + + + + @for (variable of editor.emailVariables; track variable.token) { + + } + + @if (editor.actionFor(rule).subject) { +
    +
    Subject Preview
    +
    +
    + } +
    + + Body + + + +
    +
    Body Preview
    +
    + @if (editor.actionFor(rule).body) { +
    + } @else { +
    Preview will appear here.
    + } +
    +
    +
    + + @for (variable of editor.emailVariables; track variable.token) { + + } + +
    + Tutors + Convenors +
    +
    Sent from the main convenor.
    +
    +} @else { +
    + + Subject + + + + + @for (variable of editor.emailVariables; track variable.token) { + + } + + + @if (editor.actionFor(rule).subject) { +
    +
    Subject Preview
    +
    +
    + } + +
    + + Body + + + +
    +
    Body Preview
    +
    + @if (editor.actionFor(rule).body) { +
    + } @else { +
    Preview will appear here.
    + } +
    +
    +
    + + @for (variable of editor.emailVariables; track variable.token) { + + } + + +
    + Tutors + Convenors +
    +
    Sent from the main convenor.
    +
    +} diff --git a/src/app/units/states/edit/directives/unit-communications-editor/actions/email-staff-action/email-staff-action.component.ts b/src/app/units/states/edit/directives/unit-communications-editor/actions/email-staff-action/email-staff-action.component.ts new file mode 100644 index 0000000000..91f2a22c79 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/actions/email-staff-action/email-staff-action.component.ts @@ -0,0 +1,16 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {CommunicationRule} from 'src/app/api/models/doubtfire-model'; +import type {UnitCommunicationsEditorComponent} from '../../unit-communications-editor.component'; + +@Component({ + selector: 'f-email-staff-action', + standalone: false, + templateUrl: './email-staff-action.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + host: {class: 'block w-full'}, +}) +export class EmailStaffActionComponent { + @Input({required: true}) editor: UnitCommunicationsEditorComponent; + @Input({required: true}) rule: CommunicationRule; + @Input({required: true}) mode: 'add' | 'edit'; +} diff --git a/src/app/units/states/edit/directives/unit-communications-editor/actions/email-student-action/email-student-action.component.html b/src/app/units/states/edit/directives/unit-communications-editor/actions/email-student-action/email-student-action.component.html new file mode 100644 index 0000000000..90bdbf0273 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/actions/email-student-action/email-student-action.component.html @@ -0,0 +1,113 @@ +@if (mode === 'edit') { +
    + + Subject + + + + + @for (variable of editor.emailVariables; track variable.token) { + + } + + @if (editor.actionFor(rule).subject) { +
    +
    Subject Preview
    +
    +
    + } +
    + + Body + + + +
    +
    Body Preview
    +
    + @if (editor.actionFor(rule).body) { +
    + } @else { +
    Preview will appear here.
    + } +
    +
    +
    + + @for (variable of editor.emailVariables; track variable.token) { + + } + +
    Sent from the main convenor.
    +
    +} @else { +
    + + Subject + + + + + @for (variable of editor.emailVariables; track variable.token) { + + } + + + @if (editor.actionFor(rule).subject) { +
    +
    Subject Preview
    +
    +
    + } + +
    + + Body + + + +
    +
    Body Preview
    +
    + @if (editor.actionFor(rule).body) { +
    + } @else { +
    Preview will appear here.
    + } +
    +
    +
    + + @for (variable of editor.emailVariables; track variable.token) { + + } + +
    Sent from the main convenor.
    +
    +} diff --git a/src/app/units/states/edit/directives/unit-communications-editor/actions/email-student-action/email-student-action.component.ts b/src/app/units/states/edit/directives/unit-communications-editor/actions/email-student-action/email-student-action.component.ts new file mode 100644 index 0000000000..100e890c66 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/actions/email-student-action/email-student-action.component.ts @@ -0,0 +1,16 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {CommunicationRule} from 'src/app/api/models/doubtfire-model'; +import type {UnitCommunicationsEditorComponent} from '../../unit-communications-editor.component'; + +@Component({ + selector: 'f-email-student-action', + standalone: false, + templateUrl: './email-student-action.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + host: {class: 'block w-full'}, +}) +export class EmailStudentActionComponent { + @Input({required: true}) editor: UnitCommunicationsEditorComponent; + @Input({required: true}) rule: CommunicationRule; + @Input({required: true}) mode: 'add' | 'edit'; +} diff --git a/src/app/units/states/edit/directives/unit-communications-editor/actions/task-comment-action/task-comment-action.component.html b/src/app/units/states/edit/directives/unit-communications-editor/actions/task-comment-action/task-comment-action.component.html new file mode 100644 index 0000000000..ab8034b1a1 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/actions/task-comment-action/task-comment-action.component.html @@ -0,0 +1,86 @@ +@if (mode === 'edit') { +
    + + Task + + @for (taskDefinition of editor.taskDefinitions; track taskDefinition.id) { + + {{ taskDefinition.abbreviation }} + {{ taskDefinition.name }} + + } + + +
    + + Comment + + + +
    +
    Comment Preview
    +
    + @if (editor.actionFor(rule).body) { +
    + } @else { +
    Preview will appear here.
    + } +
    +
    +
    + + @for (variable of editor.emailVariables; track variable.token) { + + } + +
    Sent from the main convenor.
    +
    +} @else { +
    + + Task + + @for (taskDefinition of editor.taskDefinitions; track taskDefinition.id) { + + {{ taskDefinition.abbreviation }} {{ taskDefinition.name }} + + } + + +
    + + Comment + + + +
    +
    Comment Preview
    +
    + @if (editor.actionFor(rule).body) { +
    + } @else { +
    Preview will appear here.
    + } +
    +
    +
    + + @for (variable of editor.emailVariables; track variable.token) { + + } + +
    Sent from the main convenor.
    +
    +} diff --git a/src/app/units/states/edit/directives/unit-communications-editor/actions/task-comment-action/task-comment-action.component.ts b/src/app/units/states/edit/directives/unit-communications-editor/actions/task-comment-action/task-comment-action.component.ts new file mode 100644 index 0000000000..6c459e3d26 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/actions/task-comment-action/task-comment-action.component.ts @@ -0,0 +1,16 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {CommunicationRule} from 'src/app/api/models/doubtfire-model'; +import type {UnitCommunicationsEditorComponent} from '../../unit-communications-editor.component'; + +@Component({ + selector: 'f-task-comment-action', + standalone: false, + templateUrl: './task-comment-action.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + host: {class: 'block w-full'}, +}) +export class TaskCommentActionComponent { + @Input({required: true}) editor: UnitCommunicationsEditorComponent; + @Input({required: true}) rule: CommunicationRule; + @Input({required: true}) mode: 'add' | 'edit'; +} diff --git a/src/app/units/states/edit/directives/unit-communications-editor/communication-schedule-modal/communication-schedule-modal.component.html b/src/app/units/states/edit/directives/unit-communications-editor/communication-schedule-modal/communication-schedule-modal.component.html new file mode 100644 index 0000000000..047dece923 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/communication-schedule-modal/communication-schedule-modal.component.html @@ -0,0 +1,91 @@ +

    + {{ draft.id ? 'Edit communication schedule' : 'Add communication schedule' }} +

    + + +
    + + Name + + + + + Timezone + + +
    + + Schedule active + +
    + + Week + + + + + Day + + @for (weekday of weekdays; track weekday.value) { + {{ weekday.label }} + } + + + + + Hour + + + + + Minute + + +
    + + + Repeat + + One time only + Daily + Weekly + Monthly + + + + @if (draft.recurrence !== 'none') { +
    + + Every + + + + + Stop after runs + + + + + Or stop on + + +
    + } + +
    +
    Summary
    +
    {{ scheduleSummary() }}
    +
    + + +
    + + + + + diff --git a/src/app/units/states/edit/directives/unit-communications-editor/communication-schedule-modal/communication-schedule-modal.component.ts b/src/app/units/states/edit/directives/unit-communications-editor/communication-schedule-modal/communication-schedule-modal.component.ts new file mode 100644 index 0000000000..6af27bd31e --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/communication-schedule-modal/communication-schedule-modal.component.ts @@ -0,0 +1,196 @@ +import {ChangeDetectionStrategy, Component, Inject, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import { + Campus, + CampusService, + CommunicationSetSchedule, + Unit, +} from 'src/app/api/models/doubtfire-model'; + +export interface CommunicationScheduleModalData { + schedule?: CommunicationSetSchedule; + unit?: Unit; +} + +export const SCHEDULE_WEEKDAYS = [ + {value: 0, label: 'Sunday', shortLabel: 'Sun'}, + {value: 1, label: 'Monday', shortLabel: 'Mon'}, + {value: 2, label: 'Tuesday', shortLabel: 'Tue'}, + {value: 3, label: 'Wednesday', shortLabel: 'Wed'}, + {value: 4, label: 'Thursday', shortLabel: 'Thu'}, + {value: 5, label: 'Friday', shortLabel: 'Fri'}, + {value: 6, label: 'Saturday', shortLabel: 'Sat'}, +] as const; + +@Component({ + selector: 'f-communication-schedule-modal', + standalone: false, + changeDetection: ChangeDetectionStrategy.Eager, + templateUrl: './communication-schedule-modal.component.html', +}) +export class CommunicationScheduleModalComponent implements OnInit { + readonly weekdays = SCHEDULE_WEEKDAYS; + campuses: Campus[] = []; + timezonePlaceholder = 'UTC'; + draft = new CommunicationSetSchedule({ + name: 'Schedule 1', + active: true, + anchor_week: 1, + anchor_day: 'Monday', + recurrence: 'none', + interval: 1, + timezone: 'UTC', + hour: 8, + minute: 0, + }); + untilDateTime = ''; + + constructor( + private campusService: CampusService, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: CommunicationScheduleModalData, + ) { + if (data.schedule) { + this.draft = new CommunicationSetSchedule({ + ...data.schedule, + }); + } + + this.untilDateTime = this.asDateTimeLocal(this.draft.until_at); + } + + ngOnInit(): void { + this.campusService.query().subscribe((campuses) => { + this.campuses = campuses; + const defaultTimezone = campuses[0]?.timezone; + if (!defaultTimezone) { + return; + } + + this.timezonePlaceholder = defaultTimezone; + if (!this.draft.timezone || this.draft.timezone === 'UTC') { + this.draft.timezone = defaultTimezone; + } + }); + } + + canSave(): boolean { + return !!this.draft.anchor_week && !!this.draft.anchor_day; + } + + save(): void { + const schedule = new CommunicationSetSchedule({ + ...this.draft, + name: this.draft.name?.trim() || 'Untitled schedule', + until_at: this.untilDateTime || undefined, + anchor_week: Math.max(1, Number(this.draft.anchor_week || 1)), + anchor_day: this.draft.anchor_day || 'Monday', + hour: this.safeHour(), + minute: this.safeMinute(), + }); + + schedule.ice_cube_schedule = this.toIceCubePayload(schedule); + this.dialogRef.close(schedule); + } + + scheduleSummary(): string { + const parts: string[] = []; + parts.push( + `Starts Week ${this.draft.anchor_week || 1} ${this.draft.anchor_day || 'Monday'} at ${this.timeLabel(this.safeHour(), this.safeMinute())}`, + ); + + switch (this.draft.recurrence) { + case 'daily': + parts.push(`Repeats every ${this.draft.interval || 1} day(s)`); + break; + case 'weekly': + parts.push(`Repeats every ${this.draft.interval || 1} week(s)`); + break; + case 'monthly': + parts.push(`Repeats every ${this.draft.interval || 1} month(s)`); + break; + default: + parts.push('Runs once'); + } + + if (this.draft.repeat_count) { + parts.push(`up to ${this.draft.repeat_count} times`); + } + if (this.untilDateTime) { + parts.push(`until ${this.untilDateTime}`); + } + + return parts.join(' | '); + } + + iceCubePreview(): string { + return JSON.stringify(this.toIceCubePayload(this.draft), null, 2); + } + + private safeHour(): number { + return Math.min(23, Math.max(0, Number(this.draft.hour ?? 8))); + } + + private safeMinute(): number { + return Math.min(59, Math.max(0, Number(this.draft.minute ?? 0))); + } + + private toIceCubePayload(schedule: CommunicationSetSchedule): Record { + const payload: Record = { + timezone: schedule.timezone || 'UTC', + anchor: this.anchorPayload(schedule), + recurrence: schedule.recurrence, + interval: schedule.interval || 1, + limits: { + count: schedule.repeat_count || null, + until: schedule.until_at || null, + }, + rules: [], + }; + + const rules = payload.rules as Record[]; + switch (schedule.recurrence) { + case 'daily': + rules.push({ + type: 'daily', + interval: schedule.interval || 1, + }); + break; + case 'weekly': + rules.push({ + type: 'weekly', + interval: schedule.interval || 1, + }); + break; + case 'monthly': + rules.push({ + type: 'monthly', + interval: schedule.interval || 1, + }); + break; + default: + rules.push({type: 'one_off'}); + } + + return payload; + } + + private anchorPayload(schedule: CommunicationSetSchedule): Record { + return { + week: schedule.anchor_week || 1, + day: schedule.anchor_day || 'Monday', + time_of_day: this.timeLabel(schedule.hour || 0, schedule.minute || 0), + }; + } + + private asDateTimeLocal(value?: string): string { + if (!value) { + return ''; + } + return value.length >= 16 ? value.slice(0, 16) : value; + } + + private timeLabel(hour: number, minute: number): string { + return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; + } +} diff --git a/src/app/units/states/edit/directives/unit-communications-editor/communication-schedule-modal/communication-schedules.component.html b/src/app/units/states/edit/directives/unit-communications-editor/communication-schedule-modal/communication-schedules.component.html new file mode 100644 index 0000000000..1d5dbcf1e8 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/communication-schedule-modal/communication-schedules.component.html @@ -0,0 +1,71 @@ +@if (editor.setPreviewLoading) { + +} + +
    +
    +
    +
    Schedules
    +
    Build one-off or recurring schedules for this set.
    +
    + +
    + + @if ((set.schedules || []).length) { +
    + @for (schedule of set.schedules || []; track editor.scheduleTrackId(schedule)) { +
    +
    +
    +
    {{ schedule.name || 'Untitled schedule' }}
    +
    {{ editor.scheduleSummary(schedule) }}
    +
    + +
    + + {{ schedule.active ? 'Active' : 'Inactive' }} + + + +
    +
    + +
    +
    +
    Anchor
    +
    {{ editor.scheduleAnchorSummary(schedule) }}
    +
    +
    +
    Time
    +
    {{ editor.scheduleTimeSummary(schedule) }}
    +
    +
    +
    Next Run
    +
    {{ editor.scheduleNextRunSummary(schedule) }}
    +
    +
    +
    Last Run
    +
    {{ editor.scheduleLastRunSummary(schedule) }}
    +
    +
    +
    + } +
    + } @else { +
    + No schedules yet. Add one to run this set on a fixed date or on a repeating cadence. +
    + } +
    diff --git a/src/app/units/states/edit/directives/unit-communications-editor/communication-schedule-modal/communication-schedules.component.ts b/src/app/units/states/edit/directives/unit-communications-editor/communication-schedule-modal/communication-schedules.component.ts new file mode 100644 index 0000000000..aa1156619e --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/communication-schedule-modal/communication-schedules.component.ts @@ -0,0 +1,14 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {CommunicationSet} from 'src/app/api/models/doubtfire-model'; +import type {UnitCommunicationsEditorComponent} from '../unit-communications-editor.component'; + +@Component({ + selector: 'f-communication-schedules', + standalone: false, + changeDetection: ChangeDetectionStrategy.Eager, + templateUrl: './communication-schedules.component.html', +}) +export class CommunicationSchedulesComponent { + @Input({required: true}) editor: UnitCommunicationsEditorComponent; + @Input({required: true}) set: CommunicationSet; +} diff --git a/src/app/units/states/edit/directives/unit-communications-editor/conditions/communication-conditions.component.html b/src/app/units/states/edit/directives/unit-communications-editor/conditions/communication-conditions.component.html new file mode 100644 index 0000000000..898b9b431e --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/conditions/communication-conditions.component.html @@ -0,0 +1,497 @@ +
    +
    + + Conditions + + @for (option of editor.logicalOperatorOptions; track option.value) { + + {{ option.label }} + + } + + +
    + + + @for (condition of rule.conditions; track condition.id) { + + @if (editor.editingConditionId[rule.id] === condition.id) { +
    + + Condition + + @for (type of editor.conditionTypes; track type) { + + {{ editor.conditionTypeLabel(type) }} + + } + + + + @if (editor.conditionFor(rule).type === 'TaskDefinitionStatusCondition') { + + Task + + @for (taskDefinition of editor.taskDefinitions; track taskDefinition.id) { + + {{ taskDefinition.abbreviation }} {{ taskDefinition.name }} + + } + + + } + + + Operator + + @for ( + operator of editor.operatorsFor(editor.conditionFor(rule).type); + track operator + ) { + + {{ editor.operatorLabel(operator) }} + + } + + + + @if (editor.conditionFor(rule).type === 'TaskStatusCountCondition') { + + Count + + + + + Task grade + + @for (grade of editor.targetGrades; track grade.value) { + + {{ grade.label }} + + } + + + } + + @switch (editor.conditionFor(rule).type) { + @case ('TargetGradeCondition') { + + Target grade + + @for (grade of editor.targetGrades; track grade.value) { + + {{ grade.label }} + + } + + + } + @case ('TaskDefinitionStatusCondition') { + + Statuses + + @for (status of editor.taskStatuses; track status) { + + {{ editor.taskStatusLabel(status) }} + + } + + + } + @case ('TaskStatusCountCondition') { + + Statuses + + @for (status of editor.taskStatuses; track status) { + + {{ editor.taskStatusLabel(status) }} + + } + + + } + @case ('LoginStatusCondition') { + + Last sign in + + + } + @case ('SpecConCondition') { + + Days + + + } + @case ('TutorialEnrolmentCondition') { + + Tutorial + + @for (tutorial of editor.tutorials; track tutorial.id) { + + {{ tutorial.abbreviation }} {{ tutorial.description }} + + } + + + } + @case ('TutorialStreamEnrolmentCondition') { + + Tutorial stream + + @for (stream of editor.tutorialStreams; track stream.id) { + + {{ stream.abbreviation }} {{ stream.name }} + + } + + + } + @case ('CampusCondition') { + + Campus + + @for (campus of editor.campuses; track campus.id) { + + {{ campus.abbreviation }} {{ campus.name }} + + } + + + } + } + + + +
    + } @else { +
    +
    +
    + {{ editor.conditionTypeLabel(condition.type) }} +
    +
    + @switch (condition.type) { + @case ('TargetGradeCondition') { + Students with a + Target Grade + + {{ editor.operatorLabel(condition.operator) }} + + + {{ editor.targetGradeName(condition.target_grade) }} + + } + @case ('TaskDefinitionStatusCondition') { + Students that have + + {{ editor.taskDefinitionLabel(condition.task_definition_id) }} + + + {{ editor.taskStatusPredicate(condition.operator) }} + + + {{ editor.taskStatusesLabel(condition.task_statuses) }} + + } + @case ('TaskStatusCountCondition') { + Students with + Task Status Count + + {{ editor.operatorLabel(condition.operator) }} + + + {{ condition.task_status_count }} + + + {{ editor.targetGradeName(condition.task_target_grade) }} + + tasks in + + {{ editor.taskStatusesLabel(condition.task_statuses) }} + + } + @case ('LoginStatusCondition') { + Students with a + Last Sign In + + {{ editor.operatorLabel(condition.operator) }} + + + {{ editor.dateLabel(condition.last_sign_in_at) }} + + } + @case ('SpecConCondition') { + Students with + Special Consideration Days + + {{ editor.operatorLabel(condition.operator) }} + + + {{ condition.spec_con_days }} + + } + @case ('TutorialEnrolmentCondition') { + Students + + {{ editor.enrolmentPredicate(condition.operator) }} + + + {{ editor.tutorialLabel(condition.tutorial_id) }} + + } + @case ('TutorialStreamEnrolmentCondition') { + Students + + {{ editor.enrolmentPredicate(condition.operator) }} + + + {{ editor.tutorialStreamLabel(condition.tutorial_stream_id) }} + + } + @case ('CampusCondition') { + Students + + {{ editor.enrolmentPredicate(condition.operator) }} + + + {{ editor.campusLabel(condition.campus_id) }} + + } + @default { + {{ editor.operatorLabel(condition.operator) }} + {{ editor.labelFor(condition) }} + } + } +
    +
    +
    + + +
    +
    + } +
    + } +
    + + @if (editor.conditionFormOpen[rule.id] && !editor.editingConditionId[rule.id]) { +
    + + Condition + + @for (type of editor.conditionTypes; track type) { + + {{ editor.conditionTypeLabel(type) }} + + } + + + + @if (editor.conditionFor(rule).type === 'TaskDefinitionStatusCondition') { + + Task + + @for (taskDefinition of editor.taskDefinitions; track taskDefinition.id) { + + {{ taskDefinition.abbreviation }} {{ taskDefinition.name }} + + } + + + } + + + Operator + + @for (operator of editor.operatorsFor(editor.conditionFor(rule).type); track operator) { + + {{ editor.operatorLabel(operator) }} + + } + + + + @if (editor.conditionFor(rule).type === 'TaskStatusCountCondition') { + + Count + + + + + Task grade + + @for (grade of editor.targetGrades; track grade.value) { + + {{ grade.label }} + + } + + + } + + @switch (editor.conditionFor(rule).type) { + @case ('TargetGradeCondition') { + + Target grade + + @for (grade of editor.targetGrades; track grade.value) { + + {{ grade.label }} + + } + + + } + + @case ('TaskDefinitionStatusCondition') { + + Statuses + + @for (status of editor.taskStatuses; track status) { + + {{ editor.taskStatusLabel(status) }} + + } + + + } + + @case ('TaskStatusCountCondition') { + + Statuses + + @for (status of editor.taskStatuses; track status) { + + {{ editor.taskStatusLabel(status) }} + + } + + + } + + @case ('LoginStatusCondition') { + + Last sign in + + + } + @case ('SpecConCondition') { + + Days + + + } + + @case ('TutorialEnrolmentCondition') { + + Tutorial + + @for (tutorial of editor.tutorials; track tutorial.id) { + + {{ tutorial.abbreviation }} {{ tutorial.description }} + + } + + + } + + @case ('TutorialStreamEnrolmentCondition') { + + Tutorial stream + + @for (stream of editor.tutorialStreams; track stream.id) { + + {{ stream.abbreviation }} {{ stream.name }} + + } + + + } + + @case ('CampusCondition') { + + Campus + + @for (campus of editor.campuses; track campus.id) { + + {{ campus.abbreviation }} {{ campus.name }} + + } + + + } + } + + + + +
    + } @else { +
    + +
    + } +
    diff --git a/src/app/units/states/edit/directives/unit-communications-editor/conditions/communication-conditions.component.ts b/src/app/units/states/edit/directives/unit-communications-editor/conditions/communication-conditions.component.ts new file mode 100644 index 0000000000..a4167c217a --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/conditions/communication-conditions.component.ts @@ -0,0 +1,14 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {CommunicationRule} from 'src/app/api/models/doubtfire-model'; +import type {UnitCommunicationsEditorComponent} from '../unit-communications-editor.component'; + +@Component({ + selector: 'f-communication-conditions', + standalone: false, + changeDetection: ChangeDetectionStrategy.Eager, + templateUrl: './communication-conditions.component.html', +}) +export class CommunicationConditionsComponent { + @Input({required: true}) editor: UnitCommunicationsEditorComponent; + @Input({required: true}) rule: CommunicationRule; +} diff --git a/src/app/units/states/edit/directives/unit-communications-editor/unit-communications-editor.component.html b/src/app/units/states/edit/directives/unit-communications-editor/unit-communications-editor.component.html new file mode 100644 index 0000000000..0a631f8bec --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/unit-communications-editor.component.html @@ -0,0 +1,345 @@ +
    +
    +

    Current unit week: {{ currentUnitWeek <= 0 ? 'Not started' : currentUnitWeek }}

    +
    +
    + +
    + + @if (loading) { + + } + + + + + + + +
    +
    + + +
    +
    +
    + + +
    +
    + + + +
    + + @if (treeControl.isExpanded(node)) { +
    + + + +
    + } +
    +
    +
    +
    + + + @if (selectedSet(); as set) { +
    +
    +
    + @if (editingSetNameId === set.id) { + + Set name + + + + + } @else { +

    {{ set.name }}

    + + } +
    +
    + + + + +
    +
    + + +
    + + + @if (selectedRule(); as rule) { +
    +
    + @if (editingRuleNameId === rule.id) { + + Rule name + + + + + } @else { +
    +
    {{ rule.name }}
    + +
    + } +
    + +
    + +
    +
    + + + + + + + + + + + +
    + + Send action log to convenors after execution + + +
    + +
    +
    +
    + + +
    +
    + +
    + + @if (setPreviewLoading || previewLoading[rule.id]) { + + } @else if (previewLoaded[rule.id]) { +
    + Selected for {{ rule.name }} ({{ studentsFor(rule).length }}) +
    + + @if (studentsFor(rule).length === 0) { +
    No students currently match this rule.
    + } @else { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Preferred Name + {{ student.preferred_name || '-' }} + First Name + {{ student.first_name || '-' }} + Last Name + {{ student.last_name || '-' }} + Full Name + {{ student.full_name || '-' }} + Username + {{ student.username || '-' }} + Student ID + {{ student.student_id || '-' }} + Campus + {{ student.campus || '-' }} + Target Grade + {{ + student.target_grade ? targetGradeName(student.target_grade) : '-' + }} + Spec Con Days + {{ student.spec_con_days ?? '-' }} + Last Sign In + {{ + student.last_sign_in_at ? dateLabel(student.last_sign_in_at) : '-' + }} +
    + } + } @else { +
    + Preview data will load when you select a communication set. +
    + } +
    +
    +
    + } @else { +
    + Select a communication rule to edit its conditions, actions, and preview. +
    + } +
    +
    + } @else { +
    + Create or select a communication set to begin. +
    + } +
    +
    +
    diff --git a/src/app/units/states/edit/directives/unit-communications-editor/unit-communications-editor.component.scss b/src/app/units/states/edit/directives/unit-communications-editor/unit-communications-editor.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/units/states/edit/directives/unit-communications-editor/unit-communications-editor.component.ts b/src/app/units/states/edit/directives/unit-communications-editor/unit-communications-editor.component.ts new file mode 100644 index 0000000000..daa444376a --- /dev/null +++ b/src/app/units/states/edit/directives/unit-communications-editor/unit-communications-editor.component.ts @@ -0,0 +1,1394 @@ +import {NestedTreeControl} from '@angular/cdk/tree'; +import { + ChangeDetectionStrategy, + Component, + Input, + OnChanges, + OnDestroy, + OnInit, + SimpleChanges, +} from '@angular/core'; +import {MatDialog} from '@angular/material/dialog'; +import {MatTreeNestedDataSource} from '@angular/material/tree'; +import {Subscription} from 'rxjs'; +import { + Campus, + CampusService, + CommunicationAction, + CommunicationActionService, + CommunicationCondition, + CommunicationConditionService, + CommunicationRule, + CommunicationRulePreviewAllocation, + CommunicationRulePreviewResponse, + CommunicationRulePreviewStudent, + CommunicationRuleService, + CommunicationSet, + CommunicationSetPreviewResponse, + CommunicationSetSchedule, + CommunicationSetService, + ProjectService, + TaskDefinition, + Tutorial, + TutorialStream, + Unit, +} from 'src/app/api/models/doubtfire-model'; +import {SidekiqJob} from 'src/app/api/models/sidekiq-job'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; +import {SidekiqProgressModalService} from 'src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import { + CommunicationScheduleModalComponent, + CommunicationScheduleModalData, +} from './communication-schedule-modal/communication-schedule-modal.component'; + +interface CommunicationTreeNode { + type: 'set' | 'rule'; + id: number; + label: string; + set?: CommunicationSet; + rule?: CommunicationRule; + children?: CommunicationTreeNode[]; +} + +@Component({ + selector: 'f-unit-communications-editor', + standalone: false, + templateUrl: './unit-communications-editor.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + styleUrl: './unit-communications-editor.component.scss', +}) +export class UnitCommunicationsEditorComponent implements OnInit, OnChanges, OnDestroy { + @Input() unit: Unit; + + readonly editorContext = this; + sets: CommunicationSet[] = []; + selectedSetId?: number; + selectedRuleId?: number; + rules: CommunicationRule[] = []; + campuses: Campus[] = []; + taskDefinitions: readonly TaskDefinition[] = []; + tutorials: readonly Tutorial[] = []; + tutorialStreams: readonly TutorialStream[] = []; + loading = false; + setPreviewLoading = false; + readonly previewStudentColumns = [ + 'preferred_name', + 'first_name', + 'last_name', + 'full_name', + 'username', + 'student_id', + 'campus', + 'target_grade', + 'spec_con_days', + 'last_sign_in_at', + ]; + + readonly logicalOperators = ['and', 'or']; + readonly logicalOperatorOptions = [ + {value: 'and', label: 'All the following conditions'}, + {value: 'or', label: 'Any of the following conditions'}, + ] as const; + readonly conditionTypes = [ + 'TargetGradeCondition', + 'TaskDefinitionStatusCondition', + 'TaskStatusCountCondition', + 'LoginStatusCondition', + 'SpecConCondition', + 'TutorialEnrolmentCondition', + 'TutorialStreamEnrolmentCondition', + 'CampusCondition', + ]; + readonly conditionTypeLabels: Record = { + TargetGradeCondition: 'Target Grade', + TaskDefinitionStatusCondition: 'Task Status', + TaskStatusCountCondition: 'Task Status Count', + LoginStatusCondition: 'Login Status', + SpecConCondition: 'Special Consideration Days', + TutorialEnrolmentCondition: 'Tutorial Enrolment', + TutorialStreamEnrolmentCondition: 'Tutorial Stream Enrolment', + CampusCondition: 'Campus', + }; + readonly actionTypes = [ + 'EmailStudentAction', + 'EmailStaffAction', + 'ChangeTargetGradeAction', + 'TaskCommentAction', + ]; + readonly actionTypeLabels: Record = { + EmailStudentAction: 'Send email to student', + EmailStaffAction: 'Send email to staff', + ChangeTargetGradeAction: 'Change Target Grade', + TaskCommentAction: 'Task Comment', + }; + readonly gradeOperators = [ + 'greater_than', + 'greater_than_or_equal_to', + 'less_than', + 'less_than_or_equal_to', + 'equal_to', + 'not_equal_to', + ]; + readonly equalityOperators = ['equal_to', 'not_equal_to']; + readonly dateOperators = ['before', 'after']; + readonly enrolmentOperators = ['enrolled_in', 'not_enrolled_in']; + readonly operatorLabels: Record = { + greater_than: 'Greater Than', + greater_than_or_equal_to: 'Greater Than Or Equal To', + less_than: 'Less Than', + less_than_or_equal_to: 'Less Than Or Equal To', + equal_to: 'Equal To', + not_equal_to: 'Not Equal To', + before: 'Before', + after: 'After', + enrolled_in: 'Enrolled In', + not_enrolled_in: 'Not Enrolled In', + }; + get targetGrades() { + return this.unit.gradeDefinitions + .filter((definition) => definition.value >= 0) + .map((definition) => ({value: definition.value, label: definition.abbreviation})); + } + readonly emailVariables = [ + {token: '{{student.first_name}}', label: 'Student First Name'}, + {token: '{{student.last_name}}', label: 'Student Last Name'}, + {token: '{{student.preferred_name}}', label: 'Student Preferred Name'}, + {token: '{{student.full_name}}', label: 'Student Full Name'}, + {token: '{{student.username}}', label: 'Student Username'}, + {token: '{{student.student_id}}', label: 'Student ID'}, + {token: '{{affected_students_count}}', label: 'Affected Students Count'}, + {token: '{{unit.code}}', label: 'Unit Code'}, + {token: '{{unit.name}}', label: 'Unit Name'}, + {token: '{{rule.name}}', label: 'Rule Name'}, + {token: '{{target_grade}}', label: 'Target Grade'}, + // {token: '{{conditions_summary}}', label: 'Conditions Summary'}, + // {token: '{{actions_summary}}', label: 'Actions Summary'}, + ]; + readonly taskStatuses = [ + 'not_started', + 'complete', + 'need_help', + 'working_on_it', + 'fix_and_resubmit', + 'feedback_exceeded', + 'redo', + 'discuss', + 'ready_for_feedback', + 'demonstrate', + 'fail', + 'time_exceeded', + 'assess_in_portfolio', + 'attention_required', + 'rediscuss', + ]; + + newConditions: Record> = {}; + conditionFormOpen: Record = {}; + editingConditionId: Record = {}; + newActions: Record> = {}; + actionFormOpen: Record = {}; + editingActionId: Record = {}; + previewTabIndex: Record = {}; + previewLoading: Record = {}; + previewLoaded: Record = {}; + previewStudents: Record = {}; + previewAllocations: Record = {}; + editingSetNameId?: number; + editingRuleNameId?: number; + setNameDraft = ''; + ruleNameDraft = ''; + readonly treeControl: NestedTreeControl = new NestedTreeControl( + (node) => node.children, + ); + readonly treeDataSource: MatTreeNestedDataSource = + new MatTreeNestedDataSource(); + private expandedSetIds: Set = new Set(); + + private subscriptions: Subscription[] = []; + + get currentUnitWeek(): number | null { + return this.unit?.currentUnitWeek ?? null; + } + + constructor( + private ruleService: CommunicationRuleService, + private conditionService: CommunicationConditionService, + private actionService: CommunicationActionService, + private setService: CommunicationSetService, + private projectService: ProjectService, + private dialog: MatDialog, + private campusService: CampusService, + private alerts: AlertService, + private sidekiqProgressModalService: SidekiqProgressModalService, + private confirmationModalService: ConfirmationModalService, + ) {} + + ngOnInit(): void { + this.campusService.query().subscribe((campuses) => { + this.campuses = campuses; + }); + this.refreshUnitLookups(); + this.loadSets(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.unit && this.unit) { + this.refreshUnitLookups(); + this.loadSets(); + } + } + + addSet(): void { + if (!this.unit) { + return; + } + + const newSet = { + name: this.defaultSetName(), + active: true, + } as Pick & Partial>; + + this.setService.createForUnit(this.unit.id, newSet).subscribe({ + next: (set) => { + this.sets.push(set); + this.expandedSetIds.add(set.id); + this.selectedSetId = set.id; + this.selectSet(); + }, + error: (error) => this.showError(error), + }); + } + + deleteSet(set: CommunicationSet): void { + this.setService.deleteForUnit(this.unit.id, set.id).subscribe({ + next: () => { + this.sets = this.sets.filter((item) => item.id !== set.id); + this.expandedSetIds.delete(set.id); + if (this.selectedSetId === set.id) { + this.selectedSetId = undefined; + } + this.selectSet(); + }, + error: (error) => this.showError(error), + }); + } + + beginEditSetName(set: CommunicationSet): void { + this.editingSetNameId = set.id; + this.setNameDraft = set.name; + } + + cancelEditSetName(): void { + this.editingSetNameId = undefined; + this.setNameDraft = ''; + } + + saveSetName(set: CommunicationSet): void { + const name = this.setNameDraft.trim(); + if (!name) { + return; + } + + this.setService.updateForUnit(this.unit.id, set.id, {name}).subscribe({ + next: (updated) => { + set.name = updated.name; + const matchingSet = this.sets.find((item) => item.id === set.id); + if (matchingSet) { + matchingSet.name = updated.name; + } + this.cancelEditSetName(); + this.rebuildTree(); + }, + error: (error) => this.showError(error), + }); + } + + confirmExecuteSet(set: CommunicationSet): void { + this.confirmationModalService.show( + 'Execute Set?', + 'This will execute every rule in this set, in sequence. Once a student is matched by an earlier rule, they are removed from consideration for the remaining rules, so each student can only be picked up once during the set run.', + () => this.executeSet(set), + undefined, + 'Execute Set', + ); + } + + executeSet(set: CommunicationSet): void { + this.setService.executeForUnit(this.unit.id, set.id).subscribe({ + next: (job) => this.showExecutionProgress(job, `Executing ${set.name}`), + error: (error) => this.showError(error), + }); + } + + addSchedule(set: CommunicationSet): void { + this.openScheduleModal(set); + } + + editSchedule(set: CommunicationSet, schedule: CommunicationSetSchedule): void { + this.openScheduleModal(set, schedule); + } + + deleteSchedule(set: CommunicationSet, schedule: CommunicationSetSchedule): void { + const updatedSchedules = (set.schedules || []).filter( + (item) => (item.id || item.client_key) !== (schedule.id || schedule.client_key), + ); + this.persistSchedules(set, updatedSchedules, 'Schedule removed'); + } + + scheduleTrackId(schedule: CommunicationSetSchedule): string | number { + return ( + schedule.id || + schedule.client_key || + `${schedule.name || 'schedule'}-${schedule.anchor_week}-${schedule.anchor_day}` + ); + } + + scheduleSummary(schedule: CommunicationSetSchedule): string { + const cadence = this.scheduleCadence(schedule); + const ending = this.scheduleEnding(schedule); + return [cadence, ending].filter(Boolean).join(' | '); + } + + scheduleAnchorSummary(schedule: CommunicationSetSchedule): string { + return `Week ${schedule.anchor_week || 1} on ${schedule.anchor_day || 'Monday'}`; + } + + scheduleTimeSummary(schedule: CommunicationSetSchedule): string { + return `${this.formatTime(schedule.hour, schedule.minute)} ${schedule.timezone || 'UTC'}`; + } + + scheduleNextRunSummary(schedule: CommunicationSetSchedule): string { + return schedule.next_run_at ? this.dateLabel(schedule.next_run_at) : 'Not scheduled'; + } + + scheduleLastRunSummary(schedule: CommunicationSetSchedule): string { + return schedule.last_run_at ? this.dateLabel(schedule.last_run_at) : 'Not yet run'; + } + + iceCubePreview(schedule: CommunicationSetSchedule): string { + return JSON.stringify(schedule.ice_cube_schedule || {}, null, 2); + } + + selectSet(): void { + const set = this.selectedSet(); + if (set) { + this.activateSet(set); + } else { + this.rules = []; + this.selectedRuleId = undefined; + this.rebuildTree(); + } + } + + ngOnDestroy(): void { + this.subscriptions.forEach((subscription) => subscription.unsubscribe()); + } + + addRule(): void { + if (!this.unit) { + return; + } + const set = this.selectedSet(); + if (!set) { + return; + } + + const newRule = { + name: this.defaultRuleName(), + operator: 'and', + } as Pick; + + this.ruleService.createForSet(this.unit.id, set.id, newRule).subscribe({ + next: (rule) => { + this.rules.push(rule); + set.rules = this.rules; + this.selectedRuleId = rule.id; + this.expandedSetIds.add(set.id); + this.loadPreviewForSet(set); + }, + error: (error) => this.showError(error), + }); + } + + deleteRule(rule: CommunicationRule): void { + this.ruleService.deleteForUnit(this.unit.id, rule.id).subscribe({ + next: () => { + this.rules = this.rules.filter((item) => item.id !== rule.id); + const set = this.selectedSet(); + if (set) { + set.rules = this.rules; + this.selectedRuleId = this.rules[0]?.id; + this.loadPreviewForSet(set); + } + }, + error: (error) => this.showError(error), + }); + } + + updateRuleOperator(rule: CommunicationRule): void { + this.ruleService.updateForUnit(this.unit.id, rule.id, {operator: rule.operator}).subscribe({ + next: (updated) => { + rule.operator = updated.operator; + this.refreshPreview(rule); + }, + error: (error) => this.showError(error), + }); + } + + updateRule(rule: CommunicationRule): void { + this.ruleService + .updateForUnit(this.unit.id, rule.id, { + name: rule.name, + operator: rule.operator, + send_log_to_convenors: rule.send_log_to_convenors, + }) + .subscribe({ + next: (updated) => { + rule.name = updated.name; + rule.operator = updated.operator; + rule.send_log_to_convenors = updated.send_log_to_convenors; + const set = this.selectedSet(); + if (set) { + set.rules = this.rules; + } + this.refreshPreview(rule); + }, + error: (error) => this.showError(error), + }); + } + + beginEditRuleName(rule: CommunicationRule): void { + this.editingRuleNameId = rule.id; + this.ruleNameDraft = rule.name; + } + + cancelEditRuleName(): void { + this.editingRuleNameId = undefined; + this.ruleNameDraft = ''; + } + + saveRuleName(rule: CommunicationRule): void { + const name = this.ruleNameDraft.trim(); + if (!name) { + return; + } + + this.ruleService + .updateForUnit(this.unit.id, rule.id, { + name, + operator: rule.operator, + send_log_to_convenors: rule.send_log_to_convenors, + }) + .subscribe({ + next: (updated) => { + rule.name = updated.name; + const set = this.selectedSet(); + if (set) { + set.rules = this.rules; + } + this.cancelEditRuleName(); + this.rebuildTree(); + }, + error: (error) => this.showError(error), + }); + } + + confirmExecuteRule(rule: CommunicationRule): void { + this.confirmationModalService.show( + 'Execute Rule?', + 'This will execute only this rule. However, any earlier rules in the set are still taken into account first, so students who would already have been matched earlier are excluded before this rule is applied.', + () => this.executeRule(rule), + undefined, + 'Execute Rule', + ); + } + + executeRule(rule: CommunicationRule): void { + this.ruleService.executeForUnit(this.unit.id, rule.id).subscribe({ + next: (job) => this.showExecutionProgress(job, `Executing ${rule.name}`), + error: (error) => this.showError(error), + }); + } + + previewRule(rule: CommunicationRule, activateStudentsTab = true): void { + if (activateStudentsTab) { + this.previewTabIndex[rule.id] = 2; + } + } + + addCondition(rule: CommunicationRule): void { + const condition = this.newConditions[rule.id] || this.blankCondition(); + this.conditionService.create(this.unit.id, rule.id, condition).subscribe({ + next: (created) => { + rule.conditions ||= []; + rule.conditions.push(created); + this.newConditions[rule.id] = this.blankCondition(); + this.conditionFormOpen[rule.id] = false; + this.editingConditionId[rule.id] = undefined; + this.refreshPreview(rule); + }, + error: (error) => this.showError(error), + }); + } + + updateCondition(rule: CommunicationRule): void { + const conditionId = this.editingConditionId[rule.id]; + if (!conditionId) { + return; + } + + const condition = this.newConditions[rule.id] || this.blankCondition(); + this.conditionService.update(this.unit.id, rule.id, conditionId, condition).subscribe({ + next: (updated) => { + rule.conditions = rule.conditions.map((item) => (item.id === updated.id ? updated : item)); + this.newConditions[rule.id] = this.blankCondition(); + this.conditionFormOpen[rule.id] = false; + this.editingConditionId[rule.id] = undefined; + this.refreshPreview(rule); + }, + error: (error) => this.showError(error), + }); + } + + showConditionForm(rule: CommunicationRule): void { + this.newConditions[rule.id] = this.blankCondition(); + this.conditionFormOpen[rule.id] = true; + this.editingConditionId[rule.id] = undefined; + } + + cancelCondition(rule: CommunicationRule): void { + this.newConditions[rule.id] = this.blankCondition(); + this.conditionFormOpen[rule.id] = false; + this.editingConditionId[rule.id] = undefined; + } + + editCondition(rule: CommunicationRule, condition: CommunicationCondition): void { + this.newConditions[rule.id] = { + ...condition, + task_statuses: condition.task_statuses ? [...condition.task_statuses] : [], + }; + this.conditionFormOpen[rule.id] = true; + this.editingConditionId[rule.id] = condition.id; + } + + deleteCondition(rule: CommunicationRule, condition: CommunicationCondition): void { + this.conditionService.delete(this.unit.id, rule.id, condition.id).subscribe({ + next: () => { + rule.conditions = rule.conditions.filter((item) => item.id !== condition.id); + this.refreshPreview(rule); + }, + error: (error) => this.showError(error), + }); + } + + addAction(rule: CommunicationRule): void { + const action = this.newActions[rule.id] || this.blankAction(); + this.actionService.create(this.unit.id, rule.id, action).subscribe({ + next: (created) => { + rule.actions ||= []; + rule.actions.push(created); + this.newActions[rule.id] = this.blankAction(); + this.actionFormOpen[rule.id] = false; + this.editingActionId[rule.id] = undefined; + }, + error: (error) => this.showError(error), + }); + } + + updateAction(rule: CommunicationRule): void { + const actionId = this.editingActionId[rule.id]; + if (!actionId) { + return; + } + + const action = this.newActions[rule.id] || this.blankAction(); + this.actionService.update(this.unit.id, rule.id, actionId, action).subscribe({ + next: (updated) => { + rule.actions = rule.actions.map((item) => (item.id === updated.id ? updated : item)); + this.newActions[rule.id] = this.blankAction(); + this.actionFormOpen[rule.id] = false; + this.editingActionId[rule.id] = undefined; + }, + error: (error) => this.showError(error), + }); + } + + showActionForm(rule: CommunicationRule, _mode: 'standard' | 'post_execution' = 'standard'): void { + this.newActions[rule.id] = this.blankAction(); + this.actionFormOpen[rule.id] = true; + this.editingActionId[rule.id] = undefined; + } + + cancelAction(rule: CommunicationRule): void { + this.newActions[rule.id] = this.blankAction(); + this.actionFormOpen[rule.id] = false; + this.editingActionId[rule.id] = undefined; + } + + editAction(rule: CommunicationRule, action: CommunicationAction): void { + this.newActions[rule.id] = {...action}; + this.actionFormOpen[rule.id] = true; + this.editingActionId[rule.id] = action.id; + } + + deleteAction(rule: CommunicationRule, action: CommunicationAction): void { + this.actionService.delete(this.unit.id, rule.id, action.id).subscribe({ + next: () => { + rule.actions = rule.actions.filter((item) => item.id !== action.id); + }, + error: (error) => this.showError(error), + }); + } + + conditionFor(rule: CommunicationRule): Partial { + this.newConditions[rule.id] ||= this.blankCondition(); + return this.newConditions[rule.id]; + } + + actionFor(rule: CommunicationRule): Partial { + this.newActions[rule.id] ||= this.blankAction(); + return this.newActions[rule.id]; + } + + selectRule(rule: CommunicationRule): void { + this.selectedRuleId = rule.id; + } + + hasTreeChild = (_: number, node: CommunicationTreeNode): boolean => node.type === 'set'; + + isSelectedSetNode(node: CommunicationTreeNode): boolean { + return node.type === 'set' && node.id === this.selectedSetId; + } + + isSelectedRuleNode(node: CommunicationTreeNode): boolean { + return node.type === 'rule' && node.id === this.selectedRuleId; + } + + toggleSetNode(node: CommunicationTreeNode, event?: Event): void { + event?.stopPropagation(); + if (!node.set) { + return; + } + + if (this.treeControl.isExpanded(node)) { + this.treeControl.collapse(node); + this.expandedSetIds.delete(node.set.id); + } else { + this.treeControl.expand(node); + this.expandedSetIds.add(node.set.id); + } + } + + selectSetNode(set: CommunicationSet): void { + this.selectedSetId = set.id; + this.expandedSetIds.add(set.id); + this.activateSet(set); + } + + selectRuleNode(set: CommunicationSet, rule: CommunicationRule): void { + const setChanged = this.selectedSetId !== set.id; + this.selectedSetId = set.id; + this.expandedSetIds.add(set.id); + + if (setChanged) { + this.activateSet(set, rule.id); + return; + } + + this.selectedRuleId = rule.id; + } + + selectedRule(): CommunicationRule | undefined { + return this.rules.find((rule) => rule.id === this.selectedRuleId); + } + + onRuleTabChange(rule: CommunicationRule, index: number): void { + this.previewTabIndex[rule.id] = index; + } + + studentsFor(rule: CommunicationRule): CommunicationRulePreviewStudent[] { + return this.previewStudents[rule.id] || []; + } + + previewAllocationsFor(rule: CommunicationRule): CommunicationRulePreviewAllocation[] { + return this.previewAllocations[rule.id] || []; + } + + isTargetPreviewAllocation( + rule: CommunicationRule, + allocation: CommunicationRulePreviewAllocation, + ): boolean { + return allocation.rule_id === rule.id; + } + + studentsTabLabel(rule: CommunicationRule): string { + const matchedCount = this.previewLoaded[rule.id] ? this.studentsFor(rule).length : 0; + const totalStudents = this.availableStudentsForRule(rule); + + return `Students (${matchedCount}/${totalStudents})`; + } + + operatorsFor(conditionType: string): string[] { + switch (conditionType) { + case 'TargetGradeCondition': + case 'TaskStatusCountCondition': + case 'SpecConCondition': + return this.gradeOperators; + case 'TaskDefinitionStatusCondition': + return this.equalityOperators; + case 'LoginStatusCondition': + return this.dateOperators; + default: + return this.enrolmentOperators; + } + } + + labelFor(record: CommunicationCondition | CommunicationAction): string { + const hiddenKeys = this.hiddenKeysForRecord(record); + + return Object.entries(record) + .filter( + ([key, value]) => + !hiddenKeys.includes(key) && value !== undefined && value !== null && value !== '', + ) + .map(([key, value]) => `${this.prettyKey(key)}: ${this.prettyValue(key, value)}`) + .join(', '); + } + + conditionTypeLabel(type: string): string { + return this.conditionTypeLabels[type] || type; + } + + operatorLabel(operator: string): string { + return this.operatorLabels[operator] || operator; + } + + actionTypeLabel(type: string): string { + return this.actionTypeLabels[type] || type; + } + + actionSummary(action: CommunicationAction): string { + switch (action.type) { + case 'ChangeTargetGradeAction': + return `Change student's target grade to ${this.targetGradeName(action.target_grade)}`; + case 'EmailStudentAction': + return 'Send email to student'; + case 'EmailStaffAction': + return `Send email to ${this.staffAudienceLabel(action)}`; + case 'TaskCommentAction': + return `Add comment to ${this.taskDefinitionLabel(action.task_definition_id)}`; + default: + return this.actionTypeLabel(action.type); + } + } + + targetGradeName(targetGrade: number | undefined): string { + if (targetGrade === undefined || targetGrade === null) { + return ''; + } + + return this.unit.gradeLabel(targetGrade) || `${targetGrade}`; + } + + taskDefinitionLabel(taskDefinitionId: number | undefined): string { + if (taskDefinitionId === undefined || taskDefinitionId === null) { + return 'Task'; + } + + const taskDefinition = this.taskDefinitions.find((task) => task.id === taskDefinitionId); + + if (!taskDefinition) { + return `Task ${taskDefinitionId}`; + } + + return `Task ${taskDefinition.abbreviation} ${taskDefinition.name}`; + } + + taskStatusLabel(taskStatus: string): string { + return this.titleize(taskStatus); + } + + taskStatusesLabel(taskStatuses: string[] = []): string { + return taskStatuses.map((status) => this.taskStatusLabel(status)).join(', '); + } + + staffAudienceLabel(action: Partial): string { + const audiences: string[] = []; + if (action.email_tutors) { + audiences.push('tutors'); + } + if (action.email_convenors) { + audiences.push('convenors'); + } + return audiences.join(' and ') || 'staff'; + } + + insertActionVariable(rule: CommunicationRule, field: 'subject' | 'body', token: string): void { + const action = this.actionFor(rule); + const currentValue = action[field] ?? ''; + const separator = + currentValue && !currentValue.endsWith(' ') && !currentValue.endsWith('\n') ? ' ' : ''; + action[field] = `${currentValue}${separator}${token}`; + } + + renderTemplatePreview(value: string | undefined, rule: CommunicationRule): string { + if (!value) { + return ''; + } + + const escaped = this.escapeHtml(value); + const rendered = escaped.replace(/\{\{[\w.]+\}\}/g, (token) => { + const replacement = this.resolveTemplateVariable(token, rule) || token; + return `${this.escapeHtml(replacement)}`; + }); + + return rendered.replace(/\n/g, '
    '); + } + + refreshPreview(_rule: CommunicationRule): void { + const set = this.selectedSet(); + if (set) { + this.loadPreviewForSet(set); + } + } + + taskStatusPredicate(operator: string): string { + return operator === 'not_equal_to' ? 'Not In' : 'In'; + } + + tutorialLabel(tutorialId: number): string { + const tutorial = this.tutorials.find((item) => item.id === tutorialId); + return tutorial ? `${tutorial.abbreviation} ${tutorial.description}` : `Tutorial ${tutorialId}`; + } + + tutorialStreamLabel(tutorialStreamId: number): string { + const tutorialStream = this.tutorialStreams.find((item) => item.id === tutorialStreamId); + return tutorialStream + ? `${tutorialStream.abbreviation} ${tutorialStream.name}` + : `Tutorial Stream ${tutorialStreamId}`; + } + + campusLabel(campusId: number): string { + const campus = this.campuses.find((item) => item.id === campusId); + return campus ? campus.name : `Campus ${campusId}`; + } + + enrolmentPredicate(operator: string): string { + return operator === 'not_enrolled_in' ? 'Not Enrolled In' : 'Enrolled In'; + } + + dateLabel(value: string): string { + return value ? new Date(value).toLocaleString() : ''; + } + + onConditionTypeChange(rule: CommunicationRule): void { + const current = this.conditionFor(rule); + this.newConditions[rule.id] = { + type: current.type, + operator: this.operatorsFor(current.type)[0], + }; + + if (current.type === 'TaskDefinitionStatusCondition') { + this.newConditions[rule.id].task_statuses = []; + } + + if (current.type === 'TaskStatusCountCondition') { + this.newConditions[rule.id].task_statuses = []; + this.newConditions[rule.id].task_status_count = 2; + this.newConditions[rule.id].task_target_grade = 1; + } + + if (current.type === 'SpecConCondition') { + this.newConditions[rule.id].spec_con_days = 0; + } + } + + onActionTypeChange(rule: CommunicationRule): void { + const current = this.actionFor(rule); + this.newActions[rule.id] = { + type: current.type || 'EmailStudentAction', + email_tutors: false, + email_convenors: false, + }; + + if (current.type === 'TaskCommentAction') { + this.newActions[rule.id].body = ''; + this.newActions[rule.id].task_definition_id = this.taskDefinitions[0]?.id; + } + } + + private loadSets(): void { + if (!this.unit) { + return; + } + + this.loading = true; + this.setService.getForUnit(this.unit.id).subscribe({ + next: (sets) => { + this.sets = sets; + if (this.selectedSetId) { + this.expandedSetIds.add(this.selectedSetId); + } + this.rebuildTree(); + this.selectSet(); + this.loading = false; + }, + error: (error) => { + this.loading = false; + this.showError(error); + }, + }); + } + + private refreshUnitLookups(): void { + this.subscriptions.forEach((subscription) => subscription.unsubscribe()); + this.subscriptions = []; + + if (!this.unit) { + return; + } + + this.taskDefinitions = this.unit.taskDefinitionCache.currentValues; + this.tutorials = this.unit.tutorials; + this.tutorialStreams = this.unit.tutorialStreams; + this.subscriptions.push( + this.unit.taskDefinitionCache.values.subscribe((taskDefinitions) => { + this.taskDefinitions = taskDefinitions; + }), + ); + this.subscriptions.push( + // TODO: use spinner until students are loaded + this.projectService.loadStudents(this.unit, false, true).subscribe({ + error: (error) => this.showError(error), + }), + ); + } + + private defaultRuleName(): string { + return `Rule ${this.rules.length + 1}`; + } + + private defaultSetName(): string { + return `Set ${this.sets.length + 1}`; + } + + private defaultScheduleName(set: CommunicationSet): string { + return `Schedule ${(set.schedules?.length || 0) + 1}`; + } + + private activateSet(set: CommunicationSet, selectedRuleId?: number): void { + this.rules = set.rules ?? []; + this.selectedRuleId = selectedRuleId ?? this.rules[0]?.id; + this.loadPreviewForSet(set); + } + + selectedSet(): CommunicationSet | undefined { + return this.sets.find((set) => set.id === this.selectedSetId); + } + + private blankCondition(): Partial { + return { + type: 'TargetGradeCondition', + operator: 'greater_than_or_equal_to', + }; + } + + private blankAction(): Partial { + return { + type: 'EmailStudentAction', + email_tutors: false, + email_convenors: false, + }; + } + + private blankSchedule(set: CommunicationSet): CommunicationSetSchedule { + return new CommunicationSetSchedule({ + client_key: this.newScheduleClientKey(), + communication_set_id: set.id, + name: this.defaultScheduleName(set), + active: true, + anchor_week: 1, + anchor_day: 'Monday', + recurrence: 'none', + interval: 1, + timezone: 'UTC', + hour: 8, + minute: 0, + ice_cube_schedule: { + timezone: 'UTC', + recurrence: 'none', + rules: [{type: 'one_off'}], + }, + }); + } + + private loadPreviewForSet(set: CommunicationSet): void { + if (!this.unit) { + this.setPreviewLoading = false; + return; + } + + this.setPreviewLoading = true; + this.setService.getForUnitById(this.unit.id, set.id).subscribe({ + next: (setResponse) => { + this.applySetPreviewResponse(setResponse); + this.setPreviewLoading = false; + }, + error: (error) => { + this.setPreviewLoading = false; + this.showError(error); + }, + }); + } + + private applySetPreviewResponse(setResponse: CommunicationSetPreviewResponse): void { + const rules = (setResponse.rules || []).map((rule) => new CommunicationRule(rule)); + const existingSet = this.sets.find((set) => set.id === setResponse.id); + const schedules = + setResponse.schedules !== undefined + ? (setResponse.schedules || []).map((schedule) => new CommunicationSetSchedule(schedule)) + : existingSet?.schedules || []; + const updatedSet = new CommunicationSet({ + id: setResponse.id, + unit_id: setResponse.unit_id, + name: setResponse.name, + active: setResponse.active, + schedules, + rules, + }); + const setIndex = this.sets.findIndex((set) => set.id === updatedSet.id); + if (setIndex >= 0) { + this.sets[setIndex] = updatedSet; + } + + if (this.selectedSetId === updatedSet.id) { + this.rules = rules; + if (!this.rules.some((rule) => rule.id === this.selectedRuleId)) { + this.selectedRuleId = this.rules[0]?.id; + } + } + + this.rules.forEach((rule) => { + this.previewLoading[rule.id] = true; + }); + + setResponse.previews.forEach((preview) => { + this.previewAllocations[preview.target_rule_id] = preview.allocations || []; + this.previewStudents[preview.target_rule_id] = this.studentsForPreviewRule( + preview.target_rule_id, + preview, + ); + this.previewLoaded[preview.target_rule_id] = true; + this.previewLoading[preview.target_rule_id] = false; + }); + + this.rules.forEach((rule) => { + this.previewLoading[rule.id] = false; + }); + + this.rebuildTree(); + } + + private studentsForPreviewRule( + ruleId: number, + preview: CommunicationRulePreviewResponse, + ): CommunicationRulePreviewStudent[] { + return preview.allocations.find((allocation) => allocation.rule_id === ruleId)?.students || []; + } + + private availableStudentsForRule(rule: CommunicationRule): number { + const totalStudents = this.unit?.students?.length ?? 0; + if (!this.previewLoaded[rule.id]) { + return totalStudents; + } + + const claimedByPreviousRules = this.previewAllocationsFor(rule) + .filter((allocation) => allocation.rule_id !== rule.id) + .reduce((sum, allocation) => sum + allocation.students.length, 0); + + return Math.max(0, totalStudents - claimedByPreviousRules); + } + + private sampleStudentForRule( + rule: CommunicationRule, + ): CommunicationRulePreviewStudent | undefined { + return this.studentsFor(rule)[0]; + } + + private openScheduleModal(set: CommunicationSet, schedule?: CommunicationSetSchedule): void { + const dialogRef = this.dialog.open(CommunicationScheduleModalComponent, { + width: '960px', + maxWidth: '96vw', + data: { + schedule: schedule + ? new CommunicationSetSchedule({ + ...schedule, + }) + : this.blankSchedule(set), + unit: this.unit, + } satisfies CommunicationScheduleModalData, + }); + + dialogRef.afterClosed().subscribe((result) => { + if (!result) { + return; + } + + const hydrated = new CommunicationSetSchedule({ + ...result, + client_key: schedule?.client_key || result.client_key || this.newScheduleClientKey(), + communication_set_id: set.id, + }); + + const schedules = [...(set.schedules || [])]; + const existingIndex = schedules.findIndex( + (item) => (item.id || item.client_key) === (schedule?.id || schedule?.client_key), + ); + + if (existingIndex >= 0) { + schedules[existingIndex] = hydrated; + } else { + schedules.push(hydrated); + } + + this.persistSchedules(set, schedules, 'Schedule saved'); + }); + } + + private persistSchedules( + set: CommunicationSet, + schedules: CommunicationSetSchedule[], + successMessage: string, + ): void { + this.setService + .updateForUnit(this.unit.id, set.id, { + name: set.name, + active: set.active, + schedules: schedules.map((schedule) => ({ + id: schedule.id, + name: schedule.name, + active: schedule.active, + anchor_week: schedule.anchor_week, + anchor_day: schedule.anchor_day, + hour: schedule.hour, + minute: schedule.minute, + timezone: schedule.timezone, + recurrence: schedule.recurrence, + interval: schedule.interval, + repeat_count: schedule.repeat_count, + until_at: schedule.until_at, + })), + }) + .subscribe({ + next: (updatedSet) => { + set.schedules = updatedSet.schedules || []; + const setIndex = this.sets.findIndex((item) => item.id === set.id); + if (setIndex >= 0) { + this.sets[setIndex].schedules = updatedSet.schedules || []; + } + this.alerts.success(successMessage); + }, + error: (error) => this.showError(error), + }); + } + + private showError(error): void { + this.alerts.error(error?.message || error?.error || error || 'Communication update failed'); + } + + private showExecutionProgress(job: SidekiqJob, title: string): void { + if (!job?.id) { + this.alerts.error('Failed to start communication execution', 6000); + return; + } + + this.sidekiqProgressModalService.show(title, job.id).subscribe({ + error: (error) => this.showError(error), + }); + } + + private prettyKey(key: string): string { + const labels: Record = { + operator: 'Operator', + target_grade: 'Target Grade', + task_definition_id: 'Task', + task_statuses: 'Task Statuses', + task_status_count: 'Task Status Count', + task_target_grade: 'Task Target Grade', + last_sign_in_at: 'Last Sign In', + spec_con_days: 'Special Consideration Days', + tutorial_id: 'Tutorial', + tutorial_stream_id: 'Tutorial Stream', + campus_id: 'Campus', + subject: 'Subject', + body: 'Body', + email_tutors: 'Email Tutors', + email_convenors: 'Email Convenors', + }; + + return labels[key] || key; + } + + private prettyValue(key: string, value: unknown): string { + if (key === 'operator' && typeof value === 'string') { + return this.operatorLabel(value); + } + + if ((key === 'target_grade' || key === 'task_target_grade') && typeof value === 'number') { + return this.unit.gradeAbbreviation(value) || value.toString(); + } + + if (key === 'task_statuses' && Array.isArray(value)) { + return this.taskStatusesLabel(value); + } + + if (typeof value === 'boolean') { + return value ? 'Yes' : 'No'; + } + + return `${value}`; + } + + private escapeHtml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + private resolveTemplateVariable(token: string, rule: CommunicationRule): string | undefined { + const student = this.sampleStudentForRule(rule); + + switch (token) { + case '{{student.first_name}}': + return student?.first_name; + case '{{student.last_name}}': + return student?.last_name; + case '{{student.preferred_name}}': + return student?.preferred_name || student?.first_name; + case '{{student.full_name}}': + return ( + student?.full_name || [student?.first_name, student?.last_name].filter(Boolean).join(' ') + ); + case '{{student.username}}': + return student?.username; + case '{{student.student_id}}': + return student?.student_id; + case '{{affected_students_count}}': + return this.studentsFor(rule).length.toString(); + case '{{unit.code}}': + return this.unit?.code; + case '{{unit.name}}': + return this.unit?.name; + case '{{rule.name}}': + return rule.name; + case '{{target_grade}}': + return student?.target_grade !== undefined && student?.target_grade !== null + ? this.targetGradeName(student.target_grade) + : undefined; + case '{{conditions_summary}}': + return this.conditionsSummary(rule); + case '{{actions_summary}}': + return this.actionsSummary(rule); + default: + return undefined; + } + } + + private hiddenKeysForRecord(record: CommunicationCondition | CommunicationAction): string[] { + const baseHiddenKeys = ['id', 'type', 'communication_rule_id', 'operator']; + + if (!('type' in record)) { + return baseHiddenKeys; + } + + switch (record.type) { + case 'ChangeTargetGradeAction': + return [...baseHiddenKeys, 'subject', 'body', 'email_tutors', 'email_convenors']; + case 'EmailStudentAction': + return [...baseHiddenKeys, 'target_grade', 'email_tutors', 'email_convenors']; + case 'EmailStaffAction': + return [...baseHiddenKeys, 'target_grade']; + default: + return baseHiddenKeys; + } + } + + conditionsSummary(rule: CommunicationRule): string { + return (rule.conditions || []) + .map((condition) => + `- ${this.conditionTypeLabel(condition.type)}: ${this.labelFor(condition)}`.trim(), + ) + .join('\n'); + } + + actionsSummary(rule: CommunicationRule): string { + return (rule.actions || []).map((action) => `- ${this.actionSummary(action)}`).join('\n'); + } + + private titleize(value: string): string { + return value + ?.split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + } + + private scheduleCadence(schedule: CommunicationSetSchedule): string { + switch (schedule.recurrence) { + case 'daily': + return `Daily every ${schedule.interval || 1} day(s)`; + case 'weekly': + return `Weekly every ${schedule.interval || 1} week(s) from ${schedule.anchor_day || 'Monday'}`; + case 'monthly': + return `Monthly every ${schedule.interval || 1} month(s) from Week ${schedule.anchor_week || 1} ${schedule.anchor_day || 'Monday'}`; + default: + return 'One-off run'; + } + } + + private scheduleEnding(schedule: CommunicationSetSchedule): string { + if (schedule.repeat_count) { + return `Stops after ${schedule.repeat_count} run(s)`; + } + + if (schedule.until_at) { + return `Stops at ${this.dateLabel(schedule.until_at)}`; + } + + return 'No expiry'; + } + + private formatTime(hour = 0, minute = 0): string { + return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; + } + + private newScheduleClientKey(): string { + return `schedule-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; + } + + private rebuildTree(): void { + const treeData = this.sets.map((set) => ({ + type: 'set' as const, + id: set.id, + label: set.name, + set, + children: (set.rules ?? []).map((rule) => ({ + type: 'rule' as const, + id: rule.id, + label: rule.name, + set, + rule, + })), + })); + + this.treeDataSource.data = treeData; + treeData.forEach((node) => { + if (this.expandedSetIds.has(node.id) || node.id === this.selectedSetId) { + this.treeControl.expand(node); + } + }); + } +} diff --git a/src/app/units/states/edit/directives/unit-details-editor/d2l-details-form/d2l-unit-details-form.component.html b/src/app/units/states/edit/directives/unit-details-editor/d2l-details-form/d2l-unit-details-form.component.html index 10618f6bc8..8c20abb5ef 100644 --- a/src/app/units/states/edit/directives/unit-details-editor/d2l-details-form/d2l-unit-details-form.component.html +++ b/src/app/units/states/edit/directives/unit-details-editor/d2l-details-form/d2l-unit-details-form.component.html @@ -7,31 +7,33 @@

    D2L Unit Details

    Created during grades transfer if not set
    - - + } + diff --git a/src/app/units/states/edit/directives/unit-details-editor/d2l-details-form/d2l-unit-details-form.component.ts b/src/app/units/states/edit/directives/unit-details-editor/d2l-details-form/d2l-unit-details-form.component.ts index f7524cce33..cd19b0b701 100644 --- a/src/app/units/states/edit/directives/unit-details-editor/d2l-details-form/d2l-unit-details-form.component.ts +++ b/src/app/units/states/edit/directives/unit-details-editor/d2l-details-form/d2l-unit-details-form.component.ts @@ -1,9 +1,8 @@ // // Modal to show Doubtfire version info // -import {Injectable, Component, Inject, AfterViewInit, OnInit} from '@angular/core'; - -import {MatDialog, MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {ChangeDetectionStrategy, Component, Inject, Injectable, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog'; import {Observable} from 'rxjs'; import {D2lAssessmentMapping} from 'src/app/api/models/d2l/d2l_assessment_mapping'; import {D2lAssessmentMappingService} from 'src/app/api/models/doubtfire-model'; @@ -14,6 +13,8 @@ import {AlertService} from 'src/app/common/services/alert.service'; selector: 'f-d2l-unit-details-form', templateUrl: 'd2l-unit-details-form.component.html', styleUrl: 'd2l-unit-details-form.component.scss', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class D2lUnitDetailsFormComponent implements OnInit { public d2lDataMapping: D2lAssessmentMapping = new D2lAssessmentMapping(this.data); diff --git a/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.html b/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.html index 60c4e1e260..14971021b1 100644 --- a/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.html +++ b/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.html @@ -1,9 +1,9 @@ -
    +

    Unit Details

    Edit and configure all details and settings for this unit.

    - + Code @@ -20,10 +20,10 @@

    Unit Details

    - + Teaching Period - + None @for (period of teachingPeriods; track period) { {{ period.name }} @@ -31,7 +31,7 @@

    Unit Details

    -
    +
    @if (!unit.teachingPeriod) { Start Date @@ -48,11 +48,21 @@

    Unit Details

    } @else { {{ unit.teachingPeriod.name }} Start Date - + {{ unit.teachingPeriod.name }} End Date - + }
    @@ -70,7 +80,7 @@

    Unit Details

    Draft Learning Summary - + None @for (td of taskDefinitions; track td) { {{ td.abbreviation }} - {{ td.name }} @@ -91,14 +101,14 @@

    Unit Details

    -
    +
    Feedback warning after (days) @@ -111,9 +121,9 @@

    Unit Details

    Show task in overflow queue after (days) @@ -123,7 +133,158 @@

    Unit Details

    - + +
    +

    Grade definitions

    +

    + Index -1 is used when work fails assessment. Target grades start at index 0, and higher + indexes include tasks from every lower level. +

    +

    + Grades already used by tasks, students, or communications can be renamed, but cannot be + removed or moved to another index. +

    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Index{{ grade.value }}Label + @if (editingGradeId === grade.id) { + + Label + + + } @else { + {{ grade.label }} + } + Abbreviation + @if (editingGradeId === grade.id) { + + Abbreviation + + + } @else { + {{ grade.abbreviation }} + } + Order +
    + @if (index > 0) { + + + } +
    +
    Actions +
    + @if (editingGradeId === grade.id) { + + } @else { + + } + + @if (index > 0) { + + } +
    +
    +
    + +
    + +
    +
    + +
    Allow flexible datesUnit Details
    Has tasks assessed in portfolio

    @@ -166,7 +327,10 @@

    Unit Details

    Allow students to change tutorial -

    When false only staff can change student tutorials.

    +

    + When false only staff can change student tutorials. When true, students may switch between + tutors who provide feedback on their work. +

    @@ -214,7 +378,7 @@

    Unit Details

    @if (overseerEnabled.value) { - +
    Overseer assessmentUnit Details } -
    - - +
    diff --git a/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.ts b/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.ts index 7ac7161a73..4cef34c732 100644 --- a/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.ts +++ b/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.ts @@ -1,9 +1,9 @@ -import {Component, Input, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; import {MatSlideToggleChange} from '@angular/material/slide-toggle'; import {OverseerImage, UnitService} from 'src/app/api/models/doubtfire-model'; import {TaskDefinition} from 'src/app/api/models/task-definition'; import {TeachingPeriod} from 'src/app/api/models/teaching-period'; -import {Unit} from 'src/app/api/models/unit'; +import {GradeDefinition, Unit} from 'src/app/api/models/unit'; import {TaskDefinitionService} from 'src/app/api/services/task-definition.service'; import {TeachingPeriodService} from 'src/app/api/services/teaching-period.service'; import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; @@ -16,6 +16,8 @@ import {D2lUnitDetailsModal} from './d2l-details-form/d2l-unit-details-form.comp selector: 'f-unit-details-editor', templateUrl: 'unit-details-editor.component.html', styleUrls: ['unit-details-editor.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class UnitDetailsEditorComponent implements OnInit { @Input() unit: Unit; @@ -34,6 +36,162 @@ export class UnitDetailsEditorComponent implements OnInit { public teachingPeriods: TeachingPeriod[]; public taskDefinitions: TaskDefinition[]; public dockerImages: OverseerImage[]; + public editingGradeId: string | null = null; + public readonly gradeDefinitionColumns = ['index', 'label', 'abbreviation', 'order', 'actions']; + private editingGradeDefinitions: GradeDefinition[] | null = null; + private newGradeId: string | null = null; + + public get gradeDefinitions(): GradeDefinition[] { + return this.unit.gradeDefinitions; + } + + public addGrade(): void { + if (this.newGradeId) { + return; + } + + const previousDefinitions = this.cloneGradeDefinitions(); + const newGradeId = `grade-${Date.now()}`; + this.unit.gradeDefinitions = [ + ...this.unit.gradeDefinitions, + { + id: newGradeId, + value: this.unit.gradeDefinitions.length - 1, + label: 'New grade', + abbreviation: 'NEW', + }, + ]; + this.reindexGrades(); + this.editingGradeDefinitions = previousDefinitions; + this.editingGradeId = newGradeId; + this.newGradeId = newGradeId; + } + + public removeGrade(index: number): void { + const grade = this.unit.gradeDefinitions[index]; + if (!grade) { + return; + } + + this.confirmationModal.show( + `Delete Grade ${grade.label}`, + 'Are you sure you want to delete this grade? This will update the available grades for this unit.', + () => { + if (grade.id === this.newGradeId) { + this.unit.gradeDefinitions = this.editingGradeDefinitions ?? this.cloneGradeDefinitions(); + this.editingGradeDefinitions = null; + this.editingGradeId = null; + this.newGradeId = null; + return; + } + if (this.newGradeId) { + return; + } + + const previousDefinitions = this.cloneGradeDefinitions(); + this.unit.gradeDefinitions = this.unit.gradeDefinitions.filter( + (_definition, definitionIndex) => definitionIndex !== index, + ); + this.reindexGrades(); + if (this.editingGradeId === grade.id) { + this.editingGradeId = null; + } + this.saveGradeDefinitions(previousDefinitions, 'Grade deleted.'); + }, + undefined, + 'Delete', + ); + } + + public moveGrade(index: number, offset: -1 | 1): void { + const targetIndex = index + offset; + if ( + this.newGradeId || + index <= 0 || + targetIndex <= 0 || + targetIndex >= this.unit.gradeDefinitions.length + ) { + return; + } + + const previousDefinitions = this.cloneGradeDefinitions(); + const definitions = [...this.unit.gradeDefinitions]; + const [definition] = definitions.splice(index, 1); + definitions.splice(targetIndex, 0, definition); + this.unit.gradeDefinitions = definitions; + this.reindexGrades(); + this.saveGradeDefinitions(previousDefinitions, 'Grade order updated.'); + } + + public editGrade(grade: GradeDefinition): void { + if (this.newGradeId) { + return; + } + + this.editingGradeDefinitions = this.cloneGradeDefinitions(); + this.editingGradeId = grade.id; + } + + public saveGrade(): void { + const previousDefinitions = this.editingGradeDefinitions ?? this.cloneGradeDefinitions(); + this.unit.gradeDefinitions = [...this.unit.gradeDefinitions]; + const successMessage = + this.editingGradeId === this.newGradeId ? 'Grade added.' : 'Grade updated.'; + this.saveGradeDefinitions(previousDefinitions, successMessage, () => { + this.editingGradeDefinitions = null; + this.editingGradeId = null; + this.newGradeId = null; + }); + } + + public isAddingGrade(): boolean { + return this.newGradeId !== null; + } + + public updateGrade(index: number, key: 'label' | 'abbreviation', value: string): void { + const normalizedValue = key === 'abbreviation' ? value.toUpperCase() : value; + const definition = this.unit.gradeDefinitions[index]; + if (definition) { + definition[key] = normalizedValue; + } + } + + private reindexGrades(): void { + this.unit.gradeDefinitions = this.unit.gradeDefinitions.map((definition, index) => ({ + ...definition, + value: index - 1, + })); + } + + private cloneGradeDefinitions(): GradeDefinition[] { + return this.unit.gradeDefinitions.map((definition) => ({...definition})); + } + + private saveGradeDefinitions( + previousDefinitions: GradeDefinition[], + successMessage: string, + successAction?: () => void, + ): void { + const gradeDefinitions = this.cloneGradeDefinitions(); + this.unitService + .update(this.unit, {body: {unit: {grade_definitions: gradeDefinitions}}}) + .subscribe({ + next: () => { + successAction?.(); + this.alertsService.success(successMessage, 2000); + }, + error: (response) => { + this.unit.gradeDefinitions = previousDefinitions; + if ( + !this.unit.gradeDefinitions.some((definition) => definition.id === this.editingGradeId) + ) { + this.editingGradeId = null; + this.editingGradeDefinitions = null; + } + this.alertsService.error(`Failed to update grades. ${response}`, 6000); + }, + }); + } public get overseerEnabled() { return this.doubtfireConstants.IsOverseerEnabled; diff --git a/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.coffee b/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.coffee deleted file mode 100644 index 9b019bdec3..0000000000 --- a/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.coffee +++ /dev/null @@ -1,89 +0,0 @@ -angular.module('doubtfire.units.states.edit.directives.unit-group-set-editor', []) - -# -# Editor for editing a unit's group sets. Can also add new groups to -# newly created group sets and then and add new members to those groups. -# -.directive('unitGroupSetEditor', -> - restrict: 'E' - templateUrl: 'units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.tpl.html' - replace: true - controller: ($scope, newGroupSetService, gradeService, alertService, CsvResultModal, fileDownloaderService) -> - - $scope.addGroupSet = -> - groupSet = newGroupSetService.createInstanceFrom({}, $scope.unit) - - gsCount = $scope.unit.groupSets.length - groupSet.name = if gsCount == 0 then "Group Work" else "Group Work Set #{gsCount + 1}" - - newGroupSetService.store(groupSet, {cache: $scope.unit.groupSetsCache}).subscribe({ - next: (gs) -> - alertService.success( "Group set created.", 2000) - error: (message) -> - alertService.error( "Failed to create group set. #{message}", 6000) - }) - - $scope.saveGroupSet = (data, groupSet) -> - groupSet.name = data.name - groupSet.allowStudentsToCreateGroups = data.allowStudentsToCreateGroups - groupSet.allowStudentsToManageGroups = data.allowStudentsToManageGroups - groupSet.keepGroupsInSameClass = data.keepGroupsInSameClass - groupSet.capacity = data.capacity - - newGroupSetService.update(groupSet).subscribe({ - next: (response) -> alertService.success( "Group set updated.", 2000) - error: (message) -> alertService.error( "Failed to update group set. #{message}", 6000) - }) - - $scope.toggleLocked = (gs) -> - gs.locked = !gs.locked - newGroupSetService.update(gs).subscribe({ - next: (response) -> - alertService.success( "#{if response.locked then 'Locked' else 'Unlocked'} #{gs.name}", 2000) - error: (message) -> - alertService.error( "Failed to #{if gs.locked then 'unlock' else 'lock'} #{gs.name}. #{message}", 6000) - }) - - $scope.removeGroupSet = (gs) -> - newGroupSetService.delete(gs, {cache: $scope.unit.groupSetsCache}).subscribe({ - next: (response) -> - if gs is $scope.selectedGroupSet - $scope.selectGroupSet($scope.unit.groupSets[0]) - alertService.success( "Group set deleted.", 2000) - error: (message) -> alertService.error( "Failed to delete group set. #{message}", 6000) - }) - - $scope.selectGroupSet = (gs) -> - $scope.selectedGroupSet = gs - # Notify children of updates - $scope.$broadcast 'UnitGroupSetEditor/SelectedGroupSetChanged', { id: gs?.id } - - $scope.studentStaffOptions = [ - { value: true, text: "Staff and Students" } - { value: false, text: "Staff Only" } - ] - - $scope.tutorialOptions = [ - { value: true, text: "Same Tutorial" } - { value: false, text: "Any Tutorial" } - ] - - if $scope.unit.groupSets.length > 0 - $scope.selectGroupSet($scope.unit.groupSets[0]) - - $scope.csvImportResponse = {} - $scope.groupCSV = { file: { name: 'Group CSV', type: 'csv' } } - $scope.groupCSVUploadUrl = -> $scope.selectedGroupSet.groupCSVUploadUrl() - $scope.groupStudentCSVUploadUrl = -> $scope.selectedGroupSet.groupStudentCSVUploadUrl() - $scope.isGroupCSVUploading = null - $scope.onGroupCSVSuccess = (response) -> - CsvResultModal.show 'Group CSV upload results.', response - $scope.selectGroupSet($scope.selectedGroupSet) - $scope.onGroupCSVComplete = -> - $scope.isGroupCSVUploading = null - - $scope.downloadGroupCSV = -> - fileDownloaderService.downloadFile($scope.selectedGroupSet.groupCSVUploadUrl(), "#{$scope.unit.code}-group-sets.csv") - $scope.downloadGroupStudentCSV = -> - fileDownloaderService.downloadFile($scope.selectedGroupSet.groupStudentCSVUploadUrl(), "#{$scope.unit.code}-#{$scope.selectedGroupSet.name}-students.csv") -) diff --git a/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.component.html b/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.component.html new file mode 100644 index 0000000000..4bdc0d0ce9 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.component.html @@ -0,0 +1,287 @@ +@if (unit) { +
    + + + Group Sets + + +

    + A group set is a set of related group-work. A unit can have multiple group sets for + various kinds of group work which has multiple teams. + +

    + + @if (showHelp) { +
    + +

    About Group Sets

    +

    + Once a group set has been created, you may wish to restrict certain permissions such + as creating and managing groups to staff only, instead of staff and students. +

    +

    + You may also decide that groups can only be restricted to students in the same + tutorial, otherwise students for a given group can be from any tutorial. +

    +

    Click Edit to modify these details.

    +
    + } + + @if (unit.groupSets.length === 0) { +
    +

    No Group Sets Created

    +

    This unit has no group sets. Click New Group Set below to create one.

    +
    + } @else { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Name + @if (editingGroupSetId === groupSet.id && editingGroupSetModel) { + + + + } @else { + {{ groupSet.name || 'No Name Set' }} + } + Capacity + @if (editingGroupSetId === groupSet.id && editingGroupSetModel) { + + + + } @else { + {{ groupSet.capacity || 'Unlimited' }} + } + Create Groups + @if (editingGroupSetId === groupSet.id && editingGroupSetModel) { + + + @for (opt of studentStaffOptions; track opt.value) { + {{ opt.text }} + } + + + } @else { + {{ groupSet.allowStudentsToCreateGroups ? 'Staff and Students' : 'Staff Only' }} + } + Manage Groups + @if (editingGroupSetId === groupSet.id && editingGroupSetModel) { + + + @for (opt of studentStaffOptions; track opt.value) { + {{ opt.text }} + } + + + } @else { + {{ groupSet.allowStudentsToManageGroups ? 'Staff and Students' : 'Staff Only' }} + } + Restrict to Tutorials + @if (editingGroupSetId === groupSet.id && editingGroupSetModel) { + + + @for (opt of tutorialOptions; track opt.value) { + {{ opt.text }} + } + + + } @else { + {{ groupSet.keepGroupsInSameClass ? 'Same Tutorial' : 'Any Tutorial' }} + } + Actions +
    + @if (editingGroupSetId === groupSet.id) { + + + } @else { + + + + } +
    +
    + } +
    + + + +
    + + @if (selectedGroupSet && unit.groupSets.length > 0) { +
    + + +
    + +
    + + + Import Groups for {{ selectedGroupSet.name }} + + +

    + Import groups into the group set named + {{ selectedGroupSet.name }} with a CSV containing the following + column headings: +

    +
      +
    • group_name for the indicated group's name
    • +
    • tutorial the group's tutorial
    • +
    • campus the tutorial's campus (if the tutorial is new)
    • +
    • capacity_adjustment any adjustment made to the group's capacity
    • +
    +
    + + + + + + + +
    + + + + Import Students into Groups for {{ selectedGroupSet.name }} + + +

    + Import students into groups for {{ selectedGroupSet.name }}. Data is listed under the following column headings: +

    +
      +
    • group_name for the indicated group's name
    • +
    • group_number for the indicated group's number
    • +
    • username for the student's username
    • +
    • tutorial for the student's tutorial
    • +
    +
    + + + + + + + +
    +
    + } +
    +} diff --git a/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.component.scss b/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.component.ts b/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.component.ts new file mode 100644 index 0000000000..f35f861698 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.component.ts @@ -0,0 +1,193 @@ +import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; +import {GroupSet, Unit, UnitRole} from 'src/app/api/models/doubtfire-model'; +import {GroupSetService} from 'src/app/api/services/group-set.service'; +import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; +import { + CsvResult, + CsvResultModalService, +} from 'src/app/common/modals/csv-result-modal/csv-result-modal.service'; +import {AlertService} from 'src/app/common/services/alert.service'; + +interface GroupSetEditModel { + name: string; + capacity: number | null; + allowStudentsToCreateGroups: boolean; + allowStudentsToManageGroups: boolean; + keepGroupsInSameClass: boolean; +} + +@Component({ + selector: 'f-unit-group-set-editor', + templateUrl: './unit-group-set-editor.component.html', + styleUrls: ['./unit-group-set-editor.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class UnitGroupSetEditorComponent implements OnInit { + @Input() unit: Unit; + @Input() unitRole: UnitRole; + + public selectedGroupSet: GroupSet | null = null; + public showHelp = false; + public isGroupCSVUploading: boolean | null = null; + + // Keep `file` key to preserve backend form field name used in legacy implementation. + public groupCSV = { + file: {name: 'Group CSV', type: 'csv'}, + }; + + public editingGroupSetId: number | null = null; + public editingGroupSetModel: GroupSetEditModel | null = null; + + public studentStaffOptions = [ + {value: true, text: 'Staff and Students'}, + {value: false, text: 'Staff Only'}, + ]; + + public tutorialOptions = [ + {value: true, text: 'Same Tutorial'}, + {value: false, text: 'Any Tutorial'}, + ]; + + constructor( + private groupSetService: GroupSetService, + private alertService: AlertService, + private fileDownloaderService: FileDownloaderService, + private csvResultModal: CsvResultModalService, + ) {} + + ngOnInit(): void { + if (this.unit?.groupSets?.length > 0) { + this.selectGroupSet(this.unit.groupSets[0]); + } + } + + addGroupSet(): void { + const groupSet = this.groupSetService.createInstanceFrom({}, this.unit); + const gsCount = this.unit.groupSets.length; + groupSet.name = gsCount === 0 ? 'Group Work' : `Group Work Set ${gsCount + 1}`; + + this.groupSetService.store(groupSet, {cache: this.unit.groupSetsCache}).subscribe({ + next: (createdGroupSet) => { + this.alertService.success('Group set created.', 2000); + this.selectGroupSet(createdGroupSet ?? groupSet); + }, + error: (message) => this.alertService.error(`Failed to create group set. ${message}`, 6000), + }); + } + + startEditGroupSet(groupSet: GroupSet): void { + this.editingGroupSetId = groupSet.id; + this.editingGroupSetModel = { + name: groupSet.name, + allowStudentsToCreateGroups: !!groupSet.allowStudentsToCreateGroups, + allowStudentsToManageGroups: !!groupSet.allowStudentsToManageGroups, + keepGroupsInSameClass: !!groupSet.keepGroupsInSameClass, + capacity: groupSet.capacity ?? null, + }; + } + + cancelEditGroupSet(): void { + this.editingGroupSetId = null; + this.editingGroupSetModel = null; + } + + saveGroupSet(groupSet: GroupSet): void { + if (!this.editingGroupSetModel) { + return; + } + + groupSet.name = this.editingGroupSetModel.name; + groupSet.allowStudentsToCreateGroups = this.editingGroupSetModel.allowStudentsToCreateGroups; + groupSet.allowStudentsToManageGroups = this.editingGroupSetModel.allowStudentsToManageGroups; + groupSet.keepGroupsInSameClass = this.editingGroupSetModel.keepGroupsInSameClass; + groupSet.capacity = this.editingGroupSetModel.capacity; + + this.groupSetService.update(groupSet).subscribe({ + next: () => { + this.alertService.success('Group set updated.', 2000); + this.cancelEditGroupSet(); + }, + error: (message) => this.alertService.error(`Failed to update group set. ${message}`, 6000), + }); + } + + toggleLocked(groupSet: GroupSet): void { + const originalLockedState = groupSet.locked; + groupSet.locked = !groupSet.locked; + + this.groupSetService.update(groupSet).subscribe({ + next: (response) => { + this.alertService.success( + `${response.locked ? 'Locked' : 'Unlocked'} ${groupSet.name}`, + 2000, + ); + }, + error: (message) => { + groupSet.locked = originalLockedState; + this.alertService.error( + `Failed to ${groupSet.locked ? 'unlock' : 'lock'} ${groupSet.name}. ${message}`, + 6000, + ); + }, + }); + } + + removeGroupSet(groupSet: GroupSet): void { + this.groupSetService.delete(groupSet, {cache: this.unit.groupSetsCache}).subscribe({ + next: () => { + if (groupSet === this.selectedGroupSet) { + this.selectGroupSet(this.unit.groupSets[0] ?? null); + } + this.alertService.success('Group set deleted.', 2000); + }, + error: (message) => this.alertService.error(`Failed to delete group set. ${message}`, 6000), + }); + } + + selectGroupSet(groupSet: GroupSet | null): void { + this.selectedGroupSet = groupSet; + if (this.editingGroupSetId && groupSet?.id !== this.editingGroupSetId) { + this.cancelEditGroupSet(); + } + } + + groupCSVUploadUrl(): string | undefined { + return this.selectedGroupSet?.groupCSVUploadUrl(); + } + + groupStudentCSVUploadUrl(): string | undefined { + return this.selectedGroupSet?.groupStudentCSVUploadUrl(); + } + + onGroupCSVSuccess(response: CsvResult): void { + this.csvResultModal.show('Group CSV upload results.', response); + this.selectGroupSet(this.selectedGroupSet); + } + + onGroupCSVComplete(): void { + this.isGroupCSVUploading = null; + } + + downloadGroupCSV(): void { + if (!this.selectedGroupSet) { + return; + } + + this.fileDownloaderService.downloadFile( + this.selectedGroupSet.groupCSVUploadUrl(), + `${this.unit.code}-group-sets.csv`, + ); + } + + downloadGroupStudentCSV(): void { + if (!this.selectedGroupSet) { + return; + } + + this.fileDownloaderService.downloadFile( + this.selectedGroupSet.groupStudentCSVUploadUrl(), + `${this.unit.code}-${this.selectedGroupSet.name}-students.csv`, + ); + } +} diff --git a/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.tpl.html b/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.tpl.html deleted file mode 100644 index db550ef42d..0000000000 --- a/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.tpl.html +++ /dev/null @@ -1,207 +0,0 @@ -
    -
    -
    -
    -

    - Group Sets -

    -
    -
    -

    - A group set is a set of related group-work. A unit can have multiple - group sets for various kinds of group work which has multiple teams. - Read more about group sets. -

    -
    - - - -

    About Group Sets

    -

    - Once a group set has been created, you may wish to restrict certain - permissions such as creating and managing groups to staff only, instead - of staff and students. If you restrict this to staff only, then tutors - and convenors will only be allowed to create groups, and manage them - (i.e., assign students to groups). Otherwise, you assume that students - will be able to self-manage themselves in that particular group set. -

    -

    - You may also decide that groups can only be restricted to students in - the same tutorial, otheriwse students for a given group can be from - any tutorial. -

    -

    - Click the pencil icon to modify these details. -

    -
    -
    -

    No Group Sets Created

    -

    - This unit has no group sets. Click the create group set button below to create one. -

    -
    -
    - - - - - - - - - - - - - - - - - - - - - -
    - Name - - Capacity - - Create Groups - - Manage Groups - - Restrict to Tutorials - Actions
    - - {{ gs.name || 'No Name Set' }} - - - - {{ gs.capacity || 'Unlimited' }} - - - - {{ ( (gs && gs.allowStudentsToCreateGroups) ? 'Staff and Students' : 'Staff Only' ) }} - - - - {{ ( (gs && gs.allowStudentsToManageGroups) ? 'Staff and Students' : 'Staff Only' ) }} - - - - {{ ( (gs && gs.keepGroupsInSameClass) ? 'Same Tutorial' : 'Any Tutorial' ) }} - - -
    - - -
    - - -
    - -
    -
    -
    - -
    -
    -
    -
    -
    -
    -

    Import Groups for {{selectedGroupSet.name}}

    -
    -
    -

    - Import groups into the group set named {{selectedGroupSet.name}} - with a CSV containing the following column headings: -

    -
      -
    • group_name for the indicated group's name,
    • -
    • tutorial the group's tutorial.
    • -
    • campus the tutorial's campus (if the tutorial is new), and
    • -
    • capacity_adjustment any adjustment made to the group's capacity
    • -
    -
    - -
    -
    -
    -

    - Import Students into Groups for {{selectedGroupSet.name}} -

    -
    -
    -

    - Import students into groups for {{selectedGroupSet.name}}. - Data is listed under the following column headings: -

    -
      -
    • group_name for the indicated group's name,
    • -
    • group_number for the indicated group's number,
    • -
    • username for the student's username, and
    • -
    • tutorial for the student's tutorial.
    • -
    -
    - -
    -
    -
    -
    diff --git a/src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.coffee b/src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.coffee deleted file mode 100644 index 151d331a77..0000000000 --- a/src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.coffee +++ /dev/null @@ -1,42 +0,0 @@ -# Component not used - -angular.module('doubtfire.units.states.edit.directives.unit-ilo-editor',[]) - -# -# Editor for modifying a unit's ILOs -# -.directive('unitIloEditor', -> - replace: true - restrict: 'E' - templateUrl: 'units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.tpl.html' - controller: ($scope, $modal, $rootScope, newLearningOutcomeService, alertService, CsvResultModal, UnitILOEditModal, fileDownloaderService) -> - $scope.unit.learningOutcomesCache.values.subscribe( - (ilos) -> - $scope.ilos = ilos - ) - - $scope.batchFiles = { file: { name: 'CSV Data', type: 'csv' } } - $scope.batchOutcomeUrl = -> - $scope.unit.getOutcomeBatchUploadUrl() - $scope.onBatchOutcomeSuccess = (response) -> - CsvResultModal.show "Outcome CSV Upload Results", response - if response.success.length > 0 - $scope.unit.refresh() - - $scope.editILO = (ilo) -> - UnitILOEditModal.show $scope.unit, ilo - - $scope.createILO = -> - $scope.editILO() - - $scope.downloadCSV = -> - fileDownloaderService.downloadFile($scope.batchOutcomeUrl(), "#{$scope.unit.code}-learning-outcomes.csv") - - $scope.deleteILO = (ilo) -> - newLearningOutcomeService.delete({ id: ilo.id, unitId: $scope.unit.id }, {entity: ilo, cache: $scope.unit.learningOutcomesCache}).subscribe({ - next: (response) -> - alertService.success( "ILO #{ilo.id} was deleted successfully", 2000) - error: (response) -> - alertService.error( "Error: " + response, 6000) - }) -) diff --git a/src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.tpl.html b/src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.tpl.html deleted file mode 100644 index 3ce30ff314..0000000000 --- a/src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.tpl.html +++ /dev/null @@ -1,71 +0,0 @@ -
    -
    -
    -
    -

    Edit Intended Learning Outcomes

    - Add, edit or delete intended learning outcomes for this unit -
    -
    -
    - This unit has no Intended Learning Outcomes -
    -
    -
    - - - - - - - - - - - - - - - - - - - -
    NumberAbbreviationNameDescriptionActions
    {{ilo.iloNumber}}{{ilo.abbreviation}}{{ilo.name}}{{ilo.description}} - -
    -
    -
    -
    - -
    -
    -
    -
    -
    -

    Batch Upload Outcome Definitions

    - Batch upload learning outcome definitions with a CSV containing: unit_code, iloNumber, abbreviation, name, and - description. -
    -
    - -
    - -
    -

    Download Outcomes

    - Download all outcomes for the unit. -
    -
    - -
    -
    -
    -
    diff --git a/src/app/units/states/edit/directives/unit-staff-editor/bulk-import-staff-modal/bulk-import-staff-modal.component.html b/src/app/units/states/edit/directives/unit-staff-editor/bulk-import-staff-modal/bulk-import-staff-modal.component.html new file mode 100644 index 0000000000..8240189388 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-staff-editor/bulk-import-staff-modal/bulk-import-staff-modal.component.html @@ -0,0 +1,22 @@ +

    Bulk Import Staff

    + +
    +

    Paste one staff email per line to add them to this unit as tutors.

    + + + Staff emails + + +
    + +
    + + +
    diff --git a/src/app/units/states/edit/directives/unit-staff-editor/bulk-import-staff-modal/bulk-import-staff-modal.component.ts b/src/app/units/states/edit/directives/unit-staff-editor/bulk-import-staff-modal/bulk-import-staff-modal.component.ts new file mode 100644 index 0000000000..0456d484ed --- /dev/null +++ b/src/app/units/states/edit/directives/unit-staff-editor/bulk-import-staff-modal/bulk-import-staff-modal.component.ts @@ -0,0 +1,27 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {MatDialogRef} from '@angular/material/dialog'; + +@Component({ + selector: 'bulk-import-staff-modal', + templateUrl: './bulk-import-staff-modal.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, +}) +export class BulkImportStaffModalComponent { + public emailList = ''; + + constructor(public dialogRef: MatDialogRef) {} + + public cancel(): void { + this.dialogRef.close(undefined); + } + + public submit(): void { + const trimmedEmails = this.emailList.trim(); + if (!trimmedEmails) { + return; + } + + this.dialogRef.close(trimmedEmails); + } +} diff --git a/src/app/units/states/edit/directives/unit-staff-editor/bulk-import-staff-modal/bulk-import-staff-modal.service.ts b/src/app/units/states/edit/directives/unit-staff-editor/bulk-import-staff-modal/bulk-import-staff-modal.service.ts new file mode 100644 index 0000000000..816a659ad8 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-staff-editor/bulk-import-staff-modal/bulk-import-staff-modal.service.ts @@ -0,0 +1,21 @@ +import {Injectable} from '@angular/core'; +import {MatDialog, MatDialogRef} from '@angular/material/dialog'; +import {BulkImportStaffModalComponent} from './bulk-import-staff-modal.component'; + +@Injectable({ + providedIn: 'root', +}) +export class BulkImportStaffModalService { + constructor(private dialog: MatDialog) {} + + public show(): MatDialogRef { + return this.dialog.open( + BulkImportStaffModalComponent, + { + position: {top: '2.5%'}, + width: '100%', + maxWidth: '700px', + }, + ); + } +} diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html index c95e5c847e..affdead4d1 100644 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html +++ b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html @@ -3,52 +3,54 @@

    Unit Staff

    Manage unit staff by adding members and assigning them as convenors or tutors.

    - +
    - + - - - + - - + - + - - + - - + - + - - + +
    NameName -
    - +
    + {{ unitRole.user.name }}
    Role + Role Tutor Convenor Main Convenor + Main Convenor @if (unitRole?.role === 'Convenor') { Observer Only + Observer Only Overflow Marking + Overflow Marking Mentor + Mentor (None) @for (unitRole of unitStaff; track unitRole) { @@ -103,37 +106,55 @@

    Unit Staff

    -
    Actions -
    -
    Actions +
    + -
    - - - + + + + @for (staff of filteredStaff; track staff) { + + {{ staff.name }} + + } + + + + +
    diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts index d481e6cb49..af051c8f14 100644 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts +++ b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts @@ -1,19 +1,27 @@ -import {Component, Input, OnInit} from '@angular/core'; -import {AlertService} from 'src/app/common/services/alert.service'; -import {UnitRoleService} from 'src/app/api/services/unit-role.service'; -import {Unit} from 'src/app/api/models/unit'; +import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; +import {MatButtonToggleChange} from '@angular/material/button-toggle'; +import {MatSelectChange} from '@angular/material/select'; +import {MatTableDataSource} from '@angular/material/table'; import {Tutorial, User} from 'src/app/api/models/doubtfire-model'; +import {Unit} from 'src/app/api/models/unit'; import {UnitRole} from 'src/app/api/models/unit-role'; -import {MatTableDataSource} from '@angular/material/table'; -import {MatButtonToggleChange} from '@angular/material/button-toggle'; +import {UnitRoleService} from 'src/app/api/services/unit-role.service'; +import {UserService} from 'src/app/api/services/user.service'; import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; -import {MatSelectChange} from '@angular/material/select'; +import { + CsvResult, + CsvResultModalService, + CsvRow, +} from 'src/app/common/modals/csv-result-modal/csv-result-modal.service'; import {TutorNotesModalService} from 'src/app/common/modals/tutor-notes-modal/tutor-notes-modal.service'; -import {UserService} from 'src/app/api/services/user.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {BulkImportStaffModalService} from './bulk-import-staff-modal/bulk-import-staff-modal.service'; @Component({ selector: 'unit-staff-editor', templateUrl: 'unit-staff-editor.component.html', + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class UnitStaffEditorComponent implements OnInit { @Input() unit: Unit; @@ -34,7 +42,7 @@ export class UnitStaffEditorComponent implements OnInit { 'mentor', 'actions', ]; - dataSource = new MatTableDataSource(); + dataSource: MatTableDataSource = new MatTableDataSource(); // Inject services here constructor( @@ -43,6 +51,8 @@ export class UnitStaffEditorComponent implements OnInit { private userService: UserService, private confirmationModalService: ConfirmationModalService, private tutorNotesModal: TutorNotesModalService, + private bulkImportStaffModal: BulkImportStaffModalService, + private csvResultModal: CsvResultModalService, ) {} ngOnInit(): void { @@ -172,6 +182,19 @@ export class UnitStaffEditorComponent implements OnInit { } } + openBulkImportModal() { + this.bulkImportStaffModal + .show() + .afterClosed() + .subscribe((emailList) => { + if (!emailList) { + return; + } + + this.bulkImportStaffFromEmailList(emailList); + }); + } + /** * Used in filtering the staff list. The `searchTerm` is bound to the auto-complete input in this class's template. * @@ -287,11 +310,114 @@ export class UnitStaffEditorComponent implements OnInit { } groupSetName(id: number) { - this.unit.groupSetsCache.get(id).name || 'Individual Work'; + return this.unit.groupSetsCache.get(id).name || 'Individual Work'; } openTutorNotes(unitRole: UnitRole) { unitRole.unit = this.unit; // HACK: ensure unit is mapped within the UnitRole this.tutorNotesModal.show(null, unitRole); } + + private bulkImportStaffFromEmailList(emailList: string): void { + const parsedEmails = this.parseEmailList(emailList); + + if (parsedEmails.length === 0) { + this.alertService.error('Please enter at least one valid email address.', 6000); + return; + } + + const existingStaffEmails: Set = new Set( + this.unit.staff + .map((unitRole) => unitRole.user.email?.trim().toLowerCase()) + .filter((email): email is string => !!email), + ); + const staffByEmail = new Map( + this.staff + .filter((staff) => staff.isStaff && staff.email) + .map((staff) => [staff.email.trim().toLowerCase(), staff] as const), + ); + + const alreadyAssignedEmails = parsedEmails.filter((email) => existingStaffEmails.has(email)); + const matchedUsers = parsedEmails + .filter((email) => !existingStaffEmails.has(email)) + .map((email) => staffByEmail.get(email)) + .filter((staff): staff is User => !!staff); + const unmatchedEmails = parsedEmails.filter( + (email) => !existingStaffEmails.has(email) && !staffByEmail.has(email), + ); + const ignoredRows = alreadyAssignedEmails.map((email) => + this.csvResultRow(email, 'Staff member is already assigned to this unit'), + ); + const unmatchedRows = unmatchedEmails.map((email) => + this.csvResultRow(email, 'No matching staff user was found'), + ); + + if (matchedUsers.length === 0) { + this.csvResultModal.show( + 'Bulk staff import results', + this.csvResultResponse([], unmatchedRows, ignoredRows), + ); + return; + } + + this.addStaffUsersSequentially(matchedUsers, [], [], ({addedEmails, failedEmails}) => { + const successRows = addedEmails.map((email) => + this.csvResultRow(email, 'Staff member added'), + ); + const failedRows = failedEmails.map((email) => + this.csvResultRow(email, 'Could not add staff member to this unit'), + ); + + this.csvResultModal.show( + 'Bulk staff import results', + this.csvResultResponse(successRows, [...unmatchedRows, ...failedRows], ignoredRows), + ); + }); + } + + private addStaffUsersSequentially( + users: User[], + addedEmails: string[], + failedEmails: string[], + onComplete: (result: {addedEmails: string[]; failedEmails: string[]}) => void, + ): void { + if (users.length === 0) { + onComplete({addedEmails, failedEmails}); + return; + } + + const [nextUser, ...remainingUsers] = users; + + this.unit.addStaff(nextUser).subscribe({ + next: () => { + addedEmails.push(nextUser.email); + this.addStaffUsersSequentially(remainingUsers, addedEmails, failedEmails, onComplete); + }, + error: () => { + failedEmails.push(nextUser.email); + this.addStaffUsersSequentially(remainingUsers, addedEmails, failedEmails, onComplete); + }, + }); + } + + private parseEmailList(emailList: string): string[] { + const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + return Array.from( + new Set( + emailList + .split(/\r?\n/) + .map((email) => email.trim().toLowerCase()) + .filter((email) => emailPattern.test(email)), + ), + ); + } + + private csvResultRow(row: string, message: string): CsvRow { + return {row, message}; + } + + private csvResultResponse(success: CsvRow[], errors: CsvRow[], ignored: CsvRow[]): CsvResult { + return {success, errors, ignored}; + } } diff --git a/src/app/units/states/edit/directives/unit-students-editor/student-campus-select/student-campus-select.component.html b/src/app/units/states/edit/directives/unit-students-editor/student-campus-select/student-campus-select.component.html index 79237858bf..0be9ed0567 100644 --- a/src/app/units/states/edit/directives/unit-students-editor/student-campus-select/student-campus-select.component.html +++ b/src/app/units/states/edit/directives/unit-students-editor/student-campus-select/student-campus-select.component.html @@ -1,4 +1,4 @@ - + None @for (campus of campuses; track campus) { diff --git a/src/app/units/states/edit/directives/unit-students-editor/student-campus-select/student-campus-select.component.ts b/src/app/units/states/edit/directives/unit-students-editor/student-campus-select/student-campus-select.component.ts index 92a80265a2..bb80001e70 100644 --- a/src/app/units/states/edit/directives/unit-students-editor/student-campus-select/student-campus-select.component.ts +++ b/src/app/units/states/edit/directives/unit-students-editor/student-campus-select/student-campus-select.component.ts @@ -1,14 +1,16 @@ -import { Component, Inject, Input, OnInit } from '@angular/core'; -import { Campus, CampusService, Project, Unit } from 'src/app/api/models/doubtfire-model'; -import { MatSelectChange } from '@angular/material/select'; -import { AlertService } from 'src/app/common/services/alert.service'; +import {ChangeDetectionStrategy, Component, Input, OnChanges, OnInit} from '@angular/core'; +import {MatSelectChange} from '@angular/material/select'; +import {Campus, CampusService, Project, Unit} from 'src/app/api/models/doubtfire-model'; +import {AlertService} from 'src/app/common/services/alert.service'; @Component({ selector: 'student-campus-select', templateUrl: 'student-campus-select.component.html', styleUrls: ['student-campus-select.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class StudentCampusSelectComponent implements OnInit { +export class StudentCampusSelectComponent implements OnChanges, OnInit { @Input() unit: Unit; @Input() student: Project; @Input() update: boolean; @@ -19,7 +21,7 @@ export class StudentCampusSelectComponent implements OnInit { constructor( private campusService: CampusService, private alerts: AlertService, - ) { } + ) {} ngOnChanges() { this.originalCampus = this.student.campus; @@ -41,8 +43,8 @@ export class StudentCampusSelectComponent implements OnInit { error: (message) => { this.student.campus = this.originalCampus; this.alerts.error(message, 6000); - } - }) + }, + }); } } } diff --git a/src/app/units/states/edit/directives/unit-students-editor/student-tutorial-select/student-tutorial-select.component.html b/src/app/units/states/edit/directives/unit-students-editor/student-tutorial-select/student-tutorial-select.component.html index d135f777e9..ba2377910a 100644 --- a/src/app/units/states/edit/directives/unit-students-editor/student-tutorial-select/student-tutorial-select.component.html +++ b/src/app/units/states/edit/directives/unit-students-editor/student-tutorial-select/student-tutorial-select.component.html @@ -1,4 +1,4 @@ - + @if (tutorialsForStreamAndStudent(student).length > 0) { diff --git a/src/app/units/states/edit/directives/unit-students-editor/student-tutorial-select/student-tutorial-select.component.ts b/src/app/units/states/edit/directives/unit-students-editor/student-tutorial-select/student-tutorial-select.component.ts index f3c47ba355..452ecd3fa6 100644 --- a/src/app/units/states/edit/directives/unit-students-editor/student-tutorial-select/student-tutorial-select.component.ts +++ b/src/app/units/states/edit/directives/unit-students-editor/student-tutorial-select/student-tutorial-select.component.ts @@ -1,10 +1,12 @@ -import { Component, Input } from '@angular/core'; -import { Project, Tutorial, TutorialStream, Unit } from 'src/app/api/models/doubtfire-model'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {Project, Tutorial, TutorialStream, Unit} from 'src/app/api/models/doubtfire-model'; @Component({ selector: 'student-tutorial-select', templateUrl: 'student-tutorial-select.component.html', styleUrls: ['student-tutorial-select.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class StudentTutorialSelectComponent { @Input() unit: Unit; @@ -26,8 +28,12 @@ export class StudentTutorialSelectComponent { public tutorialsForStreamAndStudent(student: Project, stream?: TutorialStream) { return this.unit.tutorials.filter((tutorial) => { const result: boolean = - student.campus == null || tutorial.campus == null || student.campus.id === tutorial.campus.id; - if (!result) return result; + student.campus == null || + tutorial.campus == null || + student.campus.id === tutorial.campus.id; + if (!result) { + return result; + } if (tutorial.tutorialStream && stream) { return tutorial.tutorialStream.abbreviation === stream.abbreviation; } else if (!tutorial.tutorialStream && !stream) { diff --git a/src/app/units/states/edit/directives/unit-students-editor/unit-students-editor.component.html b/src/app/units/states/edit/directives/unit-students-editor/unit-students-editor.component.html index ccba19f15a..9110f1dac4 100644 --- a/src/app/units/states/edit/directives/unit-students-editor/unit-students-editor.component.html +++ b/src/app/units/states/edit/directives/unit-students-editor/unit-students-editor.component.html @@ -8,63 +8,112 @@

    Enrolled Students

    Search - +
    - + @if (loadingStudents) { +
    +
    + @for (width of ['10%', '12%', '12%', '20%', '10%', '20%', '8%', '8%']; track $index) { + + } +
    + + @for (row of [0, 1, 2, 3, 4, 5, 6, 7]; track row) { +
    + + + + + @for (column of [0, 1, 2, 3]; track column) { + + } +
    + } +
    + } +
    - - + - - + - - + - - + - - + - - + - - + - + - - - + + +
    Username + Username {{ project.student.username }} First Name + First Name {{ project.student.firstName }} Last Name + Last Name {{ project.student.lastName }} Email + Email {{ project.student.email }} Campus - + Campus + Tutorial - + Tutorial + Enrolled + Enrolled Enrolled Students - +
    + - + - +
    diff --git a/src/app/units/states/edit/directives/unit-students-editor/unit-students-editor.component.ts b/src/app/units/states/edit/directives/unit-students-editor/unit-students-editor.component.ts index c4402e82b3..d9af32d5f6 100644 --- a/src/app/units/states/edit/directives/unit-students-editor/unit-students-editor.component.ts +++ b/src/app/units/states/edit/directives/unit-students-editor/unit-students-editor.component.ts @@ -1,28 +1,40 @@ +import {HttpClient} from '@angular/common/http'; import { - csvUploadModalService, - csvResultModalService, - unitStudentEnrolmentModal, -} from './../../../../../ajs-upgraded-providers'; -import {ViewChild, Component, Input, Inject, AfterViewInit, OnDestroy} from '@angular/core'; -import {MatTable, MatTableDataSource} from '@angular/material/table'; -import {MatSort, Sort} from '@angular/material/sort'; + AfterViewInit, + ChangeDetectionStrategy, + Component, + Input, + OnDestroy, + OnInit, + ViewChild, +} from '@angular/core'; import {MatPaginator} from '@angular/material/paginator'; -import {HttpClient} from '@angular/common/http'; -import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; +import {MatSort, Sort} from '@angular/material/sort'; +import {MatTable, MatTableDataSource} from '@angular/material/table'; +import {Router} from '@angular/router'; +import {Subscription, finalize, timer} from 'rxjs'; +import {switchMap} from 'rxjs/operators'; import {Project, ProjectService, Unit} from 'src/app/api/models/doubtfire-model'; -import {UIRouter} from '@uirouter/angular'; -import {Subscription} from 'rxjs'; -import {AlertService} from 'src/app/common/services/alert.service'; -import {SpecConModalService} from 'src/app/common/modals/spec-con-modal/spec-con-modal.service'; -import {SidekiqProgressModalService} from 'src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.service'; import {SidekiqJob} from 'src/app/api/models/sidekiq-job'; +import {FileDownloaderService} from 'src/app/common/file-downloader/file-downloader.service'; +import { + CsvResult, + CsvResultModalService, +} from 'src/app/common/modals/csv-result-modal/csv-result-modal.service'; +import {CsvUploadModalService} from 'src/app/common/modals/csv-upload-modal/csv-upload-modal.service'; +import {SidekiqProgressModalService} from 'src/app/common/modals/sidekiq-progress-modal/sidekiq-progress-modal.service'; +import {SpecConModalService} from 'src/app/common/modals/spec-con-modal/spec-con-modal.service'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {UnitStudentEnrolmentModalService} from 'src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; @Component({ selector: 'unit-students-editor', templateUrl: 'unit-students-editor.component.html', styleUrls: ['unit-students-editor.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) -export class UnitStudentsEditorComponent implements AfterViewInit, OnDestroy { +export class UnitStudentsEditorComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild(MatTable, {static: false}) table: MatTable; @ViewChild(MatSort, {static: false}) sort: MatSort; @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; @@ -41,29 +53,27 @@ export class UnitStudentsEditorComponent implements AfterViewInit, OnDestroy { 'enrolled', 'goto', ]; - dataSource: MatTableDataSource; + dataSource: MatTableDataSource = new MatTableDataSource([]); + loadingStudents = true; // Calls the parent's constructor, passing in an object // that maps all of the form controls that this form consists of. constructor( private httpClient: HttpClient, - @Inject(unitStudentEnrolmentModal) private enrolModal: any, + private enrolModal: UnitStudentEnrolmentModalService, private alerts: AlertService, - @Inject(csvUploadModalService) private csvUploadModal: any, - @Inject(csvResultModalService) private csvResultModal: any, + private csvUploadModal: CsvUploadModalService, + private csvResultModal: CsvResultModalService, private fileDownloader: FileDownloaderService, - private router: UIRouter, + private router: Router, private projectService: ProjectService, private specConModalService: SpecConModalService, private sidekiqProgressModalService: SidekiqProgressModalService, ) {} - // The paginator is inside the table - ngAfterViewInit() { - this.dataSource = new MatTableDataSource(this.unit.studentCache.currentValuesClone()); - this.dataSource.paginator = this.paginator; - this.dataSource.sort = this.sort; - this.dataSource.filterPredicate = (data: any, filter: string) => data.matches(filter); + ngOnInit(): void { + this.dataSource.data = this.unit.studentCache.currentValuesClone(); + this.dataSource.filterPredicate = (data: Project, filter: string) => data.matches(filter); this.subscriptions.push( this.unit.studentCache.values.subscribe((students) => { @@ -71,11 +81,13 @@ export class UnitStudentsEditorComponent implements AfterViewInit, OnDestroy { }), ); - this.subscriptions.push( - this.projectService.loadStudents(this.unit, true).subscribe(() => { - // projects included in unit... - }), - ); + this.refreshStudentsAfterRender(); + } + + // The paginator is inside the table + ngAfterViewInit() { + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; } ngOnDestroy(): void { @@ -90,6 +102,21 @@ export class UnitStudentsEditorComponent implements AfterViewInit, OnDestroy { } } + private refreshStudentsAfterRender(): void { + this.subscriptions.push( + timer(0) + .pipe( + switchMap(() => this.projectService.loadStudents(this.unit, false, true)), + finalize(() => { + this.loadingStudents = false; + }), + ) + .subscribe(() => { + // projects included in unit... + }), + ); + } + private sortCompare(aValue: number | string, bValue: number | string, isAsc: boolean) { return (aValue < bValue ? -1 : 1) * (isAsc ? 1 : -1); } @@ -118,11 +145,7 @@ export class UnitStudentsEditorComponent implements AfterViewInit, OnDestroy { } public gotoStudent(student: Project) { - this.router.stateService.go('projects/dashboard', { - projectId: student.id, - tutor: true, - taskAbbr: '', - }); + this.router.navigate(['/projects', student.id, 'dashboard'], {queryParams: {tutor: true}}); } enrolStudent() { @@ -132,7 +155,7 @@ export class UnitStudentsEditorComponent implements AfterViewInit, OnDestroy { uploadEnrolments() { this.csvUploadModal.show( 'Upload Students to Enrol', - 'Test message', + 'Upload a CSV to enrol students.', {file: {name: 'Enrol CSV Data', type: 'csv'}}, this.unit.enrolStudentsCSVUrl, (response: SidekiqJob) => { @@ -161,10 +184,10 @@ export class UnitStudentsEditorComponent implements AfterViewInit, OnDestroy { uploadWithdrawals() { this.csvUploadModal.show( 'Upload Students to Withdraw', - 'Test message', + 'Upload a CSV to withdraw students.', {file: {name: 'Withdraw CSV Data', type: 'csv'}}, this.unit.withdrawStudentsCSVUrl, - (response: any) => { + (response: CsvResult) => { // at least one student? this.csvResultModal.show('Withdraw Student CSV Results', response); if (response.success.length > 0) { diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-dates/task-definition-dates.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-dates/task-definition-dates.component.html index c6a3eb74c6..f94c7e6037 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-dates/task-definition-dates.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-dates/task-definition-dates.component.html @@ -1,12 +1,12 @@ -
    +
    Start Date @@ -18,9 +18,9 @@ @@ -30,11 +30,11 @@ Final Feedback Date diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-dates/task-definition-dates.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-dates/task-definition-dates.component.ts index f0d5d6dc23..40d0f325c5 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-dates/task-definition-dates.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-dates/task-definition-dates.component.ts @@ -1,11 +1,13 @@ -import { Component, Input } from '@angular/core'; -import { TaskDefinition } from 'src/app/api/models/task-definition'; -import { Unit } from 'src/app/api/models/unit'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {TaskDefinition} from 'src/app/api/models/task-definition'; +import {Unit} from 'src/app/api/models/unit'; @Component({ selector: 'f-task-definition-dates', templateUrl: 'task-definition-dates.component.html', styleUrls: ['task-definition-dates.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TaskDefinitionDatesComponent { @Input() taskDefinition: TaskDefinition; diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.html index 7453a19525..b3d0eb258b 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.html @@ -1,23 +1,23 @@
    -
    +
    - - + - - + - + - - + +
    Discussion Prompt + Discussion Prompt @if (!editing(prompt)) { {{ prompt.content }} } @else { Discussion Prompt - + } Priority + Priority @if (!editing(prompt)) { {{ prompt.priorityLabel }} } @else { @@ -37,8 +37,8 @@ - Actions + Actions
    @if (editing(prompt)) {
    @if (!dataSource.data.length) { -
    No discussion prompts
    +
    No discussion prompts
    } @if (!creatingNewDiscussionPrompt) { -
    -
    @@ -77,17 +77,17 @@ @if (creatingNewDiscussionPrompt) { -
    +
    Discussion Prompt - + Priority @@ -99,7 +99,7 @@
    - +
    diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.ts index e22072d433..7f9de46ef7 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-discussion-prompts/task-definition-discussion-prompts.component.ts @@ -1,4 +1,11 @@ -import {Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Input, + OnChanges, + OnInit, + SimpleChanges, +} from '@angular/core'; import {UntypedFormControl, Validators} from '@angular/forms'; import {MatTableDataSource} from '@angular/material/table'; import {Observable, Subscription} from 'rxjs'; @@ -17,6 +24,8 @@ import {AlertService} from 'src/app/common/services/alert.service'; selector: 'f-task-definition-discussion-prompts', templateUrl: 'task-definition-discussion-prompts.component.html', styleUrls: ['task-definition-discussion-prompts.component.scss'], + changeDetection: ChangeDetectionStrategy.Eager, + standalone: false, }) export class TaskDefinitionDiscussionPromptsComponent extends EntityFormComponent @@ -30,7 +39,7 @@ export class TaskDefinitionDiscussionPromptsComponent private prereqSub?: Subscription; - public dataSource = new MatTableDataSource(); + public dataSource: MatTableDataSource = new MatTableDataSource(); creatingNewDiscussionPrompt: boolean = false; diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.html index 567758a0ad..5958295741 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.html @@ -1,22 +1,22 @@ -
    +

    Details for {{ taskDefinition.abbreviation }} - {{ taskDefinition.name }}

    -
    -