mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
[#68710] Add Files section to existing document edit view
https://community.openproject.org/work_packages/68710 In addition to adding the files section to the document edit view, this commit makes it optional to be able to attach files to blocknote. This is, for example, not necessary when blocknote is rendered in read-only mode (that's not happening yet, but planned).
This commit is contained in:
@@ -50,6 +50,7 @@ export interface OpBlockNoteContainerProps {
|
||||
documentId:string;
|
||||
openProjectUrl:string;
|
||||
attachmentsUploadUrl:string;
|
||||
attachmentsCollectionKey:string;
|
||||
}
|
||||
|
||||
const schema = BlockNoteSchema.create({
|
||||
@@ -69,7 +70,8 @@ export default function OpBlockNoteContainer({ inputField,
|
||||
documentName,
|
||||
documentId,
|
||||
openProjectUrl,
|
||||
attachmentsUploadUrl }:OpBlockNoteContainerProps) {
|
||||
attachmentsUploadUrl,
|
||||
attachmentsCollectionKey }:OpBlockNoteContainerProps) {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
initOpenProjectApi({ baseUrl: openProjectUrl });
|
||||
@@ -109,7 +111,7 @@ export default function OpBlockNoteContainer({ inputField,
|
||||
showCursorLabels: 'activity'
|
||||
},
|
||||
dictionary: blockNoteLocale,
|
||||
uploadFile,
|
||||
...(isReadyForAttachmentUpload() && { uploadFile }),
|
||||
};
|
||||
} else { // collaboration disabled
|
||||
if (inputText) {
|
||||
@@ -133,13 +135,21 @@ export default function OpBlockNoteContainer({ inputField,
|
||||
},
|
||||
},
|
||||
dictionary: blockNoteLocale,
|
||||
uploadFile,
|
||||
...(isReadyForAttachmentUpload() && { uploadFile }),
|
||||
};
|
||||
}
|
||||
|
||||
const editor = useCreateBlockNote(editorParams, [activeUser]);
|
||||
type EditorType = typeof editor;
|
||||
|
||||
function isReadyForAttachmentUpload():boolean {
|
||||
return (
|
||||
attachmentsCollectionKey !== undefined &&
|
||||
attachmentsCollectionKey !== '' &&
|
||||
attachmentsUploadUrl !== undefined &&
|
||||
attachmentsUploadUrl !== ''
|
||||
);
|
||||
}
|
||||
const fileToIUploadFile = (file:File):IUploadFile => ({
|
||||
file: file
|
||||
});
|
||||
@@ -149,9 +159,9 @@ export default function OpBlockNoteContainer({ inputField,
|
||||
try {
|
||||
const service = pluginContext.services.attachmentsResourceService;
|
||||
const iUploadFile = fileToIUploadFile(file);
|
||||
const result = await service.addAttachments('documents', attachmentsUploadUrl, [iUploadFile]).toPromise();
|
||||
const result = await service.addAttachments(attachmentsCollectionKey, attachmentsUploadUrl, [iUploadFile]).toPromise();
|
||||
|
||||
return result?.[0]._links.downloadLocation.href ?? '';
|
||||
return result?.[0]._links.staticDownloadLocation.href ?? '';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch(error:any) {
|
||||
const toastService = pluginContext.services.notifications;
|
||||
|
||||
@@ -49,6 +49,7 @@ export default class extends Controller {
|
||||
documentId: String,
|
||||
openProjectUrl: String,
|
||||
attachmentsUploadUrl: String,
|
||||
attachmentsCollectionKey: String,
|
||||
};
|
||||
|
||||
declare readonly blockNoteEditorTarget:HTMLElement;
|
||||
@@ -61,6 +62,7 @@ export default class extends Controller {
|
||||
declare readonly documentIdValue:string;
|
||||
declare readonly openProjectUrlValue:string;
|
||||
declare readonly attachmentsUploadUrlValue:string;
|
||||
declare readonly attachmentsCollectionKeyValue:string;
|
||||
|
||||
connect() {
|
||||
const root = createRoot(this.blockNoteEditorTarget);
|
||||
@@ -78,6 +80,7 @@ export default class extends Controller {
|
||||
documentId: this.documentIdValue,
|
||||
openProjectUrl: this.openProjectUrlValue,
|
||||
attachmentsUploadUrl: this.attachmentsUploadUrlValue,
|
||||
attachmentsCollectionKey: this.attachmentsCollectionKeyValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
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,
|
||||
test_selector: "blocknote-document-description"
|
||||
}
|
||||
) do
|
||||
|
||||
@@ -43,11 +43,13 @@ module Primer
|
||||
:document_name,
|
||||
:document_id,
|
||||
:oauth_token,
|
||||
:attachments_upload_url
|
||||
:attachments_upload_url,
|
||||
:attachments_collection_key
|
||||
|
||||
delegate :name, to: :@input
|
||||
|
||||
def initialize(input:, value:, document_name:, document_id:, attachments_upload_url:, oauth_token: nil)
|
||||
def initialize(input:, value:, document_name:, document_id:, attachments_upload_url: "",
|
||||
attachments_collection_key: "", oauth_token: nil)
|
||||
super()
|
||||
@input = input
|
||||
@value = value
|
||||
@@ -61,6 +63,7 @@ module Primer
|
||||
@hocuspocus_url = Setting.collaborative_editing_hocuspocus_url
|
||||
@open_project_url = root_url
|
||||
@attachments_upload_url = attachments_upload_url
|
||||
@attachments_collection_key = attachments_collection_key
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -33,7 +33,15 @@ module Primer
|
||||
module Forms
|
||||
module Dsl
|
||||
class BlockNoteEditorInput < Primer::Forms::Dsl::Input
|
||||
attr_reader :name, :label, :value, :classes, :document_id, :document_name, :oauth_token, :attachments_upload_url
|
||||
attr_reader :name,
|
||||
:label,
|
||||
:value,
|
||||
:classes,
|
||||
:document_id,
|
||||
:document_name,
|
||||
:oauth_token,
|
||||
:attachments_upload_url,
|
||||
:attachments_collection_key
|
||||
|
||||
##
|
||||
# @param name [String] The name of the input field.
|
||||
@@ -42,8 +50,8 @@ module Primer
|
||||
# @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:, oauth_token: nil,
|
||||
**system_arguments)
|
||||
def initialize(name:, label:, value:, document_id:, document_name:, attachments_upload_url: "",
|
||||
attachments_collection_key: "", oauth_token: nil, **system_arguments)
|
||||
@name = name
|
||||
@label = label
|
||||
@value = value
|
||||
@@ -52,12 +60,14 @@ module Primer
|
||||
@document_name = document_name
|
||||
@oauth_token = oauth_token
|
||||
@attachments_upload_url = attachments_upload_url
|
||||
@attachments_collection_key = attachments_collection_key
|
||||
|
||||
super(**system_arguments)
|
||||
end
|
||||
|
||||
def to_component
|
||||
BlockNoteEditor.new(input: self, value:, document_id:, document_name:, oauth_token:, attachments_upload_url:)
|
||||
BlockNoteEditor.new(input: self, value:, document_id:, document_name:, oauth_token:, attachments_upload_url:,
|
||||
attachments_collection_key:)
|
||||
end
|
||||
|
||||
def type
|
||||
|
||||
@@ -63,7 +63,8 @@ class DocumentForm < ApplicationForm
|
||||
document_id: model.id,
|
||||
document_name: model.title,
|
||||
oauth_token: @oauth_token,
|
||||
attachments_upload_url: uploads_url
|
||||
attachments_upload_url: uploads_url,
|
||||
attachments_collection_key: ::API::V3::Utilities::PathHelper::ApiV3Path.attachments_by_document(model.id)
|
||||
)
|
||||
else
|
||||
f.rich_text_area(
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
|
||||
++#
|
||||
%>
|
||||
|
||||
<%=
|
||||
render Primer::OpenProject::PageHeader.new do |header|
|
||||
header.with_title { @document.title }
|
||||
@@ -41,12 +40,39 @@
|
||||
%>
|
||||
|
||||
<%=
|
||||
settings_primer_form_with(
|
||||
model: @document,
|
||||
url: document_path(@document),
|
||||
method: :patch,
|
||||
data: { turbo: false }
|
||||
) do |f|
|
||||
render DocumentForm.new(f, oauth_token: @oauth_token)
|
||||
render(Primer::Alpha::Layout.new(stacking_breakpoint: :md)) do |component|
|
||||
component.with_main do
|
||||
settings_primer_form_with(
|
||||
model: @document,
|
||||
url: document_path(@document),
|
||||
method: :patch,
|
||||
data: { turbo: false }
|
||||
) do |f|
|
||||
render DocumentForm.new(f, oauth_token: @oauth_token)
|
||||
end
|
||||
end
|
||||
|
||||
component.with_sidebar(row_placement: :start, col_placement: :end) do
|
||||
render(Primer::OpenProject::SidePanel.new) do |panel|
|
||||
panel.with_section do |section|
|
||||
section.with_title { t(:label_attachment_plural) }
|
||||
section.with_footer_button(
|
||||
color: :accent,
|
||||
id: "documents-add-attachments",
|
||||
classes: "hide-when-print"
|
||||
) do |button|
|
||||
button.with_leading_visual_icon(icon: "op-add-attachment")
|
||||
t("js.label_add_attachments")
|
||||
end
|
||||
list_attachments(
|
||||
api_v3_document_resource(@document),
|
||||
inputs: {
|
||||
allowUploading: true,
|
||||
externalUploadButton: "#documents-add-attachments"
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
%>
|
||||
|
||||
@@ -186,10 +186,39 @@ RSpec.describe "Upload attachment to documents",
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples "with attachments list in the sidebar" do
|
||||
it "is possible to upload attachments from the sidebar" do
|
||||
expect(page).to have_no_content("image.png")
|
||||
expect do
|
||||
attachments_list.drag_enter
|
||||
attachments_list.drop(image_fixture.path)
|
||||
expect(page).to have_no_css("op-toast") # wait for upload to finish
|
||||
attachments_list.expect_attached("image.png")
|
||||
end.to change { document.attachments.count }.by(1)
|
||||
end
|
||||
|
||||
context "when an attachment is present" do
|
||||
let!(:attachment) { create(:attachment, filename: "test.jpg", container: document) }
|
||||
|
||||
before do
|
||||
visit edit_document_path(document)
|
||||
end
|
||||
|
||||
it "is possible to delete attachments from the sidebar" do
|
||||
attachments_list.expect_attached("test.jpg")
|
||||
expect do
|
||||
attachments_list.delete("test.jpg")
|
||||
attachments_list.expect_empty
|
||||
end.to change { document.attachments.count }.by(-1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "for collaborative documents", with_flag: { block_note_editor: true } do
|
||||
let(:experimental_category) { create(:document_category, name: "Experimental", project:) }
|
||||
let(:document) { create(:document, category: experimental_category, project:) }
|
||||
let(:editor) { FormFields::Primerized::BlockNoteEditorInput.new }
|
||||
let(:attachments_list) { Components::AttachmentsList.new }
|
||||
|
||||
before do
|
||||
visit edit_document_path(document)
|
||||
@@ -200,11 +229,13 @@ RSpec.describe "Upload attachment to documents",
|
||||
context "with internal uploads" do
|
||||
it_behaves_like "can upload an image in BlockNote"
|
||||
it_behaves_like "with non-whitelisted file types"
|
||||
it_behaves_like "with attachments list in the sidebar"
|
||||
end
|
||||
|
||||
context "with uploads to an external storage", :with_direct_uploads do
|
||||
it_behaves_like "can upload an image in BlockNote"
|
||||
it_behaves_like "with non-whitelisted file types"
|
||||
it_behaves_like "with attachments list in the sidebar"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -236,7 +236,7 @@ RSpec.describe Primer::OpenProject::Forms::Dsl::InputMethods, type: :forms do
|
||||
let(:document_name) { "1234asdzxc" }
|
||||
let(:field_group) do
|
||||
form_dsl.block_note_editor(name:, label:, value: "", document_id: 8, document_name:, attachments_upload_url: "",
|
||||
**options)
|
||||
attachments_collection_key: "", **options)
|
||||
end
|
||||
|
||||
include_examples "input class", Primer::OpenProject::Forms::Dsl::BlockNoteEditorInput
|
||||
|
||||
@@ -28,6 +28,15 @@ module Components
|
||||
drop_box_element.drop(path)
|
||||
end
|
||||
|
||||
def delete(file_name)
|
||||
item = find("#{context_selector} [data-test-selector='op-attachment-list-item']", text: file_name)
|
||||
item.hover
|
||||
item.find(:button, title: I18n.t("js.label_remove_file", fileName: file_name)).click
|
||||
# confirm delete confirmation dialog
|
||||
expect(page).to have_content(I18n.t("js.attachments.delete_confirmation"))
|
||||
find(:button, text: I18n.t("js.attachments.delete")).click
|
||||
end
|
||||
|
||||
def expect_empty
|
||||
expect(page).to have_no_css("#{context_selector} [data-test-selector='op-attachment-list-item']")
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user