diff --git a/blocks/canvas/canvas.css b/blocks/canvas/canvas.css index ced573f10..04765afe7 100644 --- a/blocks/canvas/canvas.css +++ b/blocks/canvas/canvas.css @@ -14,6 +14,11 @@ html:has(aside.panel[data-position="after"]:not([hidden])) ew-canvas-header::par display: none; } +html:has(ew-canvas-header[has-conflicts]) aside.panel[data-position="before"] { + pointer-events: none; + opacity: 0.45; +} + .fragment-content:has(nx-chat) { height: 100%; diff --git a/blocks/canvas/canvas.js b/blocks/canvas/canvas.js index 257cd1711..c62ed8353 100644 --- a/blocks/canvas/canvas.js +++ b/blocks/canvas/canvas.js @@ -161,6 +161,7 @@ async function installCanvasHeader(block, { org, site }) { header.editorView = await readInitialCanvasEditorView({ org, site }); canvasBus.editorViewRequest.subscribe(({ view: rawView }) => { const view = normalizeCanvasEditorView(rawView); + header.editorView = view; persistCanvasEditorView(view); notifyCanvasEditorActive(view); syncEditorSplitLayout({ mountRoot: canvasEditorMountRoot(block), view }); @@ -251,6 +252,13 @@ export default async function decorate(block) { syncEditorSplitLayout({ mountRoot, view: header.editorView }); installEditorSplitDrag(mountRoot); + canvasBus.mergeConflictsState.subscribe((detail) => { + header.hasUnresolvedMergeConflicts = detail?.hasMergeConflicts ?? false; + if (header.hasUnresolvedMergeConflicts && header.editorView !== 'content') { + canvasBus.editorViewRequest.emit({ view: 'content' }); + } + }); + canvasBus.undoState.subscribe((detail) => { header.undoAvailable = detail?.canUndo ?? false; header.redoAvailable = detail?.canRedo ?? false; diff --git a/blocks/canvas/editor-utils/prose-merge-conflicts.js b/blocks/canvas/editor-utils/prose-merge-conflicts.js new file mode 100644 index 000000000..ab83d9e89 --- /dev/null +++ b/blocks/canvas/editor-utils/prose-merge-conflicts.js @@ -0,0 +1,20 @@ +import { Plugin } from 'da-y-wrapper'; +import { checkForLocNodes } from '../../edit/prose/diff/diff-utils.js'; +import { debounce } from '../../edit/utils/helpers.js'; + +export function createMergeConflictsPlugin(onMergeConflictsChange, hostEl) { + const debouncedCheck = debounce((view) => { + onMergeConflictsChange?.(checkForLocNodes(view, hostEl)); + }, 500); + + return new Plugin({ + view(editorView) { + debouncedCheck(editorView); + return { + update(view, prevState) { + if (view.state.doc !== prevState.doc) debouncedCheck(view); + }, + }; + }, + }); +} diff --git a/blocks/canvas/ew-canvas-header/ew-canvas-header.css b/blocks/canvas/ew-canvas-header/ew-canvas-header.css index fd3e6b5c7..db4459723 100644 --- a/blocks/canvas/ew-canvas-header/ew-canvas-header.css +++ b/blocks/canvas/ew-canvas-header/ew-canvas-header.css @@ -92,6 +92,16 @@ outline: none; } + &:disabled { + color: var(--s2-gray-400); + cursor: not-allowed; + + &:hover { + color: var(--s2-gray-400); + background-color: transparent; + } + } + &:hover:not(.is-selected) { color: var(--s2-gray-900); background-color: var(--s2-gray-200); @@ -118,6 +128,10 @@ flex-shrink: 0; overflow: hidden; } + + &:disabled.segment-icon svg.icon { + opacity: 0.45; + } } .bar { diff --git a/blocks/canvas/ew-canvas-header/ew-canvas-header.js b/blocks/canvas/ew-canvas-header/ew-canvas-header.js index 810e88406..0ad347e00 100644 --- a/blocks/canvas/ew-canvas-header/ew-canvas-header.js +++ b/blocks/canvas/ew-canvas-header/ew-canvas-header.js @@ -28,6 +28,7 @@ class EWCanvasHeader extends LitElement { undoAvailable: { type: Boolean }, redoAvailable: { type: Boolean }, authorized: { type: Boolean }, + hasUnresolvedMergeConflicts: { type: Boolean, reflect: true, attribute: 'has-conflicts' }, canWrite: { type: Boolean }, _chatDisabled: { state: true }, }; @@ -38,6 +39,7 @@ class EWCanvasHeader extends LitElement { this.undoAvailable = false; this.redoAvailable = false; this.authorized = true; + this.hasUnresolvedMergeConflicts = false; this.canWrite = true; } @@ -81,9 +83,9 @@ class EWCanvasHeader extends LitElement { canvasBus.redoRequest.emit(); } - _setEditorView(view) { + setEditorView(view) { + if (this.hasUnresolvedMergeConflicts && view !== 'content') return; if (!EDITOR_VIEWS.includes(view) || view === this.editorView) return; - this.editorView = view; canvasBus.editorViewRequest.emit({ view }); } @@ -134,21 +136,24 @@ class EWCanvasHeader extends LitElement { type="button" class="segment ${this.editorView === 'layout' ? 'is-selected' : ''}" aria-pressed=${this.editorView === 'layout'} - @click=${() => this._setEditorView('layout')} + title=${this.hasUnresolvedMergeConflicts ? 'Resolve conflicts to switch views' : nothing} + ?disabled=${this.hasUnresolvedMergeConflicts} + @click=${() => this.setEditorView('layout')} >Layout ` : nothing} diff --git a/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css b/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css new file mode 100644 index 000000000..e1d98e060 --- /dev/null +++ b/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css @@ -0,0 +1,155 @@ +/* stylelint-disable selector-class-pattern */ + +.ew-editor-doc-mount .loc-tabbed-container { + background: var(--s2-gray-25); + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); + border-radius: var(--s2-corner-radius-400); +} + +.ew-editor-doc-mount .diff-tab-content { + background: var(--s2-gray-25); +} + +.ew-editor-doc-mount .da-diff-btn-base { + border-radius: var(--s2-corner-radius-400); + height: 28px; + background: var(--s2-gray-100); +} + +.ew-editor-doc-mount .da-diff-btn .switch-btn, +.ew-editor-doc-mount .da-diff-btn-3part .diff-label { + padding: 4px 10px; +} + +.ew-editor-doc-mount .da-diff-btn .switch-btn { + cursor: default; +} + +.ew-editor-doc-mount .da-diff-btn-base-element { + font-size: var(--s2-component-s-regular-font-size, 0.875rem); + font-weight: 600; + + &:focus-visible { + outline: 2px solid var(--s2-blue-800); + outline-offset: 2px; + } +} + +.ew-editor-doc-mount .da-diff-btn .confirm-btn, +.ew-editor-doc-mount .da-diff-btn-3part .diff-accept, +.ew-editor-doc-mount .da-diff-btn-3part .diff-delete { + padding: 0; +} + +.ew-editor-doc-mount .da-diff-btn .confirm-btn::before, +.ew-editor-doc-mount .da-diff-btn-3part .diff-accept::before { + content: ''; + display: inline-block; + width: 16px; + height: 16px; + background-color: currentcolor; + mask: no-repeat center / 16px url('/img/icons/s2-icon-checkmark-20-n.svg'); +} + +.ew-editor-doc-mount .da-diff-btn-3part .diff-delete::before { + content: ''; + display: inline-block; + width: 16px; + height: 16px; + background-color: currentcolor; + mask: no-repeat center / 16px url('/img/icons/s2-icon-close-20-n.svg'); +} + +.ew-editor-doc-mount .da-diff-btn:is(.is-local, .is-upstream, .is-diff) .switch-btn, +.ew-editor-doc-mount .da-diff-btn-3part:is(.is-local, .is-upstream) .diff-label { + background: transparent; + color: var(--s2-gray-800); +} + +.ew-editor-doc-mount .da-diff-btn:is(.is-local, .is-upstream, .is-diff) .confirm-btn, +.ew-editor-doc-mount .da-diff-btn-3part:is(.is-local, .is-upstream) .diff-accept, +.ew-editor-doc-mount .da-diff-btn-3part:is(.is-local, .is-upstream) .diff-delete { + background: var(--s2-gray-200); + color: var(--s2-gray-800); + border-left: 1px solid var(--s2-gray-300); +} + +.ew-editor-doc-mount .da-diff-btn.is-active { + border-color: var(--s2-gray-900); +} + +.ew-editor-doc-mount .da-diff-btn.is-active .switch-btn, +.ew-editor-doc-mount .da-diff-btn.is-active .confirm-btn { + background: var(--s2-gray-300); + color: var(--s2-gray-900); +} + +.ew-editor-doc-mount .da-diff-btn .confirm-btn:hover, +.ew-editor-doc-mount .da-diff-btn-3part .diff-accept:hover, +.ew-editor-doc-mount .da-diff-btn-3part .diff-delete:hover { + background: var(--s2-gray-300); + color: var(--s2-gray-900); +} + +.ew-editor-doc-mount .diff-tooltip { + background: var(--s2-gray-1000); + color: var(--s2-gray-25); + font-weight: 500; + border: none; + border-radius: var(--s2-corner-radius-400); + box-shadow: 0 2px 6px rgb(0 0 0 / 15%); + backdrop-filter: none; +} + +.ew-editor-doc-mount .diff-tooltip::before { + display: none; +} + +.ew-editor-doc-mount .diff-tooltip::after { + border-top-color: var(--s2-gray-1000); + filter: none; +} + +.ew-editor-doc-mount .diff-tab-pane del.diffdel, +.ew-editor-doc-mount .diff-tab-pane del.diffmod { + background-color: var(--s2-red-200); + color: var(--s2-red-900); + border: none; +} + +.ew-editor-doc-mount .diff-tab-pane ins.diffins, +.ew-editor-doc-mount .diff-tab-pane ins.diffmod { + background-color: var(--s2-green-200); + color: var(--s2-green-900); + border: none; +} + +.ew-editor-doc-mount .loc-color-overlay.loc-regional { + background-color: color-mix(in srgb, var(--s2-green-200) 25%, transparent); +} + +.ew-editor-doc-mount .loc-color-overlay.loc-langstore { + background-color: color-mix(in srgb, var(--s2-red-200) 25%, transparent); +} + +.ew-editor-doc-mount .diff-bg-local { + background-color: color-mix(in srgb, var(--s2-green-200) 25%, transparent); +} + +.ew-editor-doc-mount .diff-bg-upstream { + background-color: color-mix(in srgb, var(--s2-red-200) 25%, transparent); +} + +.ew-editor-doc-mount .diff-bg-diff { + background-color: color-mix(in srgb, var(--s2-gray-100) 25%, transparent); +} + +.ew-editor-doc-mount .da-regional-edits-overlay { + padding: var(--s2-spacing-200); + background: var(--s2-gray-25); + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); +} + +.ew-editor-doc-mount .da-regional-edits-actions { + gap: var(--s2-spacing-200); +} diff --git a/blocks/canvas/ew-editor-doc/ew-editor-doc.js b/blocks/canvas/ew-editor-doc/ew-editor-doc.js index 54da2e2ce..686aa91be 100644 --- a/blocks/canvas/ew-editor-doc/ew-editor-doc.js +++ b/blocks/canvas/ew-editor-doc/ew-editor-doc.js @@ -1,6 +1,7 @@ import { LitElement, html, nothing } from 'da-lit'; import { yUndo, yRedo, NodeSelection, TextSelection } from 'da-y-wrapper'; -import { getNx } from '../../../scripts/utils.js'; +import { getNx, getNx2 } from '../../../scripts/utils.js'; +import getSheet from '../../shared/sheet.js'; import { updateDocument, updateCursors, getInstrumentedHTML, getEditor } from '../editor-utils/editor-utils.js'; import { getActiveBlockIndex, getBlockPositions } from '../editor-utils/blocks.js'; import { @@ -41,6 +42,8 @@ const { loadStyle } = await import(`${getNx()}/utils/utils.js`); const { CHAT_EVENT } = await import(`${getNx()}/blocks/chat/constants.js`); const style = await loadStyle(import.meta.url); +const buttons = await getSheet(`${getNx2()}/styles/buttons.css`); +const diffStyle = await getSheet('/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css'); export class EwEditorDoc extends LitElement { static properties = { @@ -95,6 +98,10 @@ export class EwEditorDoc extends LitElement { canvasBus.undoState.emit({ canUndo, canRedo }); } + _emitMergeConflictsState(hasMergeConflicts) { + canvasBus.mergeConflictsState.emit({ hasMergeConflicts }); + } + _observeUndoManager(mgr) { this._stopObservingUndoManager(); if (!mgr) return; @@ -281,6 +288,8 @@ export class EwEditorDoc extends LitElement { permissions, setEditable: (editable) => this._setEditable(editable), getToken: () => token, + onMergeConflictsChange: (hasConflicts) => this._emitMergeConflictsState(hasConflicts), + hostEl: this, extraPlugins: [ createExtensionsBridgePlugin(), createTrackingPlugin( @@ -336,7 +345,7 @@ export class EwEditorDoc extends LitElement { connectedCallback() { super.connectedCallback(); - this.shadowRoot.adoptedStyleSheets = [style]; + this.shadowRoot.adoptedStyleSheets = [style, buttons, diffStyle]; this._unsubscribeEditorActive = canvasBus.editorViewState.subscribe(({ view }) => { this.hidden = view === 'layout'; hideSelectionToolbar(); diff --git a/blocks/canvas/ew-editor-doc/prose.js b/blocks/canvas/ew-editor-doc/prose.js index b99e01d07..17daf918b 100644 --- a/blocks/canvas/ew-editor-doc/prose.js +++ b/blocks/canvas/ew-editor-doc/prose.js @@ -41,6 +41,9 @@ import { getNx } from '../../../scripts/utils.js'; import { getAuthToken } from '../../shared/utils.js'; import { generateColor, getCollabIdentity } from './utils/collab.js'; import { checkBlockLibraryConfigured } from '../editor-utils/block-slash.js'; +import { getDiffClass, addActiveView } from '../../edit/prose/diff/diff-utils.js'; +import { initDaMetadata } from '../../edit/utils/helpers.js'; +import { createMergeConflictsPlugin } from '../editor-utils/prose-merge-conflicts.js'; import { canvasBus } from '../utils/canvas-bus.js'; const { DA_ADMIN, DA_COLLAB, hashChange } = await import(`${getNx()}/utils/utils.js`); @@ -83,7 +86,7 @@ function checkLibraryConfiguredOnSync(wsProvider, canWrite) { export default async function initProse({ path, permissions, setEditable, getToken, - extraPlugins = [], + extraPlugins = [], onMergeConflictsChange, hostEl, }) { const editor = document.createElement('div'); editor.className = 'da-prose-mirror'; @@ -212,7 +215,7 @@ export default async function initProse({ if (canWrite) { plugins.unshift(createSlashMenuPlugin(), createSelectionToolbarPlugin()); - plugins.push(imageFocalPoint()); + plugins.push(imageFocalPoint(), createMergeConflictsPlugin(onMergeConflictsChange, hostEl)); } let state = EditorState.create({ schema, plugins }); @@ -220,11 +223,27 @@ export default async function initProse({ const fix = fixTables(state); if (fix) state = state.apply(fix.setMeta('addToHistory', false)); + const diffNodeViews = canWrite ? { + diff_added(node, view, getPos) { + const MergeConflictsAddedView = getDiffClass('da-diff-added', getSchema, () => {}, { isUpstream: false, hostEl }); + return new MergeConflictsAddedView(node, view, getPos); + }, + diff_deleted(node, view, getPos) { + const MergeConflictsDeletedView = getDiffClass('da-diff-deleted', getSchema, () => {}, { isUpstream: true, hostEl }); + return new MergeConflictsDeletedView(node, view, getPos); + }, + } : undefined; + viewRef = new EditorView(editor, { state, editable() { return canWrite; }, + nodeViews: diffNodeViews, }); + if (canWrite) addActiveView(viewRef); + + initDaMetadata(ydoc.getMap('daMetadata')); + const undoManager = yUndoPluginKey.getState(viewRef.state)?.undoManager ?? null; return { proseEl: editor, wsProvider, view: viewRef, ydoc, undoManager }; diff --git a/blocks/canvas/ew-editor-doc/utils/teardown.js b/blocks/canvas/ew-editor-doc/utils/teardown.js index a066b17ca..98c59279e 100644 --- a/blocks/canvas/ew-editor-doc/utils/teardown.js +++ b/blocks/canvas/ew-editor-doc/utils/teardown.js @@ -1,3 +1,5 @@ +import { removeActiveView } from '../../../edit/prose/diff/diff-utils.js'; + export function teardownEditorDocResources({ clearPortHandler, awarenessOff, @@ -14,6 +16,7 @@ export function teardownEditorDocResources({ wsProvider.disconnect({ data: 'unmount' }); } if (view) { + removeActiveView(view); view.destroy(); } if (proseEl?.parentNode) { diff --git a/blocks/canvas/utils/canvas-bus.js b/blocks/canvas/utils/canvas-bus.js index c2969aa7f..52154a143 100644 --- a/blocks/canvas/utils/canvas-bus.js +++ b/blocks/canvas/utils/canvas-bus.js @@ -42,6 +42,7 @@ export const canvasBus = Object.freeze({ newVersionRequest: createChannel(), undoState: createChannel(), + mergeConflictsState: createChannel(), editorViewState: createChannel(), editorHtmlState: createChannel({ replay: true }), editorSelectState: { diff --git a/blocks/edit/prose/diff/diff-actions.js b/blocks/edit/prose/diff/diff-actions.js index 33210242b..00b377189 100644 --- a/blocks/edit/prose/diff/diff-actions.js +++ b/blocks/edit/prose/diff/diff-actions.js @@ -117,7 +117,7 @@ function createCompositeButton({ switchBtn.appendChild(createTooltip(switchTooltip, 'diff-tooltip')); } - const confirmBtn = createButton('confirm-btn da-diff-btn-base-element', 'button', { 'aria-label': `${tooltip}` }); + const confirmBtn = createButton('nx-action-btn-icon confirm-btn da-diff-btn-base-element', 'button', { 'aria-label': `${tooltip}` }); confirmBtn.addEventListener('click', handler); if (tooltip) { confirmBtn.appendChild(createTooltip(tooltip, 'diff-tooltip')); diff --git a/blocks/edit/prose/diff/diff-global-dialog.js b/blocks/edit/prose/diff/diff-global-dialog.js index d30b66ed5..b4c7b153d 100644 --- a/blocks/edit/prose/diff/diff-global-dialog.js +++ b/blocks/edit/prose/diff/diff-global-dialog.js @@ -138,7 +138,7 @@ function createGlobalAction( const label = createElement('span', 'switch-btn da-diff-btn-base-element'); label.textContent = text; - const confirm = createButton('confirm-btn da-diff-btn-base-element', 'button', { 'aria-label': text }); + const confirm = createButton('nx-action-btn-icon confirm-btn da-diff-btn-base-element', 'button', { 'aria-label': text }); confirm.addEventListener('click', () => handleGlobalAction(action, activeViews, isLocNode)); confirm.appendChild(createTooltip(tooltipText, 'diff-tooltip')); diff --git a/blocks/edit/prose/diff/diff-overlay-ui.js b/blocks/edit/prose/diff/diff-overlay-ui.js index 6b21914fa..f4192de5e 100644 --- a/blocks/edit/prose/diff/diff-overlay-ui.js +++ b/blocks/edit/prose/diff/diff-overlay-ui.js @@ -20,10 +20,10 @@ export function getLangOverlay(upstream) { const labelBtn = createElement('span', 'diff-label da-diff-btn-base-element'); labelBtn.textContent = text; - const acceptBtn = createButton('diff-accept da-diff-btn-base-element', 'button', { 'aria-label': `Accept ${text}` }); + const acceptBtn = createButton('nx-action-btn-icon diff-accept da-diff-btn-base-element', 'button', { 'aria-label': `Accept ${text}` }); acceptBtn.appendChild(createTooltip(`Accept ${text}`, 'diff-tooltip')); - const deleteBtn = createButton('diff-delete da-diff-btn-base-element', 'button', { 'aria-label': `Delete ${text}` }); + const deleteBtn = createButton('nx-action-btn-icon diff-delete da-diff-btn-base-element', 'button', { 'aria-label': `Delete ${text}` }); deleteBtn.appendChild(createTooltip(`Delete ${text}`, 'diff-tooltip')); compositeBtn.appendChild(labelBtn); diff --git a/blocks/edit/prose/diff/diff-utils.css b/blocks/edit/prose/diff/diff-utils.css index 43fd34733..004632ae3 100644 --- a/blocks/edit/prose/diff/diff-utils.css +++ b/blocks/edit/prose/diff/diff-utils.css @@ -17,6 +17,14 @@ da-loc-added, border-radius: 6px; } +.loc-color-overlay.loc-regional { + background-color: rgb(144 42 222 / 20%); +} + +.loc-color-overlay.loc-langstore { + background-color: rgb(70 130 180 / 20%); +} + .loc-lang-overlay { z-index: 1001; } @@ -84,6 +92,10 @@ da-loc-added, pointer-events: auto; } +.da-prose-mirror.has-regional-edits .loc-sticky-buttons { + top: 64px; +} + /* When the container is short, the sticky buttons should be moved down to not block content */ .loc-single-container.is-short .loc-sticky-buttons, .loc-tabbed-container.is-short .loc-sticky-buttons, @@ -325,6 +337,10 @@ da-loc-added, z-index: 2; } +.loc-tabbed-color-overlay.is-hidden { + display: none; +} + @media (max-width: 600px) { .loc-tab-button { font-size: 12px; @@ -404,27 +420,24 @@ da-loc-added, .da-regional-edits-overlay { display: none; - position: absolute; - right: -48px; - z-index: 4; - padding: 16px 20px; - margin: 0 48px; + position: sticky; + top: 0; + z-index: 1111; + padding: 12px 20px; box-sizing: border-box; + width: 100%; + background: #fff; + box-shadow: 0 2px 4px rgb(0 0 0 / 10%); } .da-regional-edits-overlay.show { display: flex; - align-items: center; - justify-content: space-between; - gap: 20px; + justify-content: flex-end; } .da-regional-edits-actions { display: flex; gap: 12px; align-items: center; -} - -.da-prose-mirror.has-regional-edits .ProseMirror { - padding-top: 60px; + justify-content: flex-end; } diff --git a/blocks/edit/prose/diff/diff-utils.js b/blocks/edit/prose/diff/diff-utils.js index 603002ce2..2e24639a8 100644 --- a/blocks/edit/prose/diff/diff-utils.js +++ b/blocks/edit/prose/diff/diff-utils.js @@ -79,22 +79,21 @@ function setDiffLabelCssVars(daEditor) { } let locCssLoading = false; -async function loadLocCss() { +async function loadLocCss(hostEl) { if (locCssLoading) return; locCssLoading = true; try { const locSheet = await getSheet('/blocks/edit/prose/diff/diff-utils.css'); - const daEditor = document.querySelector('da-content')?.shadowRoot - ?.querySelector('da-editor'); - - if (daEditor?.shadowRoot) { - const existingSheets = daEditor.shadowRoot.adoptedStyleSheets || []; - daEditor.shadowRoot.adoptedStyleSheets = [...existingSheets, locSheet]; + if (hostEl?.shadowRoot) { + hostEl.shadowRoot.adoptedStyleSheets = [ + ...(hostEl.shadowRoot.adoptedStyleSheets || []), + locSheet, + ]; // Set CSS custom properties for diff labels - setDiffLabelCssVars(daEditor); + setDiffLabelCssVars(hostEl); } } catch (error) { // eslint-disable-next-line no-console @@ -181,7 +180,7 @@ function hasMatchingContent(nodeA, nodeB) { return true; } -export function checkForLocNodes(view) { +export function checkForLocNodes(view, hostEl) { const { doc } = view.state; const hasListLocNode = (node) => (node.type.name === 'bullet_list' || node.type.name === 'ordered_list') @@ -192,7 +191,7 @@ export function checkForLocNodes(view) { .some((node) => isLocNode(node) || hasListLocNode(node)); if (hasLocNodes) { - loadLocCss(); + loadLocCss(hostEl); showGlobalDialog(view); } else { hideGlobalDialog(); @@ -276,13 +275,18 @@ export function addActiveView(view) { activeViews.add(view); } -export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstream } = {}) { +export function removeActiveView(view) { + activeViews.delete(view); +} + +export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstream, hostEl } = {}) { return class { constructor(node, view, getPos) { this.node = node; this.view = view; this.getPos = getPos; this.schema = getSchema(); + this.hostEl = hostEl; const pos = getPos(); const { doc } = view.state; @@ -323,7 +327,7 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea } renderTabbedInterface(nodeA, view, posA, nodeB) { - loadLocCss(); + loadLocCss(this.hostEl); this.dom = createElement('div', 'loc-tabbed-container', { contentEditable: 'false' }); this.contentDOM = null; // Don't let ProseMirror manage content @@ -362,13 +366,11 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea }); if (targetTab === 'added') { - colorOverlay.style.display = 'block'; colorOverlay.className = 'loc-tabbed-color-overlay diff-bg-local'; } else if (targetTab === 'deleted') { - colorOverlay.style.display = 'block'; colorOverlay.className = 'loc-tabbed-color-overlay diff-bg-upstream'; } else if (targetTab === 'diff') { - colorOverlay.className = 'loc-tabbed-color-overlay diff-bg-diff'; + colorOverlay.className = 'loc-tabbed-color-overlay diff-bg-diff is-hidden'; const diffTab = tabContent.querySelector('[data-tab="diff"]'); if (diffTab && !diffTab.loaded) { @@ -430,7 +432,7 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea } renderSingleNode(node, view, pos, upstream) { - loadLocCss(); + loadLocCss(this.hostEl); const isDeleted = node.type.name === 'diff_deleted'; const viewClass = isDeleted ? 'loc-deleted-view' : 'loc-added-view'; @@ -447,8 +449,7 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea this.dom.appendChild(nodeDOM); // Create placeholder cover div immediately - const coverDiv = createElement('div', 'loc-color-overlay', { 'loc-temp-dom': '' }); - coverDiv.style.backgroundColor = upstream ? '#4682b433' : '#902ade33'; + const coverDiv = createElement('div', `loc-color-overlay ${upstream ? 'loc-langstore' : 'loc-regional'}`, { 'loc-temp-dom': '' }); this.dom.appendChild(coverDiv); // Create placeholder overlay @@ -483,9 +484,6 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea coverDiv.removeChild(this.langOverlay); this.langOverlay = overlay; coverDiv.appendChild(this.langOverlay); - - const className = `loc-color-overlay ${upstream ? 'loc-langstore' : 'loc-regional'}`; - coverDiv.className = className; } catch (error) { // eslint-disable-next-line no-console console.warn('Failed to load enhanced overlays:', error); diff --git a/blocks/edit/prose/index.js b/blocks/edit/prose/index.js index 54e841610..ab2df8aa5 100644 --- a/blocks/edit/prose/index.js +++ b/blocks/edit/prose/index.js @@ -195,6 +195,8 @@ async function loadCustomPlugins() { let lastCursorPosition = null; +let diffHostEl = null; + function dispatchTransaction(transaction) { if (!window.view) return; @@ -202,7 +204,7 @@ function dispatchTransaction(transaction) { window.view.updateState(newState); if (transaction.docChanged) { - debounce(checkForLocNodes, 500)(window.view); + debounce(checkForLocNodes, 500)(window.view, diffHostEl); } } @@ -520,6 +522,8 @@ export default async function initProse({ path, permissions, doc, daContent, wsP delete window.view; } + diffHostEl = daContent?.shadowRoot?.querySelector('da-editor') ?? null; + const connectionPromise = wsPromise || createConnection(path); const editor = document.createElement('div'); @@ -565,11 +569,11 @@ export default async function initProse({ path, permissions, doc, daContent, wsP dispatchTransaction, nodeViews: { diff_added(node, view, getPos) { - const LocAddedView = getDiffClass('da-diff-added', getSchema, dispatchTransaction, { isUpstream: false }); + const LocAddedView = getDiffClass('da-diff-added', getSchema, dispatchTransaction, { isUpstream: false, hostEl: diffHostEl }); return new LocAddedView(node, view, getPos); }, diff_deleted(node, view, getPos) { - const LocDeletedView = getDiffClass('da-diff-deleted', getSchema, dispatchTransaction, { isUpstream: true }); + const LocDeletedView = getDiffClass('da-diff-deleted', getSchema, dispatchTransaction, { isUpstream: true, hostEl: diffHostEl }); return new LocDeletedView(node, view, getPos); }, }, diff --git a/test/unit/blocks/canvas/editor-utils/prose-merge-conflicts.test.js b/test/unit/blocks/canvas/editor-utils/prose-merge-conflicts.test.js new file mode 100644 index 000000000..a462fb877 --- /dev/null +++ b/test/unit/blocks/canvas/editor-utils/prose-merge-conflicts.test.js @@ -0,0 +1,54 @@ +import { expect } from '@esm-bundle/chai'; +import { createMergeConflictsPlugin } from '../../../../../blocks/canvas/editor-utils/prose-merge-conflicts.js'; +import { createTestEditor, destroyEditor } from '../../edit/prose/test-helpers.js'; + +const waitFor = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); }); + +describe('createMergeConflictsPlugin', () => { + let editor; + + afterEach(() => { + if (editor) destroyEditor(editor); + editor = undefined; + }); + + it('reports false shortly after mount when the doc has no merge-conflict nodes', async () => { + const calls = []; + const plugin = createMergeConflictsPlugin((hasMergeConflicts) => calls.push(hasMergeConflicts)); + editor = await createTestEditor({ additionalPlugins: [plugin] }); + + await waitFor(600); + + expect(calls).to.deep.equal([false]); + }); + + it('reports true once a diff_added node is inserted', async () => { + const calls = []; + const plugin = createMergeConflictsPlugin((hasMergeConflicts) => calls.push(hasMergeConflicts)); + editor = await createTestEditor({ additionalPlugins: [plugin] }); + + const { schema } = editor.view.state; + const para = schema.nodes.paragraph.create(null, schema.text('new')); + const diffAdded = schema.nodes.diff_added.create({}, para); + const tr = editor.view.state.tr.insert(editor.view.state.doc.content.size, diffAdded); + editor.view.dispatch(tr); + + await waitFor(600); + + expect(calls[calls.length - 1]).to.equal(true); + }); + + it('does not re-check on transactions that leave the doc unchanged', async () => { + const calls = []; + const plugin = createMergeConflictsPlugin((hasMergeConflicts) => calls.push(hasMergeConflicts)); + editor = await createTestEditor({ additionalPlugins: [plugin] }); + + await waitFor(600); // let the initial mount check land + calls.length = 0; + + editor.view.dispatch(editor.view.state.tr); // no doc change + await waitFor(600); + + expect(calls).to.deep.equal([]); + }); +});