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 && ( + + + + )} +