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