diff --git a/webapp/cypress/component/TiptapInlineCompatibilityTest.cy.jsx b/webapp/cypress/component/TiptapInlineCompatibilityTest.cy.jsx
new file mode 100644
index 000000000..613078b46
--- /dev/null
+++ b/webapp/cypress/component/TiptapInlineCompatibilityTest.cy.jsx
@@ -0,0 +1,228 @@
+import TiptapInline from "@/components/TiptapInline.vue";
+import { createStore } from "vuex";
+
+describe("TiptapInline - TinyMCE HTML Compatibility", () => {
+ let store;
+
+ beforeEach(() => {
+ store = createStore({
+ state: {},
+ mutations: {},
+ getters: {},
+ });
+ });
+
+ it("renders basic formatting from TinyMCE HTML", () => {
+ const tinyMCEHtml = `
Bold text and italic text and underlined text and strikethrough
`;
+
+ cy.mount(TiptapInline, {
+ props: {
+ modelValue: tinyMCEHtml,
+ },
+ global: {
+ plugins: [store],
+ },
+ });
+
+ cy.get(".ProseMirror").should("contain.html", "Bold text");
+ cy.get(".ProseMirror").should("contain.html", "italic text");
+ cy.get(".ProseMirror").should("contain.html", "underlined text");
+ cy.get(".ProseMirror").should("contain.html", "strikethrough");
+ });
+
+ it("renders headings from TinyMCE HTML", () => {
+ const tinyMCEHtml = `Heading 1
Heading 2
Heading 3
`;
+
+ cy.mount(TiptapInline, {
+ props: {
+ modelValue: tinyMCEHtml,
+ },
+ global: {
+ plugins: [store],
+ },
+ });
+
+ cy.get(".ProseMirror h1").should("contain.text", "Heading 1");
+ cy.get(".ProseMirror h2").should("contain.text", "Heading 2");
+ cy.get(".ProseMirror h3").should("contain.text", "Heading 3");
+ });
+
+ it("renders lists from TinyMCE HTML", () => {
+ const tinyMCEHtml = `- First
- Second
`;
+
+ cy.mount(TiptapInline, {
+ props: {
+ modelValue: tinyMCEHtml,
+ },
+ global: {
+ plugins: [store],
+ },
+ });
+
+ cy.get(".ProseMirror ul li").should("have.length", 2);
+ cy.get(".ProseMirror ul li").first().should("contain.text", "Item 1");
+ cy.get(".ProseMirror ol li").should("have.length", 2);
+ cy.get(".ProseMirror ol li").first().should("contain.text", "First");
+ });
+
+ it("renders links from TinyMCE HTML", () => {
+ const tinyMCEHtml = `Visit this link
`;
+
+ cy.mount(TiptapInline, {
+ props: {
+ modelValue: tinyMCEHtml,
+ },
+ global: {
+ plugins: [store],
+ },
+ });
+
+ cy.get(".ProseMirror a")
+ .should("have.attr", "href", "https://example.com")
+ .and("have.attr", "target", "_blank")
+ .and("contain.text", "this link");
+ });
+
+ it("renders images from TinyMCE HTML", () => {
+ const tinyMCEHtml = `
`;
+
+ cy.mount(TiptapInline, {
+ props: {
+ modelValue: tinyMCEHtml,
+ },
+ global: {
+ plugins: [store],
+ },
+ });
+
+ cy.get(".ProseMirror img")
+ .should("have.attr", "src", "https://example.com/image.png")
+ .and("have.attr", "alt", "Test image");
+ });
+
+ it("renders tables from TinyMCE HTML", () => {
+ const tinyMCEHtml = `
+
+
+
+ Header 1 |
+ Header 2 |
+
+
+
+
+ Cell 1 |
+ Cell 2 |
+
+
+
+ `;
+
+ cy.mount(TiptapInline, {
+ props: {
+ modelValue: tinyMCEHtml,
+ },
+ global: {
+ plugins: [store],
+ },
+ });
+
+ cy.get(".ProseMirror table").should("exist");
+ cy.get(".ProseMirror th").should("have.length", 2);
+ cy.get(".ProseMirror th").first().should("contain.text", "Header 1");
+ cy.get(".ProseMirror td").should("have.length", 2);
+ cy.get(".ProseMirror td").first().should("contain.text", "Cell 1");
+ });
+
+ it("renders colored text from TinyMCE HTML", () => {
+ const tinyMCEHtml = `Red text
`;
+
+ cy.mount(TiptapInline, {
+ props: {
+ modelValue: tinyMCEHtml,
+ },
+ global: {
+ plugins: [store],
+ },
+ });
+
+ cy.get(".ProseMirror span").should("have.attr", "style").and("include", "color");
+ });
+
+ it("renders horizontal rules from TinyMCE HTML", () => {
+ const tinyMCEHtml = `Before
After
`;
+
+ cy.mount(TiptapInline, {
+ props: {
+ modelValue: tinyMCEHtml,
+ },
+ global: {
+ plugins: [store],
+ },
+ });
+
+ cy.get(".ProseMirror hr").should("exist");
+ });
+
+ it("renders cross-references from datalab HTML", () => {
+ const datalabHtml = `See sample for details
`;
+
+ cy.mount(TiptapInline, {
+ props: {
+ modelValue: datalabHtml,
+ },
+ global: {
+ plugins: [store],
+ },
+ });
+
+ cy.get(".ProseMirror .cross-reference-wrapper").should("exist");
+ cy.get(".formatted-item-name").should("contain.text", "sample1");
+ });
+
+ it("renders complex nested formatting from TinyMCE HTML", () => {
+ const tinyMCEHtml = `
+ Complex Document
+ This is a bold and italic text with a link.
+
+ - Bold list item
+ - Italic list item
+
+
+
+ Bold cell |
+ Blue cell |
+
+
+ `;
+
+ cy.mount(TiptapInline, {
+ props: {
+ modelValue: tinyMCEHtml,
+ },
+ global: {
+ plugins: [store],
+ },
+ });
+
+ cy.get(".ProseMirror h2").should("contain.text", "Complex Document");
+ cy.get(".ProseMirror strong").should("exist");
+ cy.get(".ProseMirror em").should("exist");
+ cy.get(".ProseMirror a").should("have.attr", "href");
+ cy.get(".ProseMirror ul li").should("have.length", 2);
+ cy.get(".ProseMirror table td").should("have.length", 2);
+ });
+
+ it("handles empty content gracefully", () => {
+ cy.mount(TiptapInline, {
+ props: {
+ modelValue: "",
+ },
+ global: {
+ plugins: [store],
+ },
+ });
+
+ cy.get(".ProseMirror").should("exist");
+ });
+});
diff --git a/webapp/cypress/e2e/editPage.cy.js b/webapp/cypress/e2e/editPage.cy.js
index 8101d0d67..11752c1d6 100644
--- a/webapp/cypress/e2e/editPage.cy.js
+++ b/webapp/cypress/e2e/editPage.cy.js
@@ -226,7 +226,12 @@ describe("Edit Page", () => {
cy.get(".datablock-content div").eq(0).type("\nThe first comment box; further changes.");
cy.contains("Unsaved changes");
- cy.get('[data-testid="block-description"]').eq(0).type("The second comment box");
+ cy.get('[data-testid="block-description"]').first().find(".ProseMirror").click();
+
+ cy.get('[data-testid="block-description"]')
+ .first()
+ .find(".ProseMirror")
+ .type("The second comment box");
cy.contains("Unsaved changes");
cy.get('.datablock-header [aria-label="updateBlock"]').eq(1).click();
cy.wait(500).then(() => {
@@ -234,10 +239,13 @@ describe("Edit Page", () => {
});
cy.get('.datablock-header [aria-label="updateBlock"]').eq(0).click();
cy.contains("Unsaved changes").should("not.exist");
+ cy.get('[data-testid="block-description"]').first().find(".ProseMirror").click();
cy.get('[data-testid="block-description"]')
- .eq(0)
+ .first()
+ .find(".ProseMirror")
.type("\nThe second comment box; further changes");
+
cy.findByLabelText("Name").type("name change");
cy.contains("Unsaved changes");
diff --git a/webapp/package.json b/webapp/package.json
index 784a50d74..7ed4fdcf5 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -10,9 +10,11 @@
"lint": "vue-cli-service lint"
},
"resolutions": {
- "cross-spawn": "^7.0.5"
+ "cross-spawn": "^7.0.5",
+ "prosemirror-view": "1.41.1"
},
"dependencies": {
+ "@floating-ui/dom": "^1.7.4",
"@fortawesome/fontawesome-svg-core": "^1.2.26-2",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-regular-svg-icons": "^5.15.2",
@@ -20,7 +22,20 @@
"@fortawesome/vue-fontawesome": "^3.0.0-3",
"@popperjs/core": "^2.11.8",
"@primevue/themes": "^4.0.0",
- "@tinymce/tinymce-vue": "^4.0.0",
+ "@tiptap/core": "^3.6.1",
+ "@tiptap/extension-color": "^3.6.1",
+ "@tiptap/extension-highlight": "^3.6.1",
+ "@tiptap/extension-image": "^3.6.1",
+ "@tiptap/extension-link": "^3.6.1",
+ "@tiptap/extension-mathematics": "^3.6.1",
+ "@tiptap/extension-placeholder": "^3.6.1",
+ "@tiptap/extension-table": "^3.6.1",
+ "@tiptap/extension-text-style": "^3.6.1",
+ "@tiptap/extension-typography": "^3.6.1",
+ "@tiptap/extension-underline": "^3.6.1",
+ "@tiptap/pm": "^3.6.1",
+ "@tiptap/starter-kit": "^3.6.1",
+ "@tiptap/vue-3": "^3.6.1",
"@uppy/core": "^4.4.6",
"@uppy/dashboard": "^4.3.4",
"@uppy/webcam": "^4.2.0",
@@ -36,14 +51,16 @@
"date-fns": "^2.29.3",
"highlight.js": "^11.7.0",
"js-md5": "^0.8.3",
- "markdown-it": "^13.0.1",
+ "katex": "^0.16.22",
+ "markdown-it": "^14.1.0",
"mermaid": "^11.10.0",
"primeicons": "^7.0.0",
"primevue": "^4.0.0",
"process": "^0.11.10",
+ "prosemirror-tables": "^1.8.1",
"qrcode-vue3": "^1.6.8",
"serve": "^14.2.1",
- "tinymce": "^5.10.9",
+ "turndown": "^7.2.1",
"vue": "^3.2.4",
"vue-qrcode-reader": "^5.5.7",
"vue-router": "^4.0.0-0",
diff --git a/webapp/public/index.html b/webapp/public/index.html
index 527cc131b..db1582dc4 100644
--- a/webapp/public/index.html
+++ b/webapp/public/index.html
@@ -30,6 +30,14 @@
>
-
+
+
+