diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1459f4b5..4f89a6ef 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,7 +19,6 @@ jobs: cache: 'npm' cache-dependency-path: client-v2/package-lock.json - - run: cd client-v2 - run: npm ci working-directory: ./client-v2 @@ -34,6 +33,16 @@ jobs: - run: npm run comp:ci working-directory: ./client-v2 + env: + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + + - name: Upload component test artifacts + uses: actions/upload-artifact@v3 + if: failure() + with: + name: component test artifacts + path: client-v2/.cypress/component + retention-days: 7 # - name: Upload coverage artifacts # uses: actions/upload-artifact@v3 @@ -60,11 +69,10 @@ jobs: cache: 'npm' cache-dependency-path: server/package-lock.json - - run: cd server - run: npm ci working-directory: ./server - - run: npm run build --if-present + - run: npm run build working-directory: ./server - run: npm run lint @@ -73,7 +81,7 @@ jobs: - run: npm run typecheck-snapshot-scripts working-directory: ./server - - name: Setup Postgres + - name: Setup Postgres uses: Daniel-Marynicz/postgresql-action@master with: postgres_image_tag: 12-alpine @@ -130,3 +138,13 @@ jobs: - run: npm run e2e:ci working-directory: ./client-v2 + env: + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + + - name: Upload e2e test artifacts + uses: actions/upload-artifact@v3 + if: failure() + with: + name: e2e test artifacts + path: client-v2/.cypress/e2e + retention-days: 7 diff --git a/.gitignore b/.gitignore index fd40aa3b..7e6eb829 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ node_modules .idea # Local Netlify folder .netlify +# Cypress artifacts folder +.cypress \ No newline at end of file diff --git a/client-v2/.vscode/settings.json b/client-v2/.vscode/settings.json index 0aa14e31..4ea88ad1 100644 --- a/client-v2/.vscode/settings.json +++ b/client-v2/.vscode/settings.json @@ -1,3 +1,4 @@ { - "eslint.validate": ["html", "javascript", "typescript"] + "eslint.validate": ["html", "javascript", "typescript"], + "css.customData": [".vscode/tailwind.json"] } diff --git a/client-v2/.vscode/tailwind.json b/client-v2/.vscode/tailwind.json new file mode 100644 index 00000000..e47372fe --- /dev/null +++ b/client-v2/.vscode/tailwind.json @@ -0,0 +1,55 @@ +{ + "version": 1.1, + "atDirectives": [ + { + "name": "@tailwind", + "description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#tailwind" + } + ] + }, + { + "name": "@apply", + "description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#apply" + } + ] + }, + { + "name": "@responsive", + "description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#responsive" + } + ] + }, + { + "name": "@screen", + "description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#screen" + } + ] + }, + { + "name": "@variants", + "description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n", + "references": [ + { + "name": "Tailwind Documentation", + "url": "https://tailwindcss.com/docs/functions-and-directives#variants" + } + ] + } + ] +} diff --git a/client-v2/angular.json b/client-v2/angular.json index 4b11ad42..538eb826 100644 --- a/client-v2/angular.json +++ b/client-v2/angular.json @@ -1,11 +1,11 @@ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "schematics": { - "@schematics/angular": { - "component": { - "changeDetection": "OnPush" + "@schematics/angular": { + "component": { + "changeDetection": "OnPush" + } } - } }, "version": 1, "newProjectRoot": "projects", @@ -31,7 +31,8 @@ "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico", "src/assets"], "styles": ["src/css/main.css"], - "scripts": [] + "scripts": [], + "allowedCommonJsDependencies": ["markdown-it-task-lists"] }, "configurations": { "production": { @@ -39,12 +40,12 @@ { "type": "initial", "maximumWarning": "500kb", - "maximumError": "1mb" + "maximumError": "1.5mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", - "maximumError": "5kb" + "maximumError": "6kb" } ], "fileReplacements": [ @@ -138,7 +139,8 @@ "e2e-ci": { "builder": "@cypress/schematic:cypress", "options": { - "devServerTarget": "client-v2:serve" + "devServerTarget": "client-v2:serve", + "record": true }, "configurations": { "production": { diff --git a/client-v2/colors.json b/client-v2/colors.json index 5afdff5b..50ae37e0 100644 --- a/client-v2/colors.json +++ b/client-v2/colors.json @@ -6,6 +6,7 @@ "400": "hsl(269.9, 100%, 54.8%)", "500": "hsl(269.9, 100%, 46.1%)", "600": "hsl(269.9, 100%, 40.4%)", + "650": "hsl(269.9, 100%, 33.4%)", "700": "hsl(269.9, 100%, 22.1%)", "800": "hsl(269.9, 100%, 10%)", "900": "hsl(269.9, 100%, 6.9%)" @@ -52,6 +53,7 @@ "600": "hsl(262.79999999999995, 19.8%, 37%)", "700": "hsl(262.79999999999995, 19.8%, 27%)", "800": "hsl(262.79999999999995, 19.8%, 17%)", + "850": "hsl(262.79999999999995, 19.8%, 12%)", "900": "hsl(262.79999999999995, 19.8%, 8%)" } } \ No newline at end of file diff --git a/client-v2/cypress.config.ts b/client-v2/cypress.config.ts index a4e11c0d..a7cc6715 100644 --- a/client-v2/cypress.config.ts +++ b/client-v2/cypress.config.ts @@ -1,9 +1,16 @@ import { defineConfig } from 'cypress' export default defineConfig({ + projectId: 'zwatkd', + retries: 2, e2e: { baseUrl: 'http://localhost:4200', supportFile: './cypress/support/e2e.ts', + + video: true, + videosFolder: './.cypress/e2e/videos', + screenshotsFolder: './.cypress/e2e/screenshots', + downloadsFolder: './.cypress/e2e/downloads', }, component: { @@ -12,5 +19,10 @@ export default defineConfig({ bundler: 'webpack', }, specPattern: '**/*.test.ts', + + video: true, + videosFolder: './.cypress/component/videos', + screenshotsFolder: './.cypress/component/screenshots', + downloadsFolder: './.cypress/component/downloads', }, }) diff --git a/client-v2/cypress/e2e/auth.cy.ts b/client-v2/cypress/e2e/auth.cy.ts index ddddd9c0..82f0dda0 100644 --- a/client-v2/cypress/e2e/auth.cy.ts +++ b/client-v2/cypress/e2e/auth.cy.ts @@ -25,8 +25,9 @@ describe('Authentication', () => { describe('Login', () => { it('can login', () => { + // @TODO: there's sth wrong here, fix it // this should not be necessary, but somehow a previous `signup` call from within `beforeEach` prevents the following signup - cy.clearDb() + // cy.clearDb() cy.signup() cy.clearLocalStorage() diff --git a/client-v2/cypress/e2e/workspace.cy.ts b/client-v2/cypress/e2e/workspace.cy.ts index a391a444..d44b3351 100644 --- a/client-v2/cypress/e2e/workspace.cy.ts +++ b/client-v2/cypress/e2e/workspace.cy.ts @@ -53,9 +53,9 @@ describe('Workspace', () => { cy.get('[data-test-is-loading="false"]') // wait for loading to finish cy.get(testName('entity-tree-node')) .first() - .focus() .within(() => { - cy.get(testName('open-menu')).click() + // we need to force the click because the element might not be visible in ci (even after focusing the parent which should make it visible) + cy.get(testName('open-menu')).click({ force: true }) }) cy.get(testName('drop-down-menu')).should('exist') @@ -63,7 +63,7 @@ describe('Workspace', () => { }) }) - describe.only('Entity page', () => { + describe('Entity page', () => { describe('Tasklist view', () => { it('can edit the entity name', () => { cy.get(testName('sidebar-create-new-list')).click() @@ -73,18 +73,54 @@ describe('Workspace', () => { cy.get(testName('entity-tree-node')).should('contain.text', entityName) }) - it('can edit the description', () => { + it('can add a description', () => { cy.get(testName('sidebar-create-new-list')).click() cy.get(testName('editable-entity-name')) const description = 'The testing entity description' + // wait for loading to finish + cy.get(testName('entity-name-container')).within(() => { + cy.get('[data-test-is-loading="false"]') + }) + cy.get(testName('add-description')).click() - cy.get(testName('description-editor')).type(description).blur() + cy.get(testName('add-description')).should('not.exist') + + cy.get(testName('description-editor')).focused().type(description).blur() cy.wait('@updateList').its('response.statusCode').should('equal', 200) // we currently don't have any other way to verify if updating the description has succeeded // maybe it is not a bad idea to assert on the request, but we could take this a step further and verify that the db record was updated }) + it('can update the description', () => { + cy.get(testName('sidebar-create-new-list')).click() + cy.get(testName('editable-entity-name')) + + // wait for loading to finish + cy.get(testName('entity-name-container')).within(() => { + cy.get('[data-test-is-loading="false"]') + }) + + cy.get(testName('add-description')).click() + cy.get(testName('add-description')).should('not.exist') + + const description = 'The testing entity description' + cy.get(testName('description-editor')).focused().type(description).blur() + cy.wait('@updateList').its('response.statusCode').should('equal', 200) + + // wait for loading to finish + cy.get(testName('entity-name-container')).within(() => { + cy.get('[data-test-is-loading="false"]') + }) + + cy.get(testName('add-description')).should('not.exist') + + const descriptionUpdate = ' - With updates' + cy.get(testName('description-editor')).click() + cy.get(testName('description-editor')).focused().type(descriptionUpdate).blur() + cy.wait('@updateList').its('response.statusCode').should('equal', 200) + }) + it('can add children', () => { cy.get(testName('sidebar-create-new-list')).click() cy.get(testName('editable-entity-name')) @@ -137,14 +173,51 @@ describe('Workspace', () => { // @TODO: assert that task-tree has changed }) - it('can edit the description', () => { + it('can add a description', () => { const description = 'The testing entity description' + // wait for loading to finish + cy.get(testName('entity-name-container')).within(() => { + cy.get('[data-test-is-loading="false"]') + }) + cy.get(testName('add-description')).click() - cy.get(testName('description-editor')).type(description).blur() + cy.get(testName('add-description')).should('not.exist') + + cy.get(testName('description-editor')).focused().type(description).blur() cy.wait('@updateTask').its('response.statusCode').should('equal', 200) }) + // seems to be a flaky test + it.skip('can update the description', () => { + cy.get(testName('sidebar-create-new-list')).click() + cy.get(testName('editable-entity-name')) + + // wait for loading to finish + cy.get(testName('entity-name-container')).within(() => { + cy.get('[data-test-is-loading="false"]') + }) + + cy.get(testName('add-description')).click() + cy.get(testName('add-description')).should('not.exist') + + const description = 'The testing entity description' + cy.get(testName('description-editor')).focused().type(description).blur() + cy.wait('@updateList').its('response.statusCode').should('equal', 200) + + // wait for loading to finish + cy.get(testName('entity-name-container')).within(() => { + cy.get('[data-test-is-loading="false"]') + }) + + cy.get(testName('add-description')).should('not.exist') + + const descriptionUpdate = ' - With updates' + cy.get(testName('description-editor')).click() + cy.get(testName('description-editor')).focused().type(descriptionUpdate).blur() + cy.wait('@updateList').its('response.statusCode').should('equal', 200) + }) + it('can add tasks', () => { cy.get(testName('create-subtask')).click() cy.get(testName('task-tree-node')).should('exist') @@ -200,9 +273,9 @@ describe('Workspace', () => { cy.get(testName('task-menu-button')).click() }) - // task menu + // task menu - create new subtask cy.get(testName('drop-down-menu')).within(() => { - cy.contains(/Subtask/) + cy.contains(/Subtask/i) .closest(testName('menu-item')) .click() }) @@ -210,6 +283,32 @@ describe('Workspace', () => { cy.get(testName('task-tree-node')).should('have.length', 2) cy.get(testName('task-tree-node')).last().should('have.attr', 'data-test-node-level', 1) }) + + describe('Task description', () => { + it('can add a description', () => { + cy.get(testName('task-tree-node')).within(() => { + cy.get(testName('task-menu-button')).click() + }) + + // task menu - Add description + cy.get(testName('drop-down-menu')).within(() => { + cy.contains(/Description/i) + .closest(testName('menu-item')) + .click() + }) + + const descriptionCy = cy.get(testName('task-tree-node')).focused() + descriptionCy.type('This is a description').blur() + cy.wait('@updateTask').its('response.statusCode').should('equal', 200) + }) + + it.skip('can update a description', () => { + // @TODO: + }) + it.skip('can open the task as page from the description toolbar', () => { + // @TODO: + }) + }) }) }) }) diff --git a/client-v2/cypress/support/commands.ts b/client-v2/cypress/support/commands.ts index 19de9822..bb89f071 100644 --- a/client-v2/cypress/support/commands.ts +++ b/client-v2/cypress/support/commands.ts @@ -23,7 +23,11 @@ function setLocalStorage(itemName: string, itemValue: string) { Cypress.Commands.add('setLocalStorage', setLocalStorage) function clearDb() { - cy.request('http://localhost:3001/clear-db').as('clearDb') + cy.request('http://localhost:3001/clear-db') + .as('clearDb') + .then(res => { + expect(res.status).to.eq(200) + }) } Cypress.Commands.add('clearDb', clearDb) @@ -36,6 +40,8 @@ function signup() { cy.request({ method: 'POST', url: 'http://localhost:3001/auth/signup', body: signupDto }) .as('shadow-signup') .then(res => { + expect(res.status).to.eq(201) + // token needs to be JSON parsable cy.setLocalStorage('rockket-auth-token', `${JSON.stringify(res.body.user.authToken)}`) }) diff --git a/client-v2/cypress/support/helpers.ts b/client-v2/cypress/support/helpers.ts index 8ba2e1d8..739dad6e 100644 --- a/client-v2/cypress/support/helpers.ts +++ b/client-v2/cypress/support/helpers.ts @@ -2,8 +2,8 @@ import { interceptItem } from '../../src/app/utils/menu-item.helpers' export const testName = (testName: string) => `[data-test-name="${testName}"]` -export const useStubsForActions = (stubMap?: Record>) => { - return interceptItem(({ action, title }) => { +export const useStubsForActions = (stubMap?: Record>) => { + return interceptItem(({ action, title }) => { if (!action) return {} // we cannot use optional chaining syntax here, because cypress->webpack complains diff --git a/client-v2/docs/known-issues.md b/client-v2/docs/known-issues.md new file mode 100644 index 00000000..e4785bd8 --- /dev/null +++ b/client-v2/docs/known-issues.md @@ -0,0 +1,5 @@ +# Known issues + +## Rich text editor + +- Toggling a task item does not toggle it's state? - This might happen if you have a password manager extension installed, like Keeper or Bitwarden. Related issues: https://github.com/ueberdosis/tiptap/issues/2697, https://github.com/ueberdosis/tiptap/issues/2372 \ No newline at end of file diff --git a/client-v2/package-lock.json b/client-v2/package-lock.json index 1c544639..97689283 100644 --- a/client-v2/package-lock.json +++ b/client-v2/package-lock.json @@ -27,7 +27,16 @@ "@ngrx/store-devtools": "^14.3.2", "@rx-angular/cdk": "^14.0.0", "@rx-angular/template": "^14.0.0", + "@tiptap/core": "^2.0.3", + "@tiptap/extension-link": "^2.0.3", + "@tiptap/extension-placeholder": "^2.0.3", + "@tiptap/extension-task-item": "^2.0.3", + "@tiptap/extension-task-list": "^2.0.3", + "@tiptap/extension-typography": "^2.0.3", + "@tiptap/starter-kit": "^2.0.3", + "ngx-tiptap": "^6.0.0", "rxjs": "~7.8.0", + "tiptap-markdown": "^0.7.4", "tslib": "^2.5.0", "zone.js": "^0.12.0" }, @@ -44,7 +53,7 @@ "@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/parser": "^5.59.5", "autoprefixer": "^10.4.14", - "cypress": "latest", + "cypress": "^13.5.1", "eslint": "^8.41.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^4.2.1", @@ -2138,7 +2147,6 @@ }, "node_modules/@babel/runtime": { "version": "7.18.9", - "dev": true, "license": "MIT", "dependencies": { "regenerator-runtime": "^0.13.4" @@ -2514,9 +2522,10 @@ } }, "node_modules/@cypress/request": { - "version": "2.88.10", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -2531,9 +2540,9 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "~6.5.2", + "qs": "6.10.4", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", + "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" }, @@ -2542,11 +2551,18 @@ } }, "node_modules/@cypress/request/node_modules/qs": { - "version": "6.5.3", + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", + "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", "dev": true, - "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, "engines": { "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/@cypress/schematic": { @@ -2883,6 +2899,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@linaria/core": { + "version": "3.0.0-beta.13", + "resolved": "https://registry.npmjs.org/@linaria/core/-/core-3.0.0-beta.13.tgz", + "integrity": "sha512-3zEi5plBCOsEzUneRVuQb+2SAx3qaC1dj0FfFAI6zIJQoDWu0dlSwKijMRack7oO9tUWrchfj3OkKQAd1LBdVg==", + "peer": true + }, "node_modules/@microsoft/applicationinsights-analytics-js": { "version": "2.8.11", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-analytics-js/-/applicationinsights-analytics-js-2.8.11.tgz", @@ -3258,6 +3280,69 @@ "node": ">= 8" } }, + "node_modules/@popperjs/core": { + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", + "integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@remirror/core-constants": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-2.0.0.tgz", + "integrity": "sha512-vpePPMecHJllBqCWXl6+FIcZqS+tRUM2kSCCKFeEo1H3XUEv3ocijBIPhnlSAa7g6maX+12ATTgxrOsLpWVr2g==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@remirror/core-helpers": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@remirror/core-helpers/-/core-helpers-2.0.1.tgz", + "integrity": "sha512-s8M1pn33aBUhduvD1QR02uUQMegnFkGaTr4c1iBzxTTyg0rbQstzuQ7Q8TkL6n64JtgCdJS9jLz2dONb2meBKQ==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@linaria/core": "3.0.0-beta.13", + "@remirror/core-constants": "^2.0.0", + "@remirror/types": "^1.0.0", + "@types/object.omit": "^3.0.0", + "@types/object.pick": "^1.3.1", + "@types/throttle-debounce": "^2.1.0", + "case-anything": "^2.1.10", + "dash-get": "^1.0.2", + "deepmerge": "^4.2.2", + "fast-deep-equal": "^3.1.3", + "make-error": "^1.3.6", + "object.omit": "^3.0.0", + "object.pick": "^1.3.0", + "throttle-debounce": "^3.0.1" + } + }, + "node_modules/@remirror/types": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@remirror/types/-/types-1.0.0.tgz", + "integrity": "sha512-7HQbW7k8VxrAtfzs9FxwO6XSDabn8tSFDi1wwzShOnU+cvaYpfxu0ygyTk3TpXsag1hgFKY3ZIlAfB4WVz2LkQ==", + "peer": true, + "dependencies": { + "type-fest": "^2.0.0" + } + }, + "node_modules/@remirror/types/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "peer": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@rx-angular/cdk": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/@rx-angular/cdk/-/cdk-14.0.0.tgz", @@ -3336,6 +3421,402 @@ "dev": true, "license": "MIT" }, + "node_modules/@tiptap/core": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.0.3.tgz", + "integrity": "sha512-jLyVIWAdjjlNzrsRhSE2lVL/7N8228/1R1QtaVU85UlMIwHFAcdzhD8FeiKkqxpTnGpaDVaTy7VNEtEgaYdCyA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.0.3.tgz", + "integrity": "sha512-rkUcFv2iL6f86DBBHoa4XdKNG2StvkJ7tfY9GoMpT46k3nxOaMTqak9/qZOo79TWxMLYtXzoxtKIkmWsbbcj4A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.0.3.tgz", + "integrity": "sha512-OGT62fMRovSSayjehumygFWTg2Qn0IDbqyMpigg/RUAsnoOI2yBZFVrdM2gk1StyoSay7gTn2MLw97IUfr7FXg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.3.tgz", + "integrity": "sha512-lPt1ELrYCuoQrQEUukqjp9xt38EwgPUwaKHI3wwt2Rbv+C6q1gmRsK1yeO/KqCNmFxNqF2p9ZF9srOnug/RZDQ==", + "peer": true, + "dependencies": { + "tippy.js": "^6.3.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/pm": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.3.tgz", + "integrity": "sha512-RtaLiRvZbMTOje+FW5bn+mYogiIgNxOm065wmyLPypnTbLSeHeYkoqVSqzZeqUn+7GLnwgn1shirUe6csVE/BA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.0.3.tgz", + "integrity": "sha512-LsVCKVxgBtkstAr1FjxN8T3OjlC76a2X8ouoZpELMp+aXbjqyanCKzt+sjjUhE4H0yLFd4v+5v6UFoCv4EILiw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.0.3.tgz", + "integrity": "sha512-F4xMy18EwgpyY9f5Te7UuF7UwxRLptOtCq1p2c2DfxBvHDWhAjQqVqcW/sq/I/WuED7FwCnPLyyAasPiVPkLPw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/pm": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.0.3.tgz", + "integrity": "sha512-PsYeNQQBYIU9ayz1R11Kv/kKNPFNIV8tApJ9pxelXjzcAhkjncNUazPN/dyho60mzo+WpsmS3ceTj/gK3bCtWA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.0.3.tgz", + "integrity": "sha512-McthMrfusn6PjcaynJLheZJcXto8TaIW5iVitYh8qQrDXr31MALC/5GvWuiswmQ8bAXiWPwlLDYE/OJfwtggaw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/pm": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.3.tgz", + "integrity": "sha512-zN1vRGRvyK3pO2aHRmQSOTpl4UJraXYwKYM009n6WviYKUNm0LPGo+VD4OAtdzUhPXyccnlsTv2p6LIqFty6Bg==", + "peer": true, + "dependencies": { + "tippy.js": "^6.3.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/pm": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.3.tgz", + "integrity": "sha512-6I9EzzsYOyyqDvDvxIK6Rv3EXB+fHKFj8ntHO8IXmeNJ6pkhOinuXVsW6Yo7TcDYoTj4D5I2MNFAW2rIkgassw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/pm": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.0.3.tgz", + "integrity": "sha512-RCln6ARn16jvKTjhkcAD5KzYXYS0xRMc0/LrHeV8TKdCd4Yd0YYHe0PU4F9gAgAfPQn7Dgt4uTVJLN11ICl8sQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.0.3.tgz", + "integrity": "sha512-f0IEv5ms6aCzL80WeZ1qLCXTkRVwbpRr1qAETjg3gG4eoJN18+lZNOJYpyZy3P92C5KwF2T3Av00eFyVLIbb8Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-history": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.0.3.tgz", + "integrity": "sha512-00KHIcJ8kivn2ARI6NQYphv2LfllVCXViHGm0EhzDW6NQxCrriJKE3tKDcTFCu7LlC5doMpq9Z6KXdljc4oVeQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/pm": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.0.3.tgz", + "integrity": "sha512-SZRUSh07b/M0kJHNKnfBwBMWrZBEm/E2LrK1NbluwT3DBhE+gvwiEdBxgB32zKHNxaDEXUJwUIPNC3JSbKvPUA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/pm": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.0.3.tgz", + "integrity": "sha512-cfS5sW0gu7qf4ihwnLtW/QMTBrBEXaT0sJl3RwkhjIBg/65ywJKE5Nz9ewnQHmDeT18hvMJJ1VIb4j4ze9jj9A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.0.3.tgz", + "integrity": "sha512-H72tXQ5rkVCkAhFaf08fbEU7EBUCK0uocsqOF+4th9sOlrhfgyJtc8Jv5EXPDpxNgG5jixSqWBo0zKXQm9s9eg==", + "dependencies": { + "linkifyjs": "^4.1.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/pm": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.0.3.tgz", + "integrity": "sha512-p7cUsk0LpM1PfdAuFE8wYBNJ3gvA0UhNGR08Lo++rt9UaCeFLSN1SXRxg97c0oa5+Ski7SrCjIJ5Ynhz0viTjQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.3.tgz", + "integrity": "sha512-ZB3MpZh/GEy1zKgw7XDQF4FIwycZWNof1k9WbDZOI063Ch4qHZowhVttH2mTCELuyvTMM/o9a8CS7qMqQB48bw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.0.3.tgz", + "integrity": "sha512-a+tKtmj4bU3GVCH1NE8VHWnhVexxX5boTVxsHIr4yGG3UoKo1c5AO7YMaeX2W5xB5iIA+BQqOPCDPEAx34dd2A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-placeholder": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.0.3.tgz", + "integrity": "sha512-Z42jo0termRAf0S0L8oxrts94IWX5waU4isS2CUw8xCUigYyCFslkhQXkWATO1qRbjNFLKN2C9qvCgGf4UeBrw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/pm": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.0.3.tgz", + "integrity": "sha512-RO4/EYe2iPD6ifDHORT8fF6O9tfdtnzxLGwZIKZXnEgtweH+MgoqevEzXYdS+54Wraq4TUQGNcsYhe49pv7Rlw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-task-item": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-2.0.3.tgz", + "integrity": "sha512-13u1Q769WiSNcjFieYAMuJyWXNaY9yOdw6WFg9tQg4EZ5h6+2DaxB0qmu6I3pH+wwSn2UkCkXIirAo/k7wnzbw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/pm": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-task-list": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-task-list/-/extension-task-list-2.0.3.tgz", + "integrity": "sha512-NdW0RtMF2L96qy+j946mTB5Av6Qn5L3vGVWFmJA6/JPXr9Uj/grItCmqUQKHfPBSFow7UqBY82ODblP+GQFgew==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.0.3.tgz", + "integrity": "sha512-LvzChcTCcPSMNLUjZe/A9SHXWGDHtvk73fR7CBqAeNU0MxhBPEBI03GFQ6RzW3xX0CmDmjpZoDxFMB+hDEtW1A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/extension-typography": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-typography/-/extension-typography-2.0.3.tgz", + "integrity": "sha512-5U91O2dffYOvwenWG+zT1N/pnt+RppSlocxs1KaNWFLlI2fgzDTyUyjzygIHGmskStqay2MuvmPnfVABoC+1Gw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/pm": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.0.3.tgz", + "integrity": "sha512-I9dsInD89Agdm1QjFRO9dmJtU1ldVSILNPW0pEhv9wYqYVvl4HUj/JMtYNqu2jWrCHNXQcaX/WkdSdvGJtmg5g==", + "peer": true, + "dependencies": { + "prosemirror-changeset": "^2.2.0", + "prosemirror-collab": "^1.3.0", + "prosemirror-commands": "^1.3.1", + "prosemirror-dropcursor": "^1.5.0", + "prosemirror-gapcursor": "^1.3.1", + "prosemirror-history": "^1.3.0", + "prosemirror-inputrules": "^1.2.0", + "prosemirror-keymap": "^1.2.0", + "prosemirror-markdown": "^1.10.1", + "prosemirror-menu": "^1.2.1", + "prosemirror-model": "^1.18.1", + "prosemirror-schema-basic": "^1.2.0", + "prosemirror-schema-list": "^1.2.2", + "prosemirror-state": "^1.4.1", + "prosemirror-tables": "^1.3.0", + "prosemirror-trailing-node": "^2.0.2", + "prosemirror-transform": "^1.7.0", + "prosemirror-view": "^1.28.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.0.3.tgz", + "integrity": "sha512-t4WG4w93zTpL2VxhVyJJvl3kdLF001ZrhpOuEiZqEMBMUMbM56Uiigv1CnUQpTFrjDAh3IM8hkqzAh20TYw2iQ==", + "dependencies": { + "@tiptap/core": "^2.0.3", + "@tiptap/extension-blockquote": "^2.0.3", + "@tiptap/extension-bold": "^2.0.3", + "@tiptap/extension-bullet-list": "^2.0.3", + "@tiptap/extension-code": "^2.0.3", + "@tiptap/extension-code-block": "^2.0.3", + "@tiptap/extension-document": "^2.0.3", + "@tiptap/extension-dropcursor": "^2.0.3", + "@tiptap/extension-gapcursor": "^2.0.3", + "@tiptap/extension-hard-break": "^2.0.3", + "@tiptap/extension-heading": "^2.0.3", + "@tiptap/extension-history": "^2.0.3", + "@tiptap/extension-horizontal-rule": "^2.0.3", + "@tiptap/extension-italic": "^2.0.3", + "@tiptap/extension-list-item": "^2.0.3", + "@tiptap/extension-ordered-list": "^2.0.3", + "@tiptap/extension-paragraph": "^2.0.3", + "@tiptap/extension-strike": "^2.0.3", + "@tiptap/extension-text": "^2.0.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "dev": true, @@ -3488,6 +3969,25 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==" + }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==" + }, "node_modules/@types/mime": { "version": "3.0.1", "dev": true, @@ -3499,10 +3999,25 @@ "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==" }, "node_modules/@types/node": { - "version": "18.15.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", - "dev": true + "version": "18.18.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.9.tgz", + "integrity": "sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/object.omit": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/object.omit/-/object.omit-3.0.0.tgz", + "integrity": "sha512-I27IoPpH250TUzc9FzXd0P1BV/BMJuzqD3jOz98ehf9dQqGkxlq+hO1bIqZGWqCg5bVOy0g4AUVJtnxe0klDmw==", + "peer": true + }, + "node_modules/@types/object.pick": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/object.pick/-/object.pick-1.3.2.tgz", + "integrity": "sha512-sn7L+qQ6RLPdXRoiaE7bZ/Ek+o4uICma/lBFPyJEKDTPTBP1W8u0c4baj3EiS4DiqLs+Hk+KUGvMVJtAw3ePJg==", + "peer": true }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -3565,6 +4080,12 @@ "@types/node": "*" } }, + "node_modules/@types/throttle-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz", + "integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==", + "peer": true + }, "node_modules/@types/ws": { "version": "8.5.3", "dev": true, @@ -4511,16 +5032,18 @@ }, "node_modules/asn1": { "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "dev": true, - "license": "MIT", "dependencies": { "safer-buffer": "~2.1.0" } }, "node_modules/assert-plus": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.8" } @@ -4581,16 +5104,18 @@ }, "node_modules/aws-sign2": { "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "*" } }, "node_modules/aws4": { - "version": "1.11.0", - "dev": true, - "license": "MIT" + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", + "dev": true }, "node_modules/axios": { "version": "0.27.2", @@ -4743,8 +5268,9 @@ }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "tweetnacl": "^0.14.3" } @@ -5070,10 +5596,23 @@ } ] }, + "node_modules/case-anything": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz", + "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==", + "peer": true, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/caseless": { "version": "0.12.0", - "dev": true, - "license": "Apache-2.0" + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true }, "node_modules/chalk": { "version": "2.4.2", @@ -5567,6 +6106,12 @@ "dev": true, "license": "MIT" }, + "node_modules/crelt": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz", + "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==", + "peer": true + }, "node_modules/critters": { "version": "0.0.16", "dev": true, @@ -5796,15 +6341,15 @@ "license": "MIT" }, "node_modules/cypress": { - "version": "12.10.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.10.0.tgz", - "integrity": "sha512-Y0wPc221xKKW1/4iAFCphkrG2jNR4MjOne3iGn4mcuCaE7Y5EtXL83N8BzRsAht7GYfWVjJ/UeTqEdDKHz39HQ==", + "version": "13.5.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.5.1.tgz", + "integrity": "sha512-yqLViT0D/lPI8Kkm7ciF/x/DCK/H/DnogdGyiTnQgX4OVR2aM30PtK+kvklTOD1u3TuItiD9wUQAF8EYWtyZug==", "dev": true, "hasInstallScript": true, "dependencies": { - "@cypress/request": "^2.88.10", + "@cypress/request": "^3.0.0", "@cypress/xvfb": "^1.2.4", - "@types/node": "^14.14.31", + "@types/node": "^18.17.5", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", @@ -5837,9 +6382,10 @@ "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", + "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "semver": "^7.3.2", + "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.1", "untildify": "^4.0.0", @@ -5849,14 +6395,9 @@ "cypress": "bin/cypress" }, "engines": { - "node": "^14.0.0 || ^16.0.0 || >=18.0.0" + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" } }, - "node_modules/cypress/node_modules/@types/node": { - "version": "14.18.28", - "dev": true, - "license": "MIT" - }, "node_modules/cypress/node_modules/ansi-styles": { "version": "4.3.0", "dev": true, @@ -6032,10 +6573,17 @@ "node": ">= 10.0.0" } }, + "node_modules/dash-get": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/dash-get/-/dash-get-1.0.2.tgz", + "integrity": "sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==", + "peer": true + }, "node_modules/dashdash": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "dev": true, - "license": "MIT", "dependencies": { "assert-plus": "^1.0.0" }, @@ -6077,6 +6625,15 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/default-gateway": { "version": "6.0.3", "dev": true, @@ -6310,8 +6867,9 @@ }, "node_modules/ecc-jsbn": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "dev": true, - "license": "MIT", "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -7254,11 +7812,12 @@ }, "node_modules/extsprintf": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "dev": true, "engines": [ "node >=0.6.0" - ], - "license": "MIT" + ] }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -7461,16 +8020,18 @@ }, "node_modules/forever-agent": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "*" } }, "node_modules/form-data": { "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "dev": true, - "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -7645,8 +8206,9 @@ }, "node_modules/getpass": { "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "dev": true, - "license": "MIT", "dependencies": { "assert-plus": "^1.0.0" } @@ -7948,8 +8510,9 @@ }, "node_modules/http-signature": { "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", "dev": true, - "license": "MIT", "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^2.0.2", @@ -8301,6 +8864,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "peer": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "license": "MIT", @@ -8381,7 +8956,6 @@ }, "node_modules/is-plain-object": { "version": "2.0.4", - "dev": true, "license": "MIT", "dependencies": { "isobject": "^3.0.1" @@ -8403,8 +8977,9 @@ }, "node_modules/is-typedarray": { "version": "1.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true }, "node_modules/is-unicode-supported": { "version": "0.1.0", @@ -8455,7 +9030,6 @@ }, "node_modules/isobject": { "version": "3.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8463,8 +9037,9 @@ }, "node_modules/isstream": { "version": "0.1.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", @@ -8635,8 +9210,9 @@ }, "node_modules/jsbn": { "version": "0.1.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true }, "node_modules/jsesc": { "version": "2.5.2", @@ -8656,8 +9232,9 @@ }, "node_modules/json-schema": { "version": "0.4.0", - "dev": true, - "license": "(AFL-2.1 OR BSD-3-Clause)" + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true }, "node_modules/json-schema-traverse": { "version": "1.0.0", @@ -8670,8 +9247,9 @@ }, "node_modules/json-stringify-safe": { "version": "5.0.1", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true }, "node_modules/json5": { "version": "2.2.2", @@ -8707,11 +9285,12 @@ }, "node_modules/jsprim": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", "dev": true, "engines": [ "node >=0.6.0" ], - "license": "MIT", "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -9033,6 +9612,19 @@ "dev": true, "license": "MIT" }, + "node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/linkifyjs": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.1.tgz", + "integrity": "sha512-zFN/CTVmbcVef+WaDXT63dNzzkfRBKT1j464NJQkV7iSgJU0sLBus9W0HBwnXK13/hf168pbrx/V/bjEHOXNHA==" + }, "node_modules/listr2": { "version": "3.14.0", "dev": true, @@ -9314,7 +9906,6 @@ }, "node_modules/make-error": { "version": "1.3.6", - "dev": true, "license": "ISC" }, "node_modules/make-fetch-happen": { @@ -9348,12 +9939,53 @@ "dev": true, "license": "ISC", "engines": { - "node": ">=12" + "node": ">=12" + } + }, + "node_modules/map-stream": { + "version": "0.1.0", + "dev": true + }, + "node_modules/markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==" + }, + "node_modules/markdown-it/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==" + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/map-stream": { - "version": "0.1.0", - "dev": true + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" }, "node_modules/media-typer": { "version": "0.3.0", @@ -9759,6 +10391,25 @@ "node": "*" } }, + "node_modules/ngx-tiptap": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ngx-tiptap/-/ngx-tiptap-6.0.0.tgz", + "integrity": "sha512-3wex205OhqHHF7CrJD5/OUdwiE3dNJnpZB0dugnGfyGdVqpbxs+s1eUkKKa7ePIUG7Yy96sOen1X9odIDeBzCA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=14.0.0", + "@angular/core": ">=14.0.0", + "@angular/forms": ">=14.0.0", + "@tiptap/core": "^2.0.0-beta.181", + "@tiptap/extension-bubble-menu": "^2.0.0-beta.61", + "@tiptap/extension-floating-menu": "^2.0.0-beta.56", + "prosemirror-model": "^1.18.1", + "prosemirror-state": "^1.4.1", + "prosemirror-view": "^1.26.2" + } + }, "node_modules/nice-napi": { "version": "1.0.2", "dev": true, @@ -10087,6 +10738,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.omit": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-3.0.0.tgz", + "integrity": "sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==", + "peer": true, + "dependencies": { + "is-extendable": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "peer": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/obuf": { "version": "1.1.2", "dev": true, @@ -10243,6 +10918,11 @@ "node": ">=8" } }, + "node_modules/orderedmap": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.0.tgz", + "integrity": "sha512-/pIFexOm6S70EPdznemIz3BQZoJ4VTFrhqzu0ACBqBgeLsLxq8e6Jim63ImIfwW/zAD1AlXpRMlOv3aghmo4dA==" + }, "node_modules/os-tmpdir": { "version": "1.0.2", "dev": true, @@ -10501,8 +11181,9 @@ }, "node_modules/performance-now": { "version": "2.1.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true }, "node_modules/picocolors": { "version": "1.0.0", @@ -11379,6 +12060,15 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "dev": true, @@ -11401,6 +12091,211 @@ "node": ">=10" } }, + "node_modules/prosemirror-changeset": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.2.0.tgz", + "integrity": "sha512-QM7ohGtkpVpwVGmFb8wqVhaz9+6IUXcIQBGZ81YNAKYuHiFJ1ShvSzab4pKqTinJhwciZbrtBEk/2WsqSt2PYg==", + "peer": true, + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-collab": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.0.tgz", + "integrity": "sha512-+S/IJ69G2cUu2IM5b3PBekuxs94HO1CxJIWOFrLQXUaUDKL/JfBx+QcH31ldBlBXyDEUl+k3Vltfi1E1MKp2mA==", + "peer": true, + "dependencies": { + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.5.1.tgz", + "integrity": "sha512-ga1ga/RkbzxfAvb6iEXYmrEpekn5NCwTb8w1dr/gmhSoaGcQ0VPuCzOn5qDEpC45ql2oDkKoKQbRxLJwKLpMTQ==", + "peer": true, + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.0.tgz", + "integrity": "sha512-TZMitR8nlp9Xh42pDYGcWopCoFPmJduoyGJ7FjYM2/7gZKnfD41TIaZN5Q1cQjm6Fm/P5vk/DpVYFhS8kDdigw==", + "peer": true, + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.1.tgz", + "integrity": "sha512-GKTeE7ZoMsx5uVfc51/ouwMFPq0o8YrZ7Hx4jTF4EeGbXxBveUV8CGv46mSHuBBeXGmvu50guoV2kSnOeZZnUA==", + "peer": true, + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.3.0.tgz", + "integrity": "sha512-qo/9Wn4B/Bq89/YD+eNWFbAytu6dmIM85EhID+fz9Jcl9+DfGEo8TTSrRhP15+fFEoaPqpHSxlvSzSEbmlxlUA==", + "peer": true, + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.2.0.tgz", + "integrity": "sha512-eAW/M/NTSSzpCOxfR8Abw6OagdG0MiDAiWHQMQveIsZtoKVYzm0AflSPq/ymqJd56/Su1YPbwy9lM13wgHOFmQ==", + "peer": true, + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.1.tgz", + "integrity": "sha512-kVK6WGC+83LZwuSJnuCb9PsADQnFZllt94qPP3Rx/vLcOUV65+IbBeH2nS5cFggPyEVJhGkGrgYFRrG250WhHQ==", + "peer": true, + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-markdown": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.11.2.tgz", + "integrity": "sha512-Eu5g4WPiCdqDTGhdSsG9N6ZjACQRYrsAkrF9KYfdMaCmjIApH75aVncsWYOJvEk2i1B3i8jZppv3J/tnuHGiUQ==", + "dependencies": { + "markdown-it": "^13.0.1", + "prosemirror-model": "^1.0.0" + } + }, + "node_modules/prosemirror-menu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.1.tgz", + "integrity": "sha512-sBirXxVfHalZO4f1ZS63WzewINK4182+7dOmoMeBkqYO8wqMBvBS7wQuwVOHnkMWPEh0+N0LJ856KYUN+vFkmQ==", + "peer": true, + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.19.0.tgz", + "integrity": "sha512-/CvFGJnwc41EJSfDkQLly1cAJJJmBpZwwUJtwZPTjY2RqZJfM8HVbCreOY/jti8wTRbVyjagcylyGoeJH/g/3w==", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.1.tgz", + "integrity": "sha512-vYBdIHsYKSDIqYmPBC7lnwk9DsKn8PnVqK97pMYP5MLEDFqWIX75JiaJTzndBii4bRuNqhC2UfDOfM3FKhlBHg==", + "peer": true, + "dependencies": { + "prosemirror-model": "^1.19.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.2.2.tgz", + "integrity": "sha512-rd0pqSDp86p0MUMKG903g3I9VmElFkQpkZ2iOd3EOVg1vo5Cst51rAsoE+5IPy0LPXq64eGcCYlW1+JPNxOj2w==", + "peer": true, + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.2.tgz", + "integrity": "sha512-puuzLD2mz/oTdfgd8msFbe0A42j5eNudKAAPDB0+QJRw8cO1ygjLmhLrg9RvDpf87Dkd6D4t93qdef00KKNacQ==", + "peer": true, + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.3.2.tgz", + "integrity": "sha512-/9JTeN6s58Zq66HXaxP6uf8PAmc7XXKZFPlOGVtLvxEd6xBP6WtzaJB9wBjiGUzwbdhdMEy7V62yuHqk/3VrnQ==", + "peer": true, + "dependencies": { + "prosemirror-keymap": "^1.1.2", + "prosemirror-model": "^1.8.1", + "prosemirror-state": "^1.3.1", + "prosemirror-transform": "^1.2.1", + "prosemirror-view": "^1.13.3" + } + }, + "node_modules/prosemirror-trailing-node": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.3.tgz", + "integrity": "sha512-lGrjMrn97KWkjQSW/FjdvnhJmqFACmQIyr6lKYApvHitDnKsCoZz6XzrHB7RZYHni/0NxQmZ01p/2vyK2SkvaA==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@remirror/core-constants": "^2.0.0", + "@remirror/core-helpers": "^2.0.1", + "escape-string-regexp": "^4.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1", + "prosemirror-state": "^1", + "prosemirror-view": "^1" + } + }, + "node_modules/prosemirror-trailing-node/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==", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.7.1.tgz", + "integrity": "sha512-VteoifAfpt46z0yEt6Fc73A5OID9t/y2QIeR5MgxEwTuitadEunD/V0c9jQW8ziT8pbFM54uTzRLJ/nLuQjMxg==", + "peer": true, + "dependencies": { + "prosemirror-model": "^1.0.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.30.2.tgz", + "integrity": "sha512-nTNzZvalQf9kHeEyO407LiV6DoOs/pXsid88UqW9Vvybo4ozJW2PJhkfZUxCUF1hR/9vJLdhxX84wuw9P9HsXA==", + "peer": true, + "dependencies": { + "prosemirror-model": "^1.16.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "dev": true, @@ -11448,8 +12343,9 @@ }, "node_modules/psl": { "version": "1.9.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true }, "node_modules/pump": { "version": "3.0.0", @@ -11489,6 +12385,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "funding": [ @@ -11673,7 +12575,6 @@ }, "node_modules/regenerator-runtime": { "version": "0.13.9", - "dev": true, "license": "MIT" }, "node_modules/regenerator-transform": { @@ -11862,6 +12763,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rope-sequence": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.3.tgz", + "integrity": "sha512-85aZYCxweiD5J8yTEbw+E6A27zSnLPNDL0WfPdw3YYodq7WjnTKo0q4dtyQ2gz23iPT8Q9CUyJtAaUNcTxRf5Q==", + "peer": true + }, "node_modules/run-async": { "version": "2.4.1", "dev": true, @@ -12029,9 +12936,10 @@ } }, "node_modules/semver": { - "version": "7.3.8", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, - "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -12515,9 +13423,10 @@ "license": "BSD-3-Clause" }, "node_modules/sshpk": { - "version": "1.17.0", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "dev": true, - "license": "MIT", "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -12951,6 +13860,15 @@ "dev": true, "license": "MIT" }, + "node_modules/throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "peer": true, + "engines": { + "node": ">=10" + } + }, "node_modules/throttleit": { "version": "1.0.0", "dev": true, @@ -12966,6 +13884,29 @@ "dev": true, "license": "MIT" }, + "node_modules/tippy.js": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "peer": true, + "dependencies": { + "@popperjs/core": "^2.9.0" + } + }, + "node_modules/tiptap-markdown": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/tiptap-markdown/-/tiptap-markdown-0.7.4.tgz", + "integrity": "sha512-uK+QFkHSoBe8hcJIdGIQz+mqWy0VD1cMPpVVUqq9yzYPzxV2vaxv2BSWvYDOWHJ9+68lLZlliGdPxWCyNEUqKw==", + "dependencies": { + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1", + "markdown-it-task-lists": "^2.1.1", + "prosemirror-markdown": "^1.11.0" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.3" + } + }, "node_modules/tmp": { "version": "0.0.33", "dev": true, @@ -13004,15 +13945,27 @@ } }, "node_modules/tough-cookie": { - "version": "2.5.0", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "engines": { - "node": ">=0.8" + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" } }, "node_modules/tree-kill": { @@ -13113,8 +14066,9 @@ }, "node_modules/tunnel-agent": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, - "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" }, @@ -13124,8 +14078,9 @@ }, "node_modules/tweetnacl": { "version": "0.14.5", - "dev": true, - "license": "Unlicense" + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true }, "node_modules/type-check": { "version": "0.4.0", @@ -13198,6 +14153,17 @@ "node": "*" } }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "dev": true, @@ -13306,6 +14272,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "license": "MIT" @@ -13361,11 +14337,12 @@ }, "node_modules/verror": { "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "dev": true, "engines": [ "node >=0.6.0" ], - "license": "MIT", "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -13374,8 +14351,9 @@ }, "node_modules/verror/node_modules/core-util-is": { "version": "1.0.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true }, "node_modules/void-elements": { "version": "2.0.1", @@ -13385,6 +14363,12 @@ "node": ">=0.10.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", + "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==", + "peer": true + }, "node_modules/wait-on": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.0.1.tgz", @@ -15152,7 +16136,6 @@ }, "@babel/runtime": { "version": "7.18.9", - "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } @@ -15339,7 +16322,9 @@ "requires": {} }, "@cypress/request": { - "version": "2.88.10", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", "dev": true, "requires": { "aws-sign2": "~0.7.0", @@ -15355,16 +16340,21 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "~6.5.2", + "qs": "6.10.4", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", + "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" }, "dependencies": { "qs": { - "version": "6.5.3", - "dev": true + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", + "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } } } }, @@ -15615,6 +16605,12 @@ "version": "2.0.4", "dev": true }, + "@linaria/core": { + "version": "3.0.0-beta.13", + "resolved": "https://registry.npmjs.org/@linaria/core/-/core-3.0.0-beta.13.tgz", + "integrity": "sha512-3zEi5plBCOsEzUneRVuQb+2SAx3qaC1dj0FfFAI6zIJQoDWu0dlSwKijMRack7oO9tUWrchfj3OkKQAd1LBdVg==", + "peer": true + }, "@microsoft/applicationinsights-analytics-js": { "version": "2.8.11", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-analytics-js/-/applicationinsights-analytics-js-2.8.11.tgz", @@ -15868,6 +16864,61 @@ } } }, + "@popperjs/core": { + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", + "integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==", + "peer": true + }, + "@remirror/core-constants": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-2.0.0.tgz", + "integrity": "sha512-vpePPMecHJllBqCWXl6+FIcZqS+tRUM2kSCCKFeEo1H3XUEv3ocijBIPhnlSAa7g6maX+12ATTgxrOsLpWVr2g==", + "peer": true, + "requires": { + "@babel/runtime": "^7.13.10" + } + }, + "@remirror/core-helpers": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@remirror/core-helpers/-/core-helpers-2.0.1.tgz", + "integrity": "sha512-s8M1pn33aBUhduvD1QR02uUQMegnFkGaTr4c1iBzxTTyg0rbQstzuQ7Q8TkL6n64JtgCdJS9jLz2dONb2meBKQ==", + "peer": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@linaria/core": "3.0.0-beta.13", + "@remirror/core-constants": "^2.0.0", + "@remirror/types": "^1.0.0", + "@types/object.omit": "^3.0.0", + "@types/object.pick": "^1.3.1", + "@types/throttle-debounce": "^2.1.0", + "case-anything": "^2.1.10", + "dash-get": "^1.0.2", + "deepmerge": "^4.2.2", + "fast-deep-equal": "^3.1.3", + "make-error": "^1.3.6", + "object.omit": "^3.0.0", + "object.pick": "^1.3.0", + "throttle-debounce": "^3.0.1" + } + }, + "@remirror/types": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@remirror/types/-/types-1.0.0.tgz", + "integrity": "sha512-7HQbW7k8VxrAtfzs9FxwO6XSDabn8tSFDi1wwzShOnU+cvaYpfxu0ygyTk3TpXsag1hgFKY3ZIlAfB4WVz2LkQ==", + "peer": true, + "requires": { + "type-fest": "^2.0.0" + }, + "dependencies": { + "type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "peer": true + } + } + }, "@rx-angular/cdk": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/@rx-angular/cdk/-/cdk-14.0.0.tgz", @@ -15886,39 +16937,255 @@ "tslib": "^2.4.1" } }, - "@schematics/angular": { - "version": "14.2.10", - "dev": true, + "@schematics/angular": { + "version": "14.2.10", + "dev": true, + "requires": { + "@angular-devkit/core": "14.2.10", + "@angular-devkit/schematics": "14.2.10", + "jsonc-parser": "3.1.0" + } + }, + "@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "dev": true, + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true + }, + "@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true + }, + "@socket.io/component-emitter": { + "version": "3.1.0", + "dev": true + }, + "@tiptap/core": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.0.3.tgz", + "integrity": "sha512-jLyVIWAdjjlNzrsRhSE2lVL/7N8228/1R1QtaVU85UlMIwHFAcdzhD8FeiKkqxpTnGpaDVaTy7VNEtEgaYdCyA==", + "requires": {} + }, + "@tiptap/extension-blockquote": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.0.3.tgz", + "integrity": "sha512-rkUcFv2iL6f86DBBHoa4XdKNG2StvkJ7tfY9GoMpT46k3nxOaMTqak9/qZOo79TWxMLYtXzoxtKIkmWsbbcj4A==", + "requires": {} + }, + "@tiptap/extension-bold": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.0.3.tgz", + "integrity": "sha512-OGT62fMRovSSayjehumygFWTg2Qn0IDbqyMpigg/RUAsnoOI2yBZFVrdM2gk1StyoSay7gTn2MLw97IUfr7FXg==", + "requires": {} + }, + "@tiptap/extension-bubble-menu": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.3.tgz", + "integrity": "sha512-lPt1ELrYCuoQrQEUukqjp9xt38EwgPUwaKHI3wwt2Rbv+C6q1gmRsK1yeO/KqCNmFxNqF2p9ZF9srOnug/RZDQ==", + "peer": true, + "requires": { + "tippy.js": "^6.3.7" + } + }, + "@tiptap/extension-bullet-list": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.3.tgz", + "integrity": "sha512-RtaLiRvZbMTOje+FW5bn+mYogiIgNxOm065wmyLPypnTbLSeHeYkoqVSqzZeqUn+7GLnwgn1shirUe6csVE/BA==", + "requires": {} + }, + "@tiptap/extension-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.0.3.tgz", + "integrity": "sha512-LsVCKVxgBtkstAr1FjxN8T3OjlC76a2X8ouoZpELMp+aXbjqyanCKzt+sjjUhE4H0yLFd4v+5v6UFoCv4EILiw==", + "requires": {} + }, + "@tiptap/extension-code-block": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.0.3.tgz", + "integrity": "sha512-F4xMy18EwgpyY9f5Te7UuF7UwxRLptOtCq1p2c2DfxBvHDWhAjQqVqcW/sq/I/WuED7FwCnPLyyAasPiVPkLPw==", + "requires": {} + }, + "@tiptap/extension-document": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.0.3.tgz", + "integrity": "sha512-PsYeNQQBYIU9ayz1R11Kv/kKNPFNIV8tApJ9pxelXjzcAhkjncNUazPN/dyho60mzo+WpsmS3ceTj/gK3bCtWA==", + "requires": {} + }, + "@tiptap/extension-dropcursor": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.0.3.tgz", + "integrity": "sha512-McthMrfusn6PjcaynJLheZJcXto8TaIW5iVitYh8qQrDXr31MALC/5GvWuiswmQ8bAXiWPwlLDYE/OJfwtggaw==", + "requires": {} + }, + "@tiptap/extension-floating-menu": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.3.tgz", + "integrity": "sha512-zN1vRGRvyK3pO2aHRmQSOTpl4UJraXYwKYM009n6WviYKUNm0LPGo+VD4OAtdzUhPXyccnlsTv2p6LIqFty6Bg==", + "peer": true, "requires": { - "@angular-devkit/core": "14.2.10", - "@angular-devkit/schematics": "14.2.10", - "jsonc-parser": "3.1.0" + "tippy.js": "^6.3.7" } }, - "@sideway/address": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", - "dev": true, + "@tiptap/extension-gapcursor": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.3.tgz", + "integrity": "sha512-6I9EzzsYOyyqDvDvxIK6Rv3EXB+fHKFj8ntHO8IXmeNJ6pkhOinuXVsW6Yo7TcDYoTj4D5I2MNFAW2rIkgassw==", + "requires": {} + }, + "@tiptap/extension-hard-break": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.0.3.tgz", + "integrity": "sha512-RCln6ARn16jvKTjhkcAD5KzYXYS0xRMc0/LrHeV8TKdCd4Yd0YYHe0PU4F9gAgAfPQn7Dgt4uTVJLN11ICl8sQ==", + "requires": {} + }, + "@tiptap/extension-heading": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.0.3.tgz", + "integrity": "sha512-f0IEv5ms6aCzL80WeZ1qLCXTkRVwbpRr1qAETjg3gG4eoJN18+lZNOJYpyZy3P92C5KwF2T3Av00eFyVLIbb8Q==", + "requires": {} + }, + "@tiptap/extension-history": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.0.3.tgz", + "integrity": "sha512-00KHIcJ8kivn2ARI6NQYphv2LfllVCXViHGm0EhzDW6NQxCrriJKE3tKDcTFCu7LlC5doMpq9Z6KXdljc4oVeQ==", + "requires": {} + }, + "@tiptap/extension-horizontal-rule": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.0.3.tgz", + "integrity": "sha512-SZRUSh07b/M0kJHNKnfBwBMWrZBEm/E2LrK1NbluwT3DBhE+gvwiEdBxgB32zKHNxaDEXUJwUIPNC3JSbKvPUA==", + "requires": {} + }, + "@tiptap/extension-italic": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.0.3.tgz", + "integrity": "sha512-cfS5sW0gu7qf4ihwnLtW/QMTBrBEXaT0sJl3RwkhjIBg/65ywJKE5Nz9ewnQHmDeT18hvMJJ1VIb4j4ze9jj9A==", + "requires": {} + }, + "@tiptap/extension-link": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.0.3.tgz", + "integrity": "sha512-H72tXQ5rkVCkAhFaf08fbEU7EBUCK0uocsqOF+4th9sOlrhfgyJtc8Jv5EXPDpxNgG5jixSqWBo0zKXQm9s9eg==", "requires": { - "@hapi/hoek": "^9.0.0" + "linkifyjs": "^4.1.0" } }, - "@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "dev": true + "@tiptap/extension-list-item": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.0.3.tgz", + "integrity": "sha512-p7cUsk0LpM1PfdAuFE8wYBNJ3gvA0UhNGR08Lo++rt9UaCeFLSN1SXRxg97c0oa5+Ski7SrCjIJ5Ynhz0viTjQ==", + "requires": {} }, - "@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "dev": true + "@tiptap/extension-ordered-list": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.3.tgz", + "integrity": "sha512-ZB3MpZh/GEy1zKgw7XDQF4FIwycZWNof1k9WbDZOI063Ch4qHZowhVttH2mTCELuyvTMM/o9a8CS7qMqQB48bw==", + "requires": {} }, - "@socket.io/component-emitter": { - "version": "3.1.0", - "dev": true + "@tiptap/extension-paragraph": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.0.3.tgz", + "integrity": "sha512-a+tKtmj4bU3GVCH1NE8VHWnhVexxX5boTVxsHIr4yGG3UoKo1c5AO7YMaeX2W5xB5iIA+BQqOPCDPEAx34dd2A==", + "requires": {} + }, + "@tiptap/extension-placeholder": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.0.3.tgz", + "integrity": "sha512-Z42jo0termRAf0S0L8oxrts94IWX5waU4isS2CUw8xCUigYyCFslkhQXkWATO1qRbjNFLKN2C9qvCgGf4UeBrw==", + "requires": {} + }, + "@tiptap/extension-strike": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.0.3.tgz", + "integrity": "sha512-RO4/EYe2iPD6ifDHORT8fF6O9tfdtnzxLGwZIKZXnEgtweH+MgoqevEzXYdS+54Wraq4TUQGNcsYhe49pv7Rlw==", + "requires": {} + }, + "@tiptap/extension-task-item": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-2.0.3.tgz", + "integrity": "sha512-13u1Q769WiSNcjFieYAMuJyWXNaY9yOdw6WFg9tQg4EZ5h6+2DaxB0qmu6I3pH+wwSn2UkCkXIirAo/k7wnzbw==", + "requires": {} + }, + "@tiptap/extension-task-list": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-task-list/-/extension-task-list-2.0.3.tgz", + "integrity": "sha512-NdW0RtMF2L96qy+j946mTB5Av6Qn5L3vGVWFmJA6/JPXr9Uj/grItCmqUQKHfPBSFow7UqBY82ODblP+GQFgew==", + "requires": {} + }, + "@tiptap/extension-text": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.0.3.tgz", + "integrity": "sha512-LvzChcTCcPSMNLUjZe/A9SHXWGDHtvk73fR7CBqAeNU0MxhBPEBI03GFQ6RzW3xX0CmDmjpZoDxFMB+hDEtW1A==", + "requires": {} + }, + "@tiptap/extension-typography": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-typography/-/extension-typography-2.0.3.tgz", + "integrity": "sha512-5U91O2dffYOvwenWG+zT1N/pnt+RppSlocxs1KaNWFLlI2fgzDTyUyjzygIHGmskStqay2MuvmPnfVABoC+1Gw==", + "requires": {} + }, + "@tiptap/pm": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.0.3.tgz", + "integrity": "sha512-I9dsInD89Agdm1QjFRO9dmJtU1ldVSILNPW0pEhv9wYqYVvl4HUj/JMtYNqu2jWrCHNXQcaX/WkdSdvGJtmg5g==", + "peer": true, + "requires": { + "prosemirror-changeset": "^2.2.0", + "prosemirror-collab": "^1.3.0", + "prosemirror-commands": "^1.3.1", + "prosemirror-dropcursor": "^1.5.0", + "prosemirror-gapcursor": "^1.3.1", + "prosemirror-history": "^1.3.0", + "prosemirror-inputrules": "^1.2.0", + "prosemirror-keymap": "^1.2.0", + "prosemirror-markdown": "^1.10.1", + "prosemirror-menu": "^1.2.1", + "prosemirror-model": "^1.18.1", + "prosemirror-schema-basic": "^1.2.0", + "prosemirror-schema-list": "^1.2.2", + "prosemirror-state": "^1.4.1", + "prosemirror-tables": "^1.3.0", + "prosemirror-trailing-node": "^2.0.2", + "prosemirror-transform": "^1.7.0", + "prosemirror-view": "^1.28.2" + } + }, + "@tiptap/starter-kit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.0.3.tgz", + "integrity": "sha512-t4WG4w93zTpL2VxhVyJJvl3kdLF001ZrhpOuEiZqEMBMUMbM56Uiigv1CnUQpTFrjDAh3IM8hkqzAh20TYw2iQ==", + "requires": { + "@tiptap/core": "^2.0.3", + "@tiptap/extension-blockquote": "^2.0.3", + "@tiptap/extension-bold": "^2.0.3", + "@tiptap/extension-bullet-list": "^2.0.3", + "@tiptap/extension-code": "^2.0.3", + "@tiptap/extension-code-block": "^2.0.3", + "@tiptap/extension-document": "^2.0.3", + "@tiptap/extension-dropcursor": "^2.0.3", + "@tiptap/extension-gapcursor": "^2.0.3", + "@tiptap/extension-hard-break": "^2.0.3", + "@tiptap/extension-heading": "^2.0.3", + "@tiptap/extension-history": "^2.0.3", + "@tiptap/extension-horizontal-rule": "^2.0.3", + "@tiptap/extension-italic": "^2.0.3", + "@tiptap/extension-list-item": "^2.0.3", + "@tiptap/extension-ordered-list": "^2.0.3", + "@tiptap/extension-paragraph": "^2.0.3", + "@tiptap/extension-strike": "^2.0.3", + "@tiptap/extension-text": "^2.0.3" + } }, "@tootallnate/once": { "version": "2.0.0", @@ -16051,6 +17318,25 @@ "version": "7.0.11", "dev": true }, + "@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==" + }, + "@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "requires": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==" + }, "@types/mime": { "version": "3.0.1", "dev": true @@ -16061,10 +17347,25 @@ "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==" }, "@types/node": { - "version": "18.15.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", - "dev": true + "version": "18.18.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.9.tgz", + "integrity": "sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/object.omit": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/object.omit/-/object.omit-3.0.0.tgz", + "integrity": "sha512-I27IoPpH250TUzc9FzXd0P1BV/BMJuzqD3jOz98ehf9dQqGkxlq+hO1bIqZGWqCg5bVOy0g4AUVJtnxe0klDmw==", + "peer": true + }, + "@types/object.pick": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/object.pick/-/object.pick-1.3.2.tgz", + "integrity": "sha512-sn7L+qQ6RLPdXRoiaE7bZ/Ek+o4uICma/lBFPyJEKDTPTBP1W8u0c4baj3EiS4DiqLs+Hk+KUGvMVJtAw3ePJg==", + "peer": true }, "@types/parse-json": { "version": "4.0.0", @@ -16118,6 +17419,12 @@ "@types/node": "*" } }, + "@types/throttle-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz", + "integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==", + "peer": true + }, "@types/ws": { "version": "8.5.3", "dev": true, @@ -16727,6 +18034,8 @@ }, "asn1": { "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "dev": true, "requires": { "safer-buffer": "~2.1.0" @@ -16734,6 +18043,8 @@ }, "assert-plus": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "dev": true }, "astral-regex": { @@ -16764,10 +18075,14 @@ }, "aws-sign2": { "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", "dev": true }, "aws4": { - "version": "1.11.0", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", "dev": true }, "axios": { @@ -16871,6 +18186,8 @@ }, "bcrypt-pbkdf": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "dev": true, "requires": { "tweetnacl": "^0.14.3" @@ -17081,8 +18398,16 @@ "integrity": "sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ==", "dev": true }, + "case-anything": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz", + "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==", + "peer": true + }, "caseless": { "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "dev": true }, "chalk": { @@ -17400,6 +18725,12 @@ "version": "1.1.1", "dev": true }, + "crelt": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz", + "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==", + "peer": true + }, "critters": { "version": "0.0.16", "dev": true, @@ -17530,14 +18861,14 @@ "dev": true }, "cypress": { - "version": "12.10.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-12.10.0.tgz", - "integrity": "sha512-Y0wPc221xKKW1/4iAFCphkrG2jNR4MjOne3iGn4mcuCaE7Y5EtXL83N8BzRsAht7GYfWVjJ/UeTqEdDKHz39HQ==", + "version": "13.5.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.5.1.tgz", + "integrity": "sha512-yqLViT0D/lPI8Kkm7ciF/x/DCK/H/DnogdGyiTnQgX4OVR2aM30PtK+kvklTOD1u3TuItiD9wUQAF8EYWtyZug==", "dev": true, "requires": { - "@cypress/request": "^2.88.10", + "@cypress/request": "^3.0.0", "@cypress/xvfb": "^1.2.4", - "@types/node": "^14.14.31", + "@types/node": "^18.17.5", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", @@ -17570,19 +18901,16 @@ "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", + "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "semver": "^7.3.2", + "semver": "^7.5.3", "supports-color": "^8.1.1", "tmp": "~0.2.1", "untildify": "^4.0.0", "yauzl": "^2.10.0" }, "dependencies": { - "@types/node": { - "version": "14.18.28", - "dev": true - }, "ansi-styles": { "version": "4.3.0", "dev": true, @@ -17692,8 +19020,16 @@ } } }, + "dash-get": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/dash-get/-/dash-get-1.0.2.tgz", + "integrity": "sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==", + "peer": true + }, "dashdash": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "dev": true, "requires": { "assert-plus": "^1.0.0" @@ -17718,6 +19054,12 @@ "version": "0.1.4", "dev": true }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "peer": true + }, "default-gateway": { "version": "6.0.3", "dev": true, @@ -17866,6 +19208,8 @@ }, "ecc-jsbn": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "dev": true, "requires": { "jsbn": "~0.1.0", @@ -18483,6 +19827,8 @@ }, "extsprintf": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "dev": true }, "fast-deep-equal": { @@ -18618,10 +19964,14 @@ }, "forever-agent": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "dev": true }, "form-data": { "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "dev": true, "requires": { "asynckit": "^0.4.0", @@ -18732,6 +20082,8 @@ }, "getpass": { "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "dev": true, "requires": { "assert-plus": "^1.0.0" @@ -18946,6 +20298,8 @@ }, "http-signature": { "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", "dev": true, "requires": { "assert-plus": "^1.0.0", @@ -19161,6 +20515,15 @@ "version": "2.2.1", "dev": true }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "peer": true, + "requires": { + "is-plain-object": "^2.0.4" + } + }, "is-extglob": { "version": "2.1.1" }, @@ -19202,7 +20565,6 @@ }, "is-plain-object": { "version": "2.0.4", - "dev": true, "requires": { "isobject": "^3.0.1" } @@ -19213,6 +20575,8 @@ }, "is-typedarray": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "dev": true }, "is-unicode-supported": { @@ -19242,11 +20606,12 @@ "dev": true }, "isobject": { - "version": "3.0.1", - "dev": true + "version": "3.0.1" }, "isstream": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, "istanbul-lib-coverage": { @@ -19370,6 +20735,8 @@ }, "jsbn": { "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, "jsesc": { @@ -19382,6 +20749,8 @@ }, "json-schema": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", "dev": true }, "json-schema-traverse": { @@ -19393,6 +20762,8 @@ }, "json-stringify-safe": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true }, "json5": { @@ -19417,6 +20788,8 @@ }, "jsprim": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", "dev": true, "requires": { "assert-plus": "1.0.0", @@ -19629,6 +21002,19 @@ "version": "1.2.4", "dev": true }, + "linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "requires": { + "uc.micro": "^1.0.1" + } + }, + "linkifyjs": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.1.tgz", + "integrity": "sha512-zFN/CTVmbcVef+WaDXT63dNzzkfRBKT1j464NJQkV7iSgJU0sLBus9W0HBwnXK13/hf168pbrx/V/bjEHOXNHA==" + }, "listr2": { "version": "3.14.0", "dev": true, @@ -19800,8 +21186,7 @@ } }, "make-error": { - "version": "1.3.6", - "dev": true + "version": "1.3.6" }, "make-fetch-happen": { "version": "10.2.1", @@ -19835,6 +21220,40 @@ "version": "0.1.0", "dev": true }, + "markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "requires": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==" + } + } + }, + "markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==" + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + }, "media-typer": { "version": "0.3.0", "dev": true @@ -20089,6 +21508,14 @@ } } }, + "ngx-tiptap": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ngx-tiptap/-/ngx-tiptap-6.0.0.tgz", + "integrity": "sha512-3wex205OhqHHF7CrJD5/OUdwiE3dNJnpZB0dugnGfyGdVqpbxs+s1eUkKKa7ePIUG7Yy96sOen1X9odIDeBzCA==", + "requires": { + "tslib": "^2.3.0" + } + }, "nice-napi": { "version": "1.0.2", "dev": true, @@ -20303,6 +21730,24 @@ "version": "1.12.2", "dev": true }, + "object.omit": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-3.0.0.tgz", + "integrity": "sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==", + "peer": true, + "requires": { + "is-extendable": "^1.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "peer": true, + "requires": { + "isobject": "^3.0.1" + } + }, "obuf": { "version": "1.1.2", "dev": true @@ -20399,6 +21844,11 @@ } } }, + "orderedmap": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.0.tgz", + "integrity": "sha512-/pIFexOm6S70EPdznemIz3BQZoJ4VTFrhqzu0ACBqBgeLsLxq8e6Jim63ImIfwW/zAD1AlXpRMlOv3aghmo4dA==" + }, "os-tmpdir": { "version": "1.0.2", "dev": true @@ -20570,6 +22020,8 @@ }, "performance-now": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "dev": true }, "picocolors": { @@ -20960,6 +22412,12 @@ "version": "2.0.1", "dev": true }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true + }, "process-nextick-args": { "version": "2.0.1", "dev": true @@ -20976,6 +22434,202 @@ "retry": "^0.12.0" } }, + "prosemirror-changeset": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.2.0.tgz", + "integrity": "sha512-QM7ohGtkpVpwVGmFb8wqVhaz9+6IUXcIQBGZ81YNAKYuHiFJ1ShvSzab4pKqTinJhwciZbrtBEk/2WsqSt2PYg==", + "peer": true, + "requires": { + "prosemirror-transform": "^1.0.0" + } + }, + "prosemirror-collab": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.0.tgz", + "integrity": "sha512-+S/IJ69G2cUu2IM5b3PBekuxs94HO1CxJIWOFrLQXUaUDKL/JfBx+QcH31ldBlBXyDEUl+k3Vltfi1E1MKp2mA==", + "peer": true, + "requires": { + "prosemirror-state": "^1.0.0" + } + }, + "prosemirror-commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.5.1.tgz", + "integrity": "sha512-ga1ga/RkbzxfAvb6iEXYmrEpekn5NCwTb8w1dr/gmhSoaGcQ0VPuCzOn5qDEpC45ql2oDkKoKQbRxLJwKLpMTQ==", + "peer": true, + "requires": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "prosemirror-dropcursor": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.0.tgz", + "integrity": "sha512-TZMitR8nlp9Xh42pDYGcWopCoFPmJduoyGJ7FjYM2/7gZKnfD41TIaZN5Q1cQjm6Fm/P5vk/DpVYFhS8kDdigw==", + "peer": true, + "requires": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "prosemirror-gapcursor": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.1.tgz", + "integrity": "sha512-GKTeE7ZoMsx5uVfc51/ouwMFPq0o8YrZ7Hx4jTF4EeGbXxBveUV8CGv46mSHuBBeXGmvu50guoV2kSnOeZZnUA==", + "peer": true, + "requires": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "prosemirror-history": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.3.0.tgz", + "integrity": "sha512-qo/9Wn4B/Bq89/YD+eNWFbAytu6dmIM85EhID+fz9Jcl9+DfGEo8TTSrRhP15+fFEoaPqpHSxlvSzSEbmlxlUA==", + "peer": true, + "requires": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "rope-sequence": "^1.3.0" + } + }, + "prosemirror-inputrules": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.2.0.tgz", + "integrity": "sha512-eAW/M/NTSSzpCOxfR8Abw6OagdG0MiDAiWHQMQveIsZtoKVYzm0AflSPq/ymqJd56/Su1YPbwy9lM13wgHOFmQ==", + "peer": true, + "requires": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "prosemirror-keymap": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.1.tgz", + "integrity": "sha512-kVK6WGC+83LZwuSJnuCb9PsADQnFZllt94qPP3Rx/vLcOUV65+IbBeH2nS5cFggPyEVJhGkGrgYFRrG250WhHQ==", + "peer": true, + "requires": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "prosemirror-markdown": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.11.2.tgz", + "integrity": "sha512-Eu5g4WPiCdqDTGhdSsG9N6ZjACQRYrsAkrF9KYfdMaCmjIApH75aVncsWYOJvEk2i1B3i8jZppv3J/tnuHGiUQ==", + "requires": { + "markdown-it": "^13.0.1", + "prosemirror-model": "^1.0.0" + } + }, + "prosemirror-menu": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.1.tgz", + "integrity": "sha512-sBirXxVfHalZO4f1ZS63WzewINK4182+7dOmoMeBkqYO8wqMBvBS7wQuwVOHnkMWPEh0+N0LJ856KYUN+vFkmQ==", + "peer": true, + "requires": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "prosemirror-model": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.19.0.tgz", + "integrity": "sha512-/CvFGJnwc41EJSfDkQLly1cAJJJmBpZwwUJtwZPTjY2RqZJfM8HVbCreOY/jti8wTRbVyjagcylyGoeJH/g/3w==", + "requires": { + "orderedmap": "^2.0.0" + } + }, + "prosemirror-schema-basic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.1.tgz", + "integrity": "sha512-vYBdIHsYKSDIqYmPBC7lnwk9DsKn8PnVqK97pMYP5MLEDFqWIX75JiaJTzndBii4bRuNqhC2UfDOfM3FKhlBHg==", + "peer": true, + "requires": { + "prosemirror-model": "^1.19.0" + } + }, + "prosemirror-schema-list": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.2.2.tgz", + "integrity": "sha512-rd0pqSDp86p0MUMKG903g3I9VmElFkQpkZ2iOd3EOVg1vo5Cst51rAsoE+5IPy0LPXq64eGcCYlW1+JPNxOj2w==", + "peer": true, + "requires": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "prosemirror-state": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.2.tgz", + "integrity": "sha512-puuzLD2mz/oTdfgd8msFbe0A42j5eNudKAAPDB0+QJRw8cO1ygjLmhLrg9RvDpf87Dkd6D4t93qdef00KKNacQ==", + "peer": true, + "requires": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "prosemirror-tables": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.3.2.tgz", + "integrity": "sha512-/9JTeN6s58Zq66HXaxP6uf8PAmc7XXKZFPlOGVtLvxEd6xBP6WtzaJB9wBjiGUzwbdhdMEy7V62yuHqk/3VrnQ==", + "peer": true, + "requires": { + "prosemirror-keymap": "^1.1.2", + "prosemirror-model": "^1.8.1", + "prosemirror-state": "^1.3.1", + "prosemirror-transform": "^1.2.1", + "prosemirror-view": "^1.13.3" + } + }, + "prosemirror-trailing-node": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.3.tgz", + "integrity": "sha512-lGrjMrn97KWkjQSW/FjdvnhJmqFACmQIyr6lKYApvHitDnKsCoZz6XzrHB7RZYHni/0NxQmZ01p/2vyK2SkvaA==", + "peer": true, + "requires": { + "@babel/runtime": "^7.13.10", + "@remirror/core-constants": "^2.0.0", + "@remirror/core-helpers": "^2.0.1", + "escape-string-regexp": "^4.0.0" + }, + "dependencies": { + "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==", + "peer": true + } + } + }, + "prosemirror-transform": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.7.1.tgz", + "integrity": "sha512-VteoifAfpt46z0yEt6Fc73A5OID9t/y2QIeR5MgxEwTuitadEunD/V0c9jQW8ziT8pbFM54uTzRLJ/nLuQjMxg==", + "peer": true, + "requires": { + "prosemirror-model": "^1.0.0" + } + }, + "prosemirror-view": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.30.2.tgz", + "integrity": "sha512-nTNzZvalQf9kHeEyO407LiV6DoOs/pXsid88UqW9Vvybo4ozJW2PJhkfZUxCUF1hR/9vJLdhxX84wuw9P9HsXA==", + "peer": true, + "requires": { + "prosemirror-model": "^1.16.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "proxy-addr": { "version": "2.0.7", "dev": true, @@ -21008,6 +22662,8 @@ }, "psl": { "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, "pump": { @@ -21032,6 +22688,12 @@ "side-channel": "^1.0.4" } }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "queue-microtask": { "version": "1.2.3" }, @@ -21147,8 +22809,7 @@ } }, "regenerator-runtime": { - "version": "0.13.9", - "dev": true + "version": "0.13.9" }, "regenerator-transform": { "version": "0.15.1", @@ -21272,6 +22933,12 @@ "glob": "^7.1.3" } }, + "rope-sequence": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.3.tgz", + "integrity": "sha512-85aZYCxweiD5J8yTEbw+E6A27zSnLPNDL0WfPdw3YYodq7WjnTKo0q4dtyQ2gz23iPT8Q9CUyJtAaUNcTxRf5Q==", + "peer": true + }, "run-async": { "version": "2.4.1", "dev": true @@ -21361,7 +23028,9 @@ } }, "semver": { - "version": "7.3.8", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -21704,7 +23373,9 @@ "dev": true }, "sshpk": { - "version": "1.17.0", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "dev": true, "requires": { "asn1": "~0.2.3", @@ -21963,6 +23634,12 @@ "version": "0.2.0", "dev": true }, + "throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "peer": true + }, "throttleit": { "version": "1.0.0", "dev": true @@ -21975,6 +23652,26 @@ "version": "1.1.0", "dev": true }, + "tippy.js": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "peer": true, + "requires": { + "@popperjs/core": "^2.9.0" + } + }, + "tiptap-markdown": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/tiptap-markdown/-/tiptap-markdown-0.7.4.tgz", + "integrity": "sha512-uK+QFkHSoBe8hcJIdGIQz+mqWy0VD1cMPpVVUqq9yzYPzxV2vaxv2BSWvYDOWHJ9+68lLZlliGdPxWCyNEUqKw==", + "requires": { + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1", + "markdown-it-task-lists": "^2.1.1", + "prosemirror-markdown": "^1.11.0" + } + }, "tmp": { "version": "0.0.33", "dev": true, @@ -21997,11 +23694,23 @@ "dev": true }, "tough-cookie": { - "version": "2.5.0", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "dependencies": { + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true + } } }, "tree-kill": { @@ -22066,6 +23775,8 @@ }, "tunnel-agent": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "requires": { "safe-buffer": "^5.0.1" @@ -22073,6 +23784,8 @@ }, "tweetnacl": { "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "dev": true }, "type-check": { @@ -22110,6 +23823,17 @@ "integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==", "dev": true }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "dev": true @@ -22170,6 +23894,16 @@ "punycode": "^2.1.0" } }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "util-deprecate": { "version": "1.0.2" }, @@ -22206,6 +23940,8 @@ }, "verror": { "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "dev": true, "requires": { "assert-plus": "^1.0.0", @@ -22215,6 +23951,8 @@ "dependencies": { "core-util-is": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true } } @@ -22223,6 +23961,12 @@ "version": "2.0.1", "dev": true }, + "w3c-keyname": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", + "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==", + "peer": true + }, "wait-on": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.0.1.tgz", diff --git a/client-v2/package.json b/client-v2/package.json index 77dc5595..9503abd9 100644 --- a/client-v2/package.json +++ b/client-v2/package.json @@ -43,7 +43,16 @@ "@ngrx/store-devtools": "^14.3.2", "@rx-angular/cdk": "^14.0.0", "@rx-angular/template": "^14.0.0", + "@tiptap/core": "^2.0.3", + "@tiptap/extension-link": "^2.0.3", + "@tiptap/extension-placeholder": "^2.0.3", + "@tiptap/extension-task-item": "^2.0.3", + "@tiptap/extension-task-list": "^2.0.3", + "@tiptap/extension-typography": "^2.0.3", + "@tiptap/starter-kit": "^2.0.3", + "ngx-tiptap": "^6.0.0", "rxjs": "~7.8.0", + "tiptap-markdown": "^0.7.4", "tslib": "^2.5.0", "zone.js": "^0.12.0" }, @@ -60,7 +69,7 @@ "@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/parser": "^5.59.5", "autoprefixer": "^10.4.14", - "cypress": "latest", + "cypress": "^13.5.1", "eslint": "^8.41.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^4.2.1", diff --git a/client-v2/src/app/app.module.ts b/client-v2/src/app/app.module.ts index 43aaed0c..a667480d 100644 --- a/client-v2/src/app/app.module.ts +++ b/client-v2/src/app/app.module.ts @@ -4,7 +4,7 @@ import { BrowserModule } from '@angular/platform-browser' import { AppRoutingModule } from './app-routing.module' import { AppComponent } from './pages/app.component' import { DemoComponent } from './components/demo/demo.component' -import { TaskComponent } from './components/organisms/task/task.component' +import { ElemContainerComponent, TaskComponent } from './components/organisms/task/task.component' import { ComponentPlaygroundComponent } from './pages/component-playground/component-playground.component' import { FocusableDirective } from './directives/focusable.directive' import { FormsModule, ReactiveFormsModule } from '@angular/forms' @@ -33,12 +33,9 @@ import { SettingsAccountComponent } from './pages/settings/account/account.compo import { AuthComponent } from './pages/auth/auth.component' import { LoginLoadingComponent } from './pages/auth/login-loading/login-loading.component' import { CdkMenuModule } from '@angular/cdk/menu' -import { DropDownComponent } from './components/molecules/drop-down/drop-down.component' import { ModalModule } from './modal/modal.module' import { IconsModule } from './components/atoms/icons/icons.module' import { OverlayModule } from '@angular/cdk/overlay' -import { TooltipDirective } from './directives/tooltip.directive' -import { TooltipComponent } from './components/atoms/tooltip/tooltip.component' import { CdkTreeModule } from '@angular/cdk/tree' import { EntityPageLabelComponent } from './components/atoms/entity-page-label/entity-page-label.component' import { EntityPageComponent } from './pages/home/entity-page/entity-page.component' @@ -58,10 +55,6 @@ import { LayoutModule } from '@angular/cdk/layout' import { IntersectionDirective } from './directives/intersection.directive' import { EntityDescriptionComponent } from './components/molecules/entity-description/entity-description.component' import { ApplicationinsightsAngularpluginErrorService } from '@microsoft/applicationinsights-angularplugin-js' -import { LetModule } from '@rx-angular/template/let' -import { IfModule } from '@rx-angular/template/if' -import { ForModule } from '@rx-angular/template/for' -import { PushModule } from '@rx-angular/template/push' import { SearchComponent } from './pages/home/search/search.component' import { HighlightPipe } from './pipes/highlight.pipe' import { TaskNestingDemoComponent } from './pages/landing-page/demos/task-nesting-demo.component' @@ -71,6 +64,12 @@ import { TaskDescriptionDemoComponent } from './pages/landing-page/demos/task-de import { TermsOfServiceComponent } from './pages/terms-of-service/terms-of-service.component' import { PrivacyPolicyComponent } from './pages/privacy-policy/privacy-policy.component' import { ContactComponent } from './pages/contact/contact.component' +import { RxModule } from './rx/rx.module' +import { TooltipModule } from './tooltip/tooltip.module' +import { DropdownModule } from './dropdown/dropdown.module' +import { KeyboardModule } from './keyboard/keyboard.module' +import { RichTextEditorModule } from './rich-text-editor/rich-text-editor.module' +import { ToolbarComponent } from './components/molecules/toolbar/toolbar.component' @NgModule({ declarations: [ @@ -96,9 +95,6 @@ import { ContactComponent } from './pages/contact/contact.component' SettingsAccountComponent, AuthComponent, LoginLoadingComponent, - DropDownComponent, - TooltipDirective, - TooltipComponent, EntityPageLabelComponent, EntityPageComponent, BreadcrumbsComponent, @@ -124,6 +120,8 @@ import { ContactComponent } from './pages/contact/contact.component' TermsOfServiceComponent, PrivacyPolicyComponent, ContactComponent, + ToolbarComponent, + ElemContainerComponent, ], imports: [ BrowserModule, @@ -163,10 +161,11 @@ import { ContactComponent } from './pages/contact/contact.component' OverlayModule, CdkTreeModule, LayoutModule, - LetModule, - IfModule, - ForModule, - PushModule, + RxModule, + TooltipModule, + DropdownModule, + KeyboardModule, + RichTextEditorModule, ], providers: [ { diff --git a/client-v2/src/app/components/atoms/icons/double-ellipsis-icon/double-ellipsis-icon.component.ts b/client-v2/src/app/components/atoms/icons/double-ellipsis-icon/double-ellipsis-icon.component.ts index d8af7d2f..2e6f5a72 100644 --- a/client-v2/src/app/components/atoms/icons/double-ellipsis-icon/double-ellipsis-icon.component.ts +++ b/client-v2/src/app/components/atoms/icons/double-ellipsis-icon/double-ellipsis-icon.component.ts @@ -3,7 +3,7 @@ import { Component } from '@angular/core' @Component({ selector: 'double-ellipsis-icon', template: ` - + `, styleUrls: [], }) diff --git a/client-v2/src/app/components/atoms/icons/icon/icons.ts b/client-v2/src/app/components/atoms/icons/icon/icons.ts index 9441e126..e8274569 100644 --- a/client-v2/src/app/components/atoms/icons/icon/icons.ts +++ b/client-v2/src/app/components/atoms/icons/icon/icons.ts @@ -17,6 +17,7 @@ const extraIcons = { edit: 'far fa-pencil-alt', export: 'far fa-file-export', clone: 'far fa-clone', + close: 'fas fa-times', expand: 'far fa-expand-alt', priority: 'far fa-exclamation', status: 'far fa-dot-circle', @@ -26,8 +27,42 @@ const extraIcons = { logout: 'fas fa-sign-out-alt', workspace: 'fas fa-garage', loading: 'fad fa-spinner-third animate-spin', + image: 'fas fa-image', + copy: 'fas fa-copy', + markdown: 'fab fa-markdown', + chevronRight: 'fas fa-chevron-right', chevronLeft: 'fas fa-chevron-left', + caretDown: 'fas fa-caret-down', + caretUp: 'fas fa-caret-up', + ellipsisHorizontal: 'fas fa-ellipsis-h', + ellipsisVertical: 'fas fa-ellipsis-v', + + editor: { + undo: 'far fa-undo-alt', + redo: 'far fa-redo-alt', + indent: 'far fa-indent', + outdent: 'far fa-outdent', + paragraph: 'far fa-paragraph', + text: 'far fa-text', + heading: 'far fa-heading', + heading1: 'far fa-h1', + heading2: 'far fa-h2', + heading3: 'far fa-h3', + heading4: 'far fa-h4', + bold: 'far fa-bold', + italic: 'far fa-italic', + strike: 'far fa-strikethrough', + link: 'far fa-link', + bulletList: 'far fa-list-ul', + orderedList: 'far fa-list-ol', + taskList: 'far fa-tasks', + horizontalRule: 'far fa-horizontal-rule', + code: 'far fa-code', + codeBlock: 'far fa-laptop-code', + quote: 'far fa-quote-right', + table: 'fas fa-table', + }, } export const entityStateIcons = { @@ -39,7 +74,7 @@ export const taskStatusIconMap: Record = concatMatchingKeys( { [TaskStatus.OPEN]: 'far fa-circle', [TaskStatus.IN_PROGRESS]: 'far fa-clock', - [TaskStatus.BACKLOG]: 'fas fa-spinner rotate-[-45deg]', + [TaskStatus.BACKLOG]: 'fal fa-stroopwafel', [TaskStatus.COMPLETED]: 'fas fa-check-circle', [TaskStatus.NOT_PLANNED]: 'fas fa-times-circle', } as const, @@ -52,8 +87,7 @@ export const taskPriorityIconMap: Record = concatMatchingK [TaskPriority.HIGH]: 'fas fa-exclamation-circle', [TaskPriority.MEDIUM]: 'fas fa-exclamation', [TaskPriority.NONE]: 'far fa-minus', - // [TaskPriority.OPTIONAL]: 'fas fa-question', - [TaskPriority.OPTIONAL]: 'far fa-square rotate-45 scale-[0.8]', + [TaskPriority.OPTIONAL]: 'far fa-diamond', } as const, taskPriorityColorMap ) diff --git a/client-v2/src/app/components/atoms/inline-editor/inline-editor.component.html b/client-v2/src/app/components/atoms/inline-editor/inline-editor.component.html index ffa6be0a..fa2302fe 100644 --- a/client-v2/src/app/components/atoms/inline-editor/inline-editor.component.html +++ b/client-v2/src/app/components/atoms/inline-editor/inline-editor.component.html @@ -1,6 +1,5 @@ - ` const setupComponent = (breadcrumbs: Breadcrumb[], template = defaultTemplate) => { cy.mount(template, { componentProperties: { breadcrumbs }, - imports: [CdkMenuModule, IconsModule, OverlayModule, ForModule], - declarations: [BreadcrumbsComponent, EntityPageLabelComponent, DropDownComponent, TooltipComponent], + imports: [CdkMenuModule, IconsModule, OverlayModule, RxModule, DropdownModule, TooltipModule], + declarations: [BreadcrumbsComponent, EntityPageLabelComponent], providers: [DeviceService, menuServiceMock], }) } diff --git a/client-v2/src/app/components/molecules/breadcrumbs/breadcrumbs.component.ts b/client-v2/src/app/components/molecules/breadcrumbs/breadcrumbs.component.ts index eb11440f..5aed3cee 100644 --- a/client-v2/src/app/components/molecules/breadcrumbs/breadcrumbs.component.ts +++ b/client-v2/src/app/components/molecules/breadcrumbs/breadcrumbs.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { IconKey } from '../../atoms/icons/icon/icons' -import { MenuItem } from '../drop-down/drop-down.component' +import { MenuItem } from '../../../dropdown/drop-down/drop-down.component' import { BehaviorSubject, combineLatestWith, distinctUntilChanged, map, timer } from 'rxjs' import { moveToMacroQueue } from 'src/app/utils' import { DeviceService } from 'src/app/services/device.service' diff --git a/client-v2/src/app/components/molecules/editable-entity-heading/editable-entity-title.component.html b/client-v2/src/app/components/molecules/editable-entity-heading/editable-entity-title.component.html index 8a04cf5f..5fc881cd 100644 --- a/client-v2/src/app/components/molecules/editable-entity-heading/editable-entity-title.component.html +++ b/client-v2/src/app/components/molecules/editable-entity-heading/editable-entity-title.component.html @@ -1,8 +1,15 @@ -
+
+ @@ -11,7 +18,7 @@
+> diff --git a/client-v2/src/app/components/molecules/entity-description/entity-description.component.spec.ts b/client-v2/src/app/components/molecules/entity-description/entity-description.component.spec.ts index b4fc9a82..3903f9ca 100644 --- a/client-v2/src/app/components/molecules/entity-description/entity-description.component.spec.ts +++ b/client-v2/src/app/components/molecules/entity-description/entity-description.component.spec.ts @@ -1,7 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' -import { FocusableDirective } from 'src/app/directives/focusable.directive' import { EntityDescriptionComponent } from './entity-description.component' +import { RichTextEditorModule } from 'src/app/rich-text-editor/rich-text-editor.module' +import { RxModule } from 'src/app/rx/rx.module' describe('EntityDescriptionComponent', () => { let component: EntityDescriptionComponent @@ -9,7 +10,8 @@ describe('EntityDescriptionComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [EntityDescriptionComponent, FocusableDirective], + imports: [RichTextEditorModule, RxModule], + declarations: [EntityDescriptionComponent], }).compileComponents() fixture = TestBed.createComponent(EntityDescriptionComponent) diff --git a/client-v2/src/app/components/molecules/entity-description/entity-description.component.ts b/client-v2/src/app/components/molecules/entity-description/entity-description.component.ts index 6e1380cc..ca75777a 100644 --- a/client-v2/src/app/components/molecules/entity-description/entity-description.component.ts +++ b/client-v2/src/app/components/molecules/entity-description/entity-description.component.ts @@ -1,8 +1,19 @@ import { ChangeDetectionStrategy, Component, Input, Output, ViewChild } from '@angular/core' -import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy' -import { BehaviorSubject, filter, first, map, merge, Subject, switchMap, tap } from 'rxjs' -import { FocusableDirective } from 'src/app/directives/focusable.directive' -import { createEventEmitter } from 'src/app/utils/observable.helpers' +import { UntilDestroy } from '@ngneat/until-destroy' +import { Observable, ReplaySubject, Subject, delay, distinctUntilKeyChanged, filter, map } from 'rxjs' +import { + defaultDesktopEditorLayout, + getDefaultEditorFeatures, + getDefaultEditorLayout, + provideEditorFeatures, +} from 'src/app/rich-text-editor/editor.features' +import { TipTapEditorComponent } from 'src/app/rich-text-editor/tip-tap-editor/tip-tap-editor.component' +import { DeviceService } from 'src/app/services/device.service' + +export interface DescriptionContext { + id: string + description$: Observable +} @UntilDestroy() @Component({ @@ -10,44 +21,40 @@ import { createEventEmitter } from 'src/app/utils/observable.helpers' templateUrl: './entity-description.component.html', styleUrls: ['./entity-description.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + viewProviders: [provideEditorFeatures(getDefaultEditorFeatures())], }) export class EntityDescriptionComponent { - description$ = new BehaviorSubject(null) - @Input() set description(description: string | null) { - this.description$.next(description) + constructor(private deviceService: DeviceService) {} + + @ViewChild(TipTapEditorComponent) private ttEditor!: TipTapEditorComponent + focus() { + this.ttEditor.editor.commands.focus() } - @ViewChild(FocusableDirective) focusable!: FocusableDirective // needed for outside access + toolbarLayout = defaultDesktopEditorLayout + toolbarLayout$ = this.deviceService.isTouchPrimary$.pipe(map(getDefaultEditorLayout)) - descriptionChanges$ = new BehaviorSubject(null) - blurEvents$ = new Subject() - @Output() blur = createEventEmitter(this.blurEvents$.pipe(tap(this.deselectEditor), untilDestroyed(this))) + private context$ = new ReplaySubject() + @Input() set context(context: DescriptionContext | null) { + if (context) this.context$.next(context) + } - descriptionDomState$ = merge( - this.descriptionChanges$, - this.description$.pipe( - tap(() => { - if (this.descriptionChanges$.value !== null) this.descriptionChanges$.next(null) - }) - ) + bindConfig$ = this.context$.pipe( + distinctUntilKeyChanged('id'), + map(context => ({ input$: context.description$, context: context.id })) ) - @Output() descriptionChange = createEventEmitter( - this.blurEvents$.pipe( - switchMap(() => this.descriptionChanges$.pipe(first())), - switchMap(description => { - return this.description$.pipe( - first(), - map(oldDescription => (oldDescription === description ? null : description)) - ) - }), - filter(description => description !== null), - map(description => description as string), - untilDestroyed(this) - ) - ) + @Output('isActive') isActive$ = new ReplaySubject() - deselectEditor() { - window.getSelection()?.removeAllRanges() - } + updateInput$ = new Subject<{ html: string; context: string }>() + @Output('update') update$ = this.updateInput$.pipe(map(({ html, context }) => ({ id: context, description: html }))) + + blurInput$ = new Subject() + @Output('blur') blur$ = this.blurInput$.pipe( + // @TODO: get rid of this delay code smell + delay(0), + map(() => this.ttEditor.editor.view.hasFocus()), + filter(hasFocus => !hasFocus), + map(() => null) + ) } diff --git a/client-v2/src/app/components/molecules/page-progress-bar/page-progress-bar.component.ts b/client-v2/src/app/components/molecules/page-progress-bar/page-progress-bar.component.ts index f7ab0731..3d0e68cd 100644 --- a/client-v2/src/app/components/molecules/page-progress-bar/page-progress-bar.component.ts +++ b/client-v2/src/app/components/molecules/page-progress-bar/page-progress-bar.component.ts @@ -28,7 +28,7 @@ export const mapByStatus = (taskTree: T[]) => { return statusCountMap } -const getStatusCountMapRecursive = (taskTree: TaskPreviewRecursive[]): Record => { +export const getStatusCountMapRecursive = (taskTree: TaskPreviewRecursive[]): Record => { const map = Object.fromEntries( Object.entries(mapByStatus(taskTree)).map(([status, tasks]) => [status, tasks.length]) ) as Record diff --git a/client-v2/src/app/components/molecules/toolbar/toolbar.component.css b/client-v2/src/app/components/molecules/toolbar/toolbar.component.css new file mode 100644 index 00000000..d26751a6 --- /dev/null +++ b/client-v2/src/app/components/molecules/toolbar/toolbar.component.css @@ -0,0 +1,22 @@ +:is(app-toolbar:not(.no-styles), .app-toolbar) { + @apply flex + items-center + gap-1 + rounded-xl + border + border-tinted-700/70 + bg-tinted-800 + p-1 + text-tinted-200 + shadow-lg + shadow-tinted-900 + transition-all + glass; +} +:is(app-toolbar, .app-toolbar).hide { + @apply pointer-events-none invisible translate-y-2 opacity-0; +} + +.toolbar-separator { + @apply mx-1 h-5 w-px bg-tinted-600; +} diff --git a/client-v2/src/app/components/molecules/toolbar/toolbar.component.html b/client-v2/src/app/components/molecules/toolbar/toolbar.component.html new file mode 100644 index 00000000..37918fb0 --- /dev/null +++ b/client-v2/src/app/components/molecules/toolbar/toolbar.component.html @@ -0,0 +1,3 @@ + + + diff --git a/client-v2/src/app/components/molecules/toolbar/toolbar.component.spec.ts b/client-v2/src/app/components/molecules/toolbar/toolbar.component.spec.ts new file mode 100644 index 00000000..53a46448 --- /dev/null +++ b/client-v2/src/app/components/molecules/toolbar/toolbar.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { ToolbarComponent } from './toolbar.component' + +describe('ToolbarComponent', () => { + let component: ToolbarComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ToolbarComponent], + }).compileComponents() + + fixture = TestBed.createComponent(ToolbarComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/client-v2/src/app/components/molecules/toolbar/toolbar.component.ts b/client-v2/src/app/components/molecules/toolbar/toolbar.component.ts new file mode 100644 index 00000000..9d7d96e6 --- /dev/null +++ b/client-v2/src/app/components/molecules/toolbar/toolbar.component.ts @@ -0,0 +1,29 @@ +import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core' +import { Subject, switchMap, timer, map, of, startWith } from 'rxjs' + +@Component({ + selector: 'app-toolbar', + templateUrl: './toolbar.component.html', + styleUrls: ['./toolbar.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + host: { + '[class.hide]': 'hideToolbar', + }, +}) +export class ToolbarComponent { + @Input() set hide(hideToolbar: boolean) { + this.hideToolbar = hideToolbar + this.hideToolbar$.next(hideToolbar) + } + hideToolbar$ = new Subject() + + hideToolbar = false + showToolbar$ = this.hideToolbar$.pipe( + switchMap(hidden => { + if (hidden) return timer(150).pipe(map(() => false)) + return of(true) + }), + startWith(false) + ) +} diff --git a/client-v2/src/app/components/organisms/entity-view/entity-view.component.html b/client-v2/src/app/components/organisms/entity-view/entity-view.component.html index 10df7799..a1ada75f 100644 --- a/client-v2/src/app/components/organisms/entity-view/entity-view.component.html +++ b/client-v2/src/app/components/organisms/entity-view/entity-view.component.html @@ -18,7 +18,7 @@ [cdkMenuTriggerFor]="options" #trigger="cdkMenuTriggerFor" > - + @@ -37,6 +37,6 @@
- +
diff --git a/client-v2/src/app/components/organisms/entity-view/entity-view.component.spec.ts b/client-v2/src/app/components/organisms/entity-view/entity-view.component.spec.ts index 3bbc2cf8..c9ec6d27 100644 --- a/client-v2/src/app/components/organisms/entity-view/entity-view.component.spec.ts +++ b/client-v2/src/app/components/organisms/entity-view/entity-view.component.spec.ts @@ -6,6 +6,7 @@ import { storeMock } from 'src/app/utils/unit-test.mocks' import { EntityViewComponent } from './entity-view.component' import { TasklistViewComponent } from './views/tasklist-view/tasklist-view.component' +import { RxModule } from 'src/app/rx/rx.module' describe('EntityViewComponent', () => { let component: EntityViewComponent @@ -15,7 +16,7 @@ describe('EntityViewComponent', () => { await TestBed.configureTestingModule({ declarations: [EntityViewComponent, TasklistViewComponent], providers: [Injector, storeMock], - imports: [CdkMenuModule, AsyncPipe], + imports: [CdkMenuModule, AsyncPipe, RxModule], }).compileComponents() fixture = TestBed.createComponent(EntityViewComponent) diff --git a/client-v2/src/app/components/organisms/entity-view/entity-view.component.test.ts b/client-v2/src/app/components/organisms/entity-view/entity-view.component.test.ts index 75faa031..24d97dfc 100644 --- a/client-v2/src/app/components/organisms/entity-view/entity-view.component.test.ts +++ b/client-v2/src/app/components/organisms/entity-view/entity-view.component.test.ts @@ -5,11 +5,12 @@ import { EntityPreviewRecursive, EntityType } from 'src/app/fullstack-shared-mod import { TasklistDetail } from 'src/app/fullstack-shared-models/list.model' import { storeMock } from 'src/app/utils/unit-test.mocks' import { EntityPageLabelComponent } from '../../atoms/entity-page-label/entity-page-label.component' -import { DropDownComponent } from '../../molecules/drop-down/drop-down.component' import { EditableEntityTitleComponent } from '../../molecules/editable-entity-heading/editable-entity-title.component' import { EntityViewComponent, entityViewComponentMap } from './entity-view.component' import { TaskViewComponent } from './views/task-view/task-view.component' import { TasklistViewComponent } from './views/tasklist-view/tasklist-view.component' +import { DropdownModule } from 'src/app/dropdown/dropdown.module' +import { RxModule } from 'src/app/rx/rx.module' const defaultTemplate = ` @@ -25,13 +26,12 @@ const setupComponent = (template = defaultTemplate) => { activeEntity$: new BehaviorSubject(null), entityOptionsMap$: new BehaviorSubject(null), }, - imports: [CdkMenuModule], + imports: [CdkMenuModule, DropdownModule, RxModule], declarations: [ EntityViewComponent, ...entityViewComponents, EditableEntityTitleComponent, EntityPageLabelComponent, - DropDownComponent, ], providers: [ storeMock, diff --git a/client-v2/src/app/components/organisms/entity-view/entity-view.component.ts b/client-v2/src/app/components/organisms/entity-view/entity-view.component.ts index 1d288167..86e42911 100644 --- a/client-v2/src/app/components/organisms/entity-view/entity-view.component.ts +++ b/client-v2/src/app/components/organisms/entity-view/entity-view.component.ts @@ -12,12 +12,12 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy' import { Store } from '@ngrx/store' import { BehaviorSubject, + Observable, combineLatest, combineLatestWith, delay, distinctUntilChanged, map, - Observable, shareReplay, tap, } from 'rxjs' @@ -26,8 +26,8 @@ import { TaskPreview } from 'src/app/fullstack-shared-models/task.model' import { AppState } from 'src/app/store' import { entitiesActions } from 'src/app/store/entities/entities.actions' import { useTaskForActiveItems } from 'src/app/utils/menu-item.helpers' +import { MenuItem } from '../../../dropdown/drop-down/drop-down.component' import { EntityMenuItemsMap } from '../../../shared/entity-menu-items' -import { MenuItem } from '../../molecules/drop-down/drop-down.component' import { TaskViewComponent } from './views/task-view/task-view.component' import { TasklistViewComponent } from './views/tasklist-view/tasklist-view.component' @@ -95,6 +95,8 @@ export class EntityViewComponent { // eslint-disable-next-line @typescript-eslint/no-explicit-any entityViewData: EntityViewData = { + // @TODO: (high prio): get rid of the macro queue here as its causing + // a huge delay of ~170ms on rendering the view entity$: this.entity$.pipe(delay(0)), // move to macro queue detail$: this.entityDetail$, options$: this.entityOptionsItems$, diff --git a/client-v2/src/app/components/organisms/entity-view/views/task-view/task-view.component.html b/client-v2/src/app/components/organisms/entity-view/views/task-view/task-view.component.html index f8463ef4..fbed1c74 100644 --- a/client-v2/src/app/components/organisms/entity-view/views/task-view/task-view.component.html +++ b/client-v2/src/app/components/organisms/entity-view/views/task-view/task-view.component.html @@ -1,22 +1,22 @@ - +

- -
+ + + + Priority: + {{ task.priority }} + + + + - - - - - + +
+
+ + + +
+
-
-
+
diff --git a/client-v2/src/app/components/organisms/task/task.component.test.ts b/client-v2/src/app/components/organisms/task/task.component.test.ts index b998caac..30caff18 100644 --- a/client-v2/src/app/components/organisms/task/task.component.test.ts +++ b/client-v2/src/app/components/organisms/task/task.component.test.ts @@ -9,11 +9,12 @@ import { getEntityMenuItemsMap } from 'src/app/shared/entity-menu-items' import { AppState } from 'src/app/store' import { IconsModule } from '../../atoms/icons/icons.module' import { InlineEditorComponent } from '../../atoms/inline-editor/inline-editor.component' -import { DropDownComponent } from '../../molecules/drop-down/drop-down.component' import { TaskTreeNode } from '../task-tree/task-tree.component' import { TaskComponent } from './task.component' import { HighlightPipe } from 'src/app/pipes/highlight.pipe' -import { PushModule } from '@rx-angular/template/push' +import { RxModule } from 'src/app/rx/rx.module' +import { DropdownModule } from 'src/app/dropdown/dropdown.module' +import { RichTextEditorModule } from 'src/app/rich-text-editor/rich-text-editor.module' const taskMenuItems = getEntityMenuItemsMap({} as unknown as Store)[EntityType.TASK] @@ -40,22 +41,15 @@ const setupComponent = ( ...listeners, menuItems: taskMenuItems.map(useStubsForActions(menuItemStubsMap)), }, - imports: [CdkMenuModule, IconsModule, PushModule], - declarations: [ - TaskComponent, - DropDownComponent, - InlineEditorComponent, - FocusableDirective, - MutationDirective, - HighlightPipe, - ], + imports: [CdkMenuModule, IconsModule, RxModule, DropdownModule, RichTextEditorModule], + declarations: [TaskComponent, InlineEditorComponent, FocusableDirective, MutationDirective, HighlightPipe], } ) } const taskFixture: TaskPreviewFlattend = { title: 'The title', - childrenCount: 0, + children: [], description: '', id: '', listId: '', @@ -79,7 +73,7 @@ describe('TaskComponent', () => { cy.get(testName('task-title')).contains(taskFixture.title) cy.get(testName('task-priority-button')).should('not.exist') cy.get(testName('subtask-toggle')).should('not.exist') - cy.get(testName('task-description')).should('not.be.visible') + cy.get(testName('task-description')).should('not.exist') }) describe('Title', () => { @@ -153,6 +147,7 @@ describe('TaskComponent', () => { ...taskTreeNodeFixture, taskPreview: { ...taskFixture, description }, }) + cy.get(testName('description-toggle')).click() cy.get(testName('task-description')).contains(description) }) diff --git a/client-v2/src/app/components/organisms/task/task.component.ts b/client-v2/src/app/components/organisms/task/task.component.ts index 1ec25f53..3cf2fd59 100644 --- a/client-v2/src/app/components/organisms/task/task.component.ts +++ b/client-v2/src/app/components/organisms/task/task.component.ts @@ -1,28 +1,94 @@ -import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core' +import { + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + Input, + OnInit, + Output, + ViewChild, +} from '@angular/core' import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy' -import { BehaviorSubject, distinctUntilChanged, filter, first, map, merge, shareReplay, Subject, tap } from 'rxjs' +import { createDocument, getSchema } from '@tiptap/core' +import { + BehaviorSubject, + Subject, + distinctUntilChanged, + distinctUntilKeyChanged, + filter, + first, + map, + merge, + mergeWith, + share, + shareReplay, + startWith, + throttleTime, + withLatestFrom, +} from 'rxjs' import { EntityType } from 'src/app/fullstack-shared-models/entities.model' +import { + createCounterWidget, + updateCounterWidget, +} from 'src/app/rich-text-editor/editor-features/extensions/checklist-counter.extension' +import { + getDefaultEditorFeatures, + getDefaultEditorLayout, + provideEditorFeatures, +} from 'src/app/rich-text-editor/editor.features' +import { ChecklistCount, countChecklistItems } from 'src/app/rich-text-editor/editor.helpers' +import { TipTapEditorComponent } from 'src/app/rich-text-editor/tip-tap-editor/tip-tap-editor.component' +import { DeviceService } from 'src/app/services/device.service' import { colors, taskStatusColorMap } from 'src/app/shared/colors' import { ENTITY_TITLE_DEFAULTS } from 'src/app/shared/defaults' import { insertElementAfter, moveToMacroQueue } from 'src/app/utils' -import { TaskPreviewFlattend, TaskPriority, TaskStatus } from '../../../fullstack-shared-models/task.model' +import { MenuItem } from '../../../dropdown/drop-down/drop-down.component' +import { + TaskPreview, + TaskPreviewFlattend, + TaskPreviewRecursive, + TaskPriority, + TaskStatus, +} from '../../../fullstack-shared-models/task.model' import { EntityState } from '../../atoms/icons/icon/icons' -import { MenuItem } from '../../molecules/drop-down/drop-down.component' +import { getStatusCountMapRecursive } from '../../molecules/page-progress-bar/page-progress-bar.component' import { TaskTreeNode } from '../task-tree/task-tree.component' +@Component({ + selector: 'app-elem-container', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ElemContainerComponent implements OnInit { + constructor(private elemRef: ElementRef) {} + + @Input() elem: HTMLElement | null = null + ngOnInit(): void { + if (this.elem) this.elemRef.nativeElement.appendChild(this.elem) + } +} + +const editorFeatures = getDefaultEditorFeatures({ checklistCounterFeature: false }) +const editorSchema = getSchema(editorFeatures.flatMap(feature => feature.extensions)) + @UntilDestroy() @Component({ selector: 'app-task', templateUrl: './task.component.html', styleUrls: ['./task.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + viewProviders: [provideEditorFeatures(editorFeatures)], }) export class TaskComponent { + constructor(private deviceService: DeviceService) {} + EntityType = EntityType TaskStatus = TaskStatus TaskPriority = TaskPriority PLACEHOLDER = ENTITY_TITLE_DEFAULTS[EntityType.TASK] + toolbarLayout$ = this.deviceService.isTouchPrimary$.pipe(map(getDefaultEditorLayout)) + statusColorMap = { ...taskStatusColorMap, [TaskStatus.OPEN]: 'text-tinted-100', @@ -37,9 +103,9 @@ export class TaskComponent { [TaskStatus.COMPLETED]: colors.submit[600], } - highlightQuery$ = new BehaviorSubject(null) - @Input() set highlightQuery(value: string) { - this.highlightQuery$.next(value) + searchTerm$ = new BehaviorSubject(null) + @Input() set searchTerm(value: string) { + this.searchTerm$.next(value) } task$ = new BehaviorSubject(null) @@ -50,10 +116,10 @@ export class TaskComponent { } @Input() set menuItems(items: MenuItem[]) { - this.menuItems_$.next(items) + this.menuItemsInput$.next(items) } - menuItems_$ = new BehaviorSubject(null) - menuItems$ = this.menuItems_$.pipe( + menuItemsInput$ = new BehaviorSubject(null) + menuItems$ = this.menuItemsInput$.pipe( map(items => { if (this.readonly) return null if (!items || this.task$.value?.description) return items @@ -61,7 +127,7 @@ export class TaskComponent { const descriptionItem: MenuItem = { title: 'Add description', icon: 'description', - action: () => this.addDescription(), + action: () => this.openDescription(), } const insertAfterIndex = items.findIndex(({ title }) => title && /Rename/.test(title)) @@ -73,13 +139,13 @@ export class TaskComponent { shareReplay(1) ) - statusMenuItems$ = this.menuItems_$.pipe( + statusMenuItems$ = this.menuItemsInput$.pipe( map(items => { if (this.readonly) return null return items?.find(({ title }) => title == 'Status')?.children }) ) - priorityMenuItems$ = this.menuItems_$.pipe( + priorityMenuItems$ = this.menuItemsInput$.pipe( map(items => { if (this.readonly) return null return items?.find(({ title }) => title == 'Priority')?.children @@ -89,8 +155,6 @@ export class TaskComponent { @Output() expansionChange = new EventEmitter() @Output() titleChange = new EventEmitter() - @Output() descriptionChange = new EventEmitter() - @Output() descriptionExpansionChange = new EventEmitter() @Output() statusChange = new EventEmitter() @Output() priorityChange = new EventEmitter() @@ -103,76 +167,181 @@ export class TaskComponent { @Input() readonly = false - isHovered = false + isSelected = false - descriptionExpansionChanges$ = new Subject<{ emit: boolean; isExpanded: boolean }>() - isDescriptionExpanded$ = merge( - this.nodeData$.pipe(map(nodeData => ({ emit: false, isExpanded: nodeData?.isDescriptionExpanded ?? false }))), - this.descriptionExpansionChanges$ - ).pipe( - distinctUntilChanged((prev, curr) => { - if (curr.emit) return false + @Output('descriptionChange') descriptionUpdateOnBlur$ = new EventEmitter() - return prev.isExpanded == curr.isExpanded - }), - tap(({ emit, isExpanded }) => { - if (!emit) return + // this is where the editor output lands + descriptionUpdates$ = new Subject() + descriptionState$ = merge( + this.descriptionUpdates$, + this.task$.pipe( + map(task => task?.description), + filter((description): description is string => description !== undefined && description !== null) + ) + ).pipe(share({ resetOnRefCountZero: true })) - this.descriptionExpansionChange.emit(isExpanded) - }), + @Output() isDescriptionActive$ = new BehaviorSubject(false) + descriptionBlur$ = new Subject() + + // this is where explicit toggles land + isDescriptionExpandedInput$ = new Subject<{ + emit: boolean + isExpanded: boolean + }>() + // this is what controls the flow -> combines explicit and implicit toggles (blur events) + isDescriptionExpandedBus$ = this.nodeData$.pipe( + map(nodeData => ({ emit: false, isExpanded: nodeData?.isDescriptionExpanded ?? false })), + mergeWith(this.isDescriptionExpandedInput$), + mergeWith( + this.descriptionBlur$.pipe( + withLatestFrom(this.descriptionState$.pipe(startWith(null))), + map(([, latestEditorState]) => ({ emit: true, isExpanded: !!latestEditorState })) + ) + ), + startWith({ emit: false, isExpanded: false }), + shareReplay({ bufferSize: 1, refCount: true }) + ) + // this drives the view + isDescriptionExpanded$ = this.isDescriptionExpandedBus$.pipe( map(({ isExpanded }) => isExpanded), + distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }) ) + // this is what the parent component listens to + @Output('descriptionExpansionChange') isDescriptionExpandedOutput$ = this.isDescriptionExpandedBus$.pipe( + filter(({ emit }) => emit), + map(({ isExpanded }) => isExpanded), + distinctUntilChanged() + ) - @ViewChild('description') descriptionRef!: ElementRef - addDescription() { - this.descriptionExpansionChanges$.next({ emit: false, isExpanded: true }) - moveToMacroQueue(() => { - this.descriptionRef.nativeElement.focus() + bindConfig$ = this.task$.pipe( + filter(Boolean), + distinctUntilKeyChanged('id'), + map(task => { + const description$ = this.task$.pipe( + map(task => task?.description || ''), + distinctUntilChanged() + ) + return { input$: description$, context: task.id } }) + ) + + @ViewChild(TipTapEditorComponent) descriptionEditor?: TipTapEditorComponent + + private counterWidgetId!: string + private counterWidget: HTMLDivElement | null = null + + getChecklistCounterWidget() { + if (!this.task$.value) return null + if (this.counterWidget) return this.counterWidget + + this.counterWidgetId = 'checklist-counter' + this.task$.value.id + + const checklistCount = countChecklistItems( + this.descriptionEditor?.editor.state.doc || createDocument(this.task$.value.description, editorSchema) + ) + + this.counterWidget = createCounterWidget({ + widgetId: this.counterWidgetId, + sticky: false, + withLabel: false, + style: { + display: checklistCount.totalItems == 0 ? 'none' : 'flex', + }, + overrideStyles: true, + checklistCount, + }) + + this.descriptionState$ + .pipe(untilDestroyed(this), throttleTime(400, undefined, { leading: true, trailing: true })) + .subscribe(description => { + if (!this.task$.value) return + + const checklistCount = countChecklistItems( + this.descriptionEditor?.editor.state.doc || createDocument(description, editorSchema) + ) + + updateCounterWidget({ + widgetId: this.counterWidgetId, + checklistCount, + sticky: false, + overrideStyles: true, + style: { + display: checklistCount.totalItems == 0 ? 'none' : 'flex', + }, + }) + }) + + return this.counterWidget } - resetDescription() { - this.descriptionRef.nativeElement.innerHTML = this.task$.value?.description || '' + + getTaskProgress(task: { children?: TaskPreview[] | null }): ChecklistCount { + const totalItems = task.children?.length || 0 + const checkedItems = + task.children?.filter(task => task.status == TaskStatus.COMPLETED || task.status == TaskStatus.NOT_PLANNED) + .length || 0 + const progress = (checkedItems / totalItems) * 100 || 0 + + return { totalItems, checkedItems, progress } } - toggleDescription() { - this.isDescriptionExpanded$.pipe(first()).subscribe(isExpanded => { - this.descriptionExpansionChanges$.next({ emit: true, isExpanded: !isExpanded }) - }) + getTaskProgressRecursive(task: { children?: TaskPreviewRecursive[] | null }): ChecklistCount { + const statusTaskCountMap = getStatusCountMapRecursive(task.children || []) + + const totalItems = Object.values(statusTaskCountMap).reduce((acc, curr) => acc + curr) + const checkedItems = statusTaskCountMap[TaskStatus.NOT_PLANNED] + statusTaskCountMap[TaskStatus.COMPLETED] + const progress = (checkedItems / totalItems) * 100 || 0 + + return { totalItems, checkedItems, progress } } + private taskCounterWidgetId!: string + private taskCounterWidget: HTMLDivElement | null = null + getTaskCounterWidget() { + if (!this.task$.value) return null + if (this.taskCounterWidget) return this.taskCounterWidget - descriptionBlurEvents$ = new Subject() - descriptionUpdatesSub = this.descriptionBlurEvents$ - .pipe( - map(() => ({ - html: this.descriptionRef.nativeElement.innerHTML.trim(), - text: this.descriptionRef.nativeElement.innerText.trim(), - })), - tap(({ text }) => { - this.deselectEditor() - if (!text) this.descriptionExpansionChanges$.next({ emit: true, isExpanded: false }) - }), - filter(({ text, html }) => { - if (!text && !this.task$.value?.description) return false - // if (this.task$.value?.description == text) return false - if (this.task$.value?.description == html) return false - - return true - }), - // distinctUntilChanged((prev, curr) => { - // const isTextSame = prev.text == curr.text - // const isHtmlSame = prev.html == curr.html - // return isTextSame && isHtmlSame - // }), - tap(({ text, html }) => { - this.descriptionChange.emit(text ? html : '') - - if (text) this.descriptionExpansionChanges$.next({ emit: true, isExpanded: true }) - }), - untilDestroyed(this) - ) - .subscribe() + this.taskCounterWidgetId = 'task-counter' + this.task$.value.id + const taskCount = this.getTaskProgressRecursive(this.task$.value) - deselectEditor() { - window.getSelection()?.removeAllRanges() + this.taskCounterWidget = createCounterWidget({ + widgetId: this.taskCounterWidgetId, + sticky: false, + withLabel: false, + style: { + display: taskCount.totalItems == 0 ? 'none' : 'flex', + }, + overrideStyles: true, + checklistCount: taskCount, + }) + + // @TODO: can we skip some redundant recalculations here? + this.task$.pipe(untilDestroyed(this)).subscribe(task => { + if (!task) return + + const taskCount = this.getTaskProgressRecursive(task) + updateCounterWidget({ + widgetId: this.taskCounterWidgetId, + checklistCount: taskCount, + sticky: false, + overrideStyles: true, + style: { + display: taskCount.totalItems == 0 ? 'none' : 'flex', + }, + }) + }) + + return this.taskCounterWidget + } + + openDescription() { + this.isDescriptionExpandedInput$.next({ emit: false, isExpanded: true }) + moveToMacroQueue(() => { + this.descriptionEditor?.editor.commands.focus() + }) + } + toggleDescription() { + this.isDescriptionExpanded$.pipe(first()).subscribe(isExpanded => { + this.isDescriptionExpandedInput$.next({ emit: true, isExpanded: !isExpanded }) + }) } } diff --git a/client-v2/src/app/components/organisms/user-menu/user-menu.component.ts b/client-v2/src/app/components/organisms/user-menu/user-menu.component.ts index 3a780d41..1bb8c839 100644 --- a/client-v2/src/app/components/organisms/user-menu/user-menu.component.ts +++ b/client-v2/src/app/components/organisms/user-menu/user-menu.component.ts @@ -3,7 +3,7 @@ import { Store } from '@ngrx/store' import { AppState } from 'src/app/store' import { authActions } from 'src/app/store/user/user.actions' import { userFeature } from 'src/app/store/user/user.selectors' -import { MenuItem, MenuItemVariant } from '../../molecules/drop-down/drop-down.component' +import { MenuItem, MenuItemVariant } from '../../../dropdown/drop-down/drop-down.component' @Component({ selector: 'user-menu', @@ -39,7 +39,7 @@ export class UserMenuComponent { // title: 'Garbage', // icon: 'fas fa-trash' as IconKey, // }, - { isSeperator: true }, + { isSeparator: true }, { title: 'Logout', icon: 'logout', diff --git a/client-v2/src/app/components/templates/main-pane/main-pane.component.ts b/client-v2/src/app/components/templates/main-pane/main-pane.component.ts index 860d710f..f737b24b 100644 --- a/client-v2/src/app/components/templates/main-pane/main-pane.component.ts +++ b/client-v2/src/app/components/templates/main-pane/main-pane.component.ts @@ -18,6 +18,9 @@ import { MenuService } from '../sidebar-layout/menu.service' @apply -translate-y-full; } } + main { + @apply not-hover:[&_::-webkit-scrollbar-thumb]:bg-transparent; + } `, ], changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/client-v2/src/app/components/templates/sidebar-layout/sidebar-layout.component.css b/client-v2/src/app/components/templates/sidebar-layout/sidebar-layout.component.css index ab96a91e..9832a7ba 100644 --- a/client-v2/src/app/components/templates/sidebar-layout/sidebar-layout.component.css +++ b/client-v2/src/app/components/templates/sidebar-layout/sidebar-layout.component.css @@ -5,4 +5,8 @@ .sidebar--mobile.hide { @apply -translate-x-full; } -} \ No newline at end of file +} + +.sidebar { + @apply [&_::-webkit-scrollbar-thumb]:not-hover:bg-transparent; +} diff --git a/client-v2/src/app/components/molecules/drop-down/drop-down.component.css b/client-v2/src/app/dropdown/drop-down/drop-down.component.css similarity index 55% rename from client-v2/src/app/components/molecules/drop-down/drop-down.component.css rename to client-v2/src/app/dropdown/drop-down/drop-down.component.css index c063770d..0bdd11c5 100644 --- a/client-v2/src/app/components/molecules/drop-down/drop-down.component.css +++ b/client-v2/src/app/dropdown/drop-down/drop-down.component.css @@ -4,3 +4,10 @@ :where(.route-active) { @apply text-primary-300; } + +.menu-item { + @apply inline-flex justify-between; +} +.menu-item.item-active { + @apply !bg-tinted-600; +} diff --git a/client-v2/src/app/components/molecules/drop-down/drop-down.component.html b/client-v2/src/app/dropdown/drop-down/drop-down.component.html similarity index 60% rename from client-v2/src/app/components/molecules/drop-down/drop-down.component.html rename to client-v2/src/app/dropdown/drop-down/drop-down.component.html index 79b69888..c5b558ec 100644 --- a/client-v2/src/app/components/molecules/drop-down/drop-down.component.html +++ b/client-v2/src/app/dropdown/drop-down/drop-down.component.html @@ -1,4 +1,4 @@ - @@ -134,8 +135,8 @@ class="icon-btn" (click)="$event.stopPropagation(); nodeTooltip.hideTooltip()" [cdkMenuTriggerFor]="nodeDropDown" - (cdkMenuOpened)="isHovered[node.id] = true" - (cdkMenuClosed)="isHovered[node.id] = false" + (cdkMenuOpened)="isSelected[node.id] = true" + (cdkMenuClosed)="isSelected[node.id] = false" data-test-name="open-menu" > diff --git a/client-v2/src/app/pages/home/home.component.ts b/client-v2/src/app/pages/home/home.component.ts index 2def974b..e5646ae2 100644 --- a/client-v2/src/app/pages/home/home.component.ts +++ b/client-v2/src/app/pages/home/home.component.ts @@ -1,12 +1,12 @@ import { ArrayDataSource } from '@angular/cdk/collections' import { FlatTreeControl } from '@angular/cdk/tree' -import { Component } from '@angular/core' +import { ChangeDetectionStrategy, Component } from '@angular/core' import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy' import { Store } from '@ngrx/store' import { Action } from '@ngrx/store/src/models' -import { combineLatestWith, delay, map, tap } from 'rxjs' -import { MenuItem } from 'src/app/components/molecules/drop-down/drop-down.component' +import { combineLatestWith, map, tap } from 'rxjs' import { MenuService } from 'src/app/components/templates/sidebar-layout/menu.service' +import { MenuItem } from 'src/app/dropdown/drop-down/drop-down.component' import { EntityPreviewFlattend, EntityType } from 'src/app/fullstack-shared-models/entities.model' import { TaskPreview } from 'src/app/fullstack-shared-models/task.model' import { DeviceService } from 'src/app/services/device.service' @@ -49,6 +49,7 @@ export const convertToEntityTreeNode = (entity: EntityPreviewFlattend): EntityTr selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class HomeComponent { constructor( @@ -62,21 +63,16 @@ export class HomeComponent { EntityType = EntityType // @TODO: lets have a look at this again when socket integration is ready - shouldFetchSubcription = this.deviceService.shouldFetch$ - .pipe( - delay(0), // move to macro queue - untilDestroyed(this) - ) - .subscribe(index => { - if (index == 0) { - this.store.dispatch(entitiesActions.loadPreviews()) - this.store.dispatch(taskActions.loadTaskPreviews()) - return - } + shouldFetchSubcription = this.deviceService.shouldFetch$.pipe(untilDestroyed(this)).subscribe(index => { + if (index == 0) { + this.store.dispatch(entitiesActions.loadPreviews()) + this.store.dispatch(taskActions.loadTaskPreviews()) + return + } - this.store.dispatch(entitiesActions.reloadPreviews()) - this.store.dispatch(taskActions.reloadTaskPreviews()) - }) + this.store.dispatch(entitiesActions.reloadPreviews()) + this.store.dispatch(taskActions.reloadTaskPreviews()) + }) isMobileScreen$ = this.deviceService.isMobileScreen$ @@ -103,6 +99,7 @@ export class HomeComponent { return true } + // changes are automatically reflected here, since it always stays the same object identity entityExpandedMap = this.uiStateService.sidebarUiState.entityExpandedMap toggleExpansion(node: EntityTreeNode) { node.isExpanded = !node.isExpanded @@ -158,7 +155,7 @@ export class HomeComponent { entitiesActions.loadPreviewsError, ]) - isHovered: Record = {} + isSelected: Record = {} nodeLoadingMap$ = this.loadingService.getEntitiesLoadingStateMap() dataSource = new ArrayDataSource(this.entityPreviewsTransformed$) diff --git a/client-v2/src/app/rich-text-editor/editor-features/base.feature.ts b/client-v2/src/app/rich-text-editor/editor-features/base.feature.ts new file mode 100644 index 00000000..fc2c70ea --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor-features/base.feature.ts @@ -0,0 +1,10 @@ +import { Document } from '@tiptap/extension-document' +import { Dropcursor } from '@tiptap/extension-dropcursor' +import { Gapcursor } from '@tiptap/extension-gapcursor' +import { Text } from '@tiptap/extension-text' +import { EditorFeature, EditorFeatureId } from '../editor.types' + +export const baseFeature: EditorFeature = { + featureId: EditorFeatureId.Base, + extensions: [Document, Text, Dropcursor, Gapcursor], +} diff --git a/client-v2/src/app/rich-text-editor/editor-features/blocks.feature.ts b/client-v2/src/app/rich-text-editor/editor-features/blocks.feature.ts new file mode 100644 index 00000000..4c797d93 --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor-features/blocks.feature.ts @@ -0,0 +1,222 @@ +import { Blockquote } from '@tiptap/extension-blockquote' +import { BulletList } from '@tiptap/extension-bullet-list' +import { CodeBlock } from '@tiptap/extension-code-block' +import { HardBreak } from '@tiptap/extension-hard-break' +import { Heading } from '@tiptap/extension-heading' +import { HorizontalRule } from '@tiptap/extension-horizontal-rule' +import { ListItem } from '@tiptap/extension-list-item' +import { OrderedList } from '@tiptap/extension-ordered-list' +import { Paragraph } from '@tiptap/extension-paragraph' +import { TaskItem } from '@tiptap/extension-task-item' +import { TaskList } from '@tiptap/extension-task-list' +import { IconKey } from 'src/app/components/atoms/icons/icon/icons' +import { EditorControl, EditorControlId, EditorFeatureId, separator } from '../editor.types' +import { createEditorFeature, getActiveListType } from '../editor.helpers' + +const extensionDisplayName = { + bulletList: 'Bullet list', + orderedList: 'Ordered list', + taskList: 'Checklist', + codeBlock: 'Code Block', + blockquote: 'Blockquote', + paragraph: 'Paragraph', +} + +export type HeadingLevel = 1 | 2 | 3 | 4 + +declare module '@tiptap/core' { + interface Commands { + toggleTaskItem: () => ReturnType + } +} + +export const blocksFeature = createEditorFeature({ + featureId: EditorFeatureId.Blocks, + extensions: [ + Paragraph, + Heading, + BulletList, + OrderedList, + ListItem, + TaskList, + TaskItem.extend({ + addCommands() { + return { + ...this.parent?.(), + toggleTaskItem: () => { + // if (this.editor.isActive('taskItem')) { + // return this.editor.commands.toggleTaskList() + // } + // return this.editor.commands.command(({ tr, dispatch }) => { + // const { selection } = tr + // const transaction = tr.setNodeAttribute(selection.anchor, 'checked', false) + // // const { $from, $to } = selection + // // const range = $from.blockRange($to) + // // if (!range) return false + + // // const { start } = range + // // const { checked } = node.attrs + + // // const taskItem = this.type.create({ + // // checked: !checked, + // // }) + + // // const transaction = tr.replaceRangeWith(start, start + node.nodeSize, taskItem) + + // if (dispatch) { + // dispatch(transaction) + // } + + // return true + // }) + + return this.editor.commands.toggleNode('taskItem', 'paragraph') + }, + } + }, + addKeyboardShortcuts() { + return { + ...this.parent?.(), + 'Mod-Shift-Enter': ({ editor }) => + this.editor.commands.command(({ chain }) => { + const isChecked = editor.isActive('taskItem', { checked: true }) + return chain().updateAttributes('taskItem', { checked: !isChecked }).run() + }), + } + }, + }).configure({ nested: true }), + CodeBlock.extend({ + addKeyboardShortcuts() { + return { + ...this.parent?.(), + // Remove the default shortcut + 'Mod-Alt-c': () => false, + } + }, + }), + HorizontalRule, + HardBreak, + Blockquote, + ], + layout: [ + { + controlId: EditorControlId.Blocks, + dropdown: [ + EditorControlId.Paragraph, + separator, + EditorControlId.Heading1, + EditorControlId.Heading2, + EditorControlId.Heading3, + EditorControlId.Heading4, + // EditorControlId.Heading5, + // EditorControlId.Heading6, + separator, + EditorControlId.OrderedList, + EditorControlId.BulletList, + EditorControlId.TaskList, + separator, + EditorControlId.CodeBlock, + EditorControlId.Blockquote, + EditorControlId.HorizontalRule, + ], + }, + ], + controls: [ + { + controlId: EditorControlId.Paragraph, + title: extensionDisplayName.paragraph, + icon: 'editor.paragraph', + keybinding: 'Mod-Alt-0', + action: ({ chain }) => chain().setParagraph().run(), + }, + { + controlId: EditorControlId.Blocks, + title: ({ editor }) => { + if (editor.isActive('heading')) { + const { level } = editor.getAttributes('heading') + return `Heading ${level}` + } + + const listType = getActiveListType(editor)?.list + if (listType) return extensionDisplayName[listType] + + if (editor.isActive('codeBlock')) return extensionDisplayName.codeBlock + if (editor.isActive('blockquote')) return extensionDisplayName.blockquote + + return extensionDisplayName.paragraph + }, + icon: ({ editor }) => { + if (editor.isActive('heading')) { + const { level } = editor.getAttributes('heading') + return `editor.heading${level <= 4 ? level : ''}` as `editor.heading${HeadingLevel | ''}` + } + + const listType = getActiveListType(editor)?.list + if (listType) return ('editor.' + listType) as IconKey + + if (editor.isActive('codeBlock')) return 'editor.codeBlock' + if (editor.isActive('blockquote')) return 'editor.quote' + + return 'editor.paragraph' + }, + fixedWith: '2.75rem', + }, + // @TODO: currently only limited to 4 levels by FontAwesome only having 4 heading icons + ...[1, 2, 3, 4, 5, 6].map(level => ({ + title: 'Heading ' + level, + controlId: `heading-${level}` as EditorControlId, + icon: ('editor.heading' + level) as `editor.heading${HeadingLevel}`, + keybinding: `Mod-Alt-${level}`, + isActive: ({ editor }) => editor.isActive('heading', { level }), + action: ({ chain }) => + chain() + .toggleHeading({ level: level as HeadingLevel }) + .run(), + })), + { + controlId: EditorControlId.CodeBlock, + title: extensionDisplayName.codeBlock, + icon: 'editor.codeBlock', + // keybinding: 'mod+alt+C', + isActive: ({ editor }) => editor.isActive('codeBlock'), + action: ({ chain }) => chain().toggleCodeBlock().run(), + }, + { + controlId: EditorControlId.Blockquote, + title: extensionDisplayName.blockquote, + icon: 'editor.quote', + isActive: ({ editor }) => editor.isActive('blockquote'), + action: ({ chain }) => chain().toggleBlockquote().run(), + }, + { + controlId: EditorControlId.HorizontalRule, + title: 'Horizontal Rule', + icon: 'editor.horizontalRule', + action: ({ chain }) => chain().setHorizontalRule().run(), + }, + { + controlId: EditorControlId.BulletList, + title: extensionDisplayName.bulletList, + icon: 'editor.bulletList', + keybinding: 'Mod-Shift-8', + isActive: ({ editor }) => editor.isActive('bulletList'), + action: ({ chain }) => chain().toggleBulletList().run(), + }, + { + controlId: EditorControlId.OrderedList, + title: extensionDisplayName.orderedList, + keybinding: 'Mod-Shift-7', + icon: 'editor.orderedList', + isActive: ({ editor }) => editor.isActive('orderedList'), + action: ({ chain }) => chain().toggleOrderedList().run(), + }, + { + controlId: EditorControlId.TaskList, + title: extensionDisplayName.taskList, + keybinding: 'Mod-Shift-9', + icon: 'editor.taskList', + isActive: ({ editor }) => editor.isActive('taskList'), + action: ({ chain }) => chain().toggleTaskList().run(), + }, + ], +}) diff --git a/client-v2/src/app/rich-text-editor/editor-features/blur.feature.ts b/client-v2/src/app/rich-text-editor/editor-features/blur.feature.ts new file mode 100644 index 00000000..8f4ff098 --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor-features/blur.feature.ts @@ -0,0 +1,30 @@ +import { Extension } from '@tiptap/core' +import { EditorControlId, EditorFeatureId } from '../editor.types' +import { createEditorFeature } from '../editor.helpers' + +export const blurFeature = createEditorFeature({ + featureId: EditorFeatureId.Blur, + extensions: [ + Extension.create({ + addKeyboardShortcuts() { + return { + 'Mod-Enter': () => this.editor.commands.blur(), + } + }, + }), + ], + layout: [EditorControlId.Blur], + controls: [ + { + controlId: EditorControlId.Blur, + icon: 'close', + title: 'Done', + keybinding: 'Mod-Enter', + action: ({ editor }) => { + editor.commands.focus() + editor.commands.blur() + return true + }, + }, + ], +}) diff --git a/client-v2/src/app/rich-text-editor/editor-features/checklist-counter.feature.ts b/client-v2/src/app/rich-text-editor/editor-features/checklist-counter.feature.ts new file mode 100644 index 00000000..e7eeb9d3 --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor-features/checklist-counter.feature.ts @@ -0,0 +1,7 @@ +import { EditorFeatureId } from '../editor.types' +import { ChecklistCounterExtension } from './extensions/checklist-counter.extension' + +export const checklistCounterFeature = (options?: Parameters[0]) => ({ + featureId: EditorFeatureId.ChecklistCounter, + extensions: [ChecklistCounterExtension.configure(options)], +}) diff --git a/client-v2/src/app/rich-text-editor/editor-features/custom-events.feature.ts b/client-v2/src/app/rich-text-editor/editor-features/custom-events.feature.ts new file mode 100644 index 00000000..054c305c --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor-features/custom-events.feature.ts @@ -0,0 +1,32 @@ +import { Extension } from '@tiptap/core' +import { createEditorFeature } from '../editor.helpers' +import { EditorFeatureId } from '../editor.types' +import { Observable, Subject } from 'rxjs' + +export type CustomEditorEventsStorage = { + shouldFocusToolbar$: Observable +} + +export const customEventsFeature = createEditorFeature({ + featureId: EditorFeatureId.CustomEvents, + extensions: [ + Extension.create({ + name: EditorFeatureId.CustomEvents, + addStorage() { + return { + shouldFocusToolbar$: new Subject(), + } + }, + addKeyboardShortcuts() { + return { + 'Mod-e': () => { + const events = this.editor.storage[EditorFeatureId.CustomEvents] as CustomEditorEventsStorage + ;(events?.shouldFocusToolbar$ as Subject).next() + + return true + }, + } + }, + }), + ], +}) diff --git a/client-v2/src/app/rich-text-editor/editor-features/extensions/checklist-counter.extension.ts b/client-v2/src/app/rich-text-editor/editor-features/extensions/checklist-counter.extension.ts new file mode 100644 index 00000000..56d980da --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor-features/extensions/checklist-counter.extension.ts @@ -0,0 +1,250 @@ +import { Extension } from '@tiptap/core' +import { Plugin, PluginKey } from '@tiptap/pm/state' +import { Decoration, DecorationSet } from '@tiptap/pm/view' +import { mediaQueries } from 'src/app/services/device.service' +import { colors } from 'src/app/shared/colors' +import { ChecklistCount, countChecklistItems } from '../../editor.helpers' +import { EditorState } from 'prosemirror-state' + +const getCircumferenceOffset = (radius: number, progress: number) => { + const circumference = radius * 2 * Math.PI + return circumference - (progress / 100) * circumference +} + +const createCircularProgressSvg = (props: { + progress: number + diameter?: number + strokeWidth?: number + color?: string + bgColor?: string + transitionDuration?: string +}) => { + const diameter = props.diameter || 22 + const position = diameter / 2 + const strokeWidth = props.strokeWidth || 2.5 + const radius = diameter / 2 - strokeWidth * 2 + const circumference = radius * 2 * Math.PI + const offset = getCircumferenceOffset(radius, props.progress) + + return ` + + + + + + ` +} + +const getFractionTemplate = (checklistCount: ChecklistCount) => ` + ${checklistCount.checkedItems} + / + ${checklistCount.totalItems} +` + +export interface CounterWidgetProps { + widgetId: string + checklistCount: ChecklistCount + /** Wether to apply styles for making the container sticky */ + sticky?: boolean + style?: Partial | false + overrideStyles?: boolean +} +export interface CounterWidgetCreateProps extends CounterWidgetProps { + withLabel?: boolean +} + +const COUNTER_COMPLETE_CLASS = 'counter-complete' + +export const createCounterWidget = ({ + widgetId, + checklistCount, + sticky = true, + style = {}, + overrideStyles = false, + withLabel = true, +}: CounterWidgetCreateProps) => { + const container = document.createElement('div') + container.classList.add('checklist-counter') + container.id = widgetId + if (checklistCount.progress == 100) container.classList.add(COUNTER_COMPLETE_CLASS) + + if (sticky) { + container.style.zIndex = '10' + container.style.top = '.5rem' + container.style.position = 'sticky' + } + + if (style && !overrideStyles) { + container.classList.add('app-toolbar') + container.style.marginBottom = '.5rem' + container.style.width = 'fit-content' + container.style.paddingLeft = '.5rem' + container.style.paddingRight = '.5rem' + + if (checklistCount.progress == 100) { + container.style.backgroundColor = colors.submit[800] + container.style.borderColor = colors.submit[700] + container.style.color = colors.submit[500] + } + } + if (typeof style == 'object') Object.assign(container.style, style) + + // @TODO: Accessibility: update semantics and roles here + container.innerHTML = ` + ${!withLabel ? '' : 'Checklist:'} + ${getFractionTemplate(checklistCount)} +
+ ${createCircularProgressSvg(checklistCount)} +
+ ` + return container +} + +export const updateCounterWidget = ({ + widgetId, + checklistCount, + sticky = true, + style = {}, + overrideStyles = false, +}: CounterWidgetProps) => { + const container = document.getElementById(widgetId) + if (!container) return + + if (checklistCount.progress == 100) container.classList.add(COUNTER_COMPLETE_CLASS) + else container.classList.remove(COUNTER_COMPLETE_CLASS) + + if (sticky) { + container.style.zIndex = '10' + container.style.top = '.5rem' + container.style.position = 'sticky' + } else { + container.style.zIndex = '' + container.style.top = '' + container.style.position = '' + } + + if (style && !overrideStyles) { + if (checklistCount.progress == 100) { + container.style.backgroundColor = colors.submit[800] + container.style.borderColor = colors.submit[700] + container.style.color = colors.submit[500] + } else { + container.style.backgroundColor = '' + container.style.borderColor = '' + container.style.color = '' + } + } + if (typeof style == 'object') Object.assign(container.style, style) + + const countElem = container.querySelector('.count') + if (!countElem) return + countElem.innerHTML = getFractionTemplate(checklistCount) + + const progressElem = container.querySelector('.progress') as HTMLDivElement | null + if (!progressElem) return + if (!checklistCount.progress) progressElem.classList.add('hidden') + else progressElem.classList.remove('hidden') + + const progressRing = container.querySelector('circle.progress-ring') as SVGCircleElement | null + if (!progressRing) return + const radius = progressRing.r.baseVal.value + progressRing.style.strokeDashoffset = getCircumferenceOffset(radius, checklistCount.progress).toString() +} + +const createCounterDecoration = (doc: EditorState['doc'], props: CounterWidgetCreateProps) => { + if (!props.checklistCount.totalItems) return null + + return DecorationSet.create(doc, [Decoration.widget(0, () => createCounterWidget(props))]) +} + +export const ChecklistCounterExtension = Extension.create>({ + name: 'checklistCounter', + + addProseMirrorPlugins() { + type PluginState = { + decorationSet: DecorationSet | null + widgetId: string + } + const pluginKey = new PluginKey('checklistCounter') + + return [ + new Plugin({ + key: pluginKey, + props: { + decorations: state => pluginKey.getState(state)?.decorationSet, + }, + state: { + init: (_stateConfig, state) => { + const widgetId = + 'checklistProgressDecoration' + (Math.random() * 100).toString().replace('.', '') + return { + widgetId, + decorationSet: createCounterDecoration(state.doc, { + widgetId, + checklistCount: countChecklistItems(state.doc), + sticky: !matchMedia(mediaQueries.mobileScreen).matches, + ...this.options, + }), + } + }, + apply: (tr, prevPluginState, _prevState, newState) => { + if (!tr.docChanged) return prevPluginState + + const checklistCount = countChecklistItems(newState.doc) + if (prevPluginState.decorationSet && !checklistCount.totalItems) + return { + widgetId: prevPluginState.widgetId, + decorationSet: null, + } + + const props: CounterWidgetProps = { + widgetId: prevPluginState.widgetId, + checklistCount, + sticky: !matchMedia(mediaQueries.mobileScreen).matches, + ...this.options, + } + + if (!prevPluginState.decorationSet && checklistCount.totalItems) + return { + widgetId: prevPluginState.widgetId, + decorationSet: createCounterDecoration(newState.doc, props), + } + + updateCounterWidget(props) + + return prevPluginState + }, + }, + }), + ] + }, +}) diff --git a/client-v2/src/app/rich-text-editor/editor-features/extensions/indentation.extension.ts b/client-v2/src/app/rich-text-editor/editor-features/extensions/indentation.extension.ts new file mode 100644 index 00000000..e6cd3d13 --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor-features/extensions/indentation.extension.ts @@ -0,0 +1,180 @@ +import { Editor, Extension, Range } from '@tiptap/core' +import CodeBlock from '@tiptap/extension-code-block' +import { getActiveListType } from '../../editor.helpers' + +interface IndentationOptions { + /** Wether indentation logic should only be ran for code blocks. + * + * default: `false` + */ + codeBlocksOnly?: boolean + /** (For codeBlocks only) + * + * Wether a tab should be inserted at the cursor position or the beginning of the line. + * + * default: `false` + */ + atCursorPos?: boolean +} + +declare module '@tiptap/core' { + interface Commands { + indentation: { + indent: (options?: IndentationOptions) => ReturnType + outdent: (options?: IndentationOptions) => ReturnType + } + } +} + +interface Position { + inNode: number + inDocument: number +} + +interface Coordinates { + nodeContent: string + startOfLine: Position + firstChar: Position + cursor: Position +} + +type ResolvedPos = Editor['state']['selection']['$anchor'] +const getCoordinates = (anchor: ResolvedPos): Coordinates | null => { + const nodeStart = anchor.start() + const nodeEnd = anchor.end() + const nodeContent = anchor.doc.textBetween(nodeStart, nodeEnd) + + const fromCursorToEnd = nodeEnd - anchor.pos + let cursorPosInContent = nodeContent.length - 1 - fromCursorToEnd + + if (cursorPosInContent < 0) cursorPosInContent = 0 + + let info: Coordinates | null = null + + // look for the first new line char before the cursor, i.e. start of the current line + for (let i = cursorPosInContent; i >= 0; i--) { + if (nodeContent[i] == '\n' || i == 0) { + const firstChar = i == 0 ? 0 : i + 1 + + info = { + nodeContent, + startOfLine: { + inNode: i, + inDocument: nodeStart + i, + }, + firstChar: { + inNode: firstChar, + inDocument: nodeStart + firstChar, + }, + cursor: { + inNode: nodeContent.length - fromCursorToEnd, + inDocument: anchor.pos, + }, + } + break + } + } + return info +} + +const getTabRangeToDelete = ({ + position, + startOfLine, + nodeContent, + firstChar, +}: Omit & { position: Position }): Range | null => { + /** wether to delete the range after the cursor, or before */ + const forwards = position.inDocument == firstChar.inDocument + const startPosition = position.inDocument + + // @TODO: blindly deleting the char at/before the cursor when there is a tab after the cursor is a bad approach + + // if there is a tab, remove it + if (nodeContent[position.inNode] == '\t') { + const from = forwards ? startPosition : startPosition - 1 + return { from, to: from + 1 } + } + + // if there is a tab before the cursor, remove it + if (nodeContent[position.inNode - 1] == '\t' && position.inNode != startOfLine.inNode) { + const offset = forwards ? -1 : 0 + const from = (forwards ? startPosition : startPosition - 1) + offset + return { from, to: from + 1 } + } + + // @TODO: add support for spaces: if there are two/four spaces, remove them + // const secondSpacePos = forwards ? position.inNode + 1 : position.inNode - 1 + // if (nodeContent[position.inNode] == ' ' && nodeContent[secondSpacePos] == ' ') { + // return { + // from: forwards ? deleteFrom : deleteFrom + 1, + // to: forwards ? deleteFrom + 1 : deleteFrom - 1, + // } + // } + + return null +} + +export const IndentationExtension = Extension.create({ + name: 'indentation', + + addCommands() { + // @TODO: add support for in/outdenting multiple lines at once + + return { + indent: options => { + const { codeBlocksOnly = false, atCursorPos = false } = options || {} + + return ({ editor, chain, state }) => { + if (!editor.isActive(CodeBlock.name)) { + if (codeBlocksOnly) return false + + const type = getActiveListType(editor)?.item + if (!type) return false + + return chain().sinkListItem(type).run() + } + + if (atCursorPos) return chain().insertContent('\t').run() + + const coords = getCoordinates(state.selection.$anchor) + if (!coords) return true + + return chain().insertContentAt(coords.firstChar.inDocument, '\t', { updateSelection: false }).run() + } + }, + outdent: options => { + const { codeBlocksOnly = false, atCursorPos = false } = options || {} + + return ({ editor, chain, state }) => { + if (!editor.isActive(CodeBlock.name)) { + if (codeBlocksOnly) return false + + const type = getActiveListType(editor)?.item + if (!type) return false + + return chain().liftListItem(type).run() + } + + const coords = getCoordinates(state.selection.$anchor) + if (!coords) return true + + const rangeToDelete = getTabRangeToDelete({ + startOfLine: coords.startOfLine, + position: atCursorPos ? coords.cursor : coords.firstChar, + nodeContent: coords.nodeContent, + firstChar: coords.firstChar, + }) + + if (!rangeToDelete) return true + return chain().deleteRange(rangeToDelete).run() + } + }, + } + }, + addKeyboardShortcuts(this) { + return { + Tab: () => this.editor.commands.indent({ codeBlocksOnly: true, atCursorPos: true }), + 'Shift-Tab': () => this.editor.commands.outdent({ codeBlocksOnly: true }), + } + }, +}) diff --git a/client-v2/src/app/rich-text-editor/editor-features/extensions/search-and-replace.extension.ts b/client-v2/src/app/rich-text-editor/editor-features/extensions/search-and-replace.extension.ts new file mode 100644 index 00000000..6c4add02 --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor-features/extensions/search-and-replace.extension.ts @@ -0,0 +1,290 @@ +// Stolen from https://github.com/ueberdosis/tiptap/pull/2075 + +import { Editor, Extension } from '@tiptap/core' +import { Decoration, DecorationSet } from 'prosemirror-view' +import { EditorState, Plugin, PluginKey } from 'prosemirror-state' +import { Node as ProsemirrorNode } from 'prosemirror-model' + +declare module '@tiptap/core' { + interface Commands { + search: { + /** + * @description Set search term in extension. + */ + setSearchTerm: (searchTerm: string) => ReturnType + /** + * @description Set replace term in extension. + */ + setReplaceTerm: (replaceTerm: string) => ReturnType + /** + * @description Replace first instance of search result with given replace term. + */ + replace: () => ReturnType + /** + * @description Replace all instances of search result with given replace term. + */ + replaceAll: () => ReturnType + } + } +} + +interface Result { + from: number + to: number +} + +interface SearchOptions { + searchResultClass: string + caseSensitive: boolean + disableRegex: boolean +} + +interface SearchStorage { + searchTerm: string + replaceTerm: string + results: Result[] +} + +interface TextNodesWithPosition { + text: string + pos: number +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const updateView = (state: EditorState, dispatch: any) => dispatch(state.tr) + +const regex = (s: string, disableRegex: boolean, caseSensitive: boolean): RegExp => { + return RegExp(disableRegex ? s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') : s, caseSensitive ? 'gu' : 'gui') +} + +function processSearches( + doc: ProsemirrorNode, + searchTerm: RegExp, + searchResultClass: string +): { decorationsToReturn: DecorationSet; results: Result[] } { + const decorations: Decoration[] = [] + let textNodesWithPosition: TextNodesWithPosition[] = [] + const results: Result[] = [] + + let index = 0 + + if (!searchTerm) return { decorationsToReturn: DecorationSet.empty, results: [] } + + doc?.descendants((node, pos) => { + if (node.isText) { + if (textNodesWithPosition[index]) { + textNodesWithPosition[index] = { + text: textNodesWithPosition[index].text + node.text, + pos: textNodesWithPosition[index].pos, + } + } else { + textNodesWithPosition[index] = { + text: `${node.text}`, + pos, + } + } + } else { + index += 1 + } + }) + + textNodesWithPosition = textNodesWithPosition.filter(Boolean) + + for (let i = 0; i < textNodesWithPosition.length; i += 1) { + const { text, pos } = textNodesWithPosition[i] + + const matches = [...text.matchAll(searchTerm)] + + for (let j = 0; j < matches.length; j += 1) { + const m = matches[j] + + if (m[0] === '') break + + if (m.index !== undefined) { + results.push({ + from: pos + m.index, + to: pos + m.index + m[0].length, + }) + } + } + } + + for (let i = 0; i < results.length; i += 1) { + const r = results[i] + decorations.push(Decoration.inline(r.from, r.to, { class: searchResultClass })) + } + + return { + decorationsToReturn: DecorationSet.create(doc, decorations), + results, + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const replace = (replaceTerm: string, results: Result[], { state, dispatch }: any) => { + const firstResult = results[0] + + if (!firstResult) return + + const { from, to } = results[0] + + if (dispatch) dispatch(state.tr.insertText(replaceTerm, from, to)) +} + +const rebaseNextResult = ( + replaceTerm: string, + index: number, + lastOffset: number, + results: Result[] +): [number, Result[]] | null => { + const nextIndex = index + 1 + + if (!results[nextIndex]) return null + + const { from: currentFrom, to: currentTo } = results[index] + + const offset = currentTo - currentFrom - replaceTerm.length + lastOffset + + const { from, to } = results[nextIndex] + + results[nextIndex] = { + to: to - offset, + from: from - offset, + } + + return [offset, results] +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const replaceAll = (replaceTerm: string, results: Result[], { tr, dispatch }: any) => { + let offset = 0 + + let ourResults = results.slice() + + if (!ourResults.length) return + + for (let i = 0; i < ourResults.length; i += 1) { + const { from, to } = ourResults[i] + + tr.insertText(replaceTerm, from, to) + + const rebaseNextResultResponse = rebaseNextResult(replaceTerm, i, offset, ourResults) + + if (rebaseNextResultResponse) { + offset = rebaseNextResultResponse[0] + ourResults = rebaseNextResultResponse[1] + } + } + + dispatch(tr) +} + +// need to use editor storage because the extension storage is shared among all instances +const getSearchStorage = (editor: Editor): SearchStorage => editor.storage['search'] as SearchStorage + +// eslint-disable-next-line @typescript-eslint/ban-types +export const SearchAndReplace = Extension.create({ + name: 'search', + + addOptions() { + return { + searchResultClass: 'search-result', + caseSensitive: false, + disableRegex: false, + } + }, + + onBeforeCreate() { + this.editor.storage['search'] = { + searchTerm: '', + replaceTerm: '', + results: [], + } + }, + + addCommands() { + return { + setSearchTerm: + (searchTerm: string) => + ({ state, dispatch }) => { + getSearchStorage(this.editor).searchTerm = searchTerm + getSearchStorage(this.editor).results = [] + + updateView(state, dispatch) + + return false + }, + setReplaceTerm: + (replaceTerm: string) => + ({ state, dispatch }) => { + getSearchStorage(this.editor).replaceTerm = replaceTerm + getSearchStorage(this.editor).results = [] + + updateView(state, dispatch) + + return false + }, + replace: + () => + ({ state, dispatch }) => { + const { replaceTerm, results } = getSearchStorage(this.editor) + + replace(replaceTerm, results, { state, dispatch }) + getSearchStorage(this.editor).results.shift() + + updateView(state, dispatch) + + return false + }, + replaceAll: + () => + ({ state, tr, dispatch }) => { + const { replaceTerm, results } = getSearchStorage(this.editor) + + replaceAll(replaceTerm, results, { tr, dispatch }) + getSearchStorage(this.editor).results = [] + + updateView(state, dispatch) + + return false + }, + } + }, + + addProseMirrorPlugins() { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const extensionThis = this + + return [ + new Plugin({ + key: new PluginKey('search'), + state: { + init() { + return DecorationSet.empty + }, + apply({ doc, docChanged }) { + const { searchResultClass, disableRegex, caseSensitive } = extensionThis.options + const { searchTerm } = getSearchStorage(extensionThis.editor) + + if (docChanged || searchTerm) { + const { decorationsToReturn, results } = processSearches( + doc, + regex(searchTerm, disableRegex, caseSensitive), + searchResultClass + ) + getSearchStorage(extensionThis.editor).results = results + + return decorationsToReturn + } + return DecorationSet.empty + }, + }, + props: { + decorations(state) { + return this.getState(state) + }, + }, + }), + ] + }, +}) diff --git a/client-v2/src/app/rich-text-editor/editor-features/history.feature.ts b/client-v2/src/app/rich-text-editor/editor-features/history.feature.ts new file mode 100644 index 00000000..bc8bc4a5 --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor-features/history.feature.ts @@ -0,0 +1,25 @@ +import { History } from '@tiptap/extension-history' +import { EditorFeature, EditorControlId, EditorFeatureId } from '../editor.types' + +export const historyFeature: EditorFeature = { + featureId: EditorFeatureId.History, + extensions: [History], + controls: [ + { + title: 'Undo', + controlId: EditorControlId.Undo, + icon: 'editor.undo', + // displayKeybinding: 'mod+Z', + keybinding: 'mod+Z', + action: ({ chain }) => chain().undo().run(), + }, + { + title: 'Redo', + controlId: EditorControlId.Redo, + icon: 'editor.redo', + // displayKeybinding: 'mod+shift+Z', + keybinding: 'mod+shift+Z', + action: ({ chain }) => chain().redo().run(), + }, + ], +} diff --git a/client-v2/src/app/rich-text-editor/editor-features/indentation.feature.ts b/client-v2/src/app/rich-text-editor/editor-features/indentation.feature.ts new file mode 100644 index 00000000..222cf6f7 --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor-features/indentation.feature.ts @@ -0,0 +1,23 @@ +import { IndentationExtension } from './extensions/indentation.extension' +import { EditorFeature, EditorControlId, EditorFeatureId } from '../editor.types' + +export const indentationFeature: EditorFeature = { + featureId: EditorFeatureId.Indentation, + extensions: [IndentationExtension], + controls: [ + { + controlId: EditorControlId.Indent, + title: 'Indent', + icon: 'editor.indent', + keybinding: 'Tab', + action: ({ chain }) => chain().indent().run(), + }, + { + controlId: EditorControlId.Outdent, + title: 'Outdent', + icon: 'editor.outdent', + keybinding: 'Shift-Tab', + action: ({ chain }) => chain().outdent().run(), + }, + ], +} diff --git a/client-v2/src/app/rich-text-editor/editor-features/inline.feature.ts b/client-v2/src/app/rich-text-editor/editor-features/inline.feature.ts new file mode 100644 index 00000000..021ca728 --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor-features/inline.feature.ts @@ -0,0 +1,71 @@ +import { Bold } from '@tiptap/extension-bold' +import { Code } from '@tiptap/extension-code' +import { Italic } from '@tiptap/extension-italic' +import { Strike } from '@tiptap/extension-strike' +import { EditorControlId, EditorFeatureId } from '../editor.types' +import { createEditorFeature } from '../editor.helpers' + +export const inlineFeature = createEditorFeature({ + featureId: EditorFeatureId.Inline, + extensions: [ + Bold, + Italic, + Code.extend({ + excludes: 'bold italic code', // allow links and strikethrough in code + code: true, + exitable: true, + + addKeyboardShortcuts() { + return { + // Add shift to the default keybinding + // 'Mod-E': () => this.editor.chain().focus().toggleCode().run(), + 'Control-e': () => this.editor.chain().focus().toggleCode().run(), + } + }, + }), + Strike.extend({ + addKeyboardShortcuts(this) { + const toggleStrike = () => this.editor.chain().focus().toggleStrike().run() + return { + 'Mod-shift-S': toggleStrike, + 'Mod-shift-X': toggleStrike, + } + }, + }), + ], + layout: [EditorControlId.Bold, EditorControlId.Italic, EditorControlId.Strike, EditorControlId.InlineCode], + controls: [ + { + title: 'Bold', + controlId: EditorControlId.Bold, + icon: 'editor.bold', + keybinding: 'mod+B', + isActive: ({ editor }) => editor.isActive('bold'), + action: ({ chain }) => chain().toggleBold().run(), + }, + { + title: 'Italic', + controlId: EditorControlId.Italic, + icon: 'editor.italic', + keybinding: 'mod+I', + isActive: ({ editor }) => editor.isActive('italic'), + action: ({ chain }) => chain().toggleItalic().run(), + }, + { + title: 'Strikethrough', + controlId: EditorControlId.Strike, + icon: 'editor.strike', + keybinding: 'mod+shift+S', + isActive: ({ editor }) => editor.isActive('strike'), + action: ({ chain }) => chain().toggleStrike().run(), + }, + { + title: 'Inline Code', + controlId: EditorControlId.InlineCode, + icon: 'editor.code', + keybinding: 'ctrl-E', + isActive: ({ editor }) => editor.isActive('code'), + action: ({ chain }) => chain().toggleCode().run(), + }, + ], +}) diff --git a/client-v2/src/app/rich-text-editor/editor-features/link.feature.ts b/client-v2/src/app/rich-text-editor/editor-features/link.feature.ts new file mode 100644 index 00000000..7935b8e6 --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor-features/link.feature.ts @@ -0,0 +1,43 @@ +import { Editor } from '@tiptap/core' +import Link from '@tiptap/extension-link' +import { EditorFeature, EditorControlId, EditorFeatureId } from '../editor.types' + +const toggleLink = (editor: Editor) => { + const selectionStart = editor.state.selection.from + const selectionEnd = editor.state.selection.to + const selectedText = editor.state.doc.textBetween(selectionStart, selectionEnd).trim() + // console.log(editor.getAttributes('link').href) + // @TODO: just a quick hack, needs improvement + const linkRegex = /https?:\/\/[^\s]+/ + const parsedLink = linkRegex.exec(selectedText)?.[0] + + const link = parsedLink || prompt('Enter a link')?.trim() + if (!link) return false // @TODO: check if returning `false` indicates non-successful invocation + + return editor.chain().focus().toggleLink({ href: link }).run() +} + +export const linkFeature: EditorFeature = { + featureId: EditorFeatureId.Link, + extensions: [ + Link.extend({ + addKeyboardShortcuts() { + return { + 'Ctrl-k': () => toggleLink(this.editor), + } + }, + }), + ], + controls: [ + { + title: 'Link', + controlId: EditorControlId.Link, + icon: 'editor.link', + // displayKeybinding: 'mod+K', + // registerKeybinding: 'Mod-k', + keybinding: 'ctrl+k', + isActive: ({ editor }) => editor.isActive('link'), + action: ({ editor }) => toggleLink(editor), + }, + ], +} diff --git a/client-v2/src/app/rich-text-editor/editor-features/markdown.feature.ts b/client-v2/src/app/rich-text-editor/editor-features/markdown.feature.ts new file mode 100644 index 00000000..057918fc --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor-features/markdown.feature.ts @@ -0,0 +1,42 @@ +import { Markdown } from 'tiptap-markdown' +import { EditorFeature, EditorControlId, EditorFeatureId } from '../editor.types' + +export const markdownFeature: EditorFeature = { + featureId: EditorFeatureId.Markdown, + extensions: [ + Markdown.extend({ + addCommands() { + // Remove the commands as their causing some trouble and don't seem to work anyways + // -> overridden insertContentAt() command throws when inserting a `\t` character in codeBlocks + return {} + }, + }).configure({ + linkify: true, + html: true, // Allow HTML input/output + breaks: true, // New lines (\n) in markdown input are converted to
+ }), + ], + controls: [ + { + controlId: EditorControlId.CopyAsMarkdown, + title: 'Copy as Markdown', + icon: 'markdown', + action: ({ editor, toast }) => { + const untouched = editor.storage['markdown'].getMarkdown() as string + const markdown = untouched.replace(/\n\n(\s|\t)*- /g, match => { + return match.replace(/\n\n/g, '\n') + }) + + navigator.clipboard + .writeText(markdown) + .then(() => toast.success('Copied as Markdown')) + .catch(e => { + toast.error('Could not copy to clipboard') + console.error('Could not copy to clipboard', e) + }) + + return true + }, + }, + ], +} diff --git a/client-v2/src/app/rich-text-editor/editor-features/placeholder.feature.ts b/client-v2/src/app/rich-text-editor/editor-features/placeholder.feature.ts new file mode 100644 index 00000000..c8ccb73d --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor-features/placeholder.feature.ts @@ -0,0 +1,49 @@ +import Blockquote from '@tiptap/extension-blockquote' +import Heading from '@tiptap/extension-heading' +import Placeholder from '@tiptap/extension-placeholder' +import { EditorFeature, EditorFeatureId } from '../editor.types' + +declare module '@tiptap/core' { + interface Commands { + placeholder: { + setPlaceholder: (placeholder: string) => ReturnType + } + } +} + +export const placeholderFeature = (emptyEditorPlaceholder: string): EditorFeature => ({ + featureId: EditorFeatureId.Placeholder, + extensions: [ + Placeholder.extend({ + addCommands() { + return { + setPlaceholder: placeholder => { + return ({ editor }) => { + editor.storage[EditorFeatureId.Placeholder] = placeholder + return true + } + }, + } + }, + }).configure({ + placeholder: ({ node, editor }) => { + if (editor.isEmpty) return editor.storage[EditorFeatureId.Placeholder] || emptyEditorPlaceholder + + if (node.type.name == Heading.name) return 'Heading ' + node.attrs['level'] + if (node.type.name == Blockquote.name) return 'Quote' + + // const parentNode = editor.state.selection.$anchor.parent + // console.log('node', node.type.name) + // console.log('parent', parentNode.type.name) + // if (parentNode.type.name == 'bulletList') return 'List' + // if (parentNode.type.name == 'taskList') return 'Task' + // if (parentNode.type.name == 'codeBlock') return "Write some code let's goo!" + + // if (node.type.name == Paragraph.name) return 'Type "/" for commands' + + return '' + }, + showOnlyCurrent: false, + }), + ], +}) diff --git a/client-v2/src/app/rich-text-editor/editor-features/search-and-replace.feature.ts b/client-v2/src/app/rich-text-editor/editor-features/search-and-replace.feature.ts new file mode 100644 index 00000000..5216c31e --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor-features/search-and-replace.feature.ts @@ -0,0 +1,8 @@ +import { SearchAndReplace } from './extensions/search-and-replace.extension' +import { EditorFeatureId } from '../editor.types' +import { createEditorFeature } from '../editor.helpers' + +export const searchAndReplaceFeature = createEditorFeature({ + featureId: EditorFeatureId.SearchAndReplace, + extensions: [SearchAndReplace.configure({ searchResultClass: 'highlight' })], +}) diff --git a/client-v2/src/app/rich-text-editor/editor-features/typography.feature.ts b/client-v2/src/app/rich-text-editor/editor-features/typography.feature.ts new file mode 100644 index 00000000..f348123f --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor-features/typography.feature.ts @@ -0,0 +1,17 @@ +import Typography from '@tiptap/extension-typography' +import { EditorFeature, EditorFeatureId } from '../editor.types' + +export const typographyFeature: EditorFeature = { + featureId: EditorFeatureId.Typography, + extensions: [ + Typography.configure({ + emDash: false, + // ellipsis ?? + openDoubleQuote: false, + closeDoubleQuote: false, + openSingleQuote: false, + closeSingleQuote: false, + notEqual: false, + }), + ], +} diff --git a/client-v2/src/app/rich-text-editor/editor.features.ts b/client-v2/src/app/rich-text-editor/editor.features.ts new file mode 100644 index 00000000..e56e2e45 --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor.features.ts @@ -0,0 +1,108 @@ +import { InjectionToken, inject } from '@angular/core' +import { baseFeature } from './editor-features/base.feature' +import { blocksFeature } from './editor-features/blocks.feature' +import { blurFeature } from './editor-features/blur.feature' +import { checklistCounterFeature } from './editor-features/checklist-counter.feature' +import { customEventsFeature } from './editor-features/custom-events.feature' +import { historyFeature } from './editor-features/history.feature' +import { indentationFeature } from './editor-features/indentation.feature' +import { inlineFeature } from './editor-features/inline.feature' +import { linkFeature } from './editor-features/link.feature' +import { markdownFeature } from './editor-features/markdown.feature' +import { placeholderFeature } from './editor-features/placeholder.feature' +import { typographyFeature } from './editor-features/typography.feature' +import { EditorControlId, EditorFeature, EditorFeatureId, EditorLayoutItem, separator } from './editor.types' +import { createEditorFeature } from './editor.helpers' +import { searchAndReplaceFeature } from './editor-features/search-and-replace.feature' + +const moreMenuFeature = createEditorFeature({ + featureId: EditorFeatureId.MoreMenu, + extensions: [], + controls: [ + { + controlId: EditorControlId.More, + title: 'More', + icon: 'ellipsisHorizontal', + fixedWith: '2.75rem', + }, + ], +}) + +export const defaultFeatureMap = { + baseFeature, + historyFeature, + inlineFeature, + linkFeature, + blocksFeature, + placeholderFeature: placeholderFeature('Description'), + typographyFeature, + blurFeature, + indentationFeature, + markdownFeature, + moreMenuFeature, + customEventsFeature, + checklistCounterFeature: checklistCounterFeature(), + searchAndReplaceFeature, +} // @TODO: satisfies Record +const defaultFeatureMask = Object.fromEntries(Object.keys(defaultFeatureMap).map(key => [key, true])) + +// @TODO: can be made more generic -> `getEditorFeatureGroup(featureMap)(featureMask) => EditorFeature[]` +export const getDefaultEditorFeatures = ( + featureMask: Partial> = {} +) => { + return Object.entries({ ...defaultFeatureMask, ...featureMask }) + .map(([key, enabled]) => { + if (!enabled) return + return defaultFeatureMap[key as keyof typeof defaultFeatureMap] + }) + .filter(Boolean) +} + +export const defaultDesktopEditorLayout: EditorLayoutItem[] = [ + ...blocksFeature.layout, + separator, + ...inlineFeature.layout, + EditorControlId.Link, + separator, + { + controlId: EditorControlId.More, + dropdown: [ + EditorControlId.Undo, + EditorControlId.Redo, + separator, + EditorControlId.Indent, + EditorControlId.Outdent, + separator, + EditorControlId.CopyAsMarkdown, + ], + }, + blurFeature.layout[0], +] +export const defaultMobileEditorLayout: EditorLayoutItem[] = [ + ...blocksFeature.layout, + separator, + ...inlineFeature.layout, + EditorControlId.Link, + separator, + EditorControlId.Undo, + EditorControlId.Redo, + separator, + EditorControlId.Indent, + EditorControlId.Outdent, + separator, + { + controlId: EditorControlId.More, + dropdown: [EditorControlId.CopyAsMarkdown], + }, +] + +export const getDefaultEditorLayout = (isTouchPrimary: boolean) => { + return isTouchPrimary ? defaultMobileEditorLayout : defaultDesktopEditorLayout +} + +export const EDITOR_FEATURES_TOKEN = new InjectionToken('Editor Features Injector') +export const provideEditorFeatures = (features: EditorFeature[]) => ({ + provide: EDITOR_FEATURES_TOKEN, + useValue: features, +}) +export const injectEditorFeatures = () => inject(EDITOR_FEATURES_TOKEN) diff --git a/client-v2/src/app/rich-text-editor/editor.helpers.ts b/client-v2/src/app/rich-text-editor/editor.helpers.ts new file mode 100644 index 00000000..593e72a9 --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor.helpers.ts @@ -0,0 +1,52 @@ +import { Editor } from '@tiptap/core' +import { EditorState } from 'prosemirror-state' +import { EditorFeature } from './editor.types' + +export const createEditorFeature = (feature: TFeature): TFeature => feature + +type ListType = typeof bulletListType | typeof orderedListType | typeof taskListType + +const bulletListType = { list: 'bulletList', item: 'listItem' } as const +const orderedListType = { list: 'orderedList', item: 'listItem' } as const +const taskListType = { list: 'taskList', item: 'taskItem' } as const + +export const getActiveListType = (editor: Editor): ListType | null => { + if (editor.isActive(bulletListType.list)) return bulletListType + if (editor.isActive(orderedListType.list)) return orderedListType + if (editor.isActive(taskListType.list)) return taskListType + + return null +} + +export interface ChecklistCount { + totalItems: number + checkedItems: number + progress: number +} +export const countChecklistItems = (doc: EditorState['doc']): ChecklistCount => { + let totalItems = 0 + let checkedItems = 0 + + doc.descendants(node => { + if (node.type.name != 'taskItem') return true + + totalItems++ + if (node.attrs['checked']) checkedItems++ + + return true + }) + + return { + totalItems, + checkedItems, + progress: (checkedItems / totalItems) * 100, + } +} + +export const isChecklistItem = (elem: HTMLElement | undefined) => { + if (!elem?.matches('input[type="checkbox"]')) return false + if (!elem?.parentElement?.matches('label[contenteditable]')) return false + if (!elem?.parentElement?.parentElement?.matches('li[data-checked]')) return false + + return true +} diff --git a/client-v2/src/app/rich-text-editor/editor.types.ts b/client-v2/src/app/rich-text-editor/editor.types.ts new file mode 100644 index 00000000..d8388bfd --- /dev/null +++ b/client-v2/src/app/rich-text-editor/editor.types.ts @@ -0,0 +1,96 @@ +import { ChainedCommands, Editor, Extensions } from '@tiptap/core' +import { MenuItem } from 'src/app/dropdown/drop-down/drop-down.component' +import { IconKey } from '../components/atoms/icons/icon/icons' +import { HotToastServiceMethods } from '@ngneat/hot-toast' + +export enum EditorFeatureId { + Base = 'base', + Inline = 'inline', + Blocks = 'blocks', + History = 'history', + Indentation = 'indentation', + Placeholder = 'placeholder', + Typography = 'typography', + Link = 'link', + Blur = 'blur', + StarterKit = 'starter-kit', + Markdown = 'markdown', + CustomEvents = 'custom-events', + MoreMenu = 'more-menu', + ChecklistCounter = 'checklist-counter', + SearchAndReplace = 'search-and-replace', +} + +export enum EditorControlId { + Bold = 'bold', + Italic = 'italic', + Strike = 'strike', + InlineCode = 'inline-code', + Link = 'link', + + Blocks = 'blocks', + Paragraph = 'paragraph', + Headings = 'headings', + Heading1 = 'heading-1', + Heading2 = 'heading-2', + Heading3 = 'heading-3', + Heading4 = 'heading-4', + Heading5 = 'heading-5', + Heading6 = 'heading-6', + BulletList = 'bullet-list', + OrderedList = 'ordered-list', + TaskList = 'task-list', + CodeBlock = 'code-block', + Blockquote = 'blockquote', + HorizontalRule = 'horizontal-rule', + + More = 'more', + Undo = 'undo', + Redo = 'redo', + Indent = 'indent', + Outdent = 'outdent', + CopyAsMarkdown = 'copy-as-markdown', + + Blur = 'blur', +} + +export interface EditorControlArgs { + chain: (autoFocus?: boolean) => ChainedCommands + editor: Editor + toast: HotToastServiceMethods +} + +export interface EditorControl { + controlId: EditorControlId + title: string | ((args: EditorControlArgs) => string) + icon: IconKey | ((args: EditorControlArgs) => IconKey) + keybinding?: string + isActive?: (args: EditorControlArgs) => boolean + action?: (args: EditorControlArgs) => boolean + fixedWith?: string +} + +type Separator = { isSeparator: true } +export const separator = { isSeparator: true } as Separator +export const isSeparator = (item: unknown): item is Separator => + /* item == separator || */ typeof item == 'object' && item != null && 'isSeparator' in item + +export interface EditorLayoutParentItem { + controlId: EditorControlId + dropdown: (EditorControlId | Separator)[] +} +export type EditorLayoutItem = EditorControlId | EditorLayoutParentItem | Separator + +export interface EditorFeature { + featureId: EditorFeatureId + // extensions: (Extension | Node | Mark)[] + extensions: Extensions + controls?: EditorControl[] + layout?: EditorLayoutItem[] +} + +export type ResolvedEditorControl = EditorControl & { + dropdownItems?: MenuItem[] +} + +export type ResolvedEditorControlItem = ResolvedEditorControl | Separator diff --git a/client-v2/src/app/rich-text-editor/rich-text-editor.module.ts b/client-v2/src/app/rich-text-editor/rich-text-editor.module.ts new file mode 100644 index 00000000..06ae2a5d --- /dev/null +++ b/client-v2/src/app/rich-text-editor/rich-text-editor.module.ts @@ -0,0 +1,27 @@ +import { CdkMenuModule } from '@angular/cdk/menu' +import { CommonModule } from '@angular/common' +import { NgModule } from '@angular/core' +import { NgxTiptapModule } from 'ngx-tiptap' +import { IconsModule } from '../components/atoms/icons/icons.module' +import { DropdownModule } from '../dropdown/dropdown.module' +import { KeyboardModule } from '../keyboard/keyboard.module' +import { RxModule } from '../rx/rx.module' +import { TooltipModule } from '../tooltip/tooltip.module' +import { TipTapEditorToolbarComponent } from './tip-tap-editor-toolbar/tip-tap-editor-toolbar.component' +import { TipTapEditorComponent } from './tip-tap-editor/tip-tap-editor.component' + +@NgModule({ + declarations: [TipTapEditorComponent, TipTapEditorToolbarComponent], + imports: [ + CommonModule, + RxModule, + NgxTiptapModule, + IconsModule, + CdkMenuModule, + KeyboardModule, + DropdownModule, + TooltipModule, + ], + exports: [TipTapEditorComponent, TipTapEditorToolbarComponent], +}) +export class RichTextEditorModule {} diff --git a/client-v2/src/app/rich-text-editor/tip-tap-editor-toolbar/tip-tap-editor-toolbar.component.css b/client-v2/src/app/rich-text-editor/tip-tap-editor-toolbar/tip-tap-editor-toolbar.component.css new file mode 100644 index 00000000..10637d89 --- /dev/null +++ b/client-v2/src/app/rich-text-editor/tip-tap-editor-toolbar/tip-tap-editor-toolbar.component.css @@ -0,0 +1,3 @@ +button.isActive { + @apply !bg-primary-650 font-bold text-primary-200; +} diff --git a/client-v2/src/app/rich-text-editor/tip-tap-editor-toolbar/tip-tap-editor-toolbar.component.html b/client-v2/src/app/rich-text-editor/tip-tap-editor-toolbar/tip-tap-editor-toolbar.component.html new file mode 100644 index 00000000..6fe90cd0 --- /dev/null +++ b/client-v2/src/app/rich-text-editor/tip-tap-editor-toolbar/tip-tap-editor-toolbar.component.html @@ -0,0 +1,101 @@ +
+ + + + + + + + + to focus toolbar + + + + + + + + + +
+ {{ resolveTitle(controlItem) }} + + + +
+
+
+
+ + + + +
diff --git a/client-v2/src/app/rich-text-editor/tip-tap-editor-toolbar/tip-tap-editor-toolbar.component.spec.ts b/client-v2/src/app/rich-text-editor/tip-tap-editor-toolbar/tip-tap-editor-toolbar.component.spec.ts new file mode 100644 index 00000000..313e80a8 --- /dev/null +++ b/client-v2/src/app/rich-text-editor/tip-tap-editor-toolbar/tip-tap-editor-toolbar.component.spec.ts @@ -0,0 +1,29 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { TipTapEditorToolbarComponent } from './tip-tap-editor-toolbar.component' +import { provideEditorFeatures, getDefaultEditorFeatures } from '../editor.features' +import { CdkMenuModule } from '@angular/cdk/menu' +import { TipTapEditorComponent } from '../tip-tap-editor/tip-tap-editor.component' + +describe('TipTapEditorToolbarComponent', () => { + let component: TipTapEditorToolbarComponent + let fixture: ComponentFixture + + beforeEach(async () => { + const editorFeaturesProvider = provideEditorFeatures(getDefaultEditorFeatures()) + await TestBed.configureTestingModule({ + imports: [CdkMenuModule], + declarations: [TipTapEditorToolbarComponent], + providers: [editorFeaturesProvider], + }).compileComponents() + + fixture = TestBed.createComponent(TipTapEditorToolbarComponent) + component = fixture.componentInstance + component.ttEditor = new TipTapEditorComponent(editorFeaturesProvider.useValue) + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/client-v2/src/app/rich-text-editor/tip-tap-editor-toolbar/tip-tap-editor-toolbar.component.ts b/client-v2/src/app/rich-text-editor/tip-tap-editor-toolbar/tip-tap-editor-toolbar.component.ts new file mode 100644 index 00000000..d8ac32ba --- /dev/null +++ b/client-v2/src/app/rich-text-editor/tip-tap-editor-toolbar/tip-tap-editor-toolbar.component.ts @@ -0,0 +1,195 @@ +import { CdkMenuBar } from '@angular/cdk/menu' +import { AfterViewInit, ChangeDetectionStrategy, Component, Inject, Input, ViewChild } from '@angular/core' +import { Router } from '@angular/router' +import { HotToastService } from '@ngneat/hot-toast' +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy' +import { BehaviorSubject, distinctUntilChanged, map, merge, skip, startWith, switchMap, throttleTime } from 'rxjs' +import { IconKey } from 'src/app/components/atoms/icons/icon/icons' +import { MenuItem } from 'src/app/dropdown/drop-down/drop-down.component' +import { DeviceService } from 'src/app/services/device.service' +import '../editor-features/custom-events.feature' +import { EDITOR_FEATURES_TOKEN } from '../editor.features' +import { + EditorControl, + EditorControlArgs, + EditorFeature, + EditorFeatureId, + EditorLayoutItem, + ResolvedEditorControl, + ResolvedEditorControlItem, + isSeparator, +} from '../editor.types' +import { TipTapEditorComponent } from '../tip-tap-editor/tip-tap-editor.component' + +const resolveControlsLayout = ( + controls: EditorControl[], + layout: EditorLayoutItem[], + resolvers: { + resolveTitle: (control: EditorControl) => string + resolveIcon: (control: EditorControl) => IconKey + } +) => { + const resolvedControls = layout.map(featureLayout => { + if (isSeparator(featureLayout)) return featureLayout + + const controlId = typeof featureLayout == 'string' ? featureLayout : featureLayout.controlId + const control = controls.find(control => control.controlId == controlId) + if (!control) throw new Error(`Control '${control}' not found`) + + if (typeof featureLayout != 'string' && 'dropdown' in featureLayout) { + const resolvedControl = control as ResolvedEditorControl + + resolvedControl.dropdownItems = featureLayout.dropdown.map>( + controlIdOrSeparator => { + if (isSeparator(controlIdOrSeparator)) return controlIdOrSeparator + + const control = controls.find(control => control.controlId == controlIdOrSeparator) + if (!control) throw new Error(`Control '${controlIdOrSeparator}' not found`) + + // @TODO: the dropdown should be responsible for resolving this + control.title = resolvers.resolveTitle(control) + control.icon = resolvers.resolveIcon(control) + + return control as MenuItem + } + ) + } + + return control + }) + + return resolvedControls +} + +@UntilDestroy() +@Component({ + selector: 'app-tt-editor-toolbar', + templateUrl: './tip-tap-editor-toolbar.component.html', + styleUrls: ['./tip-tap-editor-toolbar.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TipTapEditorToolbarComponent implements AfterViewInit { + constructor( + @Inject(EDITOR_FEATURES_TOKEN) private features: EditorFeature[], + public deviceService: DeviceService, + private toast: HotToastService, + private router: Router + ) {} + + ngAfterViewInit(): void { + // focus controls when editor tells us to (e.g. when the shortcut is triggered) + const customEvents = this.ttEditor.editor.storage[EditorFeatureId.CustomEvents] + customEvents?.shouldFocusToolbar$.pipe(untilDestroyed(this)).subscribe(() => this.focusControls()) + + // update editor focus state to false when the toolbar looses focus and the editor isn't focused + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.controlsMenuBar!.menuStack.hasFocus.pipe( + startWith(false), + distinctUntilChanged(), + skip(1), + untilDestroyed(this) + ).subscribe(toolbarHasFocus => { + if (!toolbarHasFocus && !this.ttEditor.editor.view.hasFocus()) { + this.ttEditor.updateFocusState(false) + this.controlsMenuBar?.menuStack.closeAll() + } + }) + } + + isSeparator = isSeparator + + @ViewChild('controlsMenuBar') controlsMenuBar?: CdkMenuBar + focusControls() { + this.controlsMenuBar?.focusFirstItem() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + @Input() ttEditor!: TipTapEditorComponent + + @Input() openAsPageRoute?: string + + private layoutInput$ = new BehaviorSubject(null) + @Input('layout') set layoutSetter(layout: EditorLayoutItem[]) { + this.layoutInput$.next(layout) + } + + @Input() leadingControls?: EditorControl[] + @Input() trailingControls?: EditorControl[] + + controls$ = this.layoutInput$.pipe( + map(layoutInput => { + const controls = this.features.flatMap(feature => feature.controls).filter(Boolean) + const layout = + layoutInput || + this.features + .flatMap(feature => feature.layout || feature.controls?.map(control => control.controlId)) + .filter(Boolean) + + const resolvedControls = resolveControlsLayout(controls, layout, { + resolveTitle: this.resolveTitle.bind(this), + resolveIcon: this.resolveIcon.bind(this), + }) + + if (this.leadingControls) resolvedControls.unshift(...this.leadingControls) + if (this.trailingControls) resolvedControls.push(...this.trailingControls) + + return resolvedControls + }), + switchMap(controls => + merge(this.ttEditor.editor.selectionUpdate$, this.ttEditor.update$).pipe( + throttleTime(120, undefined, { leading: true, trailing: true }), + startWith(null), + map(() => controls) + ) + ) + ) + + controlItemTrackBy = ((index: number, item: ResolvedEditorControlItem) => { + if (!this.ttEditor?.editor) return index + if ('isSeparator' in item) return index + + return index + this.resolveIcon(item) + item.isActive?.(this.controlArgs) + }).bind(this) + + execControlAction(callback: (args: EditorControlArgs) => boolean, isDropdownItem = false) { + if (!this.ttEditor.editor) return + + callback({ + chain: autoFocus => this.getChain(isDropdownItem ? false : autoFocus), + editor: this.ttEditor.editor, + toast: this.toast, + }) + } + getChain = ((autoFocus = true) => { + const chain = this.ttEditor.editor.chain() + return autoFocus ? chain.focus() : chain + }).bind(this) + + private controlArgs_?: EditorControlArgs + get controlArgs(): EditorControlArgs { + if (this.controlArgs_) return this.controlArgs_ + + this.controlArgs_ = { + chain: this.getChain, + editor: this.ttEditor.editor, + toast: this.toast, + } + return this.controlArgs_ + } + get controlArgsAsRecord() { + return this.controlArgs as unknown as Record + } + + resolveIcon(item: EditorControl) { + if (!this.ttEditor.editor) return '' as IconKey + return typeof item.icon == 'string' ? item.icon : item.icon(this.controlArgs) + } + resolveTitle(item: EditorControl) { + if (!this.ttEditor.editor) return '' + return typeof item.title == 'string' ? item.title : item.title(this.controlArgs) + } + + navigate(url: string) { + this.router.navigateByUrl(url) + } +} diff --git a/client-v2/src/app/rich-text-editor/tip-tap-editor.ts b/client-v2/src/app/rich-text-editor/tip-tap-editor.ts new file mode 100644 index 00000000..74155d0e --- /dev/null +++ b/client-v2/src/app/rich-text-editor/tip-tap-editor.ts @@ -0,0 +1,128 @@ +import { Content, Editor, EditorEvents, createDocument } from '@tiptap/core' +import { ParseOptions } from 'prosemirror-model' +import { EditorState } from 'prosemirror-state' +import { + Observable, + Subject, + delay, + first, + fromEvent, + fromEventPattern, + map, + merge, + share, + skipUntil, + startWith, + takeUntil, + withLatestFrom, +} from 'rxjs' +import { CustomEditorEventsStorage } from './editor-features/custom-events.feature' +import { EditorFeatureId } from './editor.types' + +interface EditorStorage { + [EditorFeatureId.CustomEvents]?: CustomEditorEventsStorage + [key: string]: unknown +} + +export class TipTapEditor extends Editor { + destroy$ = fromEventPattern( + handler => this.on('destroy', handler), + handler => this.off('destroy', handler) + ).pipe(first(), share({ resetOnRefCountZero: true })) + + override get storage() { + return super.storage as EditorStorage + } + + private getEventStream(eventName: T): Observable { + return fromEvent(this, eventName, e => e as EditorEvents[T]).pipe( + takeUntil(this.destroy$), + share({ resetOnRefCountZero: true }) + ) + } + + // @TODO: investigate why this is emitting upon editor initialisation only in the + // task description, and not in the full page description editors + private updateRaw$ = this.getEventStream('update') + selectionUpdate$ = this.getEventStream('selectionUpdate') + transaction$ = this.getEventStream('transaction') + + focus$ = this.getEventStream('focus') + blur$ = this.getEventStream('blur') + + private update$ = this.updateRaw$.pipe( + map(({ editor }) => { + const isEmpty = editor.isEmpty + return { + plainText: isEmpty ? '' : editor.getText().trim(), + html: isEmpty ? '' : editor.getHTML(), + } + }), + share({ resetOnRefCountZero: true }) + ) + + resetState(content: Content, parseOptions?: ParseOptions) { + const newState = EditorState.create({ + doc: createDocument(content, this.schema, parseOptions), + schema: this.schema, + plugins: this.state.plugins, + }) + + this.view.updateState(newState) + } + + bindEditor(input$: Observable, searchTerm$?: Observable) { + // cancel the previous binding + this.unbind() + this.commands.blur() + + input$ + .pipe( + takeUntil(this.unbind$), + withLatestFrom(this.update$.pipe(startWith(null))), + map(([input, currentState], index) => ({ + input, + currentState, + isFirst: index == 0, + })) + ) + .subscribe(({ input, currentState, isFirst }) => { + if (input == currentState?.html || input == currentState?.plainText) return + + if (isFirst) { + this.resetState(input) + } else { + this.chain().setContent(input, false).setMeta('addToHistory', false).run() + } + }) + + searchTerm$?.pipe(takeUntil(this.unbind$)).subscribe({ + next: searchTerm => this.commands.setSearchTerm(searchTerm), + complete: () => this.commands.setSearchTerm(''), + }) + + return { + unbind: () => this.unbind(), + unbind$: this.unbind$.pipe(takeUntil(this.unbind$.pipe(delay(0)))), + update$: this.update$.pipe( + // Skip all updates until we have populated the editor with the initial input. + // This is needed because the editor will emit an update event when it is created + // in some cases. (only happend in the task description editor so far, not in the full page description editor) + skipUntil(input$), + takeUntil(this.unbind$) + ), + selectionUpdate$: this.selectionUpdate$.pipe(takeUntil(this.unbind$)), + focus$: this.focus$.pipe(takeUntil(this.unbind$)), + blur$: this.blur$.pipe(takeUntil(this.unbind$)), + } + } + + private unbindTrigger$ = new Subject() + unbind$ = merge(this.unbindTrigger$, this.destroy$) + /** Cancel input binding */ + unbind() { + this.unbindTrigger$.next() + } + + deselect = () => window.getSelection()?.removeAllRanges() +} diff --git a/client-v2/src/app/rich-text-editor/tip-tap-editor/tip-tap-editor.component.css b/client-v2/src/app/rich-text-editor/tip-tap-editor/tip-tap-editor.component.css new file mode 100644 index 00000000..1046543a --- /dev/null +++ b/client-v2/src/app/rich-text-editor/tip-tap-editor/tip-tap-editor.component.css @@ -0,0 +1,74 @@ +.ProseMirror, +.rendered-content { + @apply pb-6 outline-none; +} + +:is(.ProseMirror, .rendered-content) :is(p, ul, ol, blockquote) { + margin: 0; +} +:is(.ProseMirror, .rendered-content) :is(p, ul, ol, blockquote):not(:last-child) { + @apply mb-2; +} +:is(.ProseMirror, .rendered-content) hr { + @apply my-4; +} + +:is(.ProseMirror, .rendered-content) blockquote { + @apply relative rounded bg-tinted-850 py-1 pl-3.5; +} +:is(.ProseMirror, .rendered-content) blockquote::before { + content: ''; + @apply absolute left-0 top-0 h-full w-[0.3125rem] rounded bg-tinted-400; +} + +:is(.ProseMirror, .rendered-content) a { + @apply underline cursor-pointer; +} + +:is(.ProseMirror, .rendered-content) code { + @apply rounded-md bg-tinted-800 px-[0.365em] py-[0.125em] text-[.9em] font-medium text-tinted-300; +} +:is(.ProseMirror, .rendered-content) a code { + @apply !text-inherit; +} +:is(.ProseMirror, .rendered-content) pre code { + @apply my-2 block px-3 py-2; +} + +:is(.ProseMirror, .rendered-content) h1 { + font-size: 1.8rem; +} + +:is(.ProseMirror, .rendered-content) ul[data-type='taskList'] { + @apply ml-0.5 list-none; +} +:is(.ProseMirror, .rendered-content) ul[data-type='taskList'] li { + display: flex; +} +:is(.ProseMirror, .rendered-content) ul[data-type='taskList'] input { + @apply accent-submit-400; +} +:is(.ProseMirror, .rendered-content) li p { + @apply inline; +} +:is(.ProseMirror, .rendered-content) ul[data-type='taskList'] li > label { + flex: 0 0 auto; + @apply mr-2 select-none; +} + +:is(.ProseMirror, .rendered-content) ul[data-type='taskList'] li > div { + flex: 1 1 auto; +} + +.ProseMirror :is(p.is-editor-empty:first-child, .is-empty)::before { + @apply text-tinted-400; + + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; +} + +button.isActive { + @apply !bg-primary-400 font-bold text-tinted-900; +} \ No newline at end of file diff --git a/client-v2/src/app/rich-text-editor/tip-tap-editor/tip-tap-editor.component.html b/client-v2/src/app/rich-text-editor/tip-tap-editor/tip-tap-editor.component.html new file mode 100644 index 00000000..4ca9baf2 --- /dev/null +++ b/client-v2/src/app/rich-text-editor/tip-tap-editor/tip-tap-editor.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/client-v2/src/app/rich-text-editor/tip-tap-editor/tip-tap-editor.component.spec.ts b/client-v2/src/app/rich-text-editor/tip-tap-editor/tip-tap-editor.component.spec.ts new file mode 100644 index 00000000..03548f6b --- /dev/null +++ b/client-v2/src/app/rich-text-editor/tip-tap-editor/tip-tap-editor.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { TipTapEditorComponent } from './tip-tap-editor.component' +import { getDefaultEditorFeatures, provideEditorFeatures } from '../editor.features' + +describe('TipTapEditorComponent', () => { + let component: TipTapEditorComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [TipTapEditorComponent], + providers: [provideEditorFeatures(getDefaultEditorFeatures())], + }).compileComponents() + + fixture = TestBed.createComponent(TipTapEditorComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/client-v2/src/app/rich-text-editor/tip-tap-editor/tip-tap-editor.component.ts b/client-v2/src/app/rich-text-editor/tip-tap-editor/tip-tap-editor.component.ts new file mode 100644 index 00000000..86001b57 --- /dev/null +++ b/client-v2/src/app/rich-text-editor/tip-tap-editor/tip-tap-editor.component.ts @@ -0,0 +1,141 @@ +import { ChangeDetectionStrategy, Component, Inject, Input, OnDestroy, Output, ViewEncapsulation } from '@angular/core' +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy' +import { coalesceWith } from '@rx-angular/cdk/coalescing' +import { + Observable, + ReplaySubject, + Subject, + delay, + distinctUntilKeyChanged, + filter, + map, + merge, + mergeWith, + share, + shareReplay, + startWith, + switchMap, + takeUntil, + tap, + timer, + withLatestFrom, +} from 'rxjs' +import { TipTapEditor } from '../tip-tap-editor' +import { EditorFeature } from '../editor.types' +import { EDITOR_FEATURES_TOKEN } from '../editor.features' +import { isChecklistItem } from '../editor.helpers' + +@UntilDestroy() +@Component({ + selector: 'app-tt-editor', + templateUrl: './tip-tap-editor.component.html', + styleUrls: ['./tip-tap-editor.component.css'], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'block', + }, +}) +export class TipTapEditorComponent implements OnDestroy { + constructor(@Inject(EDITOR_FEATURES_TOKEN) public features: EditorFeature[]) {} + + ngOnDestroy(): void { + this.editor?.destroy() + } + + @Input() set editable(editable: boolean) { + this.editor?.setEditable(editable) + } + get editable() { + return this.editor?.isEditable ?? true + } + + private searchTerm$ = new ReplaySubject(1) + @Input() set searchTerm(searchTerm: string | null) { + if (searchTerm !== null) this.searchTerm$.next(searchTerm) + } + + editor = new TipTapEditor({ + editable: this.editable, + extensions: this.features.flatMap(feature => feature.extensions), + }) + + private focusStateInput$ = new Subject() + updateFocusState(isFocused: boolean) { + this.focusStateInput$.next(isFocused) + } + + @Output('focus') focus$ = this.editor.focus$.pipe( + map(({ event }) => event), + mergeWith(this.focusStateInput$.pipe(filter((isFocused): isFocused is true => isFocused))) + ) + @Output('blur') blur$ = this.editor.blur$.pipe( + filter(({ event }) => { + const clickedElem = event.relatedTarget as HTMLElement | undefined + + // check if a control from the toolbar was clicked + const isControlClicked = + clickedElem?.className?.includes('format-control-item') || + clickedElem?.className?.includes('format-controls-container') || + clickedElem?.className?.includes('keep-editor-focus') || + clickedElem?.parentElement?.className?.includes('format-controls-container') || + clickedElem?.parentElement?.className?.includes('keep-editor-focus') + if (isControlClicked) return false + + // check if a task item was clicked (only inside the current editor) + if (isChecklistItem(clickedElem) && this.editor.view.dom.contains(clickedElem as Node)) return false + + // -> Its good to explicitly allow elems instead of blindly ignoring everything from inside the editor + + return true + }), + tap(() => this.editor.deselect()), + map(({ event }) => event), + mergeWith(this.focusStateInput$.pipe(filter((isFocused): isFocused is false => !isFocused))), + share({ resetOnRefCountZero: true }) + ) + + @Output('isActive') isActive$ = merge(this.focus$, this.blur$).pipe( + map(() => this.editor.isFocused), + coalesceWith(timer(70)), + mergeWith(this.editor.unbind$.pipe(map(() => false))), // because we blur the editor when (un)binding + startWith(false), + untilDestroyed(this), + shareReplay({ bufferSize: 1, refCount: true }) + ) + + private bindConfig$ = new Subject<{ input$: Observable; context: TContext }>() + @Input() set bind(bindConfig: { input$: Observable; context: TContext } | undefined) { + if (bindConfig) this.bindConfig$.next(bindConfig) + } + private bound$ = this.bindConfig$.pipe( + untilDestroyed(this), + map(({ input$, context }) => { + const bound = this.editor.bindEditor(input$, this.searchTerm$) + return { input$, context, ...bound } + }), + share({ resetOnRefCountZero: true }) + ) + + @Output('update') update$ = this.bound$.pipe( + switchMap(({ update$ }) => update$), + share({ resetOnRefCountZero: true }) + ) + + @Output('updateOnBlur') updateOnBlur$ = this.bound$.pipe( + switchMap(({ input$, unbind$, context }) => + merge(this.blur$, unbind$).pipe( + withLatestFrom(this.update$, input$.pipe(startWith(null))), + map(([, { html, plainText }, lastInput]) => ({ html, plainText, lastInput, context })), + filter(({ html, plainText, lastInput }) => html != lastInput || plainText != lastInput), + distinctUntilKeyChanged('html'), + untilDestroyed(this), + // Must be delayed so that `unbind$` triggers a last update before the bound is destroyed, + // to make sure all transactions are dispatched. + takeUntil(this.editor.unbind$.pipe(delay(0))), + share({ resetOnRefCountZero: true }) + ) + ), + share({ resetOnRefCountZero: true }) + ) +} diff --git a/client-v2/src/app/rx/rx.module.ts b/client-v2/src/app/rx/rx.module.ts new file mode 100644 index 00000000..c1ce8327 --- /dev/null +++ b/client-v2/src/app/rx/rx.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core' +import { ForModule } from '@rx-angular/template/for' +import { IfModule } from '@rx-angular/template/if' +import { LetModule } from '@rx-angular/template/let' +import { PushModule } from '@rx-angular/template/push' + +@NgModule({ + exports: [LetModule, IfModule, ForModule, PushModule], +}) +export class RxModule {} diff --git a/client-v2/src/app/services/device.service.ts b/client-v2/src/app/services/device.service.ts index 58687b43..e6946cb5 100644 --- a/client-v2/src/app/services/device.service.ts +++ b/client-v2/src/app/services/device.service.ts @@ -14,7 +14,7 @@ import { timer, } from 'rxjs' -const mediaQueries = { +export const mediaQueries = { mobileScreen: '(max-width: 768px)', touchPrimary: '(pointer: coarse)', canHover: '(hover:hover)', diff --git a/client-v2/src/app/shared/entity-menu-items.ts b/client-v2/src/app/shared/entity-menu-items.ts index 5a375bff..aab9080f 100644 --- a/client-v2/src/app/shared/entity-menu-items.ts +++ b/client-v2/src/app/shared/entity-menu-items.ts @@ -1,5 +1,5 @@ import { Store } from '@ngrx/store' -import { MenuItem, MenuItemVariant } from '../components/molecules/drop-down/drop-down.component' +import { MenuItem, MenuItemVariant } from '../dropdown/drop-down/drop-down.component' import { EntityType } from '../fullstack-shared-models/entities.model' import { TaskPriority, TaskStatus } from '../fullstack-shared-models/task.model' import { EntityCrudDto } from '../services/entities.service' @@ -17,7 +17,7 @@ export const getGeneralMenuItems = (store: Store): MenuItem[] => [ }, ] export const getDangerMenuItems = (store: Store): MenuItem[] => [ - { isSeperator: true }, + { isSeparator: true }, { title: `Delete`, icon: 'trash', @@ -27,26 +27,39 @@ export const getDangerMenuItems = (store: Store): MenuItem[] => [ ] export const getTaskStatusMenuItems = (store: Store) => - Object.values(TaskStatus).map(status => ({ + Object.values(TaskStatus).map>(status => ({ title: status.replace(/_/g, ' '), icon: status, + isActive: data => data.status !== status, action: (dto: { id: string }) => { store.dispatch(taskActions.updateStatus({ id: dto.id, status })) }, })) export const getTaskPriorityMenuItems = (store: Store) => - Object.values(TaskPriority).map(priority => ({ + Object.values(TaskPriority).map>(priority => ({ title: priority.replace(/_/g, ' '), icon: priority, + isActive: data => data.priority !== priority, action: (dto: { id: string }) => { store.dispatch(taskActions.updatePriority({ id: dto.id, priority })) }, })) -export type EntityMenuItemsMap = Record +export type EntityMenuItemData = EntityCrudDto +export type TaskMenuItemData = EntityMenuItemData & { + entityType: EntityType.TASK + status: TaskStatus + priority: TaskPriority +} + +// @TODO: how do we make this exhaustive? +export type EntityMenuItemsMap = { + [EntityType.TASK]: WrappedMenuItems + [EntityType.TASKLIST]: WrappedMenuItems +} export const getEntityMenuItemsMap = (store: Store): EntityMenuItemsMap => ({ - [EntityType.TASKLIST]: wrapMenuItems([ + [EntityType.TASKLIST]: wrapMenuItems([ ...getGeneralMenuItems(store), { title: 'New Inside', @@ -55,33 +68,32 @@ export const getEntityMenuItemsMap = (store: Store): EntityMenuItemsMa { title: 'Tasklist', icon: EntityType.TASKLIST, - action: (dto: EntityCrudDto) => - store.dispatch(listActions.createTaskList({ parentListId: dto.id })), + action: dto => store.dispatch(listActions.createTaskList({ parentListId: dto.id })), }, { title: 'Task', icon: EntityType.TASK, - action: (dto: EntityCrudDto) => store.dispatch(taskActions.create({ listId: dto.id })), + action: dto => store.dispatch(taskActions.create({ listId: dto.id })), }, ], }, { title: 'Duplicate', icon: 'clone', - action: (dto: EntityCrudDto) => store.dispatch(listActions.duplicateList(dto)), + action: dto => store.dispatch(listActions.duplicateList(dto)), }, { title: `Export`, icon: 'export', - action: (dto: EntityCrudDto) => store.dispatch(listActions.exportList(dto)), + action: dto => store.dispatch(listActions.exportList(dto)), }, ...getDangerMenuItems(store), ]), - [EntityType.TASK]: wrapMenuItems([ + [EntityType.TASK]: wrapMenuItems([ { title: 'New Subtask', icon: 'plus', - action: (dto: EntityCrudDto) => store.dispatch(taskActions.create({ parentTaskId: dto.id })), + action: dto => store.dispatch(taskActions.create({ parentTaskId: dto.id })), }, ...getGeneralMenuItems(store), { @@ -89,7 +101,7 @@ export const getEntityMenuItemsMap = (store: Store): EntityMenuItemsMa icon: 'expand', route: '/home/:id', }, - { isSeperator: true }, + { isSeparator: true }, { title: 'Status', icon: 'status', diff --git a/client-v2/src/app/store/entities/entities.actions.ts b/client-v2/src/app/store/entities/entities.actions.ts index 1850c955..f1573081 100644 --- a/client-v2/src/app/store/entities/entities.actions.ts +++ b/client-v2/src/app/store/entities/entities.actions.ts @@ -1,12 +1,10 @@ import { createActionGroup, emptyProps, props } from '@ngrx/store' import { EntitiesSearchResultDto, EntityPreview } from 'src/app/fullstack-shared-models/entities.model' -import { HttpServerErrorResponse } from 'src/app/http/types' -import { EntityCrudDto } from 'src/app/services/entities.service' +import { HttpServerErrorResponse, HttpServerErrorResponseWithMeta } from '../../http/types' +import { EntityCrudDto } from '../../services/entities.service' import { listActions } from './list/list.actions' import { taskActions } from './task/task.actions' -export type HttpServerErrorResponseWithData = HttpServerErrorResponse & T - export const entitiesActions = createActionGroup({ source: 'Entities', events: { @@ -18,21 +16,21 @@ export const entitiesActions = createActionGroup({ 'load detail': props(), // eslint-disable-next-line @typescript-eslint/no-explicit-any 'load detail success': props }>>(), - 'load detail error': props(), + 'load detail error': props(), 'open rename dialog': props(), 'abort rename dialog': emptyProps(), // rename: props>(), 'rename success': props>(), - 'rename error': props(), + 'rename error': props(), 'open delete dialog': props(), 'abort delete dialog': emptyProps(), // delete: props(), 'delete success': props(), - 'delete error': props(), + 'delete error': props(), search: props<{ query: string }>(), 'search success': props(), @@ -40,6 +38,7 @@ export const entitiesActions = createActionGroup({ }, }) +/** Entity actions which directly correspond to an entity, indicating a loading state */ export const loadingStateActions = [ entitiesActions.loadDetail, entitiesActions.loadDetailSuccess, diff --git a/client-v2/src/app/store/entities/list/list.actions.ts b/client-v2/src/app/store/entities/list/list.actions.ts index 52fefb1f..5ee82af9 100644 --- a/client-v2/src/app/store/entities/list/list.actions.ts +++ b/client-v2/src/app/store/entities/list/list.actions.ts @@ -1,7 +1,6 @@ import { createActionGroup, props } from '@ngrx/store' import { CreateTasklistDto, TaskList } from 'src/app/fullstack-shared-models/list.model' -import { HttpServerErrorResponse } from 'src/app/http/types' -import { HttpServerErrorResponseWithData } from '../entities.actions' +import { HttpServerErrorResponse, HttpServerErrorResponseWithMeta } from 'src/app/http/types' export const listActions = createActionGroup({ source: 'Entity/Lists', @@ -12,7 +11,7 @@ export const listActions = createActionGroup({ 'update description': props<{ id: string; newDescription: string }>(), 'update description success': props<{ id: string; newDescription: string }>(), - 'update description error': props(), + 'update description error': props(), 'duplicate list': props<{ id: string }>(), 'duplicate list success': props<{ id: string }>(), diff --git a/client-v2/src/app/store/entities/task/task.actions.ts b/client-v2/src/app/store/entities/task/task.actions.ts index 21fabb4d..0a33f8d9 100644 --- a/client-v2/src/app/store/entities/task/task.actions.ts +++ b/client-v2/src/app/store/entities/task/task.actions.ts @@ -1,7 +1,6 @@ import { createActionGroup, emptyProps, props } from '@ngrx/store' import { TaskPreview, CreateTaskDto, TaskStatus, TaskPriority } from 'src/app/fullstack-shared-models/task.model' -import { HttpServerErrorResponse } from 'src/app/http/types' -import { HttpServerErrorResponseWithData } from '../entities.actions' +import { HttpServerErrorResponse, HttpServerErrorResponseWithMeta } from 'src/app/http/types' export const taskActions = createActionGroup({ source: 'Entity/Tasks', @@ -13,22 +12,22 @@ export const taskActions = createActionGroup({ 'load root level tasks': props<{ listId: string }>(), 'load root level tasks success': props<{ listId: string; tasks: TaskPreview[] }>(), - 'load root level tasks error': props(), + 'load root level tasks error': props(), create: props>(), 'create success': props<{ createdTask: TaskPreview }>(), - 'create error': props(), + 'create error': props(), 'update status': props<{ id: string; status: TaskStatus }>(), 'update status success': props<{ id: string; status: TaskStatus }>(), - 'update status error': props(), + 'update status error': props(), 'update priority': props<{ id: string; priority: TaskPriority }>(), 'update priority success': props<{ id: string; priority: TaskPriority }>(), - 'update priority error': props(), + 'update priority error': props(), 'update description': props<{ id: string; newDescription: string }>(), 'update description success': props<{ id: string; newDescription: string }>(), - 'update description error': props(), + 'update description error': props(), }, }) diff --git a/client-v2/src/app/store/entities/task/task.reducer.ts b/client-v2/src/app/store/entities/task/task.reducer.ts index e01c783e..63ec625c 100644 --- a/client-v2/src/app/store/entities/task/task.reducer.ts +++ b/client-v2/src/app/store/entities/task/task.reducer.ts @@ -4,6 +4,7 @@ import { ReducerOns } from 'src/app/utils/store.helpers' import { EntitiesState, TaskTreeMap } from '../entities.state' import { buildTaskTreeMap, buildTaskTree, getTaskById } from '../utils' import { taskActions } from './task.actions' +import { EntityType } from 'src/app/fullstack-shared-models/entities.model' export const taskReducerOns: ReducerOns = [ on(taskActions.loadTaskPreviewsSuccess, (state, { previews }): EntitiesState => { @@ -76,6 +77,7 @@ export const taskReducerOns: ReducerOns = [ on(taskActions.updateDescriptionSuccess, (state, { id, newDescription }) => { const taskTreeMapCopy = structuredClone(state.taskTreeMap || {}) as TaskTreeMap + const taskDetailsCopy = structuredClone(state.entityDetails[EntityType.TASK] || {}) // @TODO: This could be optimized by using the `listId` to reduce the number of tasks to iterate over const task = getTaskById(Object.values(taskTreeMapCopy).flat(), id) @@ -83,9 +85,15 @@ export const taskReducerOns: ReducerOns = [ task.description = newDescription } + if (taskDetailsCopy[id]) taskDetailsCopy[id].description = newDescription + return { ...state, taskTreeMap: taskTreeMapCopy, + entityDetails: { + ...state.entityDetails, + [EntityType.TASK]: taskDetailsCopy, + }, } }), ] diff --git a/client-v2/src/app/store/entities/utils.ts b/client-v2/src/app/store/entities/utils.ts index 441f520d..60b6e7fd 100644 --- a/client-v2/src/app/store/entities/utils.ts +++ b/client-v2/src/app/store/entities/utils.ts @@ -48,9 +48,9 @@ export const flattenEntityTree = ( export const flattenTaskTree = (taskTree: TaskPreviewRecursive[], path: string[] = []): TaskPreviewFlattend[] => { return taskTree.flatMap(task => { const { children, ...restTask } = task - if (!children) console.warn(`Children for task '${task.id}' (${task.title}) not loaded yet!`) + // if (!children) console.warn(`Children for task '${task.id}' (${task.title}) not loaded yet!`) - const flatTask = { ...restTask, path, childrenCount: children?.length || 0 } + const flatTask = { ...restTask, path, children } const subpath = [...path, task.id] return [flatTask, ...flattenTaskTree(children || [], subpath)] diff --git a/client-v2/src/app/directives/tooltip.directive.spec.ts b/client-v2/src/app/tooltip/tooltip.directive.spec.ts similarity index 100% rename from client-v2/src/app/directives/tooltip.directive.spec.ts rename to client-v2/src/app/tooltip/tooltip.directive.spec.ts diff --git a/client-v2/src/app/directives/tooltip.directive.test.ts b/client-v2/src/app/tooltip/tooltip.directive.test.ts similarity index 97% rename from client-v2/src/app/directives/tooltip.directive.test.ts rename to client-v2/src/app/tooltip/tooltip.directive.test.ts index 2aa4e5e8..1d4530f3 100644 --- a/client-v2/src/app/directives/tooltip.directive.test.ts +++ b/client-v2/src/app/tooltip/tooltip.directive.test.ts @@ -1,6 +1,6 @@ import { OverlayModule } from '@angular/cdk/overlay' import { testName } from 'cypress/support/helpers' -import { TooltipComponent } from '../components/atoms/tooltip/tooltip.component' +import { TooltipComponent } from './tooltip/tooltip.component' import { TooltipDirective } from './tooltip.directive' const tooltipString = "And this is it's tooltip" diff --git a/client-v2/src/app/directives/tooltip.directive.ts b/client-v2/src/app/tooltip/tooltip.directive.ts similarity index 97% rename from client-v2/src/app/directives/tooltip.directive.ts rename to client-v2/src/app/tooltip/tooltip.directive.ts index f592b4c6..bbcb0f12 100644 --- a/client-v2/src/app/directives/tooltip.directive.ts +++ b/client-v2/src/app/tooltip/tooltip.directive.ts @@ -2,7 +2,7 @@ import { OverlayRef, Overlay, PositionStrategy, ConnectedPosition } from '@angul import { ComponentPortal } from '@angular/cdk/portal' import { Directive, ElementRef, HostListener, Injector, Input, TemplateRef, ViewContainerRef } from '@angular/core' import { first } from 'rxjs' -import { TooltipComponent, TOOLTIP_DATA } from '../components/atoms/tooltip/tooltip.component' +import { TooltipComponent, TOOLTIP_DATA } from './tooltip/tooltip.component' import { DeviceService } from '../services/device.service' const positions = { diff --git a/client-v2/src/app/tooltip/tooltip.module.ts b/client-v2/src/app/tooltip/tooltip.module.ts new file mode 100644 index 00000000..b0662c11 --- /dev/null +++ b/client-v2/src/app/tooltip/tooltip.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { TooltipComponent } from './tooltip/tooltip.component' +import { TooltipDirective } from './tooltip.directive' + +@NgModule({ + declarations: [TooltipDirective, TooltipComponent], + imports: [CommonModule], + exports: [TooltipDirective, TooltipComponent], +}) +export class TooltipModule {} diff --git a/client-v2/src/app/components/atoms/tooltip/tooltip.component.css b/client-v2/src/app/tooltip/tooltip/tooltip.component.css similarity index 61% rename from client-v2/src/app/components/atoms/tooltip/tooltip.component.css rename to client-v2/src/app/tooltip/tooltip/tooltip.component.css index e3562e9e..f1515bc6 100644 --- a/client-v2/src/app/components/atoms/tooltip/tooltip.component.css +++ b/client-v2/src/app/tooltip/tooltip/tooltip.component.css @@ -2,7 +2,20 @@ --reveal-delay: 0ms; } :host { - @apply box-border block max-w-xs rounded-lg border border-tinted-600/60 bg-tinted-700 py-1 px-2 text-sm text-tinted-100 shadow-xl glass overflow-hidden; + @apply box-border + block + max-w-xs + overflow-hidden + rounded-lg + border + border-tinted-600/60 + bg-tinted-700 + py-1 + px-2 + text-sm + text-tinted-100 + shadow-xl + glass; animation: reveal 130ms var(--reveal-delay, 280ms) forwards; scale: 0.8; diff --git a/client-v2/src/app/components/atoms/tooltip/tooltip.component.html b/client-v2/src/app/tooltip/tooltip/tooltip.component.html similarity index 100% rename from client-v2/src/app/components/atoms/tooltip/tooltip.component.html rename to client-v2/src/app/tooltip/tooltip/tooltip.component.html diff --git a/client-v2/src/app/components/atoms/tooltip/tooltip.component.spec.ts b/client-v2/src/app/tooltip/tooltip/tooltip.component.spec.ts similarity index 91% rename from client-v2/src/app/components/atoms/tooltip/tooltip.component.spec.ts rename to client-v2/src/app/tooltip/tooltip/tooltip.component.spec.ts index 60cc32e6..d2cb303a 100644 --- a/client-v2/src/app/components/atoms/tooltip/tooltip.component.spec.ts +++ b/client-v2/src/app/tooltip/tooltip/tooltip.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' -import { TooltipDirective } from 'src/app/directives/tooltip.directive' import { TooltipComponent, TOOLTIP_DATA } from './tooltip.component' +import { TooltipDirective } from '../tooltip.directive' describe('TooltipComponent', () => { let component: TooltipComponent diff --git a/client-v2/src/app/components/atoms/tooltip/tooltip.component.ts b/client-v2/src/app/tooltip/tooltip/tooltip.component.ts similarity index 100% rename from client-v2/src/app/components/atoms/tooltip/tooltip.component.ts rename to client-v2/src/app/tooltip/tooltip/tooltip.component.ts diff --git a/client-v2/src/app/utils/index.ts b/client-v2/src/app/utils/index.ts index f57351d5..713934f7 100644 --- a/client-v2/src/app/utils/index.ts +++ b/client-v2/src/app/utils/index.ts @@ -61,3 +61,5 @@ export const moveElement = (arr: Array, fromIndex: number, toIndex: number insertElementAfter(arr, toIndex, elem) return arr } + +export const isNotNullish = (value: T | undefined | null): value is T => value !== undefined && value !== null diff --git a/client-v2/src/app/utils/menu-item.helpers.ts b/client-v2/src/app/utils/menu-item.helpers.ts index 8a6cc0ba..08e832fa 100644 --- a/client-v2/src/app/utils/menu-item.helpers.ts +++ b/client-v2/src/app/utils/menu-item.helpers.ts @@ -1,12 +1,12 @@ import { interpolateParams } from '.' -import { MenuItem } from '../components/molecules/drop-down/drop-down.component' +import { MenuItem } from '../dropdown/drop-down/drop-down.component' import { TaskPreview } from '../fullstack-shared-models/task.model' -export interface WrappedMenuItems extends Array { - applyOperators(...operator: ((item: MenuItem) => MenuItem)[]): WrappedMenuItems +export interface WrappedMenuItems extends Array> { + applyOperators(...operator: ((item: MenuItem) => MenuItem)[]): WrappedMenuItems } -const getOperatorApplier = (items: MenuItem[]) => { - return (...operators: ((item: MenuItem) => MenuItem)[]): WrappedMenuItems => { +const getOperatorApplier = (items: MenuItem[]) => { + return (...operators: ((item: MenuItem) => MenuItem)[]): WrappedMenuItems => { const result = items.map(item => { return operators.reduce((prevMapperResult, operator) => operator(prevMapperResult), item) }) @@ -17,14 +17,14 @@ const getOperatorApplier = (items: MenuItem[]) => { /** Used to apply operators sequentially per iteration, instead of iterating over all menu items for each operator. * Though it is unnecessary to use `applyOperators` for only one operator. */ -export const wrapMenuItems = (items: MenuItem[]): WrappedMenuItems => { +export const wrapMenuItems = (items: MenuItem[]): WrappedMenuItems => { return Object.assign(items, { applyOperators: getOperatorApplier(items) }) } /** Used to update items on the fly. The callback is also recursively applied to children. */ -export const interceptItem = (callback: (item: MenuItem) => Partial>) => { - return (item: MenuItem): MenuItem => { - if (item.isSeperator) return item +export const interceptItem = (callback: (item: MenuItem) => Partial, 'children'>>) => { + return (item: MenuItem): MenuItem => { + if (item.isSeparator) return item return { ...item, ...callback(item), @@ -51,16 +51,18 @@ export const interceptItem = (callback: (item: MenuItem) => Partial { - return interceptItem(({ action }) => ({ - action: action && ((localData: unknown) => action(localData || data)), +export const useDataForAction = (data: T) => { + return interceptItem(({ action, isActive }) => ({ + action: action && ((localData: T) => action(localData || data)), + isActive: typeof isActive === 'function' ? (localData: T) => isActive(localData || data) : isActive, })) } /** Used to transform the data when the action is called. */ -export const interceptDataForAction = (callback: (data: unknown) => unknown) => { - return interceptItem(({ action }) => ({ - action: action && ((localData: unknown) => action(callback(localData))), +export const interceptDataForAction = (callback: (data: T) => T) => { + return interceptItem(({ action, isActive }) => ({ + action: action && ((localData: T) => action(callback(localData))), + isActive: typeof isActive === 'function' ? (localData: T) => isActive(callback(localData)) : isActive, })) } @@ -72,8 +74,8 @@ export const interceptDataForAction = (callback: (data: unknown) => unknown) => * ``` * and the resulting route will look like this `/route/to/foo` */ -export const useParamsForRoute = (params: Record) => { - return interceptItem(({ route }) => ({ +export const useParamsForRoute = (params: Record) => { + return interceptItem(({ route }) => ({ route: route && interpolateParams(route, params), })) } diff --git a/client-v2/src/app/utils/observable.helpers.ts b/client-v2/src/app/utils/observable.helpers.ts index 2968a8b0..a69c6a1e 100644 --- a/client-v2/src/app/utils/observable.helpers.ts +++ b/client-v2/src/app/utils/observable.helpers.ts @@ -1,5 +1,5 @@ -import { EventEmitter } from '@angular/core' -import { Observable, switchMap, of, first, filter, map, tap, takeUntil } from 'rxjs' +import { Observable, filter, first, map, of, switchMap, tap } from 'rxjs' +import { TapObserver } from 'rxjs/internal/operators/tap' /** Wraps the `filter()` operator to enable predicates with Observable results. */ export const filterWith = (predicate: (value: T) => boolean | Observable) => { @@ -18,16 +18,22 @@ export const filterWith = (predicate: (value: T) => boolean | Observable( - value$: Observable, - options?: { - until$?: Observable - isAsync?: boolean - } -): EventEmitter => { - const emitter = new EventEmitter(options?.isAsync === true) - - value$.pipe(options?.until$ ? takeUntil(options.until$) : tap()).subscribe(value => emitter.emit(value)) - - return emitter -} +export const debugObserver = ( + name: string, + { + subscribe = true, + unsubscribe = true, + next = true, + error = true, + complete = true, + finalize = false, + }: Partial, boolean>> = {} as never +) => + tap({ + subscribe: !subscribe ? undefined : () => console.log(`🧩 %csubscribed to %c${name}`, 'color:gray', ''), + unsubscribe: !unsubscribe ? undefined : () => console.log(`👋 %cunsubscribed from %c${name}`, 'color:gray', ''), + next: !next ? undefined : value => console.log(`🚀 %cnext %c${name}`, 'color:gray', '', { value }), + error: !error ? undefined : error => console.log(`🚨 %cerror %c${name}`, 'color:gray', '', { error }), + complete: !complete ? undefined : () => console.log(`✅ %ccomplete %c${name}`, 'color:gray', ''), + finalize: !finalize ? undefined : () => console.log(`🏁 %cfinalize %c${name}`, 'color:gray', ''), + }) diff --git a/client-v2/src/app/utils/store.helpers.ts b/client-v2/src/app/utils/store.helpers.ts index bec5aff7..99287cb7 100644 --- a/client-v2/src/app/utils/store.helpers.ts +++ b/client-v2/src/app/utils/store.helpers.ts @@ -91,7 +91,7 @@ export const loadingUpdates = ( /** Injects `isLoading` interpreted by the action type: * | action type matching | loading state | - * | ---------------------|---------------| + * | -------------------- | ------------- | * | `/error/i` | `false` | * | `/success/i` | `false` | * | all other cases | `true` | diff --git a/client-v2/src/css/cdk-styles.css b/client-v2/src/css/cdk-styles.css index 4ffcfba8..6111c6ed 100644 --- a/client-v2/src/css/cdk-styles.css +++ b/client-v2/src/css/cdk-styles.css @@ -2,7 +2,10 @@ .dropdown-menu { min-width: 180px; max-width: 280px; - @apply mx-2 flex flex-col gap-0.5 rounded-xl border border-tinted-700/70 bg-tinted-900 glass p-2 shadow-lg; + @apply mx-2 flex flex-col gap-0.5 rounded-xl border border-tinted-700/70 bg-tinted-900 p-2 shadow-lg glass; + } + .dropdown-menu .menu-item { + @apply pl-2.5; } .menu-item { diff --git a/client-v2/src/css/components.css b/client-v2/src/css/components.css index 6d0d3f20..c423654e 100644 --- a/client-v2/src/css/components.css +++ b/client-v2/src/css/components.css @@ -17,7 +17,7 @@ } .button-naked { - @apply rounded-lg py-1 px-2 transition-colors hover:bg-tinted-700; + @apply rounded-lg py-1 px-2 transition-colors duration-75 hover:bg-tinted-700; /* hover:text-tinted-100 */ } .button-naked i { @@ -26,13 +26,13 @@ .button-icon-naked { /* @TODO: This would need to be adjusted when touch device specific styles/behaviour are added */ - @apply py-0; + @apply py-0 duration-75; } .button-icon-naked i { @apply m-0 inline-block aspect-square rounded py-0.5 px-2.5 text-base transition-colors hover:bg-tinted-700; } .icon-btn { - @apply inline-flex aspect-square min-w-[1.75rem] items-center justify-center rounded-lg px-1 transition-colors hover:bg-tinted-500; + @apply inline-flex h-7 min-w-[1.75rem] items-center justify-center rounded-lg px-1 outline-none transition-colors duration-75 hover:bg-tinted-600; } .button--submit { @@ -49,6 +49,11 @@ @apply text-tinted-300; } + .keybinding, + kbd { + @apply inline-block h-6 min-w-[1.5rem] rounded-md border border-tinted-600/70 bg-tinted-700 py-[0.0625rem] px-0.5 text-center text-[0.8125rem] font-semibold text-tinted-300; + } + .show-placeholder::before { content: attr(data-placeholder); color: var(--placeholder-color, theme('colors.tinted.400')); @@ -60,7 +65,7 @@ .bg-dotted { background-image: radial-gradient(theme('colors.tinted.800') 8%, transparent 8%); - background-position: .7rem .6rem; + background-position: 0.7rem 0.6rem; background-size: 27px 27px; } .fade-in-out-y { @@ -82,4 +87,8 @@ min-height: -webkit-fill-available; min-height: 100dvh; } + + .highlight { + @apply rounded-[0.25em] bg-secondary-400 text-tinted-900; + } } diff --git a/client-v2/src/css/main.css b/client-v2/src/css/main.css index cb396abf..98981e29 100644 --- a/client-v2/src/css/main.css +++ b/client-v2/src/css/main.css @@ -41,18 +41,22 @@ body { [disabled] { @apply cursor-not-allowed; } +:where(button:not(.outline-none, .no-outline, .menu-item, .icon-btn)):focus-visible { + @apply outline-primary-400; +} /*/////////////////////////////// TYPOGRAPHY //////////////////////////////////*/ -ul, +ul:not([data-type='taskList']), ol { - @apply pl-4; + @apply pl-6; } + ul > li { - @apply list-inside list-disc pl-1 before:-ml-1.5; + @apply list-disc; } ol > li { - @apply list-inside list-decimal; + @apply list-decimal; } hr { @@ -61,6 +65,7 @@ hr { p { margin-bottom: 0.5rem; + line-height: 1.5; } h1, @@ -68,7 +73,7 @@ h2, h3, h4, h5 { - margin: 1.4rem 0 1.38rem; + margin: .6em 0 .45em; font-weight: 600; line-height: 1.2; } @@ -92,7 +97,3 @@ h4 { h5 { font-size: 1.1rem; } - -.highlight { - @apply bg-secondary-400 text-tinted-900 rounded; -} \ No newline at end of file diff --git a/client-v2/tailwind.config.js b/client-v2/tailwind.config.js index 8582f070..c9884127 100644 --- a/client-v2/tailwind.config.js +++ b/client-v2/tailwind.config.js @@ -39,6 +39,21 @@ module.exports = { 'var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)', }, }, + '.child-focus-ring': { + '&:has(:focus, :focus-visible)': { + // ring-2 + '--tw-ring-offset-shadow': + 'var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)', + '--tw-ring-shadow': + 'var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)', + 'box-shadow': + 'var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)', + + // !ring-primary-400 + '--tw-ring-opacity': '1 !important', + '--tw-ring-color': `${colors.primary[400]} !important`, + }, + }, }) }), ], diff --git a/client-v2/tsconfig.spec.json b/client-v2/tsconfig.spec.json index 092345b0..e495ccd4 100644 --- a/client-v2/tsconfig.spec.json +++ b/client-v2/tsconfig.spec.json @@ -3,9 +3,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", - "types": [ - "jasmine" - ] + "types": ["jasmine", "@total-typescript/ts-reset"] }, "files": [ "src/test.ts", diff --git a/server/src/app.service.ts b/server/src/app.service.ts index fd0ed1d8..c339c2d6 100644 --- a/server/src/app.service.ts +++ b/server/src/app.service.ts @@ -15,6 +15,7 @@ export class AppService { return 'Hello World!' } + // @TODO: this should not reside in application code, lets move it to a CLI/npm script async clearDb() { if (this.configService.get('TESTING_ENV') != 'true') throw new ForbiddenException()