diff --git a/frontend/src/elements/block-note-element.ts b/frontend/src/elements/block-note-element.ts
index 85244875e54..e9ceb8c65b9 100644
--- a/frontend/src/elements/block-note-element.ts
+++ b/frontend/src/elements/block-note-element.ts
@@ -30,8 +30,6 @@
import { User } from '@blocknote/core/comments';
import { HocuspocusProvider } from '@hocuspocus/provider';
-import { Application } from '@hotwired/stimulus';
-import FlashController from 'core-stimulus/controllers/flash.controller';
import { LiveCollaborationManager } from 'core-stimulus/helpers/live-collaboration-helpers';
import { ShadowDomWrapper } from 'op-blocknote-extensions';
import React from 'react';
@@ -40,11 +38,9 @@ import { createRoot } from 'react-dom/client';
import OpBlockNoteContainer from '../react/OpBlockNoteContainer';
class BlockNoteElement extends HTMLElement {
- private stimulusRoot:HTMLDivElement;
+ private editorRoot:HTMLDivElement;
private editorMount:HTMLDivElement;
- private errorContainer:HTMLDivElement;
private reactRoot:Root|null = null;
- private stimulusApp:Application|null = null;
private renderCallback:((provider:HocuspocusProvider) => void) | null = null;
constructor() {
@@ -52,30 +48,22 @@ class BlockNoteElement extends HTMLElement {
const shadowRoot = this.attachShadow({ mode: 'open' });
- // Wrapper div as Stimulus root so both errorContainer and editorMount are in scope
- this.stimulusRoot = document.createElement('div');
+ this.editorRoot = document.createElement('div');
const browserSpecificClasses = this.getAttribute('browser-specific-classes')?.split(' ') ?? [];
if (browserSpecificClasses.length > 0) {
- this.stimulusRoot.classList.add(...browserSpecificClasses);
+ this.editorRoot.classList.add(...browserSpecificClasses);
}
// Clone the blank-target link description into the shadow DOM
// so aria-describedby references resolve for links inside the editor
const blankLinkDesc = document.getElementById('open-blank-target-link-description');
if (blankLinkDesc) {
- this.stimulusRoot.appendChild(blankLinkDesc.cloneNode(true));
+ this.editorRoot.appendChild(blankLinkDesc.cloneNode(true));
}
- // Container for connection error/recovery messages (rendered by React via fetchConnectionTemplate)
- this.errorContainer = document.createElement('div');
- this.errorContainer.id = 'documents-show-edit-view-connection-error-notice-component';
- this.errorContainer.dataset.controller = 'flash';
- this.errorContainer.dataset.flashAutohideValue = 'true';
-
this.editorMount = document.createElement('div');
- this.stimulusRoot.appendChild(this.errorContainer);
- this.stimulusRoot.appendChild(this.editorMount);
- shadowRoot.appendChild(this.stimulusRoot);
+ this.editorRoot.appendChild(this.editorMount);
+ shadowRoot.appendChild(this.editorRoot);
const blockNoteStylesheetUrl = this.getAttribute('blocknote-stylesheet-url');
if (blockNoteStylesheetUrl) {
@@ -98,11 +86,6 @@ class BlockNoteElement extends HTMLElement {
const collaborationEnabled = this.getAttribute('collaboration-enabled') === 'true';
if (!collaborationEnabled) return;
- // Initialize Stimulus application within shadow DOM
- this.stimulusApp = Application.start(this.stimulusRoot);
- this.stimulusApp.register('flash', FlashController);
-
- // Initialize React application within shadow DOM
this.reactRoot = createRoot(this.editorMount);
this.renderCallback = (provider:HocuspocusProvider) => {
@@ -125,11 +108,6 @@ class BlockNoteElement extends HTMLElement {
this.reactRoot.unmount();
this.reactRoot = null;
}
-
- if (this.stimulusApp) {
- this.stimulusApp.stop();
- this.stimulusApp = null;
- }
}
private BlockNoteReactContainer = (hocuspocusProvider:HocuspocusProvider) => {
@@ -145,7 +123,6 @@ class BlockNoteElement extends HTMLElement {
attachmentsUploadUrl: this.getAttribute('attachments-upload-url') ?? '',
attachmentsCollectionKey: this.getAttribute('attachments-collection-key') ?? '',
hocuspocusProvider,
- errorContainer: this.errorContainer,
}
)
);
diff --git a/frontend/src/react/OpBlockNoteContainer.tsx b/frontend/src/react/OpBlockNoteContainer.tsx
index 37946b9038c..c4b42cf5931 100644
--- a/frontend/src/react/OpBlockNoteContainer.tsx
+++ b/frontend/src/react/OpBlockNoteContainer.tsx
@@ -34,7 +34,6 @@ import { useEffect, useRef } from 'react';
import * as Y from 'yjs';
import { DocumentLoadingSkeleton } from './components/DocumentLoadingSkeleton';
import { OpBlockNoteEditor } from './components/OpBlockNoteEditor';
-import { fetchConnectionTemplate } from './helpers/connection-template-fetcher';
import { useCollaboration } from './hooks/useCollaboration';
export interface OpBlockNoteContainerProps {
@@ -44,7 +43,6 @@ export interface OpBlockNoteContainerProps {
attachmentsUploadUrl:string;
attachmentsCollectionKey:string;
hocuspocusProvider:HocuspocusProvider;
- errorContainer?:HTMLElement;
}
export default function OpBlockNoteContainer({
@@ -54,24 +52,19 @@ export default function OpBlockNoteContainer({
attachmentsUploadUrl,
attachmentsCollectionKey,
hocuspocusProvider,
- errorContainer,
}:OpBlockNoteContainerProps) {
const doc:Y.Doc = hocuspocusProvider.document;
const { isLoading, offlineMode } = useCollaboration(hocuspocusProvider);
const hadErrorRef = useRef(false);
- // Fetch error/recovery template based on connection state
useEffect(() => {
- if (!errorContainer) return;
-
if (offlineMode) {
hadErrorRef.current = true;
- void fetchConnectionTemplate('error', errorContainer, { blocking: true });
+ window.dispatchEvent(new CustomEvent('documents:connection-error'));
} else if (hadErrorRef.current) {
- // Only fetch recovery if we previously had an error (avoid fetching on initial render)
- void fetchConnectionTemplate('recovery', errorContainer);
+ window.dispatchEvent(new CustomEvent('documents:connection-recovery'));
}
- }, [offlineMode, errorContainer]);
+ }, [offlineMode]);
if (isLoading) {
return ;
diff --git a/frontend/src/react/helpers/connection-template-fetcher.ts b/frontend/src/react/helpers/connection-template-fetcher.ts
deleted file mode 100644
index 860b8e55d49..00000000000
--- a/frontend/src/react/helpers/connection-template-fetcher.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * -- copyright
- * OpenProject is an open source project management software.
- * Copyright (C) the OpenProject GmbH
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License version 3.
- *
- * OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
- * Copyright (C) 2006-2013 Jean-Philippe Lang
- * Copyright (C) 2010-2013 the ChiliProject Team
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License
- * as published by the Free Software Foundation; either version 2
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- *
- * See COPYRIGHT and LICENSE files for more details.
- * ++
- */
-
-/**
- * Fetches connection error/recovery templates from the server.
- * Used by the BlockNote editor to display server-rendered error messages
- * when the collaboration connection fails.
- */
-
-function getDocumentIdFromUrl():string|null {
- const match = /\/documents\/(\d+)/.exec(window.location.pathname);
- return match ? match[1] : null;
-}
-
-/**
- * Parses Turbo Stream response and extracts the template content.
- * Standard Turbo.renderStreamMessage() uses document.getElementById()
- * which can't find Shadow DOM elements, so we parse manually.
- */
-function parseTurboStreamContent(html:string):string|null {
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, 'text/html');
- const turboStream = doc.querySelector('turbo-stream');
-
- if (!turboStream) {
- console.error('No turbo-stream element found in response');
- return null;
- }
-
- const template = turboStream.querySelector('template');
- if (!template) {
- console.error('No template element found in turbo-stream');
- return null;
- }
-
- return template.innerHTML;
-}
-
-export async function fetchConnectionTemplate(
- type:'error'|'recovery',
- targetElement:HTMLElement,
- options:{ blocking?:boolean } = {},
-):Promise {
- const documentId = getDocumentIdFromUrl();
- if (!documentId) {
- console.error('Could not extract document ID from URL');
- return;
- }
-
- const url = new URL(`/documents/${documentId}/render_connection_${type}`, window.location.origin);
- if (options.blocking) url.searchParams.set('blocking', 'true');
-
- try {
- const response = await fetch(url, {
- method: 'GET',
- headers: {
- Accept: 'text/vnd.turbo-stream.html',
- },
- });
-
- if (!response.ok) {
- throw new Error(`Failed to fetch ${url}: ${response.status}`);
- }
-
- const html = await response.text();
- const content = parseTurboStreamContent(html);
-
- if (content !== null) {
- targetElement.innerHTML = content;
-
- // Attach reload handler to the error button (Stimulus not available in Shadow DOM)
- const reloadButton = targetElement.querySelector('#connection-error-reload-button');
- if (reloadButton) {
- reloadButton.addEventListener('click', () => window.location.reload());
- }
- }
- } catch (error) {
- console.error('Error fetching connection template:', error);
- }
-}
diff --git a/frontend/src/stimulus/controllers/dynamic/documents/connection-status.controller.ts b/frontend/src/stimulus/controllers/dynamic/documents/connection-status.controller.ts
new file mode 100644
index 00000000000..5d61e075afd
--- /dev/null
+++ b/frontend/src/stimulus/controllers/dynamic/documents/connection-status.controller.ts
@@ -0,0 +1,72 @@
+/*
+ * -- copyright
+ * OpenProject is an open source project management software.
+ * Copyright (C) the OpenProject GmbH
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License version 3.
+ *
+ * OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+ * Copyright (C) 2006-2013 Jean-Philippe Lang
+ * Copyright (C) 2010-2013 the ChiliProject Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * See COPYRIGHT and LICENSE files for more details.
+ * ++
+ */
+
+import { Controller } from '@hotwired/stimulus';
+
+type ConnectionState = 'live' | 'offline' | 'recovered';
+
+export default class ConnectionStatusController extends Controller {
+ static targets = ['live', 'offline', 'recovered'];
+
+ declare readonly offlineTarget:HTMLElement;
+ declare readonly recoveredTarget:HTMLElement;
+ declare readonly liveTargets:HTMLElement[];
+
+ private recoveryTimeout:ReturnType|null = null;
+
+ showOffline():void {
+ this.clearRecoveryTimeout();
+ this.activateState('offline');
+ }
+
+ showRecovered():void {
+ this.clearRecoveryTimeout();
+ this.activateState('recovered');
+
+ this.recoveryTimeout = setTimeout(() => this.activateState('live'), 5000);
+ }
+
+ disconnect():void {
+ this.clearRecoveryTimeout();
+ }
+
+ private activateState(state:ConnectionState):void {
+ this.offlineTarget.hidden = state !== 'offline';
+ this.recoveredTarget.hidden = state !== 'recovered';
+ this.liveTargets.forEach((el) => { el.hidden = state !== 'live'; });
+ }
+
+ private clearRecoveryTimeout():void {
+ if (this.recoveryTimeout !== null) {
+ clearTimeout(this.recoveryTimeout);
+ this.recoveryTimeout = null;
+ }
+ }
+}
diff --git a/frontend/src/stimulus/controllers/dynamic/documents/editor-connection.controller.ts b/frontend/src/stimulus/controllers/dynamic/documents/editor-connection.controller.ts
new file mode 100644
index 00000000000..5caba4647f0
--- /dev/null
+++ b/frontend/src/stimulus/controllers/dynamic/documents/editor-connection.controller.ts
@@ -0,0 +1,48 @@
+/*
+ * -- copyright
+ * OpenProject is an open source project management software.
+ * Copyright (C) the OpenProject GmbH
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License version 3.
+ *
+ * OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+ * Copyright (C) 2006-2013 Jean-Philippe Lang
+ * Copyright (C) 2010-2013 the ChiliProject Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * See COPYRIGHT and LICENSE files for more details.
+ * ++
+ */
+
+import { Controller } from '@hotwired/stimulus';
+
+export default class EditorConnectionController extends Controller {
+ static targets = ['error', 'editor'];
+
+ declare readonly errorTarget:HTMLElement;
+ declare readonly editorTarget:HTMLElement;
+
+ showError():void {
+ this.errorTarget.hidden = false;
+ this.editorTarget.hidden = true;
+ }
+
+ showEditor():void {
+ this.errorTarget.hidden = true;
+ this.editorTarget.hidden = false;
+ }
+}
diff --git a/modules/documents/app/components/documents/show_edit_view/block_note_editor_component.html.erb b/modules/documents/app/components/documents/show_edit_view/block_note_editor_component.html.erb
index e081ed9065f..7177c56a881 100644
--- a/modules/documents/app/components/documents/show_edit_view/block_note_editor_component.html.erb
+++ b/modules/documents/app/components/documents/show_edit_view/block_note_editor_component.html.erb
@@ -54,13 +54,32 @@
end
layout.with_tab(name: I18n.t("activerecord.models.document"), show_left: true) do
- primer_form_with(
- model: document,
- url: document_path(document),
- method: :patch,
- data: { turbo: false }
- ) do |form|
- render Documents::BlockNoteEditorForm.new(form, token_payload:, readonly:)
+ tag.div(
+ data: {
+ controller: "documents--editor-connection",
+ action: "documents:connection-error@window->documents--editor-connection#showError " \
+ "documents:connection-recovery@window->documents--editor-connection#showEditor"
+ }
+ ) do
+ safe_join(
+ [
+ tag.div(
+ render(Documents::ShowEditView::ConnectionErrorNoticeComponent.new(document)),
+ hidden: true,
+ data: { "documents--editor-connection-target": "error" }
+ ),
+ tag.div(data: { "documents--editor-connection-target": "editor" }) do
+ primer_form_with(
+ model: document,
+ url: document_path(document),
+ method: :patch,
+ data: { turbo: false }
+ ) do |form|
+ render Documents::BlockNoteEditorForm.new(form, token_payload:, readonly:)
+ end
+ end
+ ]
+ )
end
end
diff --git a/modules/documents/app/components/documents/show_edit_view/connection_error_notice_component.html.erb b/modules/documents/app/components/documents/show_edit_view/connection_error_notice_component.html.erb
index 1dca7a5e8b5..7f02470df25 100644
--- a/modules/documents/app/components/documents/show_edit_view/connection_error_notice_component.html.erb
+++ b/modules/documents/app/components/documents/show_edit_view/connection_error_notice_component.html.erb
@@ -33,18 +33,12 @@
component_wrapper do
render(Primer::Alpha::Banner.new(icon: :stop, scheme: :danger, test_selector: "connection-error-notice")) do |banner|
banner.with_action_button(
- id: "connection-error-reload-button",
- data: { turbo: false },
- size: :medium
+ tag: :a,
+ href: document_path(document),
+ size: :medium,
+ data: { turbo: false }
) { I18n.t("documents.show_edit_view.connection_error_notice.action") }
- description_key = if blocking
- "documents.show_edit_view.connection_error_notice.description_server_unavailable"
- elsif readonly
- "documents.show_edit_view.connection_error_notice.description_readonly"
- else
- "documents.show_edit_view.connection_error_notice.description"
- end
- I18n.t(description_key)
+ I18n.t("documents.show_edit_view.connection_error_notice.description_server_unavailable")
end
end
%>
diff --git a/modules/documents/app/components/documents/show_edit_view/connection_error_notice_component.rb b/modules/documents/app/components/documents/show_edit_view/connection_error_notice_component.rb
index 7f06068473e..d390af57bac 100644
--- a/modules/documents/app/components/documents/show_edit_view/connection_error_notice_component.rb
+++ b/modules/documents/app/components/documents/show_edit_view/connection_error_notice_component.rb
@@ -34,20 +34,6 @@ module Documents
include OpTurbo::Streamable
alias_method :document, :model
-
- def initialize(document, blocking: false)
- super(document)
- @blocking = blocking
- end
-
- private
-
- attr_reader :blocking
-
- def readonly
- @readonly ||= User.current.allowed_in_project?(:view_documents, document.project) &&
- !User.current.allowed_in_project?(:manage_documents, document.project)
- end
end
end
end
diff --git a/modules/documents/app/components/documents/show_edit_view/page_header/connection_status_component.html.erb b/modules/documents/app/components/documents/show_edit_view/page_header/connection_status_component.html.erb
new file mode 100644
index 00000000000..a4a07d07993
--- /dev/null
+++ b/modules/documents/app/components/documents/show_edit_view/page_header/connection_status_component.html.erb
@@ -0,0 +1,73 @@
+<%#
+ -- copyright
+ OpenProject is an open source project management software.
+ Copyright (C) the OpenProject GmbH
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License version 3.
+
+ OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+ Copyright (C) 2006-2013 Jean-Philippe Lang
+ Copyright (C) 2010-2013 the ChiliProject Team
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License
+ as published by the Free Software Foundation; either version 2
+ of the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+ See COPYRIGHT and LICENSE files for more details.
+
+ ++#
+%>
+
+<%=
+ component_wrapper(
+ tag: :div,
+ data: {
+ controller: "documents--connection-status",
+ action: "documents:connection-error@window->documents--connection-status#showOffline " \
+ "documents:connection-recovery@window->documents--connection-status#showRecovered"
+ }
+ ) do
+ flex_layout(align_items: :center) do |flex|
+ flex.with_column(mr: 3, data: { "documents--connection-status-target": "live" }) do
+ render(Documents::ShowEditView::PageHeader::LiveUsersComponent.new)
+ end
+
+ flex.with_column(data: { "documents--connection-status-target": "live" }) do
+ render(Documents::ShowEditView::PageHeader::LiveSavedAtComponent.new(model))
+ end
+
+ flex.with_column(data: { "documents--connection-status-target": "offline" }, hidden: true) do
+ flex_layout(align_items: :center) do |row|
+ row.with_column(mr: 1) do
+ render(Primer::Beta::Octicon.new(icon: :alert, color: :attention, size: :small))
+ end
+ row.with_column do
+ render(Primer::Beta::Text.new(color: :attention)) { t("documents.info_line.currently_offline") }
+ end
+ end
+ end
+
+ flex.with_column(data: { "documents--connection-status-target": "recovered" }, hidden: true) do
+ flex_layout(align_items: :center) do |row|
+ row.with_column(mr: 1) do
+ render(Primer::Beta::Octicon.new(icon: :"check-circle", color: :success, size: :small))
+ end
+ row.with_column do
+ render(Primer::Beta::Text.new(color: :success)) { t("documents.info_line.connection_restored") }
+ end
+ end
+ end
+ end
+ end
+%>
diff --git a/modules/documents/app/components/documents/show_edit_view/page_header/connection_status_component.rb b/modules/documents/app/components/documents/show_edit_view/page_header/connection_status_component.rb
new file mode 100644
index 00000000000..b8b9afa3612
--- /dev/null
+++ b/modules/documents/app/components/documents/show_edit_view/page_header/connection_status_component.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+#++
+#
+
+module Documents
+ module ShowEditView
+ module PageHeader
+ class ConnectionStatusComponent < ApplicationComponent
+ include OpPrimer::ComponentHelpers
+ include OpTurbo::Streamable
+ end
+ end
+ end
+end
diff --git a/modules/documents/app/components/documents/show_edit_view/page_header/info_line_component.html.erb b/modules/documents/app/components/documents/show_edit_view/page_header/info_line_component.html.erb
index 521a9cc5458..1ba9b6eb253 100644
--- a/modules/documents/app/components/documents/show_edit_view/page_header/info_line_component.html.erb
+++ b/modules/documents/app/components/documents/show_edit_view/page_header/info_line_component.html.erb
@@ -59,12 +59,8 @@
end
end
- flex.with_column(mr: 3) do
- render(Documents::ShowEditView::PageHeader::LiveUsersComponent.new)
- end
-
flex.with_column do
- render(Documents::ShowEditView::PageHeader::LiveSavedAtComponent.new(document))
+ render(Documents::ShowEditView::PageHeader::ConnectionStatusComponent.new(document))
end
end
end
diff --git a/modules/documents/app/controllers/documents_controller.rb b/modules/documents/app/controllers/documents_controller.rb
index a5af68d90d5..4d05b12cdc6 100644
--- a/modules/documents/app/controllers/documents_controller.rb
+++ b/modules/documents/app/controllers/documents_controller.rb
@@ -78,24 +78,6 @@ class DocumentsController < ApplicationController
respond_with_turbo_streams
end
- def render_connection_error
- blocking = ActiveRecord::Type::Boolean.new.cast(params[:blocking].presence || false)
- update_via_turbo_stream(
- component: Documents::ShowEditView::ConnectionErrorNoticeComponent.new(@document, blocking:)
- )
-
- respond_with_turbo_streams
- end
-
- def render_connection_recovery
- render_success_flash_message_via_turbo_stream(
- message: I18n.t("documents.show_edit_view.connection_recovery_notice.description"),
- unique_key: "document-connection-recovery-notice-#{@document.id}"
- )
-
- respond_with_turbo_streams
- end
-
def new
@document = @project.documents.build
@document.attributes = document_params
diff --git a/modules/documents/config/locales/en.yml b/modules/documents/config/locales/en.yml
index 7a309d5a42c..a40fbc42bd5 100644
--- a/modules/documents/config/locales/en.yml
+++ b/modules/documents/config/locales/en.yml
@@ -89,17 +89,10 @@ en:
show_edit_view:
connection_error_notice:
- description: |-
- You are currently offline. You can continue editing. Your changes will be synced when the connection is restored.
- description_readonly: |-
- You are currently offline.
- Real-time updates will resume once the connection is restored.
description_server_unavailable: |-
Unable to open document because the real-time text collaboration server is unreachable.
Please contact the administrator if the problem persists.
action: Try again
- connection_recovery_notice:
- description: "The connection to the real-time text collaboration server has been restored."
tabs: "Document tabs"
index_page:
@@ -115,6 +108,10 @@ en:
last_updated_at: "Last saved %{time}."
+ info_line:
+ currently_offline: "You are currently offline."
+ connection_restored: "You are now back online."
+
active_editors: "Active editors"
active_editors_count:
one: "1 active editor"
diff --git a/modules/documents/config/routes.rb b/modules/documents/config/routes.rb
index 92d91965aa2..768ba16e279 100644
--- a/modules/documents/config/routes.rb
+++ b/modules/documents/config/routes.rb
@@ -49,8 +49,6 @@ Rails.application.routes.draw do
get :delete_dialog
get :render_avatars, defaults: { format: :turbo_stream }
get :render_last_saved_at, defaults: { format: :turbo_stream }
- get :render_connection_error, defaults: { format: :turbo_stream }
- get :render_connection_recovery, defaults: { format: :turbo_stream }
end
end
diff --git a/modules/documents/lib/open_project/documents/engine.rb b/modules/documents/lib/open_project/documents/engine.rb
index a7affa56423..eca2a63beca 100644
--- a/modules/documents/lib/open_project/documents/engine.rb
+++ b/modules/documents/lib/open_project/documents/engine.rb
@@ -57,7 +57,6 @@ module OpenProject::Documents
documents: %i[
index search show download
render_avatars render_last_saved_at
- render_connection_error render_connection_recovery
],
"documents/menus": %i[show],
"documents/refresh_tokens": %i[create]
diff --git a/modules/documents/spec/controllers/documents_controller_spec.rb b/modules/documents/spec/controllers/documents_controller_spec.rb
index ff8e4aaea72..9d762982d41 100644
--- a/modules/documents/spec/controllers/documents_controller_spec.rb
+++ b/modules/documents/spec/controllers/documents_controller_spec.rb
@@ -183,8 +183,9 @@ RSpec.describe DocumentsController do
end
describe "setup_collaboration_context",
- with_config: {
- collaborative_editing_hocuspocus_url: "wss://hocuspocus.local",
+ with_settings: {
+ real_time_text_collaboration_enabled: true,
+ collaborative_editing_hocuspocus_url: "wss://hocuspocus.example.com",
collaborative_editing_hocuspocus_secret: "secret1234"
} do
let(:user_with_manage) { create(:user, member_with_permissions: { project => %i[view_documents manage_documents] }) }
diff --git a/modules/documents/spec/features/real_time_collaboration_spec.rb b/modules/documents/spec/features/real_time_collaboration_spec.rb
index a9640210866..015ecfcdde6 100644
--- a/modules/documents/spec/features/real_time_collaboration_spec.rb
+++ b/modules/documents/spec/features/real_time_collaboration_spec.rb
@@ -100,14 +100,14 @@ RSpec.describe "Real-time collaboration with Hocuspocus for documents",
it "blocks the editor and shows a server-unavailable message" do
visit document_path(document)
- expect(page).to have_test_selector("blocknote-document-description")
- expect(editor.shadow_root).to have_css(
+ expect(page).to have_css(
"[data-test-selector='connection-error-notice']",
text: "Unable to open document because the real-time text collaboration server " \
"is unreachable. Please contact the administrator if the problem persists.",
wait: 10
)
- expect(editor.shadow_root).to have_no_css("div[role='textbox']")
+ expect(page).to have_no_test_selector("blocknote-document-description", wait: 0)
+ expect(page).to have_test_selector("document-info-line", text: "You are currently offline.")
end
end