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/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') 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