Merge pull request #21025 from opf/impl/68764-awareness-live-updates

Impl/68764 awareness live updates
This commit is contained in:
Bruno Pagno
2025-11-20 12:16:56 +01:00
committed by GitHub
22 changed files with 507 additions and 85 deletions
+21 -30
View File
@@ -36,21 +36,25 @@ import { getDefaultReactSlashMenuItems, SuggestionMenuController, useCreateBlock
import { HocuspocusProvider } from '@hocuspocus/provider';
import { OpColorMode } from 'core-app/core/setup/globals/theme-utils';
import { IUploadFile } from 'core-app/core/upload/upload.service';
import { LiveCollaborationManager } from 'core-stimulus/helpers/live-collaboration-helpers';
import { initOpenProjectApi, openProjectWorkPackageBlockSpec, openProjectWorkPackageSlashMenu } from 'op-blocknote-extensions';
import { useEffect, useState } from 'react';
import { firstValueFrom } from 'rxjs';
import * as Y from 'yjs';
interface CollaborativeUser {
name:string;
color:string;
}
export interface OpBlockNoteContainerProps {
inputField:HTMLInputElement;
inputText?:string;
hocuspocusUrl:string;
oauthToken:string,
activeUser:User;
documentName:string;
documentId:string;
openProjectUrl:string;
attachmentsUploadUrl:string;
attachmentsCollectionKey:string;
hocuspocusProvider?:HocuspocusProvider;
}
const schema = BlockNoteSchema.create().extend({
@@ -63,14 +67,11 @@ const detectTheme = ():OpColorMode => { return window.OpenProject.theme.detectOp
export default function OpBlockNoteContainer({ inputField,
inputText,
hocuspocusUrl,
oauthToken,
activeUser,
documentName,
documentId,
openProjectUrl,
attachmentsUploadUrl,
attachmentsCollectionKey }:OpBlockNoteContainerProps) {
attachmentsCollectionKey,
hocuspocusProvider }:OpBlockNoteContainerProps) {
const [isLoading, setIsLoading] = useState(true);
const [theme, setTheme] = useState<OpColorMode>(detectTheme());
@@ -80,33 +81,20 @@ export default function OpBlockNoteContainer({ inputField,
const blockNoteLocaleString = Object.keys(blockNoteLocales).includes(userLocale) ? userLocale : 'en';
const blockNoteLocale = blockNoteLocales[blockNoteLocaleString as keyof typeof blockNoteLocales];
let doc = new Y.Doc();
const collaborationEnabled = Boolean(hocuspocusUrl && documentName && oauthToken && activeUser);
let hocuspocusProvider:HocuspocusProvider | null = null;
let doc = LiveCollaborationManager.ydoc;
let editorParams:Partial<BlockNoteEditorOptions<typeof schema.blockSchema, typeof schema.inlineContentSchema, typeof schema.styleSchema>>;
if(collaborationEnabled) {
const url = new URL(hocuspocusUrl);
url.searchParams.set('document_id', documentId);
url.searchParams.set('openproject_base_path', openProjectUrl);
hocuspocusProvider = new HocuspocusProvider({
url: url.toString(),
name: documentName,
token: oauthToken,
document: doc
});
if(hocuspocusProvider) {
editorParams = {
schema,
collaboration: {
provider: hocuspocusProvider,
fragment: doc.getXmlFragment('document-store'),
user: {
id: activeUser.id,
name: activeUser.username,
color: '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0'),
},
} as unknown as CollaborativeUser,
showCursorLabels: 'activity'
},
dictionary: blockNoteLocale,
@@ -158,7 +146,9 @@ export default function OpBlockNoteContainer({ inputField,
try {
const service = pluginContext.services.attachmentsResourceService;
const iUploadFile = fileToIUploadFile(file);
const result = await service.addAttachments(attachmentsCollectionKey, attachmentsUploadUrl, [iUploadFile]).toPromise();
const result = await firstValueFrom(
service.addAttachments(attachmentsCollectionKey, attachmentsUploadUrl, [iUploadFile])
);
return result?.[0]._links.staticDownloadLocation.href ?? '';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -185,7 +175,8 @@ export default function OpBlockNoteContainer({ inputField,
inputField.value = b64;
};
if(collaborationEnabled && hocuspocusProvider) {
if(hocuspocusProvider) {
if (hocuspocusProvider.synced) { setIsLoading(false); }
hocuspocusProvider.on('synced', () => setIsLoading(false));
hocuspocusProvider.on('disconnect', () => setIsLoading(true));
} else {
@@ -194,14 +185,14 @@ export default function OpBlockNoteContainer({ inputField,
}
return () => {
if (collaborationEnabled && hocuspocusProvider) {
if (hocuspocusProvider) {
hocuspocusProvider.destroy();
} else {
// disable Yjs update listener. Opposite of doc.on('update', ...);
doc.off('update', updateInput);
}
};
}, []);
}, [hocuspocusProvider]);
useEffect(() => {
const handleThemeChange = () => {
@@ -28,11 +28,13 @@
* ++
*/
import { User } from '@blocknote/core/comments';
import type { HocuspocusProvider } from '@hocuspocus/provider';
import { Controller } from '@hotwired/stimulus';
import { LiveCollaborationManager } from 'core-stimulus/helpers/live-collaboration-helpers';
import React from 'react';
import { createRoot } from 'react-dom/client';
import OpBlockNoteContainer from '../../../react/OpBlockNoteContainer';
import { User } from '@blocknote/core/comments';
export default class extends Controller {
static targets = [
@@ -40,47 +42,48 @@ export default class extends Controller {
'blockNoteInputField',
];
declare readonly blockNoteEditorTarget:HTMLElement;
declare readonly blockNoteInputFieldTarget:HTMLInputElement;
static values = {
inputText: String,
activeUser: Object,
hocuspocusUrl: String,
oauthToken: String,
documentName: String,
documentId: String,
openProjectUrl: String,
attachmentsUploadUrl: String,
attachmentsCollectionKey: String,
collaborationEnabled: Boolean,
};
declare readonly blockNoteEditorTarget:HTMLElement;
declare readonly blockNoteInputFieldTarget:HTMLInputElement;
declare readonly inputTextValue:string;
declare readonly activeUserValue:User;
declare readonly hocuspocusUrlValue:string;
declare readonly oauthTokenValue:string;
declare readonly documentNameValue:string;
declare readonly documentIdValue:string;
declare readonly openProjectUrlValue:string;
declare readonly attachmentsUploadUrlValue:string;
declare readonly attachmentsCollectionKeyValue:string;
declare readonly collaborationEnabledValue:string;
connect() {
const root = createRoot(this.blockNoteEditorTarget);
root.render(this.BlockNoteReactContainer());
if (this.collaborationEnabledValue) {
LiveCollaborationManager.onReady((hocuspocusProvider) => {
root.render(this.BlockNoteReactContainer(hocuspocusProvider));
});
} else {
root.render(this.BlockNoteReactContainer());
}
}
BlockNoteReactContainer() {
BlockNoteReactContainer(hocuspocusProvider?:HocuspocusProvider) {
return React.createElement(OpBlockNoteContainer, {
inputField: this.blockNoteInputFieldTarget,
inputText: this.inputTextValue,
hocuspocusUrl: this.hocuspocusUrlValue,
oauthToken: this.oauthTokenValue,
activeUser: this.activeUserValue,
documentName: this.documentNameValue,
documentId: this.documentIdValue,
openProjectUrl: this.openProjectUrlValue,
attachmentsUploadUrl: this.attachmentsUploadUrlValue,
attachmentsCollectionKey: this.attachmentsCollectionKeyValue,
hocuspocusProvider: hocuspocusProvider,
});
}
}
@@ -0,0 +1,64 @@
/*
* -- 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 { HocuspocusProvider } from '@hocuspocus/provider';
import { Controller } from '@hotwired/stimulus';
import { LiveCollaborationManager } from 'core-stimulus/helpers/live-collaboration-helpers';
export default class extends Controller {
static values = {
hocuspocusUrl: String,
openProjectUrl: String,
oauthToken: String,
documentName: String,
documentId: String,
};
declare readonly hocuspocusUrlValue:string;
declare readonly openProjectUrlValue:string;
declare readonly oauthTokenValue:string;
declare readonly documentNameValue:string;
declare readonly documentIdValue:string;
connect():void {
const url = new URL(this.hocuspocusUrlValue);
url.searchParams.set('document_id', this.documentIdValue);
url.searchParams.set('openproject_base_path', this.openProjectUrlValue);
LiveCollaborationManager.initializeYjsProvider(
new HocuspocusProvider({
url: url.toString(),
name: this.documentNameValue,
token: this.oauthTokenValue,
document: LiveCollaborationManager.ydoc,
})
);
}
}
@@ -0,0 +1,115 @@
/*
* -- 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 { HocuspocusProvider, onAwarenessUpdateParameters } from '@hocuspocus/provider';
import * as Turbo from '@hotwired/turbo';
import { LiveCollaborationManager } from 'core-stimulus/helpers/live-collaboration-helpers';
import { ApplicationController, useDebounce } from 'stimulus-use';
interface LiveUser {id:string, name:string, avatarUrl:string}
export default class extends ApplicationController {
static debounces = ['triggerUpdateUI'];
static targets = ['users', 'popover'];
declare readonly usersTarget:HTMLElement;
declare readonly popoverTarget:HTMLElement;
private provider:HocuspocusProvider|undefined;
private currentUsers = new Map<number, LiveUser>();
connect() {
LiveCollaborationManager.onReady((provider:HocuspocusProvider) => {
this.provider = provider;
this.provider.on('awarenessUpdate', this.onAwarenessUpdate);
});
useDebounce(this, { wait: 1000 });
}
disconnect() {
this.currentUsers.clear();
this.provider?.off('awarenessUpdate', this.onAwarenessUpdate);
}
toggle_popover() {
this.popoverTarget.classList.toggle('d-none');
}
private onAwarenessUpdate = (data:onAwarenessUpdateParameters) => {
const changed = this.updateUsers(data.states);
if (changed) {
this.triggerUpdateUI();
}
};
private updateUsers(states:onAwarenessUpdateParameters['states']) {
const nextState = new Map<number, LiveUser>();
states.forEach((state, clientId) => {
if (state.user) {
nextState.set(clientId, state.user as LiveUser);
}
});
const previousUsers = [...this.currentUsers.keys()];
const nextUsers = [...nextState.keys()];
this.currentUsers = nextState;
return (
previousUsers.length !== nextUsers.length || previousUsers.some(id => !nextUsers.includes(id))
);
}
private triggerUpdateUI() {
const params = new URLSearchParams();
for (const user of this.currentUsers.values()) {
params.append('user_ids[]', user.id);
}
const url = `${window.location.pathname}/render_avatars?${params}`;
void fetch(url, {
method: 'GET',
headers: { Accept: 'text/vnd.turbo-stream.htm' },
})
.then((response:Response) => {
if (response.ok) {
return response.text();
}
return Promise.reject(new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`));
})
.then((html:string) => Turbo.renderStreamMessage(html))
.catch((error:Error) => console.error('Error:', error));
}
}
@@ -0,0 +1,81 @@
/*
* -- 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 { HocuspocusProvider } from '@hocuspocus/provider';
import type { Doc } from 'yjs';
import * as Y from 'yjs';
type Listener = (provider:HocuspocusProvider) => void;
class LiveCollaborationManagerClass {
ydocInstance:Doc|null = null;
yjsProviderInstance:HocuspocusProvider|null = null;
private listeners:Listener[] = [];
/**
* Initializes the YJS Provider
* @param provider The provider to use
* @returns void
*/
initializeYjsProvider(provider:HocuspocusProvider) {
this.yjsProviderInstance = provider;
this.listeners.forEach((listener) => listener(this.yjsProviderInstance!));
}
/**
* Gets a shared Y.Doc instance
*/
get ydoc():Doc {
this.ydocInstance ??= new Y.Doc();
return this.ydocInstance;
}
/**
* Gets a shared YJS Provider
* @throws Error if no provider is configured
*/
get yjsProvider():HocuspocusProvider {
if (!this.yjsProviderInstance) {
throw new Error('No YJS Provider configured');
}
return this.yjsProviderInstance;
}
onReady(listener:Listener) {
this.listeners.push(listener);
if (this.yjsProviderInstance) {
listener(this.yjsProviderInstance);
}
}
}
export const LiveCollaborationManager = new LiveCollaborationManagerClass();
@@ -37,13 +37,9 @@
controller: "block-note",
block_note_input_text_value: value,
block_note_active_user_value: active_user,
block_note_hocuspocus_url_value: hocuspocus_url,
block_note_document_name_value: document_name,
block_note_document_id_value: document_id,
block_note_open_project_url_value: open_project_url,
block_note_oauth_token_value: oauth_token,
block_note_attachments_upload_url_value: attachments_upload_url,
block_note_attachments_collection_key_value: attachments_collection_key,
block_note_collaboration_enabled_value: collaboration_enabled,
test_selector: "blocknote-document-description"
}
) do
@@ -38,18 +38,13 @@ module Primer
attr_reader :input,
:value,
:active_user,
:hocuspocus_url,
:open_project_url,
:document_name,
:document_id,
:oauth_token,
:attachments_upload_url,
:attachments_collection_key
:attachments_collection_key,
:collaboration_enabled
delegate :name, to: :@input
def initialize(input:, value:, document_name:, document_id:, attachments_upload_url: "",
attachments_collection_key: "", oauth_token: nil)
def initialize(input:, value:, attachments_upload_url: "", attachments_collection_key: "")
super()
@input = input
@value = value
@@ -57,13 +52,10 @@ module Primer
id: User.current.id,
username: User.current.name
}
@document_id = document_id
@document_name = document_name
@oauth_token = oauth_token
@hocuspocus_url = Setting.collaborative_editing_hocuspocus_url
@open_project_url = root_url
@attachments_upload_url = attachments_upload_url
@attachments_collection_key = attachments_collection_key
@collaboration_enabled = true
end
end
end
@@ -37,9 +37,6 @@ module Primer
:label,
:value,
:classes,
:document_id,
:document_name,
:oauth_token,
:attachments_upload_url,
:attachments_collection_key
@@ -47,18 +44,13 @@ module Primer
# @param name [String] The name of the input field.
# @param label [String] The label for the input field.
# @param value [String] The initial value of the input in base64 format.
# @param document_id [String] The ID of the document.
# @param document_name [String] The name of the document for the collaborative YJS provider.
# @param oauth_token [String, nil] The OAuth token for external server authentication.
def initialize(name:, label:, value:, document_id:, document_name:, attachments_upload_url: "",
attachments_collection_key: "", oauth_token: nil, **system_arguments)
# @param attachments_upload_url [String] The URL to which attachments will be uploaded.
# @param attachments_collection_key [String] The collection key for attachments.
def initialize(name:, label:, value:, attachments_upload_url: "", attachments_collection_key: "", **system_arguments)
@name = name
@label = label
@value = value
@classes = system_arguments[:classes]
@document_id = document_id
@document_name = document_name
@oauth_token = oauth_token
@attachments_upload_url = attachments_upload_url
@attachments_collection_key = attachments_collection_key
@@ -66,8 +58,7 @@ module Primer
end
def to_component
BlockNoteEditor.new(input: self, value:, document_id:, document_name:, oauth_token:, attachments_upload_url:,
attachments_collection_key:)
BlockNoteEditor.new(input: self, value:, attachments_upload_url:, attachments_collection_key:)
end
def type
@@ -29,6 +29,17 @@
++#
%>
<%
helpers.content_controller(
"documents--init-yjs-provider",
"documents--init-yjs-provider-hocuspocus-url-value": Setting.collaborative_editing_hocuspocus_url,
"documents--init-yjs-provider-open-project-url-value": root_url,
"documents--init-yjs-provider-oauth-token-value": oauth_token,
"documents--init-yjs-provider-document-name-value": document.title,
"documents--init-yjs-provider-document-id-value": document.id
)
%>
<%=
render(Primer::Alpha::Layout.new(stacking_breakpoint: :md, classes: "op-document-view")) do |component|
component.with_main(classes: "op-document-view--main") do
@@ -52,6 +52,10 @@
end
end
flex.with_column(mr: 2) do
render(Documents::ShowEditView::PageHeader::LiveUsersComponent.new)
end
flex.with_column do
last_updated_at_content
end
@@ -0,0 +1,84 @@
<%#
-- 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: "turbo-frame", refresh: :morph,
data: {
controller: "documents--live-users",
action: "click->documents--live-users#toggle_popover",
"test-selector": "live-users"
}
) do
render Primer::BaseComponent.new(tag: :div, position: :relative) do
concat(
flex_layout do |flex_container|
flex_container.with_column do
render Primer::Beta::AvatarStack.new(
data: { "documents--live-users-target": "users" }
) do |component|
users.map do |user|
component.with_avatar(src: avatar_url(user), alt: user.name)
end
end
end
flex_container.with_column { active_editors }
end
)
concat(
render Primer::Beta::Popover.new(
position: :absolute,
display: :none, # hidden by default
data: { "documents--live-users-target": "popover" }
) do |component|
component.with_heading { I18n.t("documents.active_editors") }
component.with_body(caret: :top_left) do
flex_layout do |container|
users.each do |user|
container.with_row do
render Users::AvatarComponent.new(
user: user,
show_name: true,
link: false,
hover_card: { active: false },
size: :mini
)
end
end
end
end
end
)
end
end
%>
@@ -0,0 +1,57 @@
# 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 LiveUsersComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include OpPrimer::FormHelpers
include OpTurbo::Streamable
include AvatarHelper
attr_reader :users
def initialize(users: [User.current])
super
@users = users
end
def active_editors
safe_join [
I18n.t("documents.active_editors_count", count: users.count).html_safe
]
end
end
end
end
end
@@ -66,6 +66,14 @@ class DocumentsController < ApplicationController
end
end
def render_avatars
user_ids = params[:user_ids]
@users = User.where(id: user_ids)
update_via_turbo_stream(component: Documents::ShowEditView::PageHeader::LiveUsersComponent.new(users: @users))
respond_with_turbo_streams
end
def new
@document = @project.documents.build
@document.attributes = document_params
@@ -194,7 +202,7 @@ class DocumentsController < ApplicationController
def generate_oauth_token
# do not generate a token if the user is not allowed to manage documents
if !current_user.allowed_in_project?(:manage_documents, @project)
if !current_user.allowed_in_project?(:view_documents, @project)
return
end
@@ -37,9 +37,6 @@ module Documents
visually_hide_label: true,
classes: "document-form--long-description",
value: model.content_binary,
document_id: model.id,
document_name: model.title,
oauth_token: @oauth_token,
attachments_upload_url:,
attachments_collection_key:
)
@@ -40,3 +40,4 @@
<% else %>
<%= render partial: "classic_show", locals: { document: @document, project: @project, attachments: @attachments } %>
<% end %>
+3
View File
@@ -76,6 +76,9 @@ en:
last_updated_at: "Last updated %{time}."
active_editors: "Active editors"
active_editors_count: "%{count} active editors"
label_attachment_author: "Attachment author"
label_categories: "Categories"
new_category: "New category"
+1
View File
@@ -45,6 +45,7 @@ Rails.application.routes.draw do
get :cancel_title_edit, defaults: { format: :turbo_stream }
put :update_type, defaults: { format: :turbo_stream }
get :delete_dialog
get :render_avatars, defaults: { format: :turbo_stream }
end
end
@@ -53,7 +53,7 @@ module OpenProject::Documents
project_module :documents do |_map|
permission :view_documents,
{ documents: %i[index search show download],
{ documents: %i[index search show download render_avatars],
"documents/menus": %i[show] },
permissible_on: :project
permission :manage_documents,
@@ -211,9 +211,9 @@ RSpec.describe DocumentsController do
context "when user does not have manage_documents permission" do
current_user { user_without_manage }
it "does not generate an OAuth token for show action" do
it "generates an OAuth token for show action" do
get :show, params: { id: document.id }
expect(assigns(:oauth_token)).to be_nil
expect(assigns(:oauth_token)).to be_present
end
end
end
@@ -55,6 +55,11 @@ RSpec.describe "Upload attachment to documents",
before do
login_as(user)
# This is here while we don't have a setting defined for enabling/disabling collaboration
# rubocop:disable RSpec/AnyInstance
allow_any_instance_of(Primer::OpenProject::Forms::BlockNoteEditor).to receive(:collaboration_enabled).and_return(false)
# rubocop:enable RSpec/AnyInstance
end
shared_examples "can upload an image in CKEditor" do
@@ -38,6 +38,11 @@ RSpec.describe "BlockNote editor rendering", :js, with_flag: { block_note_editor
before do
login_as(admin)
# This is here while we don't have a setting defined for enabling/disabling collaboration
# rubocop:disable RSpec/AnyInstance
allow_any_instance_of(Primer::OpenProject::Forms::BlockNoteEditor).to receive(:collaboration_enabled).and_return(false)
# rubocop:enable RSpec/AnyInstance
end
it "renders the BlockNote editor in the users locale" do
@@ -46,11 +46,24 @@ RSpec.describe "Show/Edit Document View",
current_user { member }
before do
# This is here while we don't have a setting defined for enabling/disabling collaboration
# rubocop:disable RSpec/AnyInstance
allow_any_instance_of(Primer::OpenProject::Forms::BlockNoteEditor).to receive(:collaboration_enabled).and_return(false)
# rubocop:enable RSpec/AnyInstance
end
it "renders a collaborative document" do
visit document_path(document)
expect(page).to have_content("Collaborative document")
aggregate_failures "can see live users" do
within_test_selector("live-users") do
expect(page).to have_content("1 active editors")
end
end
aggregate_failures "can edit document title" do
within_test_selector("document-page-header") do
click_button accessible_name: "Document actions"