From 0da9c0ecc77765bc09489f227a326c1bf08ddfb2 Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Fri, 9 Aug 2024 00:53:56 +0200
Subject: [PATCH 01/31] Add Tiptap and a first integration in NotesModal
* Should handle existing markdown content
* The editor is configured with extensions:
* all what's in StarterKit
* code block lowlight
* highlight
* link
* task
* typography (with french double quotes)
* Some styles have been added to fit DSFR styling
Still work in progress, but a nice start!
To do:
* image uploading and placement inside editor
* menu buttons
* accessibility tests and improvements
---
confiture-web-app/package.json | 20 +-
.../src/components/audit/NotesModal.vue | 13 +-
.../src/components/ui/Tiptap.vue | 75 +++
confiture-web-app/src/main.ts | 1 +
confiture-web-app/src/styles/main.css | 79 +++
yarn.lock | 503 ++++++++++++++++++
6 files changed, 681 insertions(+), 10 deletions(-)
create mode 100644 confiture-web-app/src/components/ui/Tiptap.vue
diff --git a/confiture-web-app/package.json b/confiture-web-app/package.json
index 1d5c66b1..5ddddb7f 100644
--- a/confiture-web-app/package.json
+++ b/confiture-web-app/package.json
@@ -15,21 +15,33 @@
"dependencies": {
"@gouvfr/dsfr": "1.12.1",
"@sentry/tracing": "^7.37.2",
+ "@sentry/vite-plugin": "^0.3.0",
"@sentry/vue": "^7.37.2",
+ "@tiptap/extension-code-block-lowlight": "^2.5.9",
+ "@tiptap/extension-highlight": "^2.5.9",
+ "@tiptap/extension-link": "^2.5.9",
+ "@tiptap/extension-task-item": "^2.5.9",
+ "@tiptap/extension-task-list": "^2.5.9",
+ "@tiptap/extension-typography": "^2.5.9",
+ "@tiptap/pm": "^2.5.9",
+ "@tiptap/starter-kit": "^2.5.9",
+ "@tiptap/vue-3": "^2.5.9",
"@unhead/vue": "^1.5.3",
+ "@vitejs/plugin-vue": "^4.4.1",
"dompurify": "^2.4.1",
+ "highlight.js": "^11.10.0",
"jwt-decode": "^3.1.2",
"ky": "^0.33.0",
"lodash-es": "^4.17.21",
+ "lowlight": "^3.1.0",
"marked": "^4.2.4",
"pinia": "^2.0.28",
"slugify": "^1.6.5",
+ "tiptap-markdown": "^0.8.10",
+ "vite": "^4.5.0",
"vue": "^3.3.8",
"vue-matomo": "^4.2.0",
- "vue-router": "^4.2.5",
- "vite": "^4.5.0",
- "@vitejs/plugin-vue": "^4.4.1",
- "@sentry/vite-plugin": "^0.3.0"
+ "vue-router": "^4.2.5"
},
"devDependencies": {
"@types/dompurify": "^2.4.0",
diff --git a/confiture-web-app/src/components/audit/NotesModal.vue b/confiture-web-app/src/components/audit/NotesModal.vue
index b84eee71..0e7d98c8 100644
--- a/confiture-web-app/src/components/audit/NotesModal.vue
+++ b/confiture-web-app/src/components/audit/NotesModal.vue
@@ -10,6 +10,7 @@ import { AuditFile, StoreName } from "../../types";
import { handleFileDeleteError, handleFileUploadError } from "../../utils";
import DsfrModal from "../ui/DsfrModal.vue";
import FileUpload from "../ui/FileUpload.vue";
+import Tiptap from "../ui/Tiptap.vue";
import MarkdownHelpButton from "./MarkdownHelpButton.vue";
import SaveIndicator from "./SaveIndicator.vue";
@@ -104,15 +105,15 @@ function handleDeleteFile(file: AuditFile) {
-
+ @update:content="($content) => (notes = $content)"
+ />
+import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
+import Heading from "@tiptap/extension-heading";
+import Highlight from "@tiptap/extension-highlight";
+import Link from "@tiptap/extension-link";
+import TaskItem from "@tiptap/extension-task-item";
+import TaskList from "@tiptap/extension-task-list";
+import Typography from "@tiptap/extension-typography";
+import { useEditor, EditorContent } from "@tiptap/vue-3";
+import StarterKit from "@tiptap/starter-kit";
+
+import css from "highlight.js/lib/languages/css";
+import js from "highlight.js/lib/languages/javascript";
+import ts from "highlight.js/lib/languages/typescript";
+import html from "highlight.js/lib/languages/xml";
+
+import { Markdown } from "tiptap-markdown";
+
+// load common languages
+import { common, createLowlight } from "lowlight";
+
+// create a lowlight instance
+const lowlight = createLowlight(common);
+
+// you can also register languages
+lowlight.register("html", html);
+lowlight.register("css", css);
+lowlight.register("js", js);
+lowlight.register("ts", ts);
+
+const props = defineProps(["content"]);
+const emit = defineEmits(["update:content"]);
+//JSON.stringify(document.querySelector(".tiptap").editor.getJSON())
+
+function getContent() {
+ let jsonContent;
+ try {
+ jsonContent = JSON.parse(props.content);
+ } catch (e) {
+ return "";
+ }
+ return jsonContent;
+}
+
+const editor = useEditor({
+ content: getContent(),
+ extensions: [
+ CodeBlockLowlight.configure({ lowlight }),
+ Highlight,
+ Heading.configure({
+ levels: [2, 3, 4, 5, 6]
+ }),
+ Link,
+ Markdown,
+ StarterKit.configure({
+ codeBlock: false,
+ heading: false
+ }),
+ TaskItem,
+ TaskList,
+ Typography.configure({
+ openDoubleQuote: "« ",
+ closeDoubleQuote: " »"
+ })
+ ],
+ onUpdate({ editor }) {
+ // The content has changed.
+ emit("update:content", JSON.stringify(editor.getJSON()));
+ }
+});
+
+
+
+
+
diff --git a/confiture-web-app/src/main.ts b/confiture-web-app/src/main.ts
index 787d9fa1..1dcc140b 100644
--- a/confiture-web-app/src/main.ts
+++ b/confiture-web-app/src/main.ts
@@ -2,6 +2,7 @@ import "./styles/main.css";
import "@gouvfr/dsfr/dist/dsfr.min.css";
import "@gouvfr/dsfr/dist/dsfr.module.min.js";
import "@gouvfr/dsfr/dist/utility/icons/icons.css";
+import "highlight.js/styles/github.css";
import { BrowserTracing } from "@sentry/tracing";
import * as Sentry from "@sentry/vue";
diff --git a/confiture-web-app/src/styles/main.css b/confiture-web-app/src/styles/main.css
index fd842b7d..5b5f2dd8 100644
--- a/confiture-web-app/src/styles/main.css
+++ b/confiture-web-app/src/styles/main.css
@@ -65,6 +65,85 @@ from DSFR links with `target="_blank"` */
color: var(--background-action-high-error) !important;
}
+.tiptap {
+ padding: 1rem;
+ border: 1px solid var(--border-default-grey);
+ min-height: 10rem;
+}
+
+.tiptap pre {
+ padding: 0.75rem;
+}
+.tiptap code {
+ padding: 0.2em 0.4em;
+ font-size: 85%;
+}
+.tiptap pre,
+.tiptap code {
+ background-color: var(--background-alt-grey);
+ border-radius: 0.25rem;
+}
+.tiptap blockquote:before {
+ --icon-size: 2rem;
+ color: var(--artwork-minor-blue-france);
+ content: "";
+ display: block;
+ margin-bottom: 0.5rem;
+ background-color: currentColor;
+ display: inline-block;
+ flex: 0 0 auto;
+ height: var(--icon-size);
+ mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTE0IDNhOCA4IDAgMSAxIDAgMTZ2My41Yy01LTItMTItNS0xMi0xMS41YTggOCAwIDAgMSA4LThoNFptMCAyaC00YTYgNiAwIDAgMC02IDZjMCAzLjYxIDIuNDYyIDUuOTY2IDggOC40OFYxN2gyYTYgNiAwIDEgMCAwLTEyWm0tMiAyLjUtMi41MDYgMy43NUwxMiAxNUg5LjI5NUw2Ljc1IDExLjI1IDkuMjk1IDcuNUgxMlptNC41IDAtMi41MDYgMy43NUwxNi41IDE1aC0yLjcwNWwtMi41NDUtMy43NSAyLjU0NS0zLjc1SDE2LjVaIi8+PC9zdmc+);
+ mask-size: 100% 100%;
+ vertical-align: calc((0.75em - var(--icon-size)) * 0.5);
+ width: var(--icon-size);
+}
+
+.tiptap blockquote p {
+ font-size: 1.25rem;
+ font-weight: 700;
+ line-height: 2rem;
+}
+
+.tiptap li > p {
+ margin-bottom: 0.25em;
+}
+
+/* FIXME: tiptap tasklist are not accessible yet. */
+/* https://github.com/ueberdosis/tiptap/issues/4774 */
+.tiptap ul[data-type="taskList"] {
+ list-style: none;
+ margin-left: 0;
+ padding: 0;
+}
+
+.tiptap ul[data-type="taskList"] li {
+ align-items: flex-start;
+ display: flex;
+}
+
+.tiptap ul[data-type="taskList"] li > label {
+ flex: 0 0 auto;
+ margin-right: 0.5rem;
+ user-select: none;
+}
+
+.tiptap ul[data-type="taskList"] li > div {
+ flex: 1 1 auto;
+}
+
+.tiptap ul[data-type="taskList"] li > div p {
+ margin-bottom: 0.25em;
+}
+
+.tiptap ul[data-type="taskList"] input[type="checkbox"] {
+ cursor: pointer;
+}
+
+.tiptap ul[data-type="taskList"] ul[data-type="taskList"] {
+ margin: 0;
+}
+
/* File upload styling */
/* input[type="file" i]::-webkit-file-upload-button {
background-color: transparent;
diff --git a/yarn.lock b/yarn.lock
index 5dd55513..072e088e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1180,6 +1180,11 @@
dependencies:
debug "^4.3.1"
+"@popperjs/core@^2.9.0":
+ version "2.11.8"
+ resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
+ integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
+
"@prisma/client@^4.1.1":
version "4.16.2"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.16.2.tgz#3bb9ebd49b35c8236b3d468d0215192267016e2b"
@@ -1330,6 +1335,11 @@
tmp "0.2.1"
ts-pattern "^4.0.1"
+"@remirror/core-constants@^2.0.2":
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-2.0.2.tgz#f05eccdc69e3a65e7d524b52548f567904a11a1a"
+ integrity sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==
+
"@sentry-internal/feedback@7.88.0":
version "7.88.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.88.0.tgz#fa4db4a27d1fa7fe51dc67af185b13519d7fbc76"
@@ -1995,6 +2005,204 @@
resolved "https://registry.yarnpkg.com/@timsuchanek/sleep-promise/-/sleep-promise-8.0.1.tgz#81c0754b345138a519b51c2059771eb5f9b97818"
integrity sha512-cxHYbrXfnCWsklydIHSw5GCMHUPqpJ/enxWSyVHNOgNe61sit/+aOXTTI+VOdWkvVaJsI2vsB9N4+YDNITawOQ==
+"@tiptap/core@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/core/-/core-2.5.9.tgz#1deb0b7c748e24ec32613263e0af8d55a3b3c2ca"
+ integrity sha512-PPUR+0tbr+wX2G8RG4FEps4qhbnAPEeXK1FUtirLXSRh8vm+TDgafu3sms7wBc4fAyw9zTO/KNNZ90GBe04guA==
+
+"@tiptap/extension-blockquote@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.5.9.tgz#d873a8496fcf572c69aaac2a7a341e035fdbae22"
+ integrity sha512-LhGyigmd/v1OjYPeoVK8UvFHbH6ffh175ZuNvseZY4PsBd7kZhrSUiuMG8xYdNX8FxamsxAzr2YpsYnOzu3W7A==
+
+"@tiptap/extension-bold@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.5.9.tgz#00c9b7b5211048b1e1c5d67e355935b9c92e3532"
+ integrity sha512-XUJdzFb31t0+bwiRquJf0btBpqOB3axQNHTKM9XADuL4S+Z6OBPj0I5rYINeElw/Q7muvdWrHWHh/ovNJA1/5A==
+
+"@tiptap/extension-bubble-menu@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.5.9.tgz#d600bbcaa1d98a99f32b3b8b8c3d35752161200c"
+ integrity sha512-NddZ8Qn5dgPPa1W4yk0jdhF4tDBh0FwzBpbnDu2Xz/0TUHrA36ugB2CvR5xS1we4zUKckgpVqOqgdelrmqqFVg==
+ dependencies:
+ tippy.js "^6.3.7"
+
+"@tiptap/extension-bullet-list@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.5.9.tgz#2852aba9a1dacbf2c673cda6a4994b1f3c33cd5c"
+ integrity sha512-hJTv1x4omFgaID4LMRT5tOZb/VKmi8Kc6jsf4JNq4Grxd2sANmr9qpmKtBZvviK+XD5PpTXHvL+1c8C1SQtuHQ==
+
+"@tiptap/extension-code-block-lowlight@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.5.9.tgz#ccd6569422d98b11813df3e0dbd09b1dcf957def"
+ integrity sha512-taIXxXQ/Lka9CegHFHQS+nx6cX9i9Ws63ZFMPbrXLMSJRhXk8+m4UAoGZQJH9CGGb5/Rv0p3Z8I59AGiyUHLEw==
+
+"@tiptap/extension-code-block@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.5.9.tgz#8cd99515b286fc62ad1215a411aea5da9a7d9701"
+ integrity sha512-+MUwp0VFFv2aFiZ/qN6q10vfIc6VhLoFFpfuETX10eIRks0Xuj2nGiqCDj7ca0/M44bRg2VvW8+tg/ZEHFNl9g==
+
+"@tiptap/extension-code@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.5.9.tgz#93c4433eca8b2aa239ea7f408b90f152b7fc4603"
+ integrity sha512-Q1PL3DUXiEe5eYUwOug1haRjSaB0doAKwx7KFVI+kSGbDwCV6BdkKAeYf3us/O2pMP9D0im8RWX4dbSnatgwBA==
+
+"@tiptap/extension-document@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.5.9.tgz#13a22b2d3bdc1463844872b1f1c926633df431a8"
+ integrity sha512-VdNZYDyCzC3W430UdeRXR9IZzPeODSbi5Xz/JEdV93THVp8AC9CrZR7/qjqdBTgbTB54VP8Yr6bKfCoIAF0BeQ==
+
+"@tiptap/extension-dropcursor@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.5.9.tgz#648f683f929056a0526620f530f73e6b052c1481"
+ integrity sha512-nEOb37UryG6bsU9JAs/HojE6Jg43LupNTAMISbnuB1CPAeAqNsFMwORd9eEPkyEwnQT7MkhsMOSJM44GoPGIFA==
+
+"@tiptap/extension-floating-menu@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.5.9.tgz#b970905f3c1af49a916dcbd477a4302086187974"
+ integrity sha512-MWJIQQT6e5MgqHny8neeH2Dx926nVPF7sv4p84nX4E0dnkRbEYUP8mCsWYhSUvxxIif6e+yY+4654f2Q9qTx1w==
+ dependencies:
+ tippy.js "^6.3.7"
+
+"@tiptap/extension-gapcursor@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.5.9.tgz#68b9e227cd7876aac353a8ac029995b4c092a763"
+ integrity sha512-yW7V2ebezsa7mWEDWCg4A1ZGsmSV5bEHKse9wzHCDkb7TutSVhLZxGo72U6hNN9PnAksv+FJQk03NuZNYvNyRQ==
+
+"@tiptap/extension-hard-break@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.5.9.tgz#4f38f06dbeb5fb3e58ff7fc0c48b9db9c4ee4ecd"
+ integrity sha512-8hQ63SgZRG4BqHOeSfeaowG2eMr2beced018pOGbpHbE3XSYoISkMVuFz4Z8UEVR3W9dTbKo4wxNufSTducocQ==
+
+"@tiptap/extension-heading@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.5.9.tgz#b9ec3b3b48dea939606d06eff56c4bdc7bed0662"
+ integrity sha512-HHowAlGUbFn1qvmY02ydM7qiPPMTGhAJn2A46enDRjNHW5UoqeMfkMpTEYaioOexyguRFSfDT3gpK68IHkQORQ==
+
+"@tiptap/extension-highlight@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-2.5.9.tgz#290426538abcbb2299809d3e1274ba5af1ba9f68"
+ integrity sha512-tRaSIIbCI7aBlvlmgUgBI5lVBqnMy49lc++UVAx1Pjey1j2KW031vUyvZfEwf6wk8Y7W3kVSkN0mW9IYCcOAOQ==
+
+"@tiptap/extension-history@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.5.9.tgz#f48f64ff95407f0ce27bcdd020762e49d0dd60d1"
+ integrity sha512-hGPtJgoZSwnVVqi/xipC2ET/9X2G2UI/Y+M3IYV1ZlM0tCYsv4spNi3uXlZqnXRwYcBXLk5u6e/dmsy5QFbL8g==
+
+"@tiptap/extension-horizontal-rule@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.5.9.tgz#9f91a17b80700670e53e241fcee40365c57aa994"
+ integrity sha512-/ES5NdxCndBmZAgIXSpCJH8YzENcpxR0S8w34coSWyv+iW0Sq7rW/mksQw8ZIVsj8a7ntpoY5OoRFpSlqcvyGw==
+
+"@tiptap/extension-italic@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.5.9.tgz#8ea0e19e650f0f1d6fc30425ec28291511143dda"
+ integrity sha512-Bw+P139L4cy+B56zpUiRjP8BZSaAUl3JFMnr/FO+FG55QhCxFMXIc6XrC3vslNy5ef3B3zv4gCttS3ee8ByMiw==
+
+"@tiptap/extension-link@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.5.9.tgz#6cb323d36b82700963ad2b9d189a7d07c81c7d6e"
+ integrity sha512-7v9yRsX7NuiY8DPslIsPIlFqcD8aGBMLqfEGXltJDvuG6kykdr+khEZeWcJ8ihHIL4yWR3/MAgeT2W72Z/nxiQ==
+ dependencies:
+ linkifyjs "^4.1.0"
+
+"@tiptap/extension-list-item@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.5.9.tgz#0805b7216371b8b54649abe5ab29bd2c2155f05f"
+ integrity sha512-d9Eo+vBz74SMxP0r25aqiErV256C+lGz+VWMjOoqJa6xWLM1keYy12JtGQWJi8UDVZrDskJKCHq81A0uLt27WA==
+
+"@tiptap/extension-ordered-list@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.5.9.tgz#44aab6ec3e19429a8e3b73e42c04156f2b0bc730"
+ integrity sha512-9MsWpvVvzILuEOd/GdroF7RI7uDuE1M6at9rzsaVGvCPVHZBvu1XR3MSVK5OdiJbbJuPGttlzEFLaN/rQdCGFg==
+
+"@tiptap/extension-paragraph@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.5.9.tgz#05210b6e7a9940b1acc09fdd4ec769fc6406da2b"
+ integrity sha512-HDXGiHTJ/V85dbDMjcFj4XfqyTQZqry6V21ucMzgBZYX60X3gIn7VpQTQnnRjvULSgtfOASSJP6BELc5TyiK0w==
+
+"@tiptap/extension-strike@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.5.9.tgz#f2d54161d24ee37dc8a41b5077c553048ed69f99"
+ integrity sha512-QezkOZpczpl09S8lp5JL7sRkwREoPY16Y/lTvBcFKm3TZbVzYZZ/KwS0zpwK9HXTfXr8os4L9AGjQf0tHonX+w==
+
+"@tiptap/extension-task-item@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-2.5.9.tgz#623590d549aa0e21ccd1d34396ebbeeea45886f5"
+ integrity sha512-g4HK3r3yNE0RcXQOkJHs94Ws/fhhTqa1L5iAy4gwYKNNFFnIQl8BpE6nn9d5h33kWDN9jjY+PZmq+0PvxCLODQ==
+
+"@tiptap/extension-task-list@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-task-list/-/extension-task-list-2.5.9.tgz#9934b3ae84dbfc27804d0d54e64aeb9e8fcaf418"
+ integrity sha512-OylVo5cAh0117PzhyM8MGaUIrCskGiF7v7x6/zAHMFIqVdcbKsq+hMueVPnABfOyLcIH5Zojo3NzNOJeKeblCg==
+
+"@tiptap/extension-text@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.5.9.tgz#a5bef0b9c5324511dbc2804a3a5ac8b9b5d5dc4c"
+ integrity sha512-W0pfiQUPsMkwaV5Y/wKW4cFsyXAIkyOFt7uN5u6LrZ/iW9KZ/IsDODPJDikWp0aeQnXzT9NNQULTpCjbHzzS6g==
+
+"@tiptap/extension-typography@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/extension-typography/-/extension-typography-2.5.9.tgz#bd6a68889ab8479be593d31a930f98ea575f4f08"
+ integrity sha512-S+r4m3J0eK4qOszUcCU7NeOEUMuOwj0pGO4YYbIJs3AjWOyLrXD04grb64u8sCGcM8hiibQ7uZKSLJOmLjuoEA==
+
+"@tiptap/pm@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/pm/-/pm-2.5.9.tgz#f97889210374993a1ce78e9ecb23461d0e4644bf"
+ integrity sha512-YSUaEQVtvZnGzGjif2Tl2o9utE+6tR2Djhz0EqFUcAUEVhOMk7UYUO+r/aPfcCRraIoKKuDQzyCpjKmJicjCUA==
+ dependencies:
+ prosemirror-changeset "^2.2.1"
+ prosemirror-collab "^1.3.1"
+ prosemirror-commands "^1.5.2"
+ prosemirror-dropcursor "^1.8.1"
+ prosemirror-gapcursor "^1.3.2"
+ prosemirror-history "^1.4.1"
+ prosemirror-inputrules "^1.4.0"
+ prosemirror-keymap "^1.2.2"
+ prosemirror-markdown "^1.13.0"
+ prosemirror-menu "^1.2.4"
+ prosemirror-model "^1.22.2"
+ prosemirror-schema-basic "^1.2.3"
+ prosemirror-schema-list "^1.4.1"
+ prosemirror-state "^1.4.3"
+ prosemirror-tables "^1.4.0"
+ prosemirror-trailing-node "^2.0.9"
+ prosemirror-transform "^1.9.0"
+ prosemirror-view "^1.33.9"
+
+"@tiptap/starter-kit@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.5.9.tgz#fec0955b873ebcbdeefdfaab0e9254011df3f41b"
+ integrity sha512-nZ4V+vRayomjxUsajFMHv1iJ5SiSaEA65LAXze/CzyZXGMXfL2OLzY7wJoaVJ4BgwINuO0dOSAtpNDN6jI+6mQ==
+ dependencies:
+ "@tiptap/core" "^2.5.9"
+ "@tiptap/extension-blockquote" "^2.5.9"
+ "@tiptap/extension-bold" "^2.5.9"
+ "@tiptap/extension-bullet-list" "^2.5.9"
+ "@tiptap/extension-code" "^2.5.9"
+ "@tiptap/extension-code-block" "^2.5.9"
+ "@tiptap/extension-document" "^2.5.9"
+ "@tiptap/extension-dropcursor" "^2.5.9"
+ "@tiptap/extension-gapcursor" "^2.5.9"
+ "@tiptap/extension-hard-break" "^2.5.9"
+ "@tiptap/extension-heading" "^2.5.9"
+ "@tiptap/extension-history" "^2.5.9"
+ "@tiptap/extension-horizontal-rule" "^2.5.9"
+ "@tiptap/extension-italic" "^2.5.9"
+ "@tiptap/extension-list-item" "^2.5.9"
+ "@tiptap/extension-ordered-list" "^2.5.9"
+ "@tiptap/extension-paragraph" "^2.5.9"
+ "@tiptap/extension-strike" "^2.5.9"
+ "@tiptap/extension-text" "^2.5.9"
+
+"@tiptap/vue-3@^2.5.9":
+ version "2.5.9"
+ resolved "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-2.5.9.tgz#822e3b52c51b582b5c7238f782d543125da0f3b1"
+ integrity sha512-Iz7HMW9A0jinYnMs2wZxjI+e5fc5MQmjgmfE0kQmimpgISBregW8vJyDKDPIZVJz5LQPLL045G3mL+7V8fExrQ==
+ dependencies:
+ "@tiptap/extension-bubble-menu" "^2.5.9"
+ "@tiptap/extension-floating-menu" "^2.5.9"
+
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
@@ -2119,6 +2327,13 @@
"@types/qs" "*"
"@types/serve-static" "*"
+"@types/hast@^3.0.0":
+ version "3.0.4"
+ resolved "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa"
+ integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==
+ dependencies:
+ "@types/unist" "*"
+
"@types/http-cache-semantics@*":
version "4.0.4"
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4"
@@ -2153,6 +2368,11 @@
dependencies:
"@types/node" "*"
+"@types/linkify-it@^3":
+ version "3.0.5"
+ resolved "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz#1e78a3ac2428e6d7e6c05c1665c242023a4601d8"
+ integrity sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==
+
"@types/lodash-es@^4.17.6":
version "4.17.12"
resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b"
@@ -2165,11 +2385,24 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8"
integrity sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==
+"@types/markdown-it@^13.0.7":
+ version "13.0.9"
+ resolved "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.9.tgz#df79221eae698df5b4e982c7e91128dd8e525743"
+ integrity sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==
+ dependencies:
+ "@types/linkify-it" "^3"
+ "@types/mdurl" "^1"
+
"@types/marked@^4.0.8":
version "4.3.2"
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-4.3.2.tgz#e2e0ad02ebf5626bd215c5bae2aff6aff0ce9eac"
integrity sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==
+"@types/mdurl@^1":
+ version "1.0.5"
+ resolved "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz#3e0d2db570e9fb6ccb2dc8fde0be1d79ac810d39"
+ integrity sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==
+
"@types/mime@*":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45"
@@ -2306,6 +2539,11 @@
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
+"@types/unist@*":
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20"
+ integrity sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==
+
"@types/yauzl@^2.9.1":
version "2.10.3"
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999"
@@ -3745,6 +3983,11 @@ create-require@^1.1.0:
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
+crelt@^1.0.0:
+ version "1.0.6"
+ resolved "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
+ integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
+
croner@~4.1.92:
version "4.1.97"
resolved "https://registry.yarnpkg.com/croner/-/croner-4.1.97.tgz#6e373dc7bb3026fab2deb0d82685feef20796766"
@@ -4008,6 +4251,11 @@ depd@2.0.0, depd@~2.0.0:
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+dequal@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
+ integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
+
destroy@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
@@ -4028,6 +4276,13 @@ detect-node@^2.0.4:
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
+devlop@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018"
+ integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==
+ dependencies:
+ dequal "^2.0.0"
+
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
@@ -5228,6 +5483,16 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+highlight.js@^11.10.0:
+ version "11.10.0"
+ resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz#6e3600dc4b33d6dc23d5bd94fbf72405f5892b92"
+ integrity sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==
+
+highlight.js@~11.9.0:
+ version "11.9.0"
+ resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0"
+ integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==
+
hookable@^5.5.3:
version "5.5.3"
resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d"
@@ -5885,6 +6150,18 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
+linkify-it@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421"
+ integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
+ dependencies:
+ uc.micro "^2.0.0"
+
+linkifyjs@^4.1.0:
+ version "4.1.3"
+ resolved "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz#0edbc346428a7390a23ea2e5939f76112c9ae07f"
+ integrity sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==
+
lint-staged@^15.2.0:
version "15.2.0"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.0.tgz#3111534ca58096a3c8f70b044b6e7fe21b36f859"
@@ -6055,6 +6332,15 @@ lowercase-keys@^2.0.0:
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
+lowlight@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/lowlight/-/lowlight-3.1.0.tgz#aa394c5f3a7689fce35fa49a7c850ba3ead4f590"
+ integrity sha512-CEbNVoSikAxwDMDPjXlqlFYiZLkDJHwyGu/MfOsJnF3d7f3tds5J3z8s/l9TMXhzfsJCCJEAsD78842mwmg0PQ==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ devlop "^1.0.0"
+ highlight.js "~11.9.0"
+
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@@ -6110,11 +6396,33 @@ make-error@^1.1.1:
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
+markdown-it-task-lists@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz#f68f4d2ac2bad5a2c373ba93081a1a6848417088"
+ integrity sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==
+
+markdown-it@^14.0.0, markdown-it@^14.1.0:
+ version "14.1.0"
+ resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45"
+ integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==
+ dependencies:
+ argparse "^2.0.1"
+ entities "^4.4.0"
+ linkify-it "^5.0.0"
+ mdurl "^2.0.0"
+ punycode.js "^2.3.1"
+ uc.micro "^2.1.0"
+
marked@^4.2.4:
version "4.3.0"
resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3"
integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==
+mdurl@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
+ integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
+
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -6947,6 +7255,11 @@ ora@5.4.1, ora@^5.4.1:
strip-ansi "^6.0.0"
wcwidth "^1.0.1"
+orderedmap@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2"
+ integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==
+
os-name@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/os-name/-/os-name-4.0.1.tgz#32cee7823de85a8897647ba4d76db46bf845e555"
@@ -7397,6 +7710,159 @@ prompts@2.4.2:
kleur "^3.0.3"
sisteransi "^1.0.5"
+prosemirror-changeset@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.2.1.tgz#dae94b63aec618fac7bb9061648e6e2a79988383"
+ integrity sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==
+ dependencies:
+ prosemirror-transform "^1.0.0"
+
+prosemirror-collab@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz#0e8c91e76e009b53457eb3b3051fb68dad029a33"
+ integrity sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==
+ dependencies:
+ prosemirror-state "^1.0.0"
+
+prosemirror-commands@^1.0.0, prosemirror-commands@^1.5.2:
+ version "1.6.0"
+ resolved "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.0.tgz#b79f034ed371576e7bf83ddd4ede689c8ccbd9ab"
+ integrity sha512-xn1U/g36OqXn2tn5nGmvnnimAj/g1pUx2ypJJIe8WkVX83WyJVC5LTARaxZa2AtQRwntu9Jc5zXs9gL9svp/mg==
+ dependencies:
+ prosemirror-model "^1.0.0"
+ prosemirror-state "^1.0.0"
+ prosemirror-transform "^1.0.0"
+
+prosemirror-dropcursor@^1.8.1:
+ version "1.8.1"
+ resolved "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz#49b9fb2f583e0d0f4021ff87db825faa2be2832d"
+ integrity sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==
+ dependencies:
+ prosemirror-state "^1.0.0"
+ prosemirror-transform "^1.1.0"
+ prosemirror-view "^1.1.0"
+
+prosemirror-gapcursor@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz#5fa336b83789c6199a7341c9493587e249215cb4"
+ integrity sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==
+ dependencies:
+ prosemirror-keymap "^1.0.0"
+ prosemirror-model "^1.0.0"
+ prosemirror-state "^1.0.0"
+ prosemirror-view "^1.0.0"
+
+prosemirror-history@^1.0.0, prosemirror-history@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.4.1.tgz#cc370a46fb629e83a33946a0e12612e934ab8b98"
+ integrity sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==
+ dependencies:
+ prosemirror-state "^1.2.2"
+ prosemirror-transform "^1.0.0"
+ prosemirror-view "^1.31.0"
+ rope-sequence "^1.3.0"
+
+prosemirror-inputrules@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz#ef1519bb2cb0d1e0cec74bad1a97f1c1555068bb"
+ integrity sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==
+ dependencies:
+ prosemirror-state "^1.0.0"
+ prosemirror-transform "^1.0.0"
+
+prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2, prosemirror-keymap@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz#14a54763a29c7b2704f561088ccf3384d14eb77e"
+ integrity sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==
+ dependencies:
+ prosemirror-state "^1.0.0"
+ w3c-keyname "^2.2.0"
+
+prosemirror-markdown@^1.11.1, prosemirror-markdown@^1.13.0:
+ version "1.13.0"
+ resolved "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.0.tgz#67ebfa40af48a22d1e4ed6cad2e29851eb61e649"
+ integrity sha512-UziddX3ZYSYibgx8042hfGKmukq5Aljp2qoBiJRejD/8MH70siQNz5RB1TrdTPheqLMy4aCe4GYNF10/3lQS5g==
+ dependencies:
+ markdown-it "^14.0.0"
+ prosemirror-model "^1.20.0"
+
+prosemirror-menu@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz#3cfdc7c06d10f9fbd1bce29082c498bd11a0a79a"
+ integrity sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==
+ dependencies:
+ crelt "^1.0.0"
+ prosemirror-commands "^1.0.0"
+ prosemirror-history "^1.0.0"
+ prosemirror-state "^1.0.0"
+
+prosemirror-model@^1.0.0, prosemirror-model@^1.19.0, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0, prosemirror-model@^1.22.2, prosemirror-model@^1.8.1:
+ version "1.22.3"
+ resolved "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.22.3.tgz#52fdf5897f348b0f07f64bea89156d90afdf645a"
+ integrity sha512-V4XCysitErI+i0rKFILGt/xClnFJaohe/wrrlT2NSZ+zk8ggQfDH4x2wNK7Gm0Hp4CIoWizvXFP7L9KMaCuI0Q==
+ dependencies:
+ orderedmap "^2.0.0"
+
+prosemirror-schema-basic@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.3.tgz#649c349bb21c61a56febf9deb71ac68fca4cedf2"
+ integrity sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==
+ dependencies:
+ prosemirror-model "^1.19.0"
+
+prosemirror-schema-list@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.4.1.tgz#78b8d25531db48ca9688836dbde50e13ac19a4a1"
+ integrity sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg==
+ dependencies:
+ prosemirror-model "^1.0.0"
+ prosemirror-state "^1.0.0"
+ prosemirror-transform "^1.7.3"
+
+prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1, prosemirror-state@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz#94aecf3ffd54ec37e87aa7179d13508da181a080"
+ integrity sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==
+ dependencies:
+ prosemirror-model "^1.0.0"
+ prosemirror-transform "^1.0.0"
+ prosemirror-view "^1.27.0"
+
+prosemirror-tables@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.4.0.tgz#59c3dc241e03fc4ba8c093995b130d2980f0ffdc"
+ integrity sha512-fxryZZkQG12fSCNuZDrYx6Xvo2rLYZTbKLRd8rglOPgNJGMKIS8uvTt6gGC38m7UCu/ENnXIP9pEz5uDaPc+cA==
+ 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"
+
+prosemirror-trailing-node@^2.0.9:
+ version "2.0.9"
+ resolved "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.9.tgz#a087e6d1372e888cd3e57c977507b6b85dc658e4"
+ integrity sha512-YvyIn3/UaLFlFKrlJB6cObvUhmwFNZVhy1Q8OpW/avoTbD/Y7H5EcjK4AZFKhmuS6/N6WkGgt7gWtBWDnmFvHg==
+ dependencies:
+ "@remirror/core-constants" "^2.0.2"
+ escape-string-regexp "^4.0.0"
+
+prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.7.3, prosemirror-transform@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.9.0.tgz#81fd1fbd887929a95369e6dd3d240c23c19313f8"
+ integrity sha512-5UXkr1LIRx3jmpXXNKDhv8OyAOeLTGuXNwdVfg8x27uASna/wQkr9p6fD3eupGOi4PLJfbezxTyi/7fSJypXHg==
+ dependencies:
+ prosemirror-model "^1.21.0"
+
+prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.33.9:
+ version "1.33.9"
+ resolved "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.33.9.tgz#0ed61ae42405cfc9799bde4db86badbb1ad99b08"
+ integrity sha512-xV1A0Vz9cIcEnwmMhKKFAOkfIp8XmJRnaZoPqNXrPS7EK5n11Ov8V76KhR0RsfQd/SIzmWY+bg+M44A2Lx/Nnw==
+ dependencies:
+ prosemirror-model "^1.20.0"
+ prosemirror-state "^1.0.0"
+ prosemirror-transform "^1.1.0"
+
proto-list@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@@ -7447,6 +7913,11 @@ pump@^3.0.0:
end-of-stream "^1.1.0"
once "^1.3.1"
+punycode.js@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
+ integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
+
punycode@^2.1.0, punycode@^2.1.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
@@ -7737,6 +8208,11 @@ rollup@^3.27.1:
optionalDependencies:
fsevents "~2.3.2"
+rope-sequence@^1.3.0:
+ version "1.3.4"
+ resolved "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425"
+ integrity sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==
+
run-applescript@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-5.0.0.tgz#e11e1c932e055d5c6b40d98374e0268d9b11899c"
@@ -8423,6 +8899,23 @@ through@^2.3.6, through@^2.3.8:
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
+tippy.js@^6.3.7:
+ version "6.3.7"
+ resolved "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"
+ integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==
+ dependencies:
+ "@popperjs/core" "^2.9.0"
+
+tiptap-markdown@^0.8.10:
+ version "0.8.10"
+ resolved "https://registry.npmjs.org/tiptap-markdown/-/tiptap-markdown-0.8.10.tgz#864a54befc17b25e7f475ff6072de3d49814f09b"
+ integrity sha512-iDVkR2BjAqkTDtFX0h94yVvE2AihCXlF0Q7RIXSJPRSR5I0PA1TMuAg6FHFpmqTn4tPxJ0by0CK7PUMlnFLGEQ==
+ dependencies:
+ "@types/markdown-it" "^13.0.7"
+ markdown-it "^14.1.0"
+ markdown-it-task-lists "^2.1.1"
+ prosemirror-markdown "^1.11.1"
+
titleize@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/titleize/-/titleize-3.0.0.tgz#71c12eb7fdd2558aa8a44b0be83b8a76694acd53"
@@ -8651,6 +9144,11 @@ typescript@^5.2.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
+uc.micro@^2.0.0, uc.micro@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee"
+ integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
+
uglify-js@^3.1.4, uglify-js@^3.5.1:
version "3.17.4"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c"
@@ -8889,6 +9387,11 @@ vue@^3.3.8:
"@vue/server-renderer" "3.3.11"
"@vue/shared" "3.3.11"
+w3c-keyname@^2.2.0:
+ version "2.2.8"
+ resolved "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
+ integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
+
watchpack@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
From af2388071eb3c629db487ef48e7b9230f916b0f4 Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Fri, 9 Aug 2024 11:15:15 +0200
Subject: [PATCH 02/31] Fix markdown content recovery
---
confiture-web-app/src/components/ui/Tiptap.vue | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/confiture-web-app/src/components/ui/Tiptap.vue b/confiture-web-app/src/components/ui/Tiptap.vue
index a9c08a6c..ca5909c4 100644
--- a/confiture-web-app/src/components/ui/Tiptap.vue
+++ b/confiture-web-app/src/components/ui/Tiptap.vue
@@ -30,16 +30,14 @@ lowlight.register("ts", ts);
const props = defineProps(["content"]);
const emit = defineEmits(["update:content"]);
-//JSON.stringify(document.querySelector(".tiptap").editor.getJSON())
function getContent() {
- let jsonContent;
+ let jsonContent = props.content;
try {
jsonContent = JSON.parse(props.content);
- } catch (e) {
- return "";
+ } finally {
+ return jsonContent;
}
- return jsonContent;
}
const editor = useEditor({
From ffb60467f7f8f50d8bf2f46621ace7c7b1ce6d42 Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Fri, 16 Aug 2024 19:48:01 +0200
Subject: [PATCH 03/31] Set default code block language to html
---
confiture-web-app/src/components/ui/Tiptap.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/confiture-web-app/src/components/ui/Tiptap.vue b/confiture-web-app/src/components/ui/Tiptap.vue
index ca5909c4..6a4bef9b 100644
--- a/confiture-web-app/src/components/ui/Tiptap.vue
+++ b/confiture-web-app/src/components/ui/Tiptap.vue
@@ -43,7 +43,7 @@ function getContent() {
const editor = useEditor({
content: getContent(),
extensions: [
- CodeBlockLowlight.configure({ lowlight }),
+ CodeBlockLowlight.configure({ lowlight, defaultLanguage: "html" }),
Highlight,
Heading.configure({
levels: [2, 3, 4, 5, 6]
From 4becae3662a9f32d09e3988dcec1b66465bcdbd0 Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Fri, 16 Aug 2024 20:25:13 +0200
Subject: [PATCH 04/31] Fix eslint imports
---
confiture-web-app/src/components/ui/Tiptap.vue | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/confiture-web-app/src/components/ui/Tiptap.vue b/confiture-web-app/src/components/ui/Tiptap.vue
index 6a4bef9b..ed15a1d5 100644
--- a/confiture-web-app/src/components/ui/Tiptap.vue
+++ b/confiture-web-app/src/components/ui/Tiptap.vue
@@ -6,18 +6,15 @@ import Link from "@tiptap/extension-link";
import TaskItem from "@tiptap/extension-task-item";
import TaskList from "@tiptap/extension-task-list";
import Typography from "@tiptap/extension-typography";
-import { useEditor, EditorContent } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
-
+import { EditorContent, useEditor } from "@tiptap/vue-3";
import css from "highlight.js/lib/languages/css";
import js from "highlight.js/lib/languages/javascript";
import ts from "highlight.js/lib/languages/typescript";
import html from "highlight.js/lib/languages/xml";
-
-import { Markdown } from "tiptap-markdown";
-
// load common languages
import { common, createLowlight } from "lowlight";
+import { Markdown } from "tiptap-markdown";
// create a lowlight instance
const lowlight = createLowlight(common);
From 8bfbe3568620f5126fc5b399420a8c313de3c39b Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Fri, 16 Aug 2024 20:26:27 +0200
Subject: [PATCH 05/31] Fix eslint finally
(and possibly importing JSON content?)
---
confiture-web-app/src/components/ui/Tiptap.vue | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/confiture-web-app/src/components/ui/Tiptap.vue b/confiture-web-app/src/components/ui/Tiptap.vue
index ed15a1d5..63293483 100644
--- a/confiture-web-app/src/components/ui/Tiptap.vue
+++ b/confiture-web-app/src/components/ui/Tiptap.vue
@@ -29,12 +29,14 @@ const props = defineProps(["content"]);
const emit = defineEmits(["update:content"]);
function getContent() {
- let jsonContent = props.content;
+ let jsonContent;
try {
jsonContent = JSON.parse(props.content);
- } finally {
- return jsonContent;
+ } catch {
+ jsonContent = props.content;
}
+
+ return jsonContent;
}
const editor = useEditor({
From ba218d0c4327a6e8f5612add5bcaed728a8b3308 Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Sat, 17 Aug 2024 22:34:35 +0200
Subject: [PATCH 06/31] Fix tiptap code blocks style
So you can see it in dark mode
---
confiture-web-app/src/styles/main.css | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/confiture-web-app/src/styles/main.css b/confiture-web-app/src/styles/main.css
index 5b5f2dd8..5b75ff74 100644
--- a/confiture-web-app/src/styles/main.css
+++ b/confiture-web-app/src/styles/main.css
@@ -75,12 +75,14 @@ from DSFR links with `target="_blank"` */
padding: 0.75rem;
}
.tiptap code {
- padding: 0.2em 0.4em;
font-size: 85%;
}
+.tiptap :not(pre) code {
+ padding: 0.2em 0.4em;
+}
.tiptap pre,
.tiptap code {
- background-color: var(--background-alt-grey);
+ background-color: var(--background-contrast-overlap-grey);
border-radius: 0.25rem;
}
.tiptap blockquote:before {
From df48b9621ec48bf7b3114288f55a893b935b4e32 Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Mon, 26 Aug 2024 18:05:01 +0200
Subject: [PATCH 07/31] Fix eslint warning (type)
Prop 'content' should define at least its type
---
confiture-web-app/src/components/ui/Tiptap.vue | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/confiture-web-app/src/components/ui/Tiptap.vue b/confiture-web-app/src/components/ui/Tiptap.vue
index 63293483..e0f36448 100644
--- a/confiture-web-app/src/components/ui/Tiptap.vue
+++ b/confiture-web-app/src/components/ui/Tiptap.vue
@@ -25,7 +25,9 @@ lowlight.register("css", css);
lowlight.register("js", js);
lowlight.register("ts", ts);
-const props = defineProps(["content"]);
+const props = defineProps<{
+ content: string;
+}>();
const emit = defineEmits(["update:content"]);
function getContent() {
From c8d12625206d1843100899214d2f99b62fa0139b Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Sun, 8 Sep 2024 11:11:13 +0200
Subject: [PATCH 08/31] Factorize code in AuditService
Use uploadFileToStorage within saveExampleImage and saveNotesFile
Fix dark background on notes thumbnails
---
.../src/audits/audit.service.ts | 89 +++++++++----------
1 file changed, 43 insertions(+), 46 deletions(-)
diff --git a/confiture-rest-api/src/audits/audit.service.ts b/confiture-rest-api/src/audits/audit.service.ts
index f613c9e8..7acc7208 100644
--- a/confiture-rest-api/src/audits/audit.service.ts
+++ b/confiture-rest-api/src/audits/audit.service.ts
@@ -420,28 +420,11 @@ export class AuditService {
criterium: number,
file: Express.Multer.File
) {
- const randomPrefix = nanoid();
-
- const key = `audits/${editUniqueId}/${randomPrefix}/${file.originalname}`;
-
- const thumbnailKey = `audits/${editUniqueId}/${randomPrefix}/thumbnail_${file.originalname}`;
-
- const thumbnailBuffer = await sharp(file.buffer)
- .jpeg({
- mozjpeg: true
- })
- .flatten({ background: { r: 255, g: 255, b: 255, alpha: 0 } })
- .resize(200, 200, { fit: "inside" })
- .toBuffer();
-
- await Promise.all([
- this.fileStorageService.uploadFile(file.buffer, file.mimetype, key),
- this.fileStorageService.uploadFile(
- thumbnailBuffer,
- "image/jpeg",
- thumbnailKey
- )
- ]);
+ const { key, thumbnailKey } = await this.uploadFileToStorage(
+ editUniqueId,
+ file,
+ { createThumbnail: display === FileDisplay.ATTACHMENT }
+ );
const storedFile = await this.prisma.storedFile.create({
data: {
@@ -501,21 +484,53 @@ export class AuditService {
}
async saveNotesFile(editUniqueId: string, file: Express.Multer.File) {
+ const { key, thumbnailKey } = await this.uploadFileToStorage(
+ editUniqueId,
+ file,
+ { createThumbnail: display === FileDisplay.ATTACHMENT }
+ );
+
+ const storedFile = await this.prisma.auditFile.create({
+ data: {
+ audit: {
+ connect: {
+ editUniqueId
+ }
+ },
+
+ key,
+ originalFilename: file.originalname,
+ mimetype: file.mimetype,
+ size: file.size,
+
+ thumbnailKey,
+ }
+ });
+
+ return storedFile;
+ }
+
+ async uploadFileToStorage(
+ uniqueId: string,
+ file: Express.Multer.File,
+ options?: { createThumbnail: boolean }
+ ): Promise<{ key: string; thumbnailKey?: string }> {
const randomPrefix = nanoid();
- const key = `audits/${editUniqueId}/${randomPrefix}/${file.originalname}`;
+ const key: string = `audits/${uniqueId}/${randomPrefix}/${file.originalname}`;
- let thumbnailKey;
+ let thumbnailKey: string;
- if (file.mimetype.startsWith("image")) {
+ if (file.mimetype.startsWith("image") && options.createThumbnail) {
// If it's an image, create a thumbnail and upload it
- thumbnailKey = `audits/${editUniqueId}/${randomPrefix}/thumbnail_${file.originalname}`;
+ thumbnailKey = `audits/${uniqueId}/${randomPrefix}/thumbnail_${file.originalname}`;
const thumbnailBuffer = await sharp(file.buffer)
- .resize(200, 200, { fit: "inside" })
.jpeg({
mozjpeg: true
})
+ .flatten({ background: { r: 255, g: 255, b: 255, alpha: 0 } })
+ .resize(200, 200, { fit: "inside" })
.toBuffer();
await Promise.all([
@@ -529,25 +544,7 @@ export class AuditService {
} else {
await this.fileStorageService.uploadFile(file.buffer, file.mimetype, key);
}
-
- const storedFile = await this.prisma.auditFile.create({
- data: {
- audit: {
- connect: {
- editUniqueId
- }
- },
-
- key,
- originalFilename: file.originalname,
- mimetype: file.mimetype,
- size: file.size,
-
- thumbnailKey
- }
- });
-
- return storedFile;
+ return { key, thumbnailKey };
}
/**
From beeef1ba39405310ce2ac4de2b9e0b287a030680 Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Mon, 9 Sep 2024 11:06:34 +0200
Subject: [PATCH 09/31] Add display field for uploads
(ATTACHMENT or EDITOR)
---
.../migration.sql | 8 ++++++++
confiture-rest-api/prisma/schema.prisma | 13 +++++++++++-
.../src/audits/audit.service.ts | 20 ++++++++++++++-----
.../src/audits/audits.controller.ts | 9 ++++++---
.../src/audits/dto/audit-report.dto.ts | 18 +++++++++++++----
.../src/audits/dto/notes-file.dto.ts | 9 +++++++++
.../src/audits/dto/upload-image.dto.ts | 3 +++
.../src/components/audit/NotesModal.vue | 9 +++++++--
confiture-web-app/src/store/audit.ts | 10 +++++++++-
confiture-web-app/src/store/results.ts | 9 +++++++--
confiture-web-app/src/types/types.ts | 6 ++++++
11 files changed, 96 insertions(+), 18 deletions(-)
create mode 100644 confiture-rest-api/prisma/migrations/20240906161141_add_display_field_on_files_attachment_or_editor/migration.sql
create mode 100644 confiture-rest-api/src/audits/dto/notes-file.dto.ts
diff --git a/confiture-rest-api/prisma/migrations/20240906161141_add_display_field_on_files_attachment_or_editor/migration.sql b/confiture-rest-api/prisma/migrations/20240906161141_add_display_field_on_files_attachment_or_editor/migration.sql
new file mode 100644
index 00000000..503297a7
--- /dev/null
+++ b/confiture-rest-api/prisma/migrations/20240906161141_add_display_field_on_files_attachment_or_editor/migration.sql
@@ -0,0 +1,8 @@
+-- CreateEnum
+CREATE TYPE "FileDisplay" AS ENUM ('EDITOR', 'ATTACHMENT');
+
+-- AlterTable
+ALTER TABLE "AuditFile" ADD COLUMN "display" "FileDisplay" NOT NULL DEFAULT 'ATTACHMENT';
+
+-- AlterTable
+ALTER TABLE "StoredFile" ADD COLUMN "display" "FileDisplay" NOT NULL DEFAULT 'ATTACHMENT';
diff --git a/confiture-rest-api/prisma/schema.prisma b/confiture-rest-api/prisma/schema.prisma
index 3c1a51b2..3da624b4 100644
--- a/confiture-rest-api/prisma/schema.prisma
+++ b/confiture-rest-api/prisma/schema.prisma
@@ -169,6 +169,11 @@ model AuditTrace {
Audit Audit?
}
+enum FileDisplay {
+ EDITOR
+ ATTACHMENT
+}
+
model StoredFile {
id Int @id @default(autoincrement())
originalFilename String
@@ -184,6 +189,9 @@ model StoredFile {
key String
thumbnailKey String
+ // Inside TipTap editor (EDITOR) or added as an attachment (ATTACHMENT)?
+ display FileDisplay @default(ATTACHMENT)
+
criterionResult CriterionResult? @relation(fields: [criterionResultId], references: [id], onDelete: Cascade, onUpdate: Cascade)
criterionResultId Int?
}
@@ -203,7 +211,10 @@ model AuditFile {
key String
thumbnailKey String?
- audit Audit? @relation(fields: [auditUniqueId], references: [editUniqueId], onDelete: Cascade)
+ audit Audit? @relation(fields: [auditUniqueId], references: [editUniqueId], onDelete: Cascade)
+ // Inside TipTap editor (EDITOR) or added as an attachment (ATTACHMENT)?
+ display FileDisplay @default(ATTACHMENT)
+
auditUniqueId String?
}
diff --git a/confiture-rest-api/src/audits/audit.service.ts b/confiture-rest-api/src/audits/audit.service.ts
index 7acc7208..61d5e602 100644
--- a/confiture-rest-api/src/audits/audit.service.ts
+++ b/confiture-rest-api/src/audits/audit.service.ts
@@ -4,6 +4,7 @@ import {
CriterionResult,
CriterionResultStatus,
CriterionResultUserImpact,
+ FileDisplay,
Prisma,
StoredFile
} from "@prisma/client";
@@ -418,7 +419,8 @@ export class AuditService {
pageId: number,
topic: number,
criterium: number,
- file: Express.Multer.File
+ file: Express.Multer.File,
+ display?: FileDisplay
) {
const { key, thumbnailKey } = await this.uploadFileToStorage(
editUniqueId,
@@ -442,7 +444,8 @@ export class AuditService {
originalFilename: file.originalname,
mimetype: file.mimetype,
size: file.size,
- thumbnailKey
+ thumbnailKey,
+ display
}
});
@@ -483,7 +486,11 @@ export class AuditService {
return true;
}
- async saveNotesFile(editUniqueId: string, file: Express.Multer.File) {
+ async saveNotesFile(
+ editUniqueId: string,
+ file: Express.Multer.File,
+ display: FileDisplay = FileDisplay.ATTACHMENT
+ ) {
const { key, thumbnailKey } = await this.uploadFileToStorage(
editUniqueId,
file,
@@ -504,6 +511,7 @@ export class AuditService {
size: file.size,
thumbnailKey,
+ display
}
});
@@ -829,7 +837,8 @@ export class AuditService {
key: file.key,
thumbnailKey: file.thumbnailKey,
size: file.size,
- mimetype: file.mimetype
+ mimetype: file.mimetype,
+ display: file.display
})),
criteriaCount: {
@@ -996,7 +1005,8 @@ export class AuditService {
exampleImages: r.exampleImages.map((img) => ({
filename: img.originalFilename,
key: img.key,
- thumbnailKey: img.thumbnailKey
+ thumbnailKey: img.thumbnailKey,
+ display: img.display
}))
}))
};
diff --git a/confiture-rest-api/src/audits/audits.controller.ts b/confiture-rest-api/src/audits/audits.controller.ts
index d2a7c92a..391737ae 100644
--- a/confiture-rest-api/src/audits/audits.controller.ts
+++ b/confiture-rest-api/src/audits/audits.controller.ts
@@ -28,6 +28,7 @@ import {
import { Audit } from "src/generated/nestjs-dto/audit.entity";
import { CriterionResult } from "src/generated/nestjs-dto/criterionResult.entity";
import { MailService } from "../mail/mail.service";
+import { NotesFileDto } from "./dto/notes-file.dto";
import { AuditExportService } from "./audit-export.service";
import { AuditService } from "./audit.service";
import { CreateAuditDto } from "./dto/create-audit.dto";
@@ -174,7 +175,8 @@ export class AuditsController {
body.pageId,
body.topic,
body.criterium,
- file
+ file,
+ body.display
);
}
@@ -191,7 +193,8 @@ export class AuditsController {
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY
})
)
- file: Express.Multer.File
+ file: Express.Multer.File,
+ @Body() body: NotesFileDto
) {
const audit = await this.auditService.getAuditWithEditUniqueId(uniqueId);
@@ -199,7 +202,7 @@ export class AuditsController {
return this.sendAuditNotFoundStatus(uniqueId);
}
- return await this.auditService.saveNotesFile(uniqueId, file);
+ return await this.auditService.saveNotesFile(uniqueId, file, body.display);
}
@Delete("/:uniqueId/results/examples/:exampleId")
diff --git a/confiture-rest-api/src/audits/dto/audit-report.dto.ts b/confiture-rest-api/src/audits/dto/audit-report.dto.ts
index 0ac3e5be..dca8d774 100644
--- a/confiture-rest-api/src/audits/dto/audit-report.dto.ts
+++ b/confiture-rest-api/src/audits/dto/audit-report.dto.ts
@@ -2,7 +2,8 @@ import { ApiProperty } from "@nestjs/swagger";
import {
AuditType,
CriterionResultStatus,
- CriterionResultUserImpact
+ CriterionResultUserImpact,
+ FileDisplay
} from "@prisma/client";
export class AuditReportDto {
@@ -190,18 +191,27 @@ class ReportCriterionResult {
}
class ExampleImage {
- /** @example "mon-image.jpg" */
+ /** @example "my-image.jpg" */
filename: string;
- /** @example "audit/xxxx/my-image.jpg" */
+ /** @example "audit/EWIsM6sYI2cC0lI7Ok2PE/3gnCTQ5ztOdEnKRraIMYG/my-image.jpg" */
key: string;
- /** @example "audit/xxxx/my-image_thumbnail.jpg" */
+ /** @example "audit/EWIsM6sYI2cC0lI7Ok2PE/3gnCTQ5ztOdEnKRraIMYG/my-image_thumbnail.jpg" */
thumbnailKey: string;
+ /** @example "ATTACHMENT" */
+ display: FileDisplay;
}
class NotesFile {
+ /** @example "screenshot_001.png" */
originalFilename: string;
+ /** @example "audits/EWIsM6sYI2cC0lI7Ok2PE/uqoOes4QqhFyKV8v0s2AQ/screenshot_001.png" */
key: string;
+ /** @example "audits/EWIsM6sYI2cC0lI7Ok2PE/uqoOes4QqhFyKV8v0s2AQ/thumbnail_screenshot_001.png" */
thumbnailKey: string;
+ /** @example 4631 */
size: number;
+ /** @example "image/png" */
mimetype: string;
+ /** @example "ATTACHMENT" */
+ display: FileDisplay;
}
diff --git a/confiture-rest-api/src/audits/dto/notes-file.dto.ts b/confiture-rest-api/src/audits/dto/notes-file.dto.ts
new file mode 100644
index 00000000..8e3bc9cb
--- /dev/null
+++ b/confiture-rest-api/src/audits/dto/notes-file.dto.ts
@@ -0,0 +1,9 @@
+import { FileDisplay } from "@prisma/client";
+import { IsIn, IsOptional, IsString } from "class-validator";
+
+export class NotesFileDto {
+ @IsOptional()
+ @IsString()
+ @IsIn(Object.values(FileDisplay))
+ display?: FileDisplay;
+}
diff --git a/confiture-rest-api/src/audits/dto/upload-image.dto.ts b/confiture-rest-api/src/audits/dto/upload-image.dto.ts
index 810f5847..5b9c9826 100644
--- a/confiture-rest-api/src/audits/dto/upload-image.dto.ts
+++ b/confiture-rest-api/src/audits/dto/upload-image.dto.ts
@@ -1,6 +1,7 @@
import { Type } from "class-transformer";
import { IsInt, IsNumber, IsPositive, Max, Min } from "class-validator";
import { IsRgaaCriterium } from "./update-results.dto";
+import { FileDisplay } from "@prisma/client";
/*
The `@Type(() => Number)` decorator is required to correctly parse strings into numbers
@@ -34,4 +35,6 @@ export class UploadImageDto {
"topic and criterium numbers must be a valid RGAA criterium combination"
})
criterium: number;
+
+ display: FileDisplay;
}
diff --git a/confiture-web-app/src/components/audit/NotesModal.vue b/confiture-web-app/src/components/audit/NotesModal.vue
index 0e7d98c8..f793c463 100644
--- a/confiture-web-app/src/components/audit/NotesModal.vue
+++ b/confiture-web-app/src/components/audit/NotesModal.vue
@@ -6,7 +6,7 @@ import { useRoute } from "vue-router";
import { useIsOffline } from "../../composables/useIsOffline";
import { FileErrorMessage } from "../../enums";
import { useAuditStore } from "../../store/audit";
-import { AuditFile, StoreName } from "../../types";
+import { AuditFile, FileDisplay, StoreName } from "../../types";
import { handleFileDeleteError, handleFileUploadError } from "../../utils";
import DsfrModal from "../ui/DsfrModal.vue";
import FileUpload from "../ui/FileUpload.vue";
@@ -40,7 +40,12 @@ const isOffline = useIsOffline();
const notes = ref(auditStore.currentAudit?.notes || "");
const uniqueId = computed(() => route.params.uniqueId as string);
-const files = computed(() => auditStore.currentAudit?.notesFiles || []);
+const files = computed(
+ () =>
+ auditStore.currentAudit?.notesFiles?.filter(
+ (e) => e.display === FileDisplay.ATTACHMENT
+ ) || []
+);
const handleNotesChange = debounce(() => emit("confirm", notes.value), 500);
diff --git a/confiture-web-app/src/store/audit.ts b/confiture-web-app/src/store/audit.ts
index 4db3a3a3..92a484f2 100644
--- a/confiture-web-app/src/store/audit.ts
+++ b/confiture-web-app/src/store/audit.ts
@@ -5,6 +5,7 @@ import {
Audit,
AuditFile,
CreateAuditRequestData,
+ FileDisplay,
UpdateAuditRequestData
} from "../types";
import { AccountAudit } from "../types/account";
@@ -126,10 +127,17 @@ export const useAuditStore = defineStore("audit", {
}
},
- async uploadAuditFile(uniqueId: string, file: File) {
+ async uploadAuditFile(
+ uniqueId: string,
+ file: File,
+ display?: FileDisplay
+ ): Promise {
const formData = new FormData();
// To handle non-ascii characters, we encode the filename here and decode it on the back
formData.set("file", file, encodeURI(file.name));
+ if (display) {
+ formData.set("display", display.toString());
+ }
this.increaseCurrentRequestCount();
const notesFile = (await ky
diff --git a/confiture-web-app/src/store/results.ts b/confiture-web-app/src/store/results.ts
index 709064eb..b52a4e7d 100644
--- a/confiture-web-app/src/store/results.ts
+++ b/confiture-web-app/src/store/results.ts
@@ -6,7 +6,8 @@ import {
AuditFile,
CriterionResultUserImpact,
CriteriumResult,
- CriteriumResultStatus
+ CriteriumResultStatus,
+ FileDisplay
} from "../types";
import { useAuditStore } from "./audit";
import { useFiltersStore } from "./filters";
@@ -343,7 +344,8 @@ export const useResultsStore = defineStore("results", {
pageId: number,
topic: number,
criterium: number,
- file: File
+ file: File,
+ display?: FileDisplay
) {
const formData = new FormData();
formData.set("pageId", pageId.toString());
@@ -351,6 +353,9 @@ export const useResultsStore = defineStore("results", {
formData.set("criterium", criterium.toString());
// To handle non-ascii characters, we encode the filename here and decode it on the back
formData.set("image", file, encodeURI(file.name));
+ if (display) {
+ formData.set("display", display.toString());
+ }
this.increaseCurrentRequestCount();
diff --git a/confiture-web-app/src/types/types.ts b/confiture-web-app/src/types/types.ts
index fc48a08d..7621b44e 100644
--- a/confiture-web-app/src/types/types.ts
+++ b/confiture-web-app/src/types/types.ts
@@ -113,6 +113,12 @@ export interface AuditFile {
key: string;
mimetype: string;
thumbnailKey: string;
+ display: FileDisplay;
+}
+
+export enum FileDisplay {
+ ATTACHMENT = "ATTACHMENT",
+ EDITOR = "EDITOR"
}
export interface CriteriumResult {
From 7a165f8b38abbe368ab0d983e6d18e954e2acd83 Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Fri, 13 Sep 2024 15:59:39 +0200
Subject: [PATCH 10/31] Tiptap: handle images drag and drop
Add a new extension: ImageUploadTiptapExtension
---
confiture-web-app/package.json | 1 +
.../src/components/audit/NotesModal.vue | 12 +-
.../src/components/ui/Tiptap.vue | 19 +-
confiture-web-app/src/store/audit.ts | 1 +
confiture-web-app/src/styles/main.css | 39 ++++
.../src/tiptap/ImageUploadTiptapExtension.ts | 166 ++++++++++++++++++
yarn.lock | 5 +
7 files changed, 236 insertions(+), 7 deletions(-)
create mode 100644 confiture-web-app/src/tiptap/ImageUploadTiptapExtension.ts
diff --git a/confiture-web-app/package.json b/confiture-web-app/package.json
index 5ddddb7f..1a8eea77 100644
--- a/confiture-web-app/package.json
+++ b/confiture-web-app/package.json
@@ -19,6 +19,7 @@
"@sentry/vue": "^7.37.2",
"@tiptap/extension-code-block-lowlight": "^2.5.9",
"@tiptap/extension-highlight": "^2.5.9",
+ "@tiptap/extension-image": "^2.6.6",
"@tiptap/extension-link": "^2.5.9",
"@tiptap/extension-task-item": "^2.5.9",
"@tiptap/extension-task-list": "^2.5.9",
diff --git a/confiture-web-app/src/components/audit/NotesModal.vue b/confiture-web-app/src/components/audit/NotesModal.vue
index f793c463..8869f352 100644
--- a/confiture-web-app/src/components/audit/NotesModal.vue
+++ b/confiture-web-app/src/components/audit/NotesModal.vue
@@ -47,7 +47,10 @@ const files = computed(
) || []
);
-const handleNotesChange = debounce(() => emit("confirm", notes.value), 500);
+const handleNotesChange = debounce((notesContent: string) => {
+ notes.value = notesContent;
+ emit("confirm", notes.value);
+}, 500);
function handleUploadFile(file: File) {
auditStore
@@ -116,8 +119,7 @@ function handleDeleteFile(file: AuditFile) {
rows="10"
:disabled="isOffline"
aria-describedby="notes-markdown"
- @input="handleNotesChange"
- @update:content="($content) => (notes = $content)"
+ @update:content="handleNotesChange"
/>
();
const emit = defineEmits(["update:content"]);
+const uniqueId = computed(() => route.params.uniqueId as string);
+
function getContent() {
let jsonContent;
try {
@@ -57,6 +68,10 @@ const editor = useEditor({
}),
TaskItem,
TaskList,
+ ImageExtension.configure({ inline: false }),
+ ImageUploadTiptapExtension.configure({
+ uniqueId: uniqueId.value
+ }),
Typography.configure({
openDoubleQuote: "« ",
closeDoubleQuote: " »"
@@ -66,7 +81,7 @@ const editor = useEditor({
// The content has changed.
emit("update:content", JSON.stringify(editor.getJSON()));
}
-});
+}) as ShallowRef;
diff --git a/confiture-web-app/src/store/audit.ts b/confiture-web-app/src/store/audit.ts
index 92a484f2..6cff3bb4 100644
--- a/confiture-web-app/src/store/audit.ts
+++ b/confiture-web-app/src/store/audit.ts
@@ -151,6 +151,7 @@ export const useAuditStore = defineStore("audit", {
const notesFiles = this.entities[uniqueId].notesFiles || [];
notesFiles.push(notesFile);
+ return notesFile;
},
async deleteAuditFile(uniqueId: string, fileId: number) {
diff --git a/confiture-web-app/src/styles/main.css b/confiture-web-app/src/styles/main.css
index 5b75ff74..4a63afc0 100644
--- a/confiture-web-app/src/styles/main.css
+++ b/confiture-web-app/src/styles/main.css
@@ -66,11 +66,40 @@ from DSFR links with `target="_blank"` */
}
.tiptap {
+ background-color: var(--background-default-grey);
padding: 1rem;
border: 1px solid var(--border-default-grey);
min-height: 10rem;
+ height: 70vh;
+ height: 70dvh;
+ max-height: 80vh;
+ max-height: 80dvh;
+ overflow-y: auto;
}
+.tiptap img {
+ cursor: pointer;
+ max-width: 100%;
+}
+
+.tiptap p {
+ vertical-align: middle;
+}
+
+/* Testing some different UI for TipTap editor: */
+/* .tiptap[contenteditable]:not([contenteditable="false"]),
+.tiptap[tabindex] {
+ color: rgba(10, 118, 246, 0);
+ transition: outline-color 0.3s ease-in;
+}
+
+.tiptap[contenteditable]:not([contenteditable="false"]):focus,
+.tiptap[tabindex]:focus {
+ outline-color: rgba(10, 118, 246, 0.2);
+ outline-offset: 2px;
+ outline-width: 500px;
+} */
+
.tiptap pre {
padding: 0.75rem;
}
@@ -146,6 +175,16 @@ from DSFR links with `target="_blank"` */
margin: 0;
}
+.ProseMirror-selectednode {
+ outline-style: dotted;
+ outline-width: 2px;
+ outline-color: var(--dsfr-outline);
+}
+
+.ProseMirror-widget {
+ opacity: 0.5;
+}
+
/* File upload styling */
/* input[type="file" i]::-webkit-file-upload-button {
background-color: transparent;
diff --git a/confiture-web-app/src/tiptap/ImageUploadTiptapExtension.ts b/confiture-web-app/src/tiptap/ImageUploadTiptapExtension.ts
new file mode 100644
index 00000000..7bb89316
--- /dev/null
+++ b/confiture-web-app/src/tiptap/ImageUploadTiptapExtension.ts
@@ -0,0 +1,166 @@
+import { Extension } from "@tiptap/core";
+import { Slice } from "@tiptap/pm/model";
+import { EditorState, Plugin } from "@tiptap/pm/state";
+import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
+
+import { FileErrorMessage } from "../enums";
+import { useAuditStore } from "../store/audit";
+import { AuditFile, FileDisplay } from "../types";
+import { getUploadUrl, handleFileUploadError } from "../utils";
+
+export interface ImageUploadTiptapExtensionOptions {
+ uniqueId: string;
+}
+
+/**
+ * Placeholder: the image blob (local to browser), with 50% opacity
+ */
+const placeholderPlugin = new Plugin({
+ state: {
+ init() {
+ return DecorationSet.empty;
+ },
+ apply(tr, set) {
+ // Adjust decoration positions to changes made by the transaction
+ set = set.map(tr.mapping, tr.doc);
+ // See if the transaction adds or removes any placeholders
+ const action = tr.getMeta(placeholderPlugin);
+ if (action && action.add) {
+ const deco = Decoration.widget(
+ action.add.pos,
+ () => {
+ const phImg: HTMLImageElement = document.createElement("img");
+ phImg.setAttribute("src", action.add.blobUrl);
+ phImg.onload = () => {
+ phImg.setAttribute("width", phImg.width.toString());
+ phImg.setAttribute("height", phImg.height.toString());
+ };
+ return phImg;
+ },
+ {
+ id: action.add.id
+ }
+ );
+ set = set.add(tr.doc, [deco]);
+ } else if (action && action.remove) {
+ set = set.remove(
+ set.find(undefined, undefined, (spec) => spec.id == action.remove.id)
+ );
+ }
+ return set;
+ }
+ },
+ props: {
+ decorations(state) {
+ return this.getState(state);
+ }
+ }
+});
+
+const HandleDropPlugin = (options: ImageUploadTiptapExtensionOptions) => {
+ const { uniqueId } = options;
+ return new Plugin({
+ props: {
+ handleDrop(
+ this: Plugin,
+ view: EditorView,
+ dragEvent: DragEvent,
+ slice: Slice,
+ moved: boolean
+ ): boolean | void {
+ if (
+ !moved &&
+ dragEvent.dataTransfer &&
+ dragEvent.dataTransfer.files &&
+ dragEvent.dataTransfer.files[0]
+ ) {
+ // If dropping external files
+ const file = dragEvent.dataTransfer.files[0];
+ if (file.size < 2000000) {
+ // A fresh object to act as the ID for this upload
+ const id = {};
+
+ // Place the now uploaded image in the editor where it was dropped
+ const { tr } = view.state;
+ const coordinates = view.posAtCoords({
+ left: dragEvent.clientX,
+ top: dragEvent.clientY
+ });
+ if (!coordinates) {
+ console.log("No coordinates?!");
+ return;
+ }
+ const _URL = window.URL || window.webkitURL;
+ const blobUrl = _URL.createObjectURL(file);
+ tr.setMeta(placeholderPlugin, {
+ add: { id, blobUrl, pos: coordinates.pos }
+ });
+ view.dispatch(tr);
+
+ uploadAndReplacePlaceHolder(view, file, id);
+ } else {
+ //FIXME: use a notification
+ window.alert(FileErrorMessage.UPLOAD_SIZE);
+ }
+
+ // handled
+ return true;
+ }
+ }
+ }
+ });
+
+ function uploadAndReplacePlaceHolder(view: EditorView, file: File, id: any) {
+ const auditStore = useAuditStore();
+ auditStore.uploadAuditFile(uniqueId, file, FileDisplay.EDITOR).then(
+ (response: AuditFile) => {
+ const pos = findPlaceholder(view.state, id);
+ // If the content around the placeholder has been deleted, drop
+ // the image
+ if (pos === undefined) {
+ //TODO remove image from server
+ return;
+ }
+ // Otherwise, insert it at the placeholder's position, and remove
+ // the placeholder
+ view.dispatch(
+ view.state.tr
+ .replaceWith(
+ pos,
+ pos,
+ //FIXME: add `width` and `height` to avoid layout shift
+ view.state.schema.nodes.image.create({
+ src: getUploadUrl(response.key)
+ })
+ )
+ .setMeta(placeholderPlugin, { remove: { id } })
+ );
+ },
+ async (reason: any) => {
+ // On failure, just clean up the placeholder
+ view.dispatch(
+ view.state.tr.setMeta(placeholderPlugin, { remove: { id } })
+ );
+ //FIXME: use a notification
+ window.alert(await handleFileUploadError(reason));
+ }
+ );
+ }
+
+ function findPlaceholder(state: EditorState, id: any) {
+ const decos = placeholderPlugin.getState(state);
+ const found = decos?.find(undefined, undefined, (spec) => spec.id == id);
+ return found?.[0].from;
+ }
+};
+
+export const ImageUploadTiptapExtension =
+ Extension.create({
+ name: "imageUpload",
+ addProseMirrorPlugins() {
+ return [
+ HandleDropPlugin({ uniqueId: this.options.uniqueId }),
+ placeholderPlugin
+ ];
+ }
+ });
diff --git a/yarn.lock b/yarn.lock
index 072e088e..ba42e186 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2094,6 +2094,11 @@
resolved "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.5.9.tgz#9f91a17b80700670e53e241fcee40365c57aa994"
integrity sha512-/ES5NdxCndBmZAgIXSpCJH8YzENcpxR0S8w34coSWyv+iW0Sq7rW/mksQw8ZIVsj8a7ntpoY5OoRFpSlqcvyGw==
+"@tiptap/extension-image@^2.6.6":
+ version "2.6.6"
+ resolved "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.6.6.tgz#d3c2b4c6234dc8d475a5ee534447605c4e1408d5"
+ integrity sha512-dwJKvoqsr72B4tcTH8hXhfBJzUMs/jXUEE9MnfzYnSXf+CYALLjF8r/IkGYbxce62GP/bMDoj8BgpF8saeHtqA==
+
"@tiptap/extension-italic@^2.5.9":
version "2.5.9"
resolved "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.5.9.tgz#8ea0e19e650f0f1d6fc30425ec28291511143dda"
From bf48bc21b4dff8892ff4a8fd9105bedca80ed16a Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Wed, 9 Oct 2024 14:29:16 +0200
Subject: [PATCH 11/31] refactor(tiptap): simplify code
---
confiture-web-app/src/components/ui/Tiptap.vue | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/confiture-web-app/src/components/ui/Tiptap.vue b/confiture-web-app/src/components/ui/Tiptap.vue
index ccc6c953..eedbe8a8 100644
--- a/confiture-web-app/src/components/ui/Tiptap.vue
+++ b/confiture-web-app/src/components/ui/Tiptap.vue
@@ -1,6 +1,5 @@
-
+
+
+ Éditeur de texte riche, vous pouvez utiliser le format Markdown ou bien
+ utiliser les raccourcis clavier.
+
+
+
From 8a946b1b852b0e5903b7041fd672dcf00252729d Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Wed, 23 Oct 2024 19:04:30 +0200
Subject: [PATCH 19/31] Image upload: add error message
FETCH_ERROR is used, for the moment, when dragging and dropping an external image and a CORS issue happens
---
confiture-web-app/src/enums.ts | 2 +-
confiture-web-app/src/utils.ts | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/confiture-web-app/src/enums.ts b/confiture-web-app/src/enums.ts
index 9a3d856c..d464b2d9 100644
--- a/confiture-web-app/src/enums.ts
+++ b/confiture-web-app/src/enums.ts
@@ -24,11 +24,11 @@ export enum Browsers {
EDGE = "Microsoft Edge"
}
-/* UPLOAD_FORMAT should never happen… */
export enum FileErrorMessage {
UPLOAD_SIZE = "Votre fichier dépasse la limite de 2 Mo. Veuillez choisir un fichier plus léger.",
UPLOAD_FORMAT = "Format de fichier non supporté.",
UPLOAD_FORMAT_EXAMPLE = "Format de fichier non supporté. Veuillez choisir un fichier jpg, jpeg ou png.",
+ FETCH_ERROR = "Impossible de récupérer le fichier distant",
UPLOAD_UNKNOWN = "Une erreur inconnue empêche le téléchargement du fichier. Veuillez réessayer.",
DELETE_UNKNOWN = "Une erreur inconnue empêche la suppression du fichier. Veuillez réessayer."
}
diff --git a/confiture-web-app/src/utils.ts b/confiture-web-app/src/utils.ts
index 31c3bdcb..640cbdee 100644
--- a/confiture-web-app/src/utils.ts
+++ b/confiture-web-app/src/utils.ts
@@ -253,6 +253,7 @@ export async function handleFileUploadError(
}
// Unprocessable Entity
+ /* UPLOAD_FORMAT should never happen… */
if (error.response.status === 422) {
const body = await error.response.json();
From 0e6190cae633a043f07d4e90b3c2ac7a869c30dd Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Thu, 24 Oct 2024 16:10:23 +0200
Subject: [PATCH 20/31] Minor typo: Tiptap instead of TipTap
---
confiture-web-app/src/styles/main.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/confiture-web-app/src/styles/main.css b/confiture-web-app/src/styles/main.css
index 848405bb..9d739d39 100644
--- a/confiture-web-app/src/styles/main.css
+++ b/confiture-web-app/src/styles/main.css
@@ -97,7 +97,7 @@ from DSFR links with `target="_blank"` */
vertical-align: middle;
}
-/* Testing some different UI for TipTap editor: */
+/* Testing some different UI for Tiptap editor: */
/* .tiptap[contenteditable]:not([contenteditable="false"]),
.tiptap[tabindex] {
color: rgba(10, 118, 246, 0);
From b571d241fa4e13a24b959dff979411d8f51d3c97 Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Thu, 24 Oct 2024 16:17:26 +0200
Subject: [PATCH 21/31] Tiptap: improve drag and drop + upload
Handle multiple files, external URL, data URL
---
.../src/tiptap/ImageUploadTiptapExtension.ts | 240 +++++++++++++-----
1 file changed, 180 insertions(+), 60 deletions(-)
diff --git a/confiture-web-app/src/tiptap/ImageUploadTiptapExtension.ts b/confiture-web-app/src/tiptap/ImageUploadTiptapExtension.ts
index 4c185724..4ecc18e4 100644
--- a/confiture-web-app/src/tiptap/ImageUploadTiptapExtension.ts
+++ b/confiture-web-app/src/tiptap/ImageUploadTiptapExtension.ts
@@ -13,9 +13,12 @@ export interface ImageUploadTiptapExtensionOptions {
}
/**
- * Placeholder: the image blob (local to browser), with 50% opacity
+ * Placeholder plugin
+ *
+ * The placeholder is an image blob (local to browser), with 50% opacity.
+ * Within ProseMirror it’s a Decoration.
*/
-const placeholderPlugin = new Plugin({
+const PlaceholderPlugin = new Plugin({
state: {
init() {
return DecorationSet.empty;
@@ -24,17 +27,17 @@ const placeholderPlugin = new Plugin({
// Adjust decoration positions to changes made by the transaction
set = set.map(tr.mapping, tr.doc);
// See if the transaction adds or removes any placeholders
- const action = tr.getMeta(placeholderPlugin);
+ const action = tr.getMeta(PlaceholderPlugin);
if (action && action.add) {
const deco = Decoration.widget(
action.add.pos,
() => {
- return action.add.blobElement;
+ return action.add.element;
},
{
id: action.add.id,
- width: action.add.blobElement.width.toString(),
- height: action.add.blobElement.height.toString()
+ width: action.add.element.width.toString(),
+ height: action.add.element.height.toString()
}
);
set = set.add(tr.doc, [deco]);
@@ -53,10 +56,28 @@ const placeholderPlugin = new Plugin({
}
});
+/**
+ * HandleDrop plugin
+ *
+ * Handles drag and drop inside editor:
+ * - multiple image files
+ * - dataURL
+ * - external image from URL (⚠️ CORS)
+ */
const HandleDropPlugin = (options: ImageUploadTiptapExtensionOptions) => {
const { uniqueId } = options;
return new Plugin({
props: {
+ /**
+ * handleDrop: called when something is dropped on the editor.
+ *
+ * @param {Plugin} this
+ * @param {EditorView} view
+ * @param {DragEvent} dragEvent
+ * @param {Slice} slice
+ * @param {boolean} moved
+ * @returns true if event is handled, otherwise false
+ */
handleDrop(
this: Plugin,
view: EditorView,
@@ -68,62 +89,120 @@ const HandleDropPlugin = (options: ImageUploadTiptapExtensionOptions) => {
return false;
}
if (dragEvent.dataTransfer.files.length === 0) {
- // TODO external URL?
- return false;
+ // Handle a single URL (ex: when an external image is dragged from another window)
+ // TODO multiple URLs
+ // See: "text/uri-list" and
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types
+ const url = dragEvent.dataTransfer.getData("URL");
+ if (url) {
+ createFileFromImageUrl(url).then((file) => {
+ if (file) {
+ handleFileDrop(view, dragEvent, file);
+ }
+ });
+ }
+ return true;
}
+ // Handle multiple files
+ // FIXME: sometimes placeholders order differs from final images order
const files: FileList = dragEvent.dataTransfer.files;
for (let i = 0, il = files.length, file: File; i < il; i++) {
file = files.item(i)!;
-
- // If dropping external files
- if (file.size < 2000000) {
- // A fresh object to act as the ID for this upload
- const id = {};
-
- // Place the now uploaded image in the editor where it was dropped
- const { tr } = view.state;
- const position = view.posAtCoords({
- left: dragEvent.clientX,
- top: dragEvent.clientY
- });
- if (!position) {
- console.warn("No position?!");
- return false;
- }
-
- // If image is being dropped *inside* a node,
- // move it to next "gap", between 2 nodes
- let pos = position.pos;
- if (isDropCursorVertical(view, pos)) {
- pos = view.state.doc.resolve(position.pos).end() + 1;
- }
-
- const _URL = window.URL || window.webkitURL;
- const blobUrl = _URL.createObjectURL(file);
- const blobElement: HTMLImageElement = document.createElement("img");
- blobElement.setAttribute("src", blobUrl);
- blobElement.onload = () => {
- tr.setMeta(placeholderPlugin, {
- add: { id, blobElement, pos }
- });
- view.dispatch(tr);
-
- uploadAndReplacePlaceHolder(view, file, id);
- };
- } else {
- //FIXME: use a notification
- window.alert(FileErrorMessage.UPLOAD_SIZE);
+ if (!handleFileDrop(view, dragEvent, file)) {
+ return false;
}
}
- // handled
return true;
}
}
});
- function uploadAndReplacePlaceHolder(view: EditorView, file: File, id: any) {
+ /**
+ * Handles file drop
+ *
+ * @param {EditorView} view
+ * @param {DragEvent} dragEvent
+ * @param {File} file
+ * @returns {boolean} true or false if file is not dropped inside of the editor (should not happen)
+ */
+ function handleFileDrop(
+ view: EditorView,
+ dragEvent: DragEvent,
+ file: File
+ ): boolean {
+ const position = view.posAtCoords({
+ left: dragEvent.clientX,
+ top: dragEvent.clientY
+ });
+ if (!position) {
+ console.warn(
+ `the given coordinates aren't inside of the editor: {${dragEvent.clientX}, ${dragEvent.clientY}}`
+ );
+ return false;
+ }
+
+ if (file.size > 2000000) {
+ //FIXME: use a notification
+ window.alert(FileErrorMessage.UPLOAD_SIZE);
+ return true;
+ }
+
+ // A fresh object to act as the ID for this upload
+ const id = {};
+
+ // If image is being dropped *inside* a node,
+ // move it to next "gap", between 2 nodes
+ let pos = position.pos;
+ if (isPosInsideInlineContent(view, pos)) {
+ pos = view.state.doc.resolve(position.pos).end() + 1;
+ }
+
+ const _URL = window.URL || window.webkitURL;
+ const localURL = _URL.createObjectURL(file);
+ let element: HTMLImageElement | HTMLVideoElement;
+ if (file.type.startsWith("image")) {
+ element = document.createElement("img");
+ element.onerror = () => {
+ //FIXME: use a notification
+ window.alert(FileErrorMessage.UPLOAD_FORMAT);
+ };
+ element.onload = () => {
+ URL.revokeObjectURL(element.src);
+ element.setAttribute("width", element.width.toString());
+ element.setAttribute("height", element.height.toString());
+ const { tr } = view.state;
+ tr.setMeta(PlaceholderPlugin, {
+ add: { id, element, pos }
+ });
+ view.dispatch(tr);
+
+ uploadAndReplacePlaceholder(view, file, id);
+ };
+ element.src = localURL;
+ } else if (file.type.startsWith("video")) {
+ //FIXME: Handle videos
+ // element = document.createElement("video");
+ // …
+ //FIXME: use a notification
+ window.alert(FileErrorMessage.UPLOAD_FORMAT);
+ } else {
+ //FIXME: use a notification
+ window.alert(FileErrorMessage.UPLOAD_FORMAT);
+ }
+
+ return true;
+ }
+
+ /**
+ * Uploads and then replaces the placeholder
+ *
+ * @param {EditorView} view
+ * @param {DragEvent} dragEvent
+ * @param {File} file
+ */
+ function uploadAndReplacePlaceholder(view: EditorView, file: File, id: any) {
const auditStore = useAuditStore();
auditStore.uploadAuditFile(uniqueId, file, FileDisplay.EDITOR).then(
(response: AuditFile) => {
@@ -133,7 +212,7 @@ const HandleDropPlugin = (options: ImageUploadTiptapExtensionOptions) => {
// If the content around the placeholder has been deleted, drop
// the image
if (pos === undefined) {
- //TODO remove image from server
+ // TODO remove image from server
return;
}
@@ -154,13 +233,13 @@ const HandleDropPlugin = (options: ImageUploadTiptapExtensionOptions) => {
src: imgUrl
})
)
- .setMeta(placeholderPlugin, { remove: { id } })
+ .setMeta(PlaceholderPlugin, { remove: { id } })
);
},
async (reason: any) => {
// On failure, just clean up the placeholder
view.dispatch(
- view.state.tr.setMeta(placeholderPlugin, { remove: { id } })
+ view.state.tr.setMeta(PlaceholderPlugin, { remove: { id } })
);
//FIXME: use a notification
window.alert(await handleFileUploadError(reason));
@@ -168,23 +247,63 @@ const HandleDropPlugin = (options: ImageUploadTiptapExtensionOptions) => {
);
}
+ /**
+ * Finds the given placeholder (by id) within the given editor state.
+ *
+ * @param {EditorState} state
+ * @param {any} id
+ * @returns {Decoration} the placeholder (a ProseMirror decoration)
+ */
function findPlaceholderDecoration(
state: EditorState,
id: any
): Decoration | undefined {
- const decos = placeholderPlugin.getState(state);
+ const decos = PlaceholderPlugin.getState(state);
const found = decos?.find(undefined, undefined, (spec) => spec.id == id);
return found?.[0];
}
+
+ /**
+ * Creates a File object from a given URL
+ *
+ * @param {string} url
+ * @returns {Promise} the created File or null if any error
+ */
+ function createFileFromImageUrl(url: string): Promise {
+ let mimeType: string | undefined = undefined;
+ return fetch(url)
+ .then((res: Response) => {
+ mimeType = res.headers.get("content-type") || undefined;
+ return res.arrayBuffer();
+ })
+ .then((buf: ArrayBuffer) => {
+ return new File([buf], "external", { type: mimeType });
+ })
+ .catch(() => {
+ window.alert(FileErrorMessage.FETCH_ERROR);
+ return null;
+ });
+ }
};
+/**
+ * Extension ImageUploadTiptapExtension
+ *
+ * Tiptap extension handling images “drag and drop” and upload
+ * Adds 2 custom ProseMirror plugins (@see https://tiptap.dev/docs/editor/extensions/custom-extensions/extend-existing#prosemirror-plugins-advanced):
+ * - HandleDropPlugin
+ * - PlaceholderPlugin
+ * Modifies schema: adds a disableDropCursor property to Nodes spec to control
+ * the showing of a drop cursor inside them (only shows horizontal cursors)
+ * @see https://github.com/ProseMirror/prosemirror-dropcursor
+ */
export const ImageUploadTiptapExtension =
Extension.create({
name: "imageUpload",
addProseMirrorPlugins() {
return [
HandleDropPlugin({ uniqueId: this.options.uniqueId }),
- placeholderPlugin
+ PlaceholderPlugin
];
},
extendNodeSchema() {
@@ -193,21 +312,22 @@ export const ImageUploadTiptapExtension =
view: EditorView,
position: { pos: number; inside: number }
) => {
- return isDropCursorVertical(view, position.pos);
+ return isPosInsideInlineContent(view, position.pos);
}
};
}
});
/**
- * Tells if the drop cursor is vertical (inline content)
+ * Tells if the given position is inside inline content
+ * (meaning the drop cursor would be vertical)
* @see prosemirror-dropcursor extension
*
- * @param view:EditorView
- * @param pos:number
- * @returns boolean
+ * @param {EditorView} view
+ * @param {number} pos
+ * @returns {boolean} true if position is inside inline content, otherwise false
*/
-function isDropCursorVertical(view: EditorView, pos: number): boolean {
+function isPosInsideInlineContent(view: EditorView, pos: number): boolean {
if (!pos) {
return false;
}
From d98b78e492458291f64032c47d08b8595e974bc8 Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Mon, 28 Oct 2024 20:31:59 +0100
Subject: [PATCH 22/31] Add upload/delete timeout error messages
---
confiture-web-app/src/enums.ts | 2 ++
confiture-web-app/src/utils.ts | 14 +++++++++-----
2 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/confiture-web-app/src/enums.ts b/confiture-web-app/src/enums.ts
index d464b2d9..f9d9bc9b 100644
--- a/confiture-web-app/src/enums.ts
+++ b/confiture-web-app/src/enums.ts
@@ -29,6 +29,8 @@ export enum FileErrorMessage {
UPLOAD_FORMAT = "Format de fichier non supporté.",
UPLOAD_FORMAT_EXAMPLE = "Format de fichier non supporté. Veuillez choisir un fichier jpg, jpeg ou png.",
FETCH_ERROR = "Impossible de récupérer le fichier distant",
+ UPLOAD_TIMEOUT = "Une erreur réseau empêche le téléchargement du fichier (expiration du délai d'attente). Veuillez réessayer.",
UPLOAD_UNKNOWN = "Une erreur inconnue empêche le téléchargement du fichier. Veuillez réessayer.",
+ DELETE_TIMEOUT = "Une erreur réseau empêche la suppression du fichier (expiration du délai d'attente). Veuillez réessayer.",
DELETE_UNKNOWN = "Une erreur inconnue empêche la suppression du fichier. Veuillez réessayer."
}
diff --git a/confiture-web-app/src/utils.ts b/confiture-web-app/src/utils.ts
index 640cbdee..ba0f7e8f 100644
--- a/confiture-web-app/src/utils.ts
+++ b/confiture-web-app/src/utils.ts
@@ -1,6 +1,6 @@
import { captureException, Scope } from "@sentry/vue";
import jwtDecode from "jwt-decode";
-import { HTTPError } from "ky";
+import { HTTPError, TimeoutError } from "ky";
import { noop } from "lodash-es";
import baseSlugify from "slugify";
@@ -246,7 +246,11 @@ export async function handleFileUploadError(
): Promise {
let errorType: FileErrorMessage | null = null;
if (!(error instanceof HTTPError)) {
- return null;
+ if (error instanceof TimeoutError) {
+ return FileErrorMessage.UPLOAD_TIMEOUT;
+ } else {
+ return null;
+ }
}
if (error.response.status === 413) {
errorType = FileErrorMessage.UPLOAD_SIZE;
@@ -276,9 +280,9 @@ export async function handleFileUploadError(
export async function handleFileDeleteError(
error: Error
): Promise {
- if (!(error instanceof HTTPError)) {
+ if (error instanceof TimeoutError) {
+ return FileErrorMessage.DELETE_TIMEOUT;
+ } else {
return null;
}
-
- return FileErrorMessage.DELETE_UNKNOWN;
}
From c92eddfcaf865bdd6f3888e1224ec8c69acf718c Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Mon, 28 Oct 2024 20:33:46 +0100
Subject: [PATCH 23/31] Improve error messages (avoid displaying null)
---
confiture-web-app/src/utils.ts | 60 +++++++++++++++++++---------------
1 file changed, 33 insertions(+), 27 deletions(-)
diff --git a/confiture-web-app/src/utils.ts b/confiture-web-app/src/utils.ts
index ba0f7e8f..489943ec 100644
--- a/confiture-web-app/src/utils.ts
+++ b/confiture-web-app/src/utils.ts
@@ -243,46 +243,52 @@ export function getUploadUrl(key: string): string {
export async function handleFileUploadError(
error: Error
-): Promise {
- let errorType: FileErrorMessage | null = null;
- if (!(error instanceof HTTPError)) {
- if (error instanceof TimeoutError) {
- return FileErrorMessage.UPLOAD_TIMEOUT;
- } else {
- return null;
+): Promise {
+ if (error instanceof HTTPError) {
+ let errorType: FileErrorMessage | null = null;
+ if (error.response.status === 413) {
+ errorType = FileErrorMessage.UPLOAD_SIZE;
}
- }
- if (error.response.status === 413) {
- errorType = FileErrorMessage.UPLOAD_SIZE;
- }
-
- // Unprocessable Entity
- /* UPLOAD_FORMAT should never happen… */
- if (error.response.status === 422) {
- const body = await error.response.json();
- if (body.message.includes("expected type")) {
- errorType = FileErrorMessage.UPLOAD_FORMAT;
- } else if (body.message.includes("expected size")) {
- errorType = FileErrorMessage.UPLOAD_SIZE;
+ // Unprocessable Entity
+ /* UPLOAD_FORMAT should never happen… */
+ if (error.response.status === 422) {
+ const body = await error.response.json();
+
+ if (body.message.includes("expected type")) {
+ errorType = FileErrorMessage.UPLOAD_FORMAT;
+ } else if (body.message.includes("expected size")) {
+ errorType = FileErrorMessage.UPLOAD_SIZE;
+ } else {
+ errorType = FileErrorMessage.UPLOAD_UNKNOWN;
+ captureWithPayloads(error);
+ }
} else {
errorType = FileErrorMessage.UPLOAD_UNKNOWN;
captureWithPayloads(error);
}
- } else {
- errorType = FileErrorMessage.UPLOAD_UNKNOWN;
- captureWithPayloads(error);
+
+ return errorType;
+ }
+
+ if (error instanceof TimeoutError) {
+ return FileErrorMessage.UPLOAD_TIMEOUT;
}
- return errorType;
+ console.warn(error);
+ return error.message;
}
export async function handleFileDeleteError(
error: Error
-): Promise {
+): Promise {
+ if (error instanceof HTTPError) {
+ return FileErrorMessage.DELETE_UNKNOWN;
+ }
if (error instanceof TimeoutError) {
return FileErrorMessage.DELETE_TIMEOUT;
- } else {
- return null;
}
+
+ console.warn(error);
+ return error.message;
}
From 5197c70539fda60cbe87bea32372dbe8327f91e5 Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Mon, 28 Oct 2024 20:35:19 +0100
Subject: [PATCH 24/31] Tiptap: improve drop cursor style
blue and bigger
---
confiture-web-app/src/components/ui/Tiptap.vue | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/confiture-web-app/src/components/ui/Tiptap.vue b/confiture-web-app/src/components/ui/Tiptap.vue
index 822fee10..0178871c 100644
--- a/confiture-web-app/src/components/ui/Tiptap.vue
+++ b/confiture-web-app/src/components/ui/Tiptap.vue
@@ -1,5 +1,6 @@
@@ -114,5 +138,15 @@ const editor = useEditor({
utiliser les raccourcis clavier.
+
+
diff --git a/confiture-web-app/src/enums.ts b/confiture-web-app/src/enums.ts
index f9d9bc9b..08265be9 100644
--- a/confiture-web-app/src/enums.ts
+++ b/confiture-web-app/src/enums.ts
@@ -24,6 +24,10 @@ export enum Browsers {
EDGE = "Microsoft Edge"
}
+export enum Limitations {
+ FILE_SIZE = 2000000
+}
+
export enum FileErrorMessage {
UPLOAD_SIZE = "Votre fichier dépasse la limite de 2 Mo. Veuillez choisir un fichier plus léger.",
UPLOAD_FORMAT = "Format de fichier non supporté.",
diff --git a/confiture-web-app/src/tiptap/ImageUploadTiptapExtension.ts b/confiture-web-app/src/tiptap/ImageUploadTiptapExtension.ts
index baa800e0..e6b6de3d 100644
--- a/confiture-web-app/src/tiptap/ImageUploadTiptapExtension.ts
+++ b/confiture-web-app/src/tiptap/ImageUploadTiptapExtension.ts
@@ -1,10 +1,10 @@
-import { Extension } from "@tiptap/core";
+import { Editor, Extension } from "@tiptap/core";
import { Slice } from "@tiptap/pm/model";
import { EditorState, Plugin, Selection, Transaction } from "@tiptap/pm/state";
import { canSplit } from "@tiptap/pm/transform";
import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
-import { FileErrorMessage } from "../enums";
+import { FileErrorMessage, Limitations } from "../enums";
import { useAuditStore } from "../store/audit";
import { AuditFile, FileDisplay } from "../types";
import { getUploadUrl, handleFileUploadError } from "../utils";
@@ -101,7 +101,12 @@ const HandleFileImportPlugin = (options: ImageUploadTiptapExtensionOptions) => {
return false;
}
- return handleDataTransfer(view, dragEvent.dataTransfer, position.pos);
+ return handleDataTransfer(
+ uniqueId,
+ view,
+ dragEvent.dataTransfer,
+ position.pos
+ );
},
/**
@@ -126,229 +131,265 @@ const HandleFileImportPlugin = (options: ImageUploadTiptapExtensionOptions) => {
}
const pos = view.state.selection.from;
- return handleDataTransfer(view, clipboardEvent.clipboardData, pos, {
- replaceSelection: true
- });
+ return handleDataTransfer(
+ uniqueId,
+ view,
+ clipboardEvent.clipboardData,
+ pos,
+ {
+ replaceSelection: true
+ }
+ );
}
}
});
+};
- /**
- * handleDataTransfer: called for both drop and paste.
- *
- * @param {EditorView} view
- * @param {DataTransfer} dataTransfer
- * @param {number} pos
- * @param {{replaceSelection: boolean}} options
- * @returns true if event is handled, otherwise false
- */
- function handleDataTransfer(
- view: EditorView,
- dataTransfer: DataTransfer,
- pos: number,
- options?: { replaceSelection: boolean }
- ) {
- if (dataTransfer.files.length === 0) {
- // Handle a single URL (ex: when an external image is dragged from another window)
- // TODO multiple URLs
- // See: "text/uri-list" and
- // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types
- const url = dataTransfer.getData("URL");
- if (url) {
- createFileFromImageUrl(url).then((file) => {
- if (file) {
- handleFileImport(view, pos, file, options);
- }
- });
- }
- return true;
+/**
+ * handleDataTransfer: called for both drop and paste.
+ *
+ * @param {string} uniqueId
+ * @param {EditorView} view
+ * @param {DataTransfer} dataTransfer
+ * @param {number} pos
+ * @param {{replaceSelection: boolean}} options
+ * @returns true if event is handled, otherwise false
+ */
+function handleDataTransfer(
+ uniqueId: string,
+ view: EditorView,
+ dataTransfer: DataTransfer,
+ pos: number,
+ options?: { replaceSelection: boolean }
+) {
+ if (dataTransfer.files.length === 0) {
+ // Handle a single URL (ex: when an external image is dragged from another window)
+ // TODO multiple URLs
+ // See: "text/uri-list" and
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types
+ const url = dataTransfer.getData("URL");
+ if (url) {
+ createFileFromImageUrl(url).then((file) => {
+ if (file) {
+ handleFileImport(uniqueId, view, pos, file, options);
+ }
+ });
}
+ return true;
+ }
- // Handle multiple files
- // FIXME: sometimes placeholders order differs from final images order
- const files: FileList = dataTransfer.files;
- for (let i = 0, il = files.length, file: File; i < il; i++) {
- file = files.item(i)!;
- if (!handleFileImport(view, pos, file, options)) {
- return false;
- }
+ // Handle multiple files
+ return handleFilesImport(uniqueId, view, pos, dataTransfer.files, options);
+}
+
+/**
+ * Handles multiple files import (drop or paste)
+ *
+ * @param {string} uniqueId
+ * @param {EditorView} view
+ * @param {number} pos
+ * @param {FileList} files
+ * @param {{replaceSelection: boolean}} options
+ * @returns {boolean} true or false if:
+ * - files array is empty
+ * - any file is not dropped inside of the editor (should not happen)
+ */
+function handleFilesImport(
+ uniqueId: string,
+ view: EditorView,
+ pos: number,
+ files: FileList,
+ options?: { replaceSelection: boolean }
+): boolean {
+ // FIXME: sometimes placeholders order differs from final images order
+ for (let i = 0, il = files.length, file: File; i < il; i++) {
+ file = files.item(i)!;
+ if (!handleFileImport(uniqueId, view, pos, file, options)) {
+ return false;
}
+ }
+ return true;
+}
+/**
+ * Handles file import (drop or paste)
+ *
+ * @param {string} uniqueId
+ * @param {EditorView} view
+ * @param {number} pos
+ * @param {File} file
+ * @param {{replaceSelection: boolean}} options
+ * @returns {boolean} true or false if file is not dropped inside of the editor (should not happen)
+ */
+function handleFileImport(
+ uniqueId: string,
+ view: EditorView,
+ pos: number,
+ file: File,
+ options?: { replaceSelection: boolean }
+): boolean {
+ if (file.size > Limitations.FILE_SIZE) {
+ //FIXME: use a notification
+ window.alert(FileErrorMessage.UPLOAD_SIZE);
return true;
}
- /**
- * Handles file import (drop or paste)
- *
- * @param {EditorView} view
- * @param {number} pos
- * @param {File} file
- * @param {{replaceSelection: boolean}} options
- * @returns {boolean} true or false if file is not dropped inside of the editor (should not happen)
- */
- function handleFileImport(
- view: EditorView,
- pos: number,
- file: File,
- options?: { replaceSelection: boolean }
- ): boolean {
- if (file.size > 2000000) {
- //FIXME: use a notification
- window.alert(FileErrorMessage.UPLOAD_SIZE);
- return true;
- }
-
- // A fresh object to act as the ID for this upload
- const id = {};
+ // A fresh object to act as the ID for this upload
+ const id = {};
- const _URL = window.URL || window.webkitURL;
- const localURL = _URL.createObjectURL(file);
- // const container: HTMLParagraphElement = document.createElement("p");
- let element: HTMLImageElement | HTMLVideoElement;
- if (file.type.startsWith("image")) {
- element = document.createElement("img");
- // container.appendChild(element);
- element.onerror = () => {
- //FIXME: use a notification
- window.alert(FileErrorMessage.UPLOAD_FORMAT);
- };
- element.onload = () => {
- URL.revokeObjectURL(element.src);
- element.setAttribute("width", element.width.toString());
- element.setAttribute("height", element.height.toString());
- const state: EditorState = view.state;
- const tr: Transaction = state.tr;
+ const _URL = window.URL || window.webkitURL;
+ const localURL = _URL.createObjectURL(file);
+ // const container: HTMLParagraphElement = document.createElement("p");
+ let element: HTMLImageElement | HTMLVideoElement;
+ if (file.type.startsWith("image")) {
+ element = document.createElement("img");
+ // container.appendChild(element);
+ element.onerror = () => {
+ //FIXME: use a notification
+ window.alert(FileErrorMessage.UPLOAD_FORMAT);
+ };
+ element.onload = () => {
+ URL.revokeObjectURL(element.src);
+ element.setAttribute("width", element.width.toString());
+ element.setAttribute("height", element.height.toString());
+ const state: EditorState = view.state;
+ const tr: Transaction = state.tr;
- if (options?.replaceSelection) {
- tr.deleteSelection();
+ if (options?.replaceSelection && !state.selection.empty) {
+ tr.deleteSelection();
- // Delete the paragraph if it becomes empty
- if (tr.doc.resolve(pos).parent.textContent === "") {
- tr.deleteRange(pos - 1, pos + 1);
- }
+ // Delete the paragraph if it becomes empty
+ if (tr.doc.resolve(pos).parent.textContent === "") {
+ tr.deleteRange(pos - 1, pos + 1);
}
+ }
- const $pos = tr.doc.resolve(pos);
- if (canSplit(state.tr.doc, pos)) {
- if (pos === $pos.start()) {
- pos -= 1;
- } else {
- if (pos < $pos.end()) {
- tr.split(pos);
- }
- pos += 1;
+ const $pos = tr.doc.resolve(pos);
+ if (canSplit(state.tr.doc, pos)) {
+ if (pos === $pos.start()) {
+ pos -= 1;
+ } else {
+ if (pos < $pos.end()) {
+ tr.split(pos);
}
+ pos += 1;
}
- tr.setMeta(PlaceholderPlugin, {
- add: { id, container: null, element, pos }
- });
- tr.setSelection(Selection.near(tr.doc.resolve(pos), 1));
- view.dispatch(tr);
- uploadAndReplacePlaceholder(view, file, id);
- };
- element.src = localURL;
- } else if (file.type.startsWith("video")) {
- //FIXME: Handle videos
- // element = document.createElement("video");
- // …
- //FIXME: use a notification
- window.alert(FileErrorMessage.UPLOAD_FORMAT);
- } else {
- //FIXME: use a notification
- window.alert(FileErrorMessage.UPLOAD_FORMAT);
- }
-
- return true;
+ }
+ tr.setMeta(PlaceholderPlugin, {
+ add: { id, container: null, element, pos }
+ });
+ tr.setSelection(Selection.near(tr.doc.resolve(pos), 1));
+ view.dispatch(tr);
+ uploadAndReplacePlaceholder(uniqueId, view, file, id);
+ };
+ element.src = localURL;
+ } else if (file.type.startsWith("video")) {
+ //FIXME: Handle videos
+ // element = document.createElement("video");
+ // …
+ //FIXME: use a notification
+ window.alert(FileErrorMessage.UPLOAD_FORMAT);
+ } else {
+ //FIXME: use a notification
+ window.alert(FileErrorMessage.UPLOAD_FORMAT);
}
- /**
- * Uploads and then replaces the placeholder
- *
- * @param {EditorView} view
- * @param {DragEvent} dragEvent
- * @param {File} file
- * @param {{replaceSelection: boolean}} options
- */
- function uploadAndReplacePlaceholder(view: EditorView, file: File, id: any) {
- const auditStore = useAuditStore();
- auditStore.uploadAuditFile(uniqueId, file, FileDisplay.EDITOR).then(
- (response: AuditFile) => {
- const placeholder = findPlaceholderDecoration(view.state, id);
- const pos: number | undefined = placeholder?.from;
-
- // If the content around the placeholder has been deleted, drop
- // the image
- if (pos === undefined) {
- // TODO remove image from server
- return;
- }
+ return true;
+}
- // Otherwise, insert it at the placeholder's position, and remove
- // the placeholder
- const imgUrl: string = getUploadUrl(response.key);
- const state = view.state;
- const tr = state.tr;
- const node = state.schema.nodes.image.create({
- width: placeholder?.spec.width,
- height: placeholder?.spec.height,
- src: imgUrl
- });
- tr.replaceWith(pos, pos, node);
- tr.setMeta(PlaceholderPlugin, { remove: { id } });
+/**
+ * Uploads and then replaces the placeholder
+ *
+ * @param {string} uniqueId
+ * @param {EditorView} view
+ * @param {DragEvent} dragEvent
+ * @param {File} file
+ * @param {{replaceSelection: boolean}} options
+ */
+function uploadAndReplacePlaceholder(
+ uniqueId: string,
+ view: EditorView,
+ file: File,
+ id: any
+) {
+ const auditStore = useAuditStore();
+ auditStore.uploadAuditFile(uniqueId, file, FileDisplay.EDITOR).then(
+ (response: AuditFile) => {
+ const placeholder = findPlaceholderDecoration(view.state, id);
+ const pos: number | undefined = placeholder?.from;
- // Selects the image
- // tr.setSelection(new NodeSelection(tr.doc.resolve(pos)));
- view.dispatch(tr);
- },
- async (reason: any) => {
- // On failure, just clean up the placeholder
- view.dispatch(
- view.state.tr.setMeta(PlaceholderPlugin, { remove: { id } })
- );
- //FIXME: use a notification
- window.alert(await handleFileUploadError(reason));
+ // If the content around the placeholder has been deleted, drop
+ // the image
+ if (pos === undefined) {
+ // TODO remove image from server
+ return;
}
- );
- }
- /**
- * Finds the given placeholder (by id) within the given editor state.
- *
- * @param {EditorState} state
- * @param {any} id
- * @returns {Decoration} the placeholder (a ProseMirror decoration)
- */
- function findPlaceholderDecoration(
- state: EditorState,
- id: any
- ): Decoration | undefined {
- const decos = PlaceholderPlugin.getState(state);
- const found = decos?.find(undefined, undefined, (spec) => spec.id == id);
- return found?.[0];
- }
-
- /**
- * Creates a File object from a given URL
- *
- * @param {string} url
- * @returns {Promise} the created File or null if any error
- */
- function createFileFromImageUrl(url: string): Promise {
- let mimeType: string | undefined = undefined;
- return fetch(url)
- .then((res: Response) => {
- mimeType = res.headers.get("content-type") || undefined;
- return res.arrayBuffer();
- })
- .then((buf: ArrayBuffer) => {
- return new File([buf], "external", { type: mimeType });
- })
- .catch(() => {
- window.alert(FileErrorMessage.FETCH_ERROR);
- return null;
+ // Otherwise, insert it at the placeholder's position, and remove
+ // the placeholder
+ const imgUrl: string = getUploadUrl(response.key);
+ const state = view.state;
+ const tr = state.tr;
+ const node = state.schema.nodes.image.create({
+ width: placeholder?.spec.width,
+ height: placeholder?.spec.height,
+ src: imgUrl
});
- }
-};
+ tr.replaceWith(pos, pos, node);
+ tr.setMeta(PlaceholderPlugin, { remove: { id } });
+
+ // Selects the image
+ // tr.setSelection(new NodeSelection(tr.doc.resolve(pos)));
+ view.dispatch(tr);
+ },
+ async (reason: any) => {
+ // On failure, just clean up the placeholder
+ view.dispatch(
+ view.state.tr.setMeta(PlaceholderPlugin, { remove: { id } })
+ );
+ //FIXME: use a notification
+ window.alert(await handleFileUploadError(reason));
+ }
+ );
+}
+
+/**
+ * Finds the given placeholder (by id) within the given editor state.
+ *
+ * @param {EditorState} state
+ * @param {any} id
+ * @returns {Decoration} the placeholder (a ProseMirror decoration)
+ */
+function findPlaceholderDecoration(
+ state: EditorState,
+ id: any
+): Decoration | undefined {
+ const decos = PlaceholderPlugin.getState(state);
+ const found = decos?.find(undefined, undefined, (spec) => spec.id == id);
+ return found?.[0];
+}
+
+/**
+ * Creates a File object from a given URL
+ *
+ * @param {string} url
+ * @returns {Promise} the created File or null if any error
+ */
+function createFileFromImageUrl(url: string): Promise {
+ let mimeType: string | undefined = undefined;
+ return fetch(url)
+ .then((res: Response) => {
+ mimeType = res.headers.get("content-type") || undefined;
+ return res.arrayBuffer();
+ })
+ .then((buf: ArrayBuffer) => {
+ return new File([buf], "external", { type: mimeType });
+ })
+ .catch(() => {
+ window.alert(FileErrorMessage.FETCH_ERROR);
+ return null;
+ });
+}
/**
* Extension ImageUploadTiptapExtension
@@ -383,3 +424,21 @@ export const ImageUploadTiptapExtension =
// };
// }
});
+
+export function insertFilesAtSelection(
+ uniqueId: string,
+ editor: Editor,
+ files: FileList
+) {
+ const view: EditorView = editor.view;
+ const state: EditorState = view.state;
+ const tr: Transaction = state.tr;
+ const pos = state.selection.from;
+
+ view.focus();
+ tr.deleteSelection();
+
+ return handleFilesImport(uniqueId, view, pos, files, {
+ replaceSelection: true
+ });
+}
From a28acde824956ad2486a7ba8dc39072a3b317606 Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Fri, 15 Nov 2024 23:52:36 +0100
Subject: [PATCH 29/31] Fix CSS errors
---
confiture-web-app/src/styles/main.css | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/confiture-web-app/src/styles/main.css b/confiture-web-app/src/styles/main.css
index 68e0b733..a464022b 100644
--- a/confiture-web-app/src/styles/main.css
+++ b/confiture-web-app/src/styles/main.css
@@ -137,9 +137,10 @@ from DSFR links with `target="_blank"` */
display: inline-block;
flex: 0 0 auto;
height: var(--icon-size);
+ -webkit-mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTE0IDNhOCA4IDAgMSAxIDAgMTZ2My41Yy01LTItMTItNS0xMi0xMS41YTggOCAwIDAgMSA4LThoNFptMCAyaC00YTYgNiAwIDAgMC02IDZjMCAzLjYxIDIuNDYyIDUuOTY2IDggOC40OFYxN2gyYTYgNiAwIDEgMCAwLTEyWm0tMiAyLjUtMi41MDYgMy43NUwxMiAxNUg5LjI5NUw2Ljc1IDExLjI1IDkuMjk1IDcuNUgxMlptNC41IDAtMi41MDYgMy43NUwxNi41IDE1aC0yLjcwNWwtMi41NDUtMy43NSAyLjU0NS0zLjc1SDE2LjVaIi8+PC9zdmc+);
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTE0IDNhOCA4IDAgMSAxIDAgMTZ2My41Yy01LTItMTItNS0xMi0xMS41YTggOCAwIDAgMSA4LThoNFptMCAyaC00YTYgNiAwIDAgMC02IDZjMCAzLjYxIDIuNDYyIDUuOTY2IDggOC40OFYxN2gyYTYgNiAwIDEgMCAwLTEyWm0tMiAyLjUtMi41MDYgMy43NUwxMiAxNUg5LjI5NUw2Ljc1IDExLjI1IDkuMjk1IDcuNUgxMlptNC41IDAtMi41MDYgMy43NUwxNi41IDE1aC0yLjcwNWwtMi41NDUtMy43NSAyLjU0NS0zLjc1SDE2LjVaIi8+PC9zdmc+);
+ -webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
- vertical-align: calc((0.75em - var(--icon-size)) * 0.5);
width: var(--icon-size);
}
@@ -169,6 +170,7 @@ from DSFR links with `target="_blank"` */
.tiptap ul[data-type="taskList"] li > label {
flex: 0 0 auto;
margin-right: 0.5rem;
+ -webkit-user-select: none;
user-select: none;
}
From 46f21ebfcb9cd3d75b2e0fcb993866576058216c Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Fri, 15 Nov 2024 23:54:18 +0100
Subject: [PATCH 30/31] Fix type issues (error messages can be strings)
---
.../src/components/audit/AuditGenerationCriterium.vue | 2 +-
.../src/components/audit/CriteriumNotCompliantAccordion.vue | 2 +-
confiture-web-app/src/components/audit/NotesModal.vue | 2 +-
confiture-web-app/src/components/ui/FileUpload.vue | 2 +-
confiture-web-app/src/utils.ts | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/confiture-web-app/src/components/audit/AuditGenerationCriterium.vue b/confiture-web-app/src/components/audit/AuditGenerationCriterium.vue
index f4d40356..b6149172 100644
--- a/confiture-web-app/src/components/audit/AuditGenerationCriterium.vue
+++ b/confiture-web-app/src/components/audit/AuditGenerationCriterium.vue
@@ -119,7 +119,7 @@ function toggleTransverseComment() {
const notify = useNotifications();
-const errorMessage: Ref = ref(null);
+const errorMessage: Ref = ref(null);
const criteriumNotCompliantAccordion =
ref>();
diff --git a/confiture-web-app/src/components/audit/CriteriumNotCompliantAccordion.vue b/confiture-web-app/src/components/audit/CriteriumNotCompliantAccordion.vue
index c544da97..55cb7320 100644
--- a/confiture-web-app/src/components/audit/CriteriumNotCompliantAccordion.vue
+++ b/confiture-web-app/src/components/audit/CriteriumNotCompliantAccordion.vue
@@ -14,7 +14,7 @@ import MarkdownHelpButton from "./MarkdownHelpButton.vue";
export interface Props {
id: string;
comment: string | null;
- errorMessage?: FileErrorMessage | null;
+ errorMessage?: FileErrorMessage | string | null;
exampleImages: AuditFile[];
quickWin?: boolean;
userImpact: CriterionResultUserImpact | null;
diff --git a/confiture-web-app/src/components/audit/NotesModal.vue b/confiture-web-app/src/components/audit/NotesModal.vue
index 4042504c..fcd0c0e6 100644
--- a/confiture-web-app/src/components/audit/NotesModal.vue
+++ b/confiture-web-app/src/components/audit/NotesModal.vue
@@ -28,7 +28,7 @@ defineExpose({
hide: () => modal.value?.hide()
});
-const errorMessage: Ref = ref(null);
+const errorMessage: Ref = ref(null);
const fileUpload = ref>();
const auditStore = useAuditStore();
diff --git a/confiture-web-app/src/components/ui/FileUpload.vue b/confiture-web-app/src/components/ui/FileUpload.vue
index 2d80e179..05895812 100644
--- a/confiture-web-app/src/components/ui/FileUpload.vue
+++ b/confiture-web-app/src/components/ui/FileUpload.vue
@@ -10,7 +10,7 @@ import { formatBytes, getUploadUrl } from "../../utils";
export interface Props {
acceptedFormats?: Array;
auditFiles: AuditFile[];
- errorMessage?: FileErrorMessage | null;
+ errorMessage?: FileErrorMessage | string | null;
maxFileSize?: string;
multiple?: boolean;
readonly?: boolean;
diff --git a/confiture-web-app/src/utils.ts b/confiture-web-app/src/utils.ts
index 489943ec..868a3804 100644
--- a/confiture-web-app/src/utils.ts
+++ b/confiture-web-app/src/utils.ts
@@ -245,7 +245,7 @@ export async function handleFileUploadError(
error: Error
): Promise {
if (error instanceof HTTPError) {
- let errorType: FileErrorMessage | null = null;
+ let errorType: FileErrorMessage;
if (error.response.status === 413) {
errorType = FileErrorMessage.UPLOAD_SIZE;
}
From b3b41d23ec02adcf07fb4ff19fdf7bbd2ec25ec0 Mon Sep 17 00:00:00 2001
From: Yaacov
Date: Wed, 4 Dec 2024 09:57:25 +0100
Subject: [PATCH 31/31] Tiptap: add buttons
+ fix text drag and drop
---
.../src/assets/images/code-block.svg | 1 +
.../src/assets/images/strikethrough-2.svg | 1 +
.../src/components/ui/Tiptap.vue | 252 ++++++++++++++++--
.../src/components/ui/TiptapButton.vue | 42 +++
confiture-web-app/src/styles/main.css | 116 +++++++-
.../src/tiptap/AraTiptapExtension.ts | 57 ++++
.../src/tiptap/ImageUploadTiptapExtension.ts | 2 +-
7 files changed, 439 insertions(+), 32 deletions(-)
create mode 100644 confiture-web-app/src/assets/images/code-block.svg
create mode 100644 confiture-web-app/src/assets/images/strikethrough-2.svg
create mode 100644 confiture-web-app/src/components/ui/TiptapButton.vue
create mode 100644 confiture-web-app/src/tiptap/AraTiptapExtension.ts
diff --git a/confiture-web-app/src/assets/images/code-block.svg b/confiture-web-app/src/assets/images/code-block.svg
new file mode 100644
index 00000000..9db393b1
--- /dev/null
+++ b/confiture-web-app/src/assets/images/code-block.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/confiture-web-app/src/assets/images/strikethrough-2.svg b/confiture-web-app/src/assets/images/strikethrough-2.svg
new file mode 100644
index 00000000..b96583fc
--- /dev/null
+++ b/confiture-web-app/src/assets/images/strikethrough-2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/confiture-web-app/src/components/ui/Tiptap.vue b/confiture-web-app/src/components/ui/Tiptap.vue
index 8d01492c..d748d8e3 100644
--- a/confiture-web-app/src/components/ui/Tiptap.vue
+++ b/confiture-web-app/src/components/ui/Tiptap.vue
@@ -1,6 +1,8 @@
-
+
Éditeur de texte riche, vous pouvez utiliser le format Markdown ou bien
utiliser les raccourcis clavier.
-
-
+
diff --git a/confiture-web-app/src/components/ui/TiptapButton.vue b/confiture-web-app/src/components/ui/TiptapButton.vue
new file mode 100644
index 00000000..2bc8202d
--- /dev/null
+++ b/confiture-web-app/src/components/ui/TiptapButton.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
diff --git a/confiture-web-app/src/styles/main.css b/confiture-web-app/src/styles/main.css
index a464022b..07ae654a 100644
--- a/confiture-web-app/src/styles/main.css
+++ b/confiture-web-app/src/styles/main.css
@@ -65,11 +65,18 @@ from DSFR links with `target="_blank"` */
color: var(--background-action-high-error) !important;
}
+/* Tiptap */
+.tiptap-container {
+ position: relative;
+}
+
.tiptap {
- background-color: var(--background-default-grey);
- padding: 1rem;
- border: 1px solid var(--border-default-grey);
- min-height: 10rem;
+ background-color: var(--background-alt-grey);
+ border-radius: 0.5rem 0.5rem 0 0;
+ padding: 4rem 1.5rem 1rem;
+ border: 0 solid var(--border-plain-grey);
+ border-bottom-width: 1px;
+ min-height: 30rem;
overflow-y: auto;
}
@@ -85,7 +92,7 @@ from DSFR links with `target="_blank"` */
.tiptap:focus,
.tiptap:focus-visible {
- outline-width: 4px !important;
+ outline-width: 3px !important;
outline-offset: 0 !important;
}
@@ -190,6 +197,86 @@ from DSFR links with `target="_blank"` */
margin: 0;
}
+.tiptap-buttons,
+.tiptap-buttons ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.tiptap-buttons {
+ margin: 0.5rem 0.75rem;
+ width: calc(100% - 1.5rem);
+ position: absolute;
+ overflow-x: auto;
+ white-space: nowrap;
+ top: 0;
+ scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* Internet Explorer 10+ */
+}
+.titptap-buttons::-webkit-scrollbar {
+ /* WebKit */
+ width: 0;
+ height: 0;
+}
+
+.tiptap-buttons li,
+.tiptap-buttons ul {
+ display: inline-block;
+}
+
+.tiptap-buttons li + li {
+ margin-left: 0.2rem;
+}
+
+.tiptap-buttons > li + li:before {
+ box-shadow: inset 0 0 0 1px #ddd;
+ box-shadow: inset 0 0 0 1px var(--border-default-grey);
+ content: "";
+ display: inline-block;
+ height: 1.5rem;
+ padding: 0;
+ margin-left: 0.5rem;
+ margin-right: 0.5rem;
+ position: relative;
+ vertical-align: baseline;
+ width: 1px;
+}
+
+.tiptap-buttons .fr-btn--tertiary {
+ color: var(--text-defult-grey);
+ box-shadow: none;
+ border-radius: 0.2rem;
+}
+.tiptap-buttons .fr-btn--tertiary:not([aria-pressed="true"]) {
+ --hover-tint: var(--background-alt-grey-hover);
+}
+
+.tiptap-buttons .fr-btn--tertiary[disabled] {
+ box-shadow: unset;
+ opacity: 0.5;
+}
+
+.tiptap-buttons .fr-btn--tertiary:not(:disabled):active {
+ background-color: var(--background-alt-grey-active);
+}
+
+.tiptap-buttons .fr-btn--tertiary[aria-pressed="true"] {
+ background-color: var(--background-alt-grey-active);
+}
+
+.tiptap-buttons .fr-btn--tertiary[aria-pressed="true"]:hover {
+ background-color: var(--background-alt-grey-hover);
+}
+
+/* @media (width < 36rem) {
+ .tiptap-buttons .fr-btn--icon-left[class*=" fr-icon-"] {
+ overflow: hidden;
+ max-width: 2.5rem;
+ max-height: 2.5rem;
+ }
+} */
+
.ProseMirror-selectednode {
outline-style: dotted;
outline-width: 2px;
@@ -200,6 +287,25 @@ from DSFR links with `target="_blank"` */
opacity: 0.5;
}
+/* Extra icons */
+.fr-icon-strikethrough::before,
+.fr-icon-strikethrough::after {
+ -webkit-mask-image: url("../assets/images/strikethrough-2.svg");
+ mask-image: url("../assets/images/strikethrough-2.svg");
+}
+.fr-icon-code-block::before,
+.fr-icon-code-block::after {
+ -webkit-mask-image: url("../assets/images/code-block.svg");
+ mask-image: url("../assets/images/code-block.svg");
+}
+
+.tiptap-buttons .fr-btn--icon-left[class*="fr-icon-image-add-line"] {
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+}
+.tiptap-buttons .fr-btn--icon-left[class*="fr-icon-image-add-line"]:before {
+ --icon-size: 1.5rem;
+}
/* File upload styling */
/* input[type="file" i]::-webkit-file-upload-button {
background-color: transparent;
diff --git a/confiture-web-app/src/tiptap/AraTiptapExtension.ts b/confiture-web-app/src/tiptap/AraTiptapExtension.ts
new file mode 100644
index 00000000..5407ac24
--- /dev/null
+++ b/confiture-web-app/src/tiptap/AraTiptapExtension.ts
@@ -0,0 +1,57 @@
+import { Extension } from "@tiptap/core";
+import { Plugin, Transaction } from "@tiptap/pm/state";
+
+export interface AraTiptapExtensionOptions {
+ uniqueId: string;
+}
+
+/**
+ * Link title plugin
+ *
+ * The placeholder is an image blob (local to browser), with 50% opacity.
+ * Within ProseMirror it’s a Decoration.
+ */
+const linkTitlePlugin = new Plugin({
+ appendTransaction: (
+ transactions: readonly Transaction[],
+ oldState,
+ newState
+ ) => {
+ const tr = newState.tr;
+
+ newState.doc.descendants((node, pos) => {
+ if (node.marks.length > 0) {
+ node.marks.forEach((mark) => {
+ const newAttrs = {
+ ...mark.attrs,
+ title: node.text + " - nouvelle fenêtre"
+ };
+ tr.removeMark(pos, pos + node.nodeSize, mark.type);
+ tr.addMark(pos, pos + node.nodeSize, mark.type.create(newAttrs));
+ });
+ }
+ });
+
+ return tr;
+ }
+});
+
+/**
+ * Extension AraTiptapExtension
+ *
+ * Tiptap extension for Ara specificities
+ * @see https://github.com/ProseMirror/prosemirror-dropcursor
+ */
+export const AraTiptapExtension = Extension.create
({
+ name: "ara",
+ addProseMirrorPlugins() {
+ return [linkTitlePlugin];
+ },
+ extendNodeSchema() {
+ return {
+ image: {
+ marks: "_"
+ }
+ };
+ }
+});
diff --git a/confiture-web-app/src/tiptap/ImageUploadTiptapExtension.ts b/confiture-web-app/src/tiptap/ImageUploadTiptapExtension.ts
index e6b6de3d..151be1f5 100644
--- a/confiture-web-app/src/tiptap/ImageUploadTiptapExtension.ts
+++ b/confiture-web-app/src/tiptap/ImageUploadTiptapExtension.ts
@@ -175,7 +175,7 @@ function handleDataTransfer(
}
});
}
- return true;
+ return false;
}
// Handle multiple files