mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Implementation/72687 new connection error restore banner v2 (#22591)
--------- Co-authored-by: Kabiru Mwenja <k.mwenja@openproject.com>
This commit is contained in:
@@ -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,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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 <DocumentLoadingSkeleton />;
|
||||
|
||||
@@ -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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<typeof setTimeout>|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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+26
-7
@@ -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
|
||||
|
||||
|
||||
+5
-11
@@ -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
|
||||
%>
|
||||
|
||||
-14
@@ -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
|
||||
|
||||
+73
@@ -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
|
||||
%>
|
||||
+41
@@ -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
|
||||
+1
-5
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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] }) }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user