Compare commits

...

8 Commits

Author SHA1 Message Date
Anthony LC 8dcba65ddf test 2024-11-14 13:13:07 +01:00
Jacques ROUSSEL 7e8667f80f 🔐(frontend) bump secret
Add sentry secret for the frontend
2024-11-13 15:05:49 +01:00
Anthony LC 8f73b78476 🩹(backend) get current release from pyproject.toml
In Sentry it is nice to have the release version,
it will help us to track the errors.
get_release was returning NA, we fixed it by
getting the version from pyproject.toml.
To do so we use tomllib, native library since
Python 3.11.
Our docker image is using Python 3.12.6, but our
CI was using Python 3.10, so we bump the CI python
version as well to be able to use tomllib.
2024-11-13 15:05:49 +01:00
Anthony LC 60a66e8783 🧐(backend) adapt Sentry config
We add SENTRY_ENV var, to be able to distinguish
errors between different environments in Sentry
(staging / preprod / prod).
2024-11-13 13:26:09 +01:00
Anthony LC 14a1642fb8 🧐(frontend) add sentry on the frontend side
We want to monitor the frontend side with sentry.
This commit adds the necessary configuration
to the frontend side to send errors to sentry.
2024-11-13 13:26:09 +01:00
Anthony LC 5ee6a43f08 (frontend) add useBroadcastStore
Add the useBroadcastStore.
It will give us the ability to easily
broadcast actions to all connected clients.

In this case, we requery the doc to everyone
when a change relative to the doc rights is made.
2024-11-09 10:21:24 +01:00
Anthony LC 8bd83cbfcd 🚚(frontend) move useDocStore to doc-management
We want to make more accessible the doc store
to every feature, so we move it to the
doc-management folder.
2024-11-09 10:21:24 +01:00
Anthony LC bc14d1d0f8 🐛(editor) collaborative user tag hidden when read only
When the user was in read-only mode, the user
tag could be displayed when they were touching the
doc. This commit fixes this issue.
We add the full name instead of the email in the
cursor tag.
2024-11-08 12:01:23 +01:00
37 changed files with 1289 additions and 150 deletions
+6 -1
View File
@@ -6,11 +6,13 @@ on:
push:
branches:
- 'main'
- 'feature/add-sentry'
tags:
- 'v*'
env:
DOCKER_USER: 1001:127
SENTRY_DSN: ""
jobs:
build-and-push-backend:
@@ -109,7 +111,10 @@ jobs:
context: .
file: ./src/frontend/Dockerfile
target: frontend-production
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
build-args: |
DOCKER_USER=${{ env.DOCKER_USER }}:-1000
SENTRY_DSN=${{ env.SENTRY_DSN }}
SENTRY_ENV=${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && 'production' || 'staging' }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+4 -2
View File
@@ -107,7 +107,9 @@ jobs:
- name: Install Python
uses: actions/setup-python@v3
with:
python-version: "3.10"
python-version: "3.12.6"
- name: Upgrade pip and setuptools
run: pip install --upgrade pip setuptools
- name: Install development dependencies
run: pip install --user .[dev]
- name: Check code formatting with ruff
@@ -199,7 +201,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v3
with:
python-version: "3.10"
python-version: "3.12.6"
- name: Install development dependencies
run: pip install --user .[dev]
+4
View File
@@ -12,6 +12,8 @@ and this project adheres to
## Added
- 🌐(frontend) Add German translation #255
- ✨(frontend) Add a broadcast store #387
- 🔊(project) Add sentry #410
## Changed
@@ -21,6 +23,8 @@ and this project adheres to
## Fixed
- 🦺(backend) add comma to sub regex #408
- 🐛(editor) collaborative user tag hidden when read only #385
- 🐛(frontend) user have view access when revoked #387
## [1.7.0] - 2024-10-24
+1 -1
View File
@@ -15,7 +15,7 @@ services:
- "1081:1080"
minio:
user: ${DOCKER_USER:-1000}
# user: ${DOCKER_USER:-1000}
image: minio/minio
environment:
- MINIO_ROOT_USER=impress
+3
View File
@@ -44,3 +44,6 @@ OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
AI_BASE_URL=https://openaiendpoint.com
AI_API_KEY=password
AI_MODEL=llama
# Sentry
SENTRY_ENV=development
+1 -1
Submodule secrets updated: 38594182e8...d91797b97f
+2
View File
@@ -72,6 +72,8 @@ class AIService:
json_response = json.loads(sanitized_content)
raise RuntimeError("Error Test Sentry")
if "answer" not in json_response:
raise RuntimeError("AI response does not contain an answer")
+15 -14
View File
@@ -10,14 +10,17 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import json
import os
import tomllib
from django.utils.translation import gettext_lazy as _
import sentry_sdk
from configurations import Configuration, values
from sentry_sdk.integrations.django import DjangoIntegration
from logging import getLogger
logger = getLogger(__name__)
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -27,19 +30,12 @@ DATA_DIR = os.path.join("/", "data")
def get_release():
"""
Get the current release of the application
By release, we mean the release from the version.json file à la Mozilla [1]
(if any). If this file has not been found, it defaults to "NA".
[1]
https://github.com/mozilla-services/Dockerflow/blob/master/docs/version_object.md
"""
# Try to get the current release from the version.json file generated by the
# CI during the Docker image build
try:
with open(os.path.join(BASE_DIR, "version.json"), encoding="utf8") as version:
return json.load(version)["version"]
except FileNotFoundError:
with open(os.path.join(BASE_DIR, "pyproject.toml"), "rb") as f:
pyproject_data = tomllib.load(f)
return pyproject_data["project"]["version"]
except (FileNotFoundError, KeyError):
return "NA" # Default: not available
@@ -63,7 +59,7 @@ class Base(Configuration):
* DB_USER
"""
DEBUG = False
DEBUG = True
USE_SWAGGER = False
API_VERSION = "v1.0"
@@ -372,7 +368,8 @@ class Base(Configuration):
CORS_ALLOWED_ORIGIN_REGEXES = values.ListValue([])
# Sentry
SENTRY_DSN = values.Value(None, environ_name="SENTRY_DSN")
SENTRY_DSN = values.Value(None, environ_name="SENTRY_DSN", environ_prefix=None)
SENTRY_ENV = values.Value(None, environ_name="SENTRY_ENV", environ_prefix=None)
# Easy thumbnails
THUMBNAIL_EXTENSION = "webp"
@@ -522,6 +519,10 @@ class Base(Configuration):
# The SENTRY_DSN setting should be available to activate sentry for an environment
if cls.SENTRY_DSN is not None:
logger.debug("Sentry is SENTRY_ENV. %s ", cls.SENTRY_ENV)
logger.debug("Sentry is cls.__name__.lower(): %s ", cls.__name__.lower())
print("Sentry is cls.__name__.lower(): %s ", cls.__name__.lower())
print("Sentry is SENTRY_ENV. %s ", cls.SENTRY_ENV)
sentry_sdk.init(
dsn=cls.SENTRY_DSN,
environment=cls.__name__.lower(),
+1
View File
@@ -127,6 +127,7 @@ select = [
[tool.ruff.lint.isort]
section-order = ["future","standard-library","django","third-party","impress","first-party","local-folder"]
sections = { impress=["core"], django=["django"] }
extra-standard-library = ["tomllib"]
[tool.ruff.lint.per-file-ignores]
"**/tests/*" = ["S", "SLF"]
+6
View File
@@ -76,6 +76,12 @@ ENV NEXT_PUBLIC_MEDIA_URL=${MEDIA_URL}
ARG SW_DEACTIVATED
ENV NEXT_PUBLIC_SW_DEACTIVATED=${SW_DEACTIVATED}
ARG SENTRY_DSN
ENV NEXT_PUBLIC_SENTRY_DSN=${SENTRY_DSN}
ARG SENTRY_ENV
ENV NEXT_PUBLIC_SENTRY_ENV=${SENTRY_ENV}
RUN yarn build
# ---- Front-end image ----
+2
View File
@@ -3,3 +3,5 @@ NEXT_PUBLIC_Y_PROVIDER_URL=
NEXT_PUBLIC_MEDIA_URL=
NEXT_PUBLIC_THEME=dsfr
NEXT_PUBLIC_SW_DEACTIVATED=
NEXT_PUBLIC_SENTRY_DSN=
NEXT_PUBLIC_SENTRY_ENV=production
@@ -2,3 +2,4 @@ NEXT_PUBLIC_API_ORIGIN=http://localhost:8071
NEXT_PUBLIC_Y_PROVIDER_URL=ws://localhost:4444
NEXT_PUBLIC_MEDIA_URL=http://localhost:8083
NEXT_PUBLIC_SW_DEACTIVATED=true
NEXT_PUBLIC_SENTRY_ENV=development
+4 -1
View File
@@ -1,5 +1,6 @@
const crypto = require('crypto');
const { withSentryConfig } = require('@sentry/nextjs');
const { InjectManifest } = require('workbox-webpack-plugin');
const buildId = crypto.randomBytes(256).toString('hex').slice(0, 8);
@@ -65,4 +66,6 @@ const nextConfig = {
},
};
module.exports = nextConfig;
module.exports = withSentryConfig(nextConfig, {
silent: false,
});
+1
View File
@@ -21,6 +21,7 @@
"@gouvfr-lasuite/integration": "1.0.2",
"@hocuspocus/provider": "2.13.7",
"@openfun/cunningham-react": "2.9.4",
"@sentry/nextjs": "8.37.1",
"@tanstack/react-query": "5.59.15",
"i18next": "23.16.2",
"i18next-browser-languagedetector": "8.0.0",
@@ -0,0 +1,13 @@
import * as Sentry from '@sentry/nextjs';
import packageJson from './package.json';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NEXT_PUBLIC_SENTRY_ENV,
integrations: [Sentry.replayIntegration()],
release: packageJson.version,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
tracesSampleRate: 1.0,
});
@@ -14,14 +14,13 @@ import { useTranslation } from 'react-i18next';
import { isAPIError } from '@/api';
import { Box, Text } from '@/components';
import { useDocOptions } from '@/features/docs/doc-management/';
import { useDocOptions, useDocStore } from '@/features/docs/doc-management/';
import {
AITransformActions,
useDocAITransform,
useDocAITranslate,
} from '../api/';
import { useDocStore } from '../stores';
type LanguageTranslate = {
value: string;
@@ -4,18 +4,18 @@ import { BlockNoteView } from '@blocknote/mantine';
import '@blocknote/mantine/style.css';
import { useCreateBlockNote } from '@blocknote/react';
import { HocuspocusProvider } from '@hocuspocus/provider';
import { t } from 'i18next';
import React, { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
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 { Doc, useDocStore } from '@/features/docs/doc-management';
import { useCreateDocAttachment } from '../api/useCreateDocUpload';
import useSaveDoc from '../hook/useSaveDoc';
import { useDocStore, useHeadingStore } from '../stores';
import { useHeadingStore } from '../stores';
import { randomColor } from '../utils';
import { BlockNoteToolbar } from './BlockNoteToolbar';
@@ -26,7 +26,13 @@ const cssEditor = (readonly: boolean) => `
};
& .bn-editor {
padding-right: 30px;
${readonly && `padding-left: 30px;`}
${
readonly &&
`
padding-left: 30px;
pointer-events: none;
`
}
};
& .collaboration-cursor__caret.ProseMirror-widget{
word-wrap: initial;
@@ -68,40 +74,16 @@ const cssEditor = (readonly: boolean) => `
`;
interface BlockNoteEditorProps {
doc: Doc;
version?: Version;
}
export const BlockNoteEditor = ({ doc, version }: BlockNoteEditorProps) => {
const { createProvider, docsStore } = useDocStore();
const storeId = version?.id || doc.id;
const initialContent = version?.content || doc.content;
const provider = docsStore?.[storeId]?.provider;
useEffect(() => {
if (!provider || provider.document.guid !== storeId) {
createProvider(storeId, initialContent);
}
}, [createProvider, initialContent, provider, storeId]);
if (!provider) {
return null;
}
return <BlockNoteContent doc={doc} provider={provider} storeId={storeId} />;
};
interface BlockNoteContentProps {
doc: Doc;
provider: HocuspocusProvider;
storeId: string;
}
export const BlockNoteContent = ({
export const BlockNoteEditor = ({
doc,
provider,
storeId,
}: BlockNoteContentProps) => {
}: BlockNoteEditorProps) => {
const isVersion = doc.id !== storeId;
const { userData } = useAuthStore();
const { setStore, docsStore } = useDocStore();
@@ -139,14 +121,14 @@ export const BlockNoteContent = ({
provider,
fragment: provider.document.getXmlFragment('document-store'),
user: {
name: userData?.email || 'Anonymous',
name: userData?.full_name || userData?.email || t('Anonymous'),
color: randomColor(),
},
},
dictionary: locales[lang as keyof typeof locales],
uploadFile,
},
[provider, uploadFile, userData?.email, lang],
[lang, provider, uploadFile, userData?.email, userData?.full_name],
);
useEffect(() => {
@@ -1,13 +1,13 @@
import { Alert, Loader, VariantType } from '@openfun/cunningham-react';
import { useRouter as useNavigate } from 'next/navigation';
import { useRouter } from 'next/router';
import React from 'react';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Card, Text, TextErrors } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { DocHeader } from '@/features/docs/doc-header';
import { Doc } from '@/features/docs/doc-management';
import { Doc, useDocStore } from '@/features/docs/doc-management';
import { Versions, useDocVersion } from '@/features/docs/doc-versioning/';
import { useResponsiveStore } from '@/stores';
@@ -32,6 +32,13 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
const { colorsTokens } = useCunninghamTheme();
const { docsStore } = useDocStore();
const provider = docsStore?.[doc.id]?.provider;
if (!provider) {
return null;
}
return (
<>
<DocHeader doc={doc} versionId={versionId as Versions['version_id']} />
@@ -66,7 +73,7 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
{isVersion ? (
<DocVersionEditor doc={doc} versionId={versionId} />
) : (
<BlockNoteEditor doc={doc} />
<BlockNoteEditor doc={doc} storeId={doc.id} provider={provider} />
)}
{!isMobile && <IconOpenPanelEditor headings={headings} />}
</Card>
@@ -91,9 +98,21 @@ export const DocVersionEditor = ({ doc, versionId }: DocVersionEditorProps) => {
docId: doc.id,
versionId,
});
const { createProvider, docsStore } = useDocStore();
const navigate = useNavigate();
useEffect(() => {
if (!version?.id) {
return;
}
const provider = docsStore?.[version.id]?.provider;
if (!provider || provider.document.guid !== version.id) {
createProvider(version.id, version.content);
}
}, [createProvider, docsStore, version]);
if (isError && error) {
if (error.status === 404) {
navigate.replace(`/404`);
@@ -124,5 +143,11 @@ export const DocVersionEditor = ({ doc, versionId }: DocVersionEditorProps) => {
);
}
return <BlockNoteEditor doc={doc} version={version} />;
const provider = docsStore?.[version.id]?.provider;
if (!provider) {
return null;
}
return <BlockNoteEditor doc={doc} storeId={version.id} provider={provider} />;
};
@@ -1,3 +1,2 @@
export * from './useDocStore';
export * from './useHeadingStore';
export * from './usePanelEditorStore';
@@ -1,5 +1,3 @@
import * as Y from 'yjs';
export const randomColor = () => {
const randomInt = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
@@ -28,20 +26,3 @@ function hslToHex(h: number, s: number, l: number) {
export const toBase64 = (
str: WithImplicitCoercion<ArrayBuffer | SharedArrayBuffer>,
) => Buffer.from(str).toString('base64');
type BasicBlock = {
type: string;
content: string;
};
export const blocksToYDoc = (blocks: BasicBlock[], doc: Y.Doc) => {
const xmlFragment = doc.getXmlFragment('document-store');
blocks.forEach((block) => {
const xmlElement = new Y.XmlElement(block.type);
if (block.content) {
xmlElement.insert(0, [new Y.XmlText(block.content)]);
}
xmlFragment.push([xmlElement]);
});
};
@@ -18,7 +18,7 @@ import {
useTrans,
useUpdateDoc,
} from '@/features/docs/doc-management';
import { useResponsiveStore } from '@/stores';
import { useBroadcastStore, useResponsiveStore } from '@/stores';
import { isFirefox } from '@/utils/userAgent';
interface DocTitleProps {
@@ -54,6 +54,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
const headingText = headings?.[0]?.contentText;
const debounceRef = useRef<NodeJS.Timeout>();
const { isMobile } = useResponsiveStore();
const { broadcast } = useBroadcastStore();
const { mutate: updateDoc } = useUpdateDoc({
listInvalideQueries: [KEY_DOC, KEY_LIST_DOC],
@@ -61,6 +62,9 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
if (data.title !== untitledDocument) {
toast(t('Document title updated successfully'), VariantType.SUCCESS);
}
// Broadcast to every user connected to the document
broadcast(`${KEY_DOC}-${data.id}`);
},
});
@@ -8,11 +8,12 @@ import { useTranslation } from 'react-i18next';
import { Box, DropButton, IconOptions } from '@/components';
import { useAuthStore } from '@/core';
import { useDocStore, usePanelEditorStore } from '@/features/docs/doc-editor/';
import { usePanelEditorStore } from '@/features/docs/doc-editor/';
import {
Doc,
ModalRemoveDoc,
ModalShare,
useDocStore,
} from '@/features/docs/doc-management';
import { useResponsiveStore } from '@/stores';
@@ -14,8 +14,7 @@ import { t } from 'i18next';
import { useEffect, useMemo, useState } from 'react';
import { Box, Text } from '@/components';
import { useDocStore } from '@/features/docs/doc-editor/';
import { Doc } from '@/features/docs/doc-management';
import { Doc, useDocStore } from '@/features/docs/doc-management';
import { useExport } from '../api/useExport';
import { TemplatesOrdering, useTemplates } from '../api/useTemplates';
@@ -1,7 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { Doc } from '@/features/docs';
import { Doc, KEY_DOC } from '@/features/docs/doc-management';
import { useBroadcastStore } from '@/stores';
export type UpdateDocLinkParams = Pick<Doc, 'id'> &
Partial<Pick<Doc, 'link_role' | 'link_reach'>>;
@@ -37,14 +38,20 @@ export function useUpdateDocLink({
listInvalideQueries,
}: UpdateDocLinkProps = {}) {
const queryClient = useQueryClient();
const { broadcast } = useBroadcastStore();
return useMutation<Doc, APIError, UpdateDocLinkParams>({
mutationFn: updateDocLink,
onSuccess: (data) => {
onSuccess: (data, variable) => {
listInvalideQueries?.forEach((queryKey) => {
void queryClient.resetQueries({
queryKey: [queryKey],
});
});
// Broadcast to every user connected to the document
broadcast(`${KEY_DOC}-${variable.id}`);
onSuccess?.(data);
},
});
@@ -1,5 +1,6 @@
export * from './api';
export * from './components';
export * from './hooks';
export * from './stores';
export * from './types';
export * from './utils';
@@ -0,0 +1 @@
export * from './useDocStore';
@@ -4,9 +4,7 @@ import * as Y from 'yjs';
import { create } from 'zustand';
import { providerUrl } from '@/core';
import { Base64, Doc } from '@/features/docs/doc-management';
import { blocksToYDoc } from '../utils';
import { Base64, Doc, blocksToYDoc } from '@/features/docs/doc-management';
interface DocStore {
provider: HocuspocusProvider;
@@ -1,3 +1,5 @@
import * as Y from 'yjs';
import { Doc, Role } from './types';
export const currentDocRole = (abilities: Doc['abilities']): Role => {
@@ -9,3 +11,20 @@ export const currentDocRole = (abilities: Doc['abilities']): Role => {
? Role.EDITOR
: Role.READER;
};
type BasicBlock = {
type: string;
content: string;
};
export const blocksToYDoc = (blocks: BasicBlock[], doc: Y.Doc) => {
const xmlFragment = doc.getXmlFragment('document-store');
blocks.forEach((block) => {
const xmlElement = new Y.XmlElement(block.type);
if (block.content) {
xmlElement.insert(0, [new Y.XmlText(block.content)]);
}
xmlFragment.push([xmlElement]);
});
};
@@ -2,8 +2,8 @@ import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, BoxButton, Text } from '@/components';
import { HeadingBlock, useDocStore } from '@/features/docs/doc-editor';
import { Doc } from '@/features/docs/doc-management';
import { HeadingBlock } from '@/features/docs/doc-editor';
import { Doc, useDocStore } from '@/features/docs/doc-management';
import { useResponsiveStore } from '@/stores';
import { Heading } from './Heading';
@@ -11,8 +11,8 @@ import { useRouter } from 'next/navigation';
import * as Y from 'yjs';
import { Box, Text } from '@/components';
import { toBase64, useDocStore } from '@/features/docs/doc-editor';
import { Doc, useUpdateDoc } from '@/features/docs/doc-management';
import { toBase64 } from '@/features/docs/doc-editor';
import { Doc, useDocStore, useUpdateDoc } from '@/features/docs/doc-management';
import { KEY_LIST_DOC_VERSIONS } from '../api/useDocVersions';
import { Versions } from '../types';
@@ -5,11 +5,13 @@ import { User } from '@/core/auth';
import {
Access,
Doc,
KEY_DOC,
KEY_LIST_DOC,
Role,
} from '@/features/docs/doc-management';
import { KEY_LIST_DOC_ACCESSES } from '@/features/docs/members/members-list';
import { ContentLanguage } from '@/i18n/types';
import { useBroadcastStore } from '@/stores';
import { OptionType } from '../types';
@@ -53,9 +55,11 @@ export const createDocAccess = async ({
export function useCreateDocAccess() {
const queryClient = useQueryClient();
const { broadcast } = useBroadcastStore();
return useMutation<Access, APIError, CreateDocAccessParams>({
mutationFn: createDocAccess,
onSuccess: () => {
onSuccess: (_data, variable) => {
void queryClient.resetQueries({
queryKey: [KEY_LIST_DOC],
});
@@ -65,6 +69,9 @@ export function useCreateDocAccess() {
void queryClient.resetQueries({
queryKey: [KEY_LIST_DOC_ACCESSES],
});
// Broadcast to every user connected to the document
broadcast(`${KEY_DOC}-${variable.docId}`);
},
});
}
@@ -7,6 +7,7 @@ import {
import { APIError, errorCauses, fetchAPI } from '@/api';
import { KEY_DOC, KEY_LIST_DOC } from '@/features/docs/doc-management';
import { KEY_LIST_USER } from '@/features/docs/members/members-add';
import { useBroadcastStore } from '@/stores';
import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses';
@@ -39,6 +40,8 @@ type UseDeleteDocAccessOptions = UseMutationOptions<
export const useDeleteDocAccess = (options?: UseDeleteDocAccessOptions) => {
const queryClient = useQueryClient();
const { broadcast } = useBroadcastStore();
return useMutation<void, APIError, DeleteDocAccessProps>({
mutationFn: deleteDocAccess,
...options,
@@ -49,6 +52,10 @@ export const useDeleteDocAccess = (options?: UseDeleteDocAccessOptions) => {
void queryClient.invalidateQueries({
queryKey: [KEY_DOC],
});
// Broadcast to every user connected to the document
broadcast(`${KEY_DOC}-${variables.docId}`);
void queryClient.resetQueries({
queryKey: [KEY_LIST_DOC],
});
@@ -11,6 +11,7 @@ import {
KEY_LIST_DOC,
Role,
} from '@/features/docs/doc-management';
import { useBroadcastStore } from '@/stores';
import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses';
@@ -49,6 +50,8 @@ type UseUpdateDocAccessOptions = UseMutationOptions<
export const useUpdateDocAccess = (options?: UseUpdateDocAccessOptions) => {
const queryClient = useQueryClient();
const { broadcast } = useBroadcastStore();
return useMutation<Access, APIError, UpdateDocAccessProps>({
mutationFn: updateDocAccess,
...options,
@@ -59,6 +62,10 @@ export const useUpdateDocAccess = (options?: UseUpdateDocAccessOptions) => {
void queryClient.invalidateQueries({
queryKey: [KEY_DOC],
});
// Broadcast to every user connected to the document
broadcast(`${KEY_DOC}-${variables.docId}`);
void queryClient.invalidateQueries({
queryKey: [KEY_LIST_DOC],
});
@@ -1,4 +1,5 @@
import { Loader } from '@openfun/cunningham-react';
import { useQueryClient } from '@tanstack/react-query';
import Head from 'next/head';
import { useRouter as useNavigate } from 'next/navigation';
import { useRouter } from 'next/router';
@@ -7,9 +8,10 @@ import { useEffect, useState } from 'react';
import { Box, Text } from '@/components';
import { TextErrors } from '@/components/TextErrors';
import { useAuthStore } from '@/core/auth';
import { DocEditor, useDocStore } from '@/features/docs';
import { useDoc } from '@/features/docs/doc-management';
import { DocEditor } from '@/features/docs/doc-editor';
import { KEY_DOC, useDoc, useDocStore } from '@/features/docs/doc-management';
import { MainLayout } from '@/layouts';
import { useBroadcastStore } from '@/stores';
import { NextPageWithLayout } from '@/types/next';
export function DocLayout() {
@@ -41,9 +43,11 @@ const DocPage = ({ id }: DocProps) => {
const { login } = useAuthStore();
const { data: docQuery, isError, error } = useDoc({ id });
const [doc, setDoc] = useState(docQuery);
const { setCurrentDoc } = useDocStore();
const { setCurrentDoc, createProvider, docsStore } = useDocStore();
const { setBroadcastProvider, addTask } = useBroadcastStore();
const queryClient = useQueryClient();
const navigate = useNavigate();
const provider = docsStore?.[id]?.provider;
useEffect(() => {
if (doc?.title) {
@@ -66,6 +70,35 @@ const DocPage = ({ id }: DocProps) => {
};
}, [docQuery, setCurrentDoc]);
useEffect(() => {
if (!doc?.id) {
return;
}
let newProvider = provider;
if (!provider || provider.document.guid !== doc.id) {
newProvider = createProvider(doc.id, doc.content);
}
setBroadcastProvider(newProvider);
}, [createProvider, doc, provider, setBroadcastProvider]);
/**
* We add a broadcast task to reset the query cache
* when the document visibility changes.
*/
useEffect(() => {
if (!doc?.id) {
return;
}
addTask(`${KEY_DOC}-${doc.id}`, () => {
void queryClient.resetQueries({
queryKey: [KEY_DOC, { id: doc.id }],
});
});
}, [addTask, doc?.id, queryClient]);
if (isError && error) {
if (error.status === 404) {
navigate.replace(`/404`);
@@ -1 +1,2 @@
export * from './useBroadcastStore';
export * from './useResponsiveStore';
@@ -0,0 +1,54 @@
import { HocuspocusProvider } from '@hocuspocus/provider';
import * as Y from 'yjs';
import { create } from 'zustand';
interface BroadcastState {
addTask: (taskLabel: string, action: () => void) => void;
broadcast: (taskLabel: string) => void;
getBroadcastProvider: () => HocuspocusProvider | undefined;
provider?: HocuspocusProvider;
setBroadcastProvider: (provider: HocuspocusProvider) => void;
tasks: { [taskLabel: string]: Y.Array<string> };
}
export const useBroadcastStore = create<BroadcastState>((set, get) => ({
provider: undefined,
tasks: {},
setBroadcastProvider: (provider) => set({ provider }),
getBroadcastProvider: () => {
const provider = get().provider;
if (!provider) {
console.warn('Provider is not defined');
return;
}
return provider;
},
addTask: (taskLabel, action) => {
const taskExistAlready = get().tasks[taskLabel];
const provider = get().getBroadcastProvider();
if (taskExistAlready || !provider) {
return;
}
const task = provider.document.getArray<string>(taskLabel);
task.observe(() => {
action();
});
set((state) => ({
tasks: {
...state.tasks,
[taskLabel]: task,
},
}));
},
broadcast: (taskLabel) => {
const task = get().tasks[taskLabel];
if (!task) {
console.warn(`Task ${taskLabel} is not defined`);
return;
}
task.push([`broadcast: ${taskLabel}`]);
},
}));
+1024 -54
View File
File diff suppressed because it is too large Load Diff