From 4647545739744db50d1f7bf359a09435d7382173 Mon Sep 17 00:00:00 2001 From: Peter Birrer Date: Thu, 21 Nov 2024 12:25:02 +0100 Subject: [PATCH 1/4] feat(attachments): add support for creating new attachments - Introduced a new endpoint (`Attachment#createAttachment`) to create attachment files via POST requests. - Added `createAttachment` method in `AttachmentService` to handle file creation logic with permission checks. - Updated `MediaHandler.vue` and `MediaHandler.provider.js` to integrate the new attachment creation functionality in the editor. - Enhanced `ActionAttachmentUpload.vue` to support dynamic attachment creation from predefined templates. - Included a new "Plus" icon for attachment creation actions in `icons.js`. - Extended `SessionApi.js` with a `createAttachment` method to interface with the new API endpoint. - Minor refactor and UI enhancements to support seamless attachment creation in the text editor. These changes enable users to create and insert new attachment files directly from the editor, improving workflow efficiency. Signed-off-by: Peter Birrer --- appinfo/routes.php | 2 ++ lib/Controller/AttachmentController.php | 24 +++++++++++++++ lib/Service/AttachmentService.php | 29 +++++++++++++++++++ .../Editor/MediaHandler.provider.js | 7 +++++ src/components/Editor/MediaHandler.vue | 25 ++++++++++++++++ .../Menu/ActionAttachmentUpload.vue | 29 +++++++++++++++++-- src/components/icons.js | 2 ++ src/services/SessionApi.js | 9 ++++++ src/services/SyncService.js | 4 +++ 9 files changed, 129 insertions(+), 2 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index ae8dbb54a56..c0948b62873 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -18,6 +18,8 @@ ['name' => 'Attachment#insertAttachmentFile', 'url' => '/attachment/filepath', 'verb' => 'POST'], /** @see Controller\AttachmentController::uploadAttachment() */ ['name' => 'Attachment#uploadAttachment', 'url' => '/attachment/upload', 'verb' => 'POST'], + /** @see Controller\AttachmentController::createAttachment() */ + ['name' => 'Attachment#createAttachment', 'url' => '/attachment/create', 'verb' => 'POST'], /** @see Controller\AttachmentController::getImageFile() */ ['name' => 'Attachment#getImageFile', 'url' => '/image', 'verb' => 'GET'], /** @see Controller\AttachmentController::getMediaFile() */ diff --git a/lib/Controller/AttachmentController.php b/lib/Controller/AttachmentController.php index 60eb25b06cb..a4fca259cc3 100644 --- a/lib/Controller/AttachmentController.php +++ b/lib/Controller/AttachmentController.php @@ -144,6 +144,30 @@ public function uploadAttachment(string $token = ''): DataResponse { } } + #[NoAdminRequired] + #[PublicPage] + #[RequireDocumentSession] + public function createAttachment(string $token = ''): DataResponse { + $documentId = $this->getSession()->getDocumentId(); + try { + $userId = $this->getSession()->getUserId(); + $newFileName = $this->request->getParam('fileName', 'text.md'); + $createResult = $this->attachmentService->createAttachmentFile($documentId, $newFileName, $userId); + if (isset($createResult['error'])) { + return new DataResponse($createResult, Http::STATUS_BAD_REQUEST); + } else { + return new DataResponse($createResult); + } + } catch (InvalidPathException $e) { + $this->logger->error('File creation error', ['exception' => $e]); + $error = $e->getMessage() ?: 'Upload error'; + return new DataResponse(['error' => $error], Http::STATUS_BAD_REQUEST); + } catch (Exception $e) { + $this->logger->error('File creation error', ['exception' => $e]); + return new DataResponse(['error' => 'File creation error'], Http::STATUS_BAD_REQUEST); + } + } + private function getUploadedFile(string $key): array { $file = $this->request->getUploadedFile($key); $error = null; diff --git a/lib/Service/AttachmentService.php b/lib/Service/AttachmentService.php index de8ca42e362..0f53b5353d9 100644 --- a/lib/Service/AttachmentService.php +++ b/lib/Service/AttachmentService.php @@ -331,6 +331,35 @@ public function insertAttachmentFile(int $documentId, string $path, string $user return $this->copyFile($originalFile, $saveDir, $textFile); } + /** + * create a new file in the attachment folder + * + * @param int $documentId + * @param string $userId + * + * @return array + * @throws NotFoundException + * @throws NotPermittedException + * @throws InvalidPathException + * @throws NoUserException + */ + public function createAttachmentFile(int $documentId, string $newFileName, string $userId): array { + $textFile = $this->getTextFile($documentId, $userId); + if (!$textFile->isUpdateable()) { + throw new NotPermittedException('No write permissions'); + } + $saveDir = $this->getAttachmentDirectoryForFile($textFile, true); + $fileName = self::getUniqueFileName($saveDir, $newFileName); + $newFile = $saveDir->newFile($fileName); + return [ + 'name' => $fileName, + 'dirname' => $saveDir->getName(), + 'id' => $newFile->getId(), + 'documentId' => $newFile->getId(), + 'mimetype' => $newFile->getMimetype(), + ]; + } + /** * @param File $originalFile * @param Folder $saveDir diff --git a/src/components/Editor/MediaHandler.provider.js b/src/components/Editor/MediaHandler.provider.js index 7f624673b1f..314655ea500 100644 --- a/src/components/Editor/MediaHandler.provider.js +++ b/src/components/Editor/MediaHandler.provider.js @@ -6,6 +6,7 @@ export const STATE_UPLOADING = Symbol('state:uploading-state') export const ACTION_ATTACHMENT_PROMPT = Symbol('editor:action:attachment-prompt') export const ACTION_CHOOSE_LOCAL_ATTACHMENT = Symbol('editor:action:upload-attachment') +export const ACTION_CREATE_ATTACHMENT = Symbol('editor:action:create-attachment') export const useUploadingStateMixin = { inject: { @@ -29,3 +30,9 @@ export const useActionChooseLocalAttachmentMixin = { $callChooseLocalAttachment: { from: ACTION_CHOOSE_LOCAL_ATTACHMENT, default: () => {} }, }, } + +export const useActionCreateAttachmentMixin = { + inject: { + $callCreateAttachment: { from: ACTION_CREATE_ATTACHMENT, default: () => (template) => {} }, + }, +} diff --git a/src/components/Editor/MediaHandler.vue b/src/components/Editor/MediaHandler.vue index ef9c6231516..d5160eb8763 100644 --- a/src/components/Editor/MediaHandler.vue +++ b/src/components/Editor/MediaHandler.vue @@ -26,6 +26,7 @@ import { getCurrentUser } from '@nextcloud/auth' import { showError } from '@nextcloud/dialogs' import { emit } from '@nextcloud/event-bus' +import { generateUrl } from '@nextcloud/router' import { logger } from '../../helpers/logger.js' import { @@ -37,6 +38,7 @@ import { import { ACTION_ATTACHMENT_PROMPT, ACTION_CHOOSE_LOCAL_ATTACHMENT, + ACTION_CREATE_ATTACHMENT, STATE_UPLOADING, } from './MediaHandler.provider.js' @@ -55,6 +57,9 @@ export default { [ACTION_CHOOSE_LOCAL_ATTACHMENT]: { get: () => this.chooseLocalFile, }, + [ACTION_CREATE_ATTACHMENT]: { + get: () => this.createAttachment, + }, [STATE_UPLOADING]: { get: () => this.state, }, @@ -166,6 +171,26 @@ export default { this.state.isUploadingAttachments = false }) }, + createAttachment(template) { + this.state.isUploadingAttachments = true + return this.$syncService.createAttachment(template).then((response) => { + this.insertAttachmentPreview(response.data?.id) + }).catch((error) => { + logger.error('Failed to create attachment', { error }) + showError(t('text', 'Failed to create attachment')) + }).then(() => { + this.state.isUploadingAttachments = false + }) + }, + insertAttachmentPreview(fileId) { + const url = new URL(generateUrl(`/f/${fileId}`), window.origin) + const href = url.href.replaceAll(' ', '%20') + this.$editor + .chain() + .focus() + .insertPreview(href) + .run() + }, insertAttachment(name, fileId, mimeType, position = null, dirname = '') { // inspired by the fixedEncodeURIComponent function suggested in // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent diff --git a/src/components/Menu/ActionAttachmentUpload.vue b/src/components/Menu/ActionAttachmentUpload.vue index 87d7c62c6fc..bcd98c6ea68 100644 --- a/src/components/Menu/ActionAttachmentUpload.vue +++ b/src/components/Menu/ActionAttachmentUpload.vue @@ -34,12 +34,25 @@ {{ t('text', 'Insert from Files') }} + + + {{ template.actionLabel }} + diff --git a/src/components/icons.js b/src/components/icons.js index 54f31b8096e..e20cbd22483 100644 --- a/src/components/icons.js +++ b/src/components/icons.js @@ -62,6 +62,7 @@ import MDI_Upload from 'vue-material-design-icons/Upload.vue' import MDI_Warn from 'vue-material-design-icons/Alert.vue' import MDI_Web from 'vue-material-design-icons/Web.vue' import MDI_TranslateVariant from 'vue-material-design-icons/TranslateVariant.vue' +import MDI_Plus from 'vue-material-design-icons/Plus.vue' const DEFAULT_ICON_SIZE = 20 @@ -144,3 +145,4 @@ export const UnfoldMoreHorizontal = makeIcon(MDI_UnfoldMoreHorizontal) export const Upload = makeIcon(MDI_Upload) export const Warn = makeIcon(MDI_Warn) export const Web = makeIcon(MDI_Web) +export const Plus = makeIcon(MDI_Plus) diff --git a/src/services/SessionApi.js b/src/services/SessionApi.js index 45ff771b69b..5fed23ba93d 100644 --- a/src/services/SessionApi.js +++ b/src/services/SessionApi.js @@ -155,6 +155,15 @@ export class Connection { }) } + createAttachment(template) { + return this.#post(_endpointUrl('attachment/create'), { + documentId: this.#document.id, + sessionId: this.#session.id, + sessionToken: this.#session.token, + fileName: `${template.app}${template.extension}`, + }) + } + insertAttachmentFile(filePath) { return this.#post(_endpointUrl('attachment/filepath'), { documentId: this.#document.id, diff --git a/src/services/SyncService.js b/src/services/SyncService.js index 92fa361b129..ce6c2a8fb4a 100644 --- a/src/services/SyncService.js +++ b/src/services/SyncService.js @@ -334,6 +334,10 @@ class SyncService { return this.#connection.insertAttachmentFile(filePath) } + createAttachment(template) { + return this.#connection.createAttachment(template) + } + on(event, callback) { this._bus.on(event, callback) return this From 21ce3551f3c6adab1f7c88400c6ce4325de3f951 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 17 Dec 2024 11:43:18 +0100 Subject: [PATCH 2/4] enh(cy): test create new text attachment from toolbar Signed-off-by: Max --- cypress/e2e/attachments.spec.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cypress/e2e/attachments.spec.js b/cypress/e2e/attachments.spec.js index bc4bc5a19e5..97c44fc0fef 100644 --- a/cypress/e2e/attachments.spec.js +++ b/cypress/e2e/attachments.spec.js @@ -12,6 +12,7 @@ const attachmentFileNameToId = {} const ACTION_UPLOAD_LOCAL_FILE = 'insert-attachment-upload' const ACTION_INSERT_FROM_FILES = 'insert-attachment-insert' +const ACTION_CREATE_NEW_TEXT_FILE = 'insert-attachment-add-text-0' /** * @param {string} name name of file @@ -279,6 +280,19 @@ describe('Test all attachment insertion methods', () => { cy.closeFile() }) + it('Create a new text file as an attachment', () => { + cy.visit('/apps/files') + cy.openFile('test.md') + + cy.log('Create a new text file as an attachment') + const requestAlias = 'create-attachment-request' + cy.intercept({ method: 'POST', url: '**/text/attachment/create' }).as(requestAlias) + clickOnAttachmentAction(ACTION_CREATE_NEW_TEXT_FILE) + .then(() => { + return waitForRequestAndCheckAttachment(requestAlias, undefined, false) + }) + }) + it('test if attachment files are in the attachment folder', () => { cy.visit('/apps/files') From 70afc3985ad38385a38295c478fc669c1057667f Mon Sep 17 00:00:00 2001 From: Peter Birrer Date: Thu, 16 Jan 2025 10:53:01 +0100 Subject: [PATCH 3/4] enh(editor): add separator to visually separate "create attachment" entries Signed-off-by: Peter Birrer --- .../Menu/ActionAttachmentUpload.vue | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/components/Menu/ActionAttachmentUpload.vue b/src/components/Menu/ActionAttachmentUpload.vue index bcd98c6ea68..5644f9a6cee 100644 --- a/src/components/Menu/ActionAttachmentUpload.vue +++ b/src/components/Menu/ActionAttachmentUpload.vue @@ -34,23 +34,26 @@ {{ t('text', 'Insert from Files') }} - - - {{ template.actionLabel }} - +