diff --git a/CHANGELOG.md b/CHANGELOG.md
index 76fa1e3a..b559d621 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,11 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
# Added
- ✨Add image attachments with access control
-
-## [Unreleased]
+- ✨(frontend) Upload image to a document
## [1.2.1] - 2024-08-23
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/assets/logo-suite-numerique.png b/src/frontend/apps/e2e/__tests__/app-impress/assets/logo-suite-numerique.png
new file mode 100644
index 00000000..243c9662
Binary files /dev/null and b/src/frontend/apps/e2e/__tests__/app-impress/assets/logo-suite-numerique.png differ
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts
index 1bdde8a6..54c8f98a 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts
@@ -1,3 +1,5 @@
+import path from 'path';
+
import { expect, test } from '@playwright/test';
import { createDoc, goToGridDoc, mockedDocument } from './common';
@@ -159,4 +161,31 @@ test.describe('Doc Editor', () => {
page.getByText('Read only, you cannot edit this document.'),
).toBeVisible();
});
+
+ test('it adds an image to the doc editor', async ({ page }) => {
+ await goToGridDoc(page);
+
+ const fileChooserPromise = page.waitForEvent('filechooser');
+
+ await page.locator('.bn-block-outer').last().fill('Hello World');
+
+ await page.keyboard.press('Enter');
+ await page.locator('.bn-block-outer').last().fill('/');
+ await page.getByText('Resizable image with caption').click();
+ await page.getByText('Upload image').click();
+
+ const fileChooser = await fileChooserPromise;
+ await fileChooser.setFiles(
+ path.join(__dirname, 'assets/logo-suite-numerique.png'),
+ );
+
+ const image = page.getByRole('img', { name: 'logo-suite-numerique.png' });
+
+ await expect(image).toBeVisible();
+
+ // Check src of image
+ expect(await image.getAttribute('src')).toMatch(
+ /http:\/\/localhost:8083\/media\/.*\/attachments\/.*.png/,
+ );
+ });
});
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts
index 065f8488..9c9933a0 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts
@@ -175,6 +175,11 @@ test.describe('Doc Export', () => {
name: 'Image',
})
.click();
+ await page
+ .getByRole('tab', {
+ name: 'Embed',
+ })
+ .click();
await page
.getByPlaceholder('Enter URL')
.fill('https://example.com/image.jpg');
diff --git a/src/frontend/apps/impress/.env b/src/frontend/apps/impress/.env
index 5c413314..9e631e42 100644
--- a/src/frontend/apps/impress/.env
+++ b/src/frontend/apps/impress/.env
@@ -1,3 +1,4 @@
-NEXT_PUBLIC_THEME=dsfr
-NEXT_PUBLIC_SIGNALING_URL=
NEXT_PUBLIC_API_ORIGIN=
+NEXT_PUBLIC_MEDIA_URL=
+NEXT_PUBLIC_SIGNALING_URL=
+NEXT_PUBLIC_THEME=dsfr
\ No newline at end of file
diff --git a/src/frontend/apps/impress/src/core/conf.ts b/src/frontend/apps/impress/src/core/conf.ts
index dc778311..ba79a144 100644
--- a/src/frontend/apps/impress/src/core/conf.ts
+++ b/src/frontend/apps/impress/src/core/conf.ts
@@ -1,10 +1,13 @@
-export const baseApiUrl = (apiVersion: string = '1.0') => {
- const origin =
- process.env.NEXT_PUBLIC_API_ORIGIN ||
- (typeof window !== 'undefined' ? window.location.origin : '');
+export const mediaUrl = () =>
+ process.env.NEXT_PUBLIC_MEDIA_URL ||
+ (typeof window !== 'undefined' ? window.location.origin : '');
- return `${origin}/api/v${apiVersion}/`;
-};
+export const backendUrl = () =>
+ process.env.NEXT_PUBLIC_API_ORIGIN ||
+ (typeof window !== 'undefined' ? window.location.origin : '');
+
+export const baseApiUrl = (apiVersion: string = '1.0') =>
+ `${backendUrl()}/api/v${apiVersion}/`;
export const signalingUrl = (docId: string) => {
const base =
diff --git a/src/frontend/apps/impress/src/custom-next.d.ts b/src/frontend/apps/impress/src/custom-next.d.ts
index b8bbf9bf..064d02bc 100644
--- a/src/frontend/apps/impress/src/custom-next.d.ts
+++ b/src/frontend/apps/impress/src/custom-next.d.ts
@@ -20,6 +20,7 @@ declare module '*.svg?url' {
namespace NodeJS {
interface ProcessEnv {
NEXT_PUBLIC_API_ORIGIN?: string;
+ NEXT_PUBLIC_MEDIA_URL?: string;
NEXT_PUBLIC_SIGNALING_URL?: string;
NEXT_PUBLIC_SW_DEACTIVATED?: string;
NEXT_PUBLIC_THEME?: string;
diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx
index b1447397..89c7ccda 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx
@@ -2,14 +2,16 @@ import { BlockNoteEditor as BlockNoteEditorCore } from '@blocknote/core';
import '@blocknote/core/fonts/inter.css';
import { BlockNoteView } from '@blocknote/mantine';
import '@blocknote/mantine/style.css';
-import React, { useEffect, useMemo } from 'react';
+import React, { useCallback, useEffect, useMemo } from 'react';
import { WebrtcProvider } from 'y-webrtc';
-import { Box } from '@/components';
+import { Box, TextErrors } from '@/components';
+import { mediaUrl } from '@/core';
import { useAuthStore } from '@/core/auth';
import { Doc } from '@/features/docs/doc-management';
import { Version } from '@/features/docs/doc-versioning/';
+import { useCreateDocAttachment } from '../api/useCreateDocUpload';
import useSaveDoc from '../hook/useSaveDoc';
import { useDocStore } from '../stores';
import { randomColor } from '../utils';
@@ -56,8 +58,28 @@ export const BlockNoteContent = ({
const { setStore, docsStore } = useDocStore();
const canSave = doc.abilities.partial_update && !isVersion;
useSaveDoc(doc.id, provider.doc, canSave);
-
const storedEditor = docsStore?.[storeId]?.editor;
+ const {
+ mutateAsync: createDocAttachment,
+ isError: isErrorAttachment,
+ error: errorAttachment,
+ } = useCreateDocAttachment();
+
+ const uploadFile = useCallback(
+ async (file: File) => {
+ const body = new FormData();
+ body.append('file', file);
+
+ const ret = await createDocAttachment({
+ docId: doc.id,
+ body,
+ });
+
+ return `${mediaUrl()}${ret.file}`;
+ },
+ [createDocAttachment, doc.id],
+ );
+
const editor = useMemo(() => {
if (storedEditor) {
return storedEditor;
@@ -72,8 +94,9 @@ export const BlockNoteContent = ({
color: randomColor(),
},
},
+ uploadFile,
});
- }, [provider, storedEditor, userData?.email]);
+ }, [provider, storedEditor, uploadFile, userData?.email]);
useEffect(() => {
setStore(storeId, { editor });
@@ -90,6 +113,12 @@ export const BlockNoteContent = ({
}
`}
>
+ {isErrorAttachment && (
+
+
+
+ )}
+