Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3eff663a78 | |||
| feabab030d | |||
| 4002a049eb | |||
| c7d8211aac | |||
| 25a24eaf12 | |||
| 9adecf93ec | |||
| 6a3a07db31 | |||
| 79b56ccd4e | |||
| 611d77f3bb | |||
| de0acedc0a |
+2
-9
@@ -11,21 +11,13 @@ and this project adheres to
|
||||
|
||||
## Added
|
||||
|
||||
- ✨Add link public/authenticated/restricted access with read/editor roles #234
|
||||
- ✨(frontend) add copy link button #235
|
||||
- 🛂(frontend) access public docs without being logged #235
|
||||
|
||||
## Changed
|
||||
|
||||
- ♻️ Allow null titles on documents for easier creation #234
|
||||
- 🛂(backend) stop to list public doc to everyone #234
|
||||
- 🚚(frontend) change visibility in share modal #235
|
||||
|
||||
## Fixed
|
||||
|
||||
- 🐛 Fix forcing ID when creating a document via API endpoint #234
|
||||
- 🐛 Rebuild frontend dev container from makefile #248
|
||||
|
||||
|
||||
## [1.3.0] - 2024-09-05
|
||||
|
||||
@@ -40,6 +32,7 @@ and this project adheres to
|
||||
|
||||
- 💄(frontend) code background darkened on editor #214
|
||||
- 🔥(frontend) hide markdown button if not text #213
|
||||
- 🛂(backend) stop to list public doc to everyone #234
|
||||
|
||||
## Fixed
|
||||
|
||||
@@ -155,4 +148,4 @@ and this project adheres to
|
||||
[1.2.0]: https://github.com/numerique-gouv/impress/releases/v1.2.0
|
||||
[1.1.0]: https://github.com/numerique-gouv/impress/releases/v1.1.0
|
||||
[1.0.0]: https://github.com/numerique-gouv/impress/releases/v1.0.0
|
||||
[0.1.0]: https://github.com/numerique-gouv/impress/releases/v0.1.0
|
||||
[0.1.0]: https://github.com/numerique-gouv/impress/releases/v0.1.0
|
||||
@@ -92,7 +92,6 @@ bootstrap: \
|
||||
# -- Docker/compose
|
||||
build: ## build the app-dev container
|
||||
@$(COMPOSE) build app-dev --no-cache
|
||||
@$(COMPOSE) build frontend-dev --no-cache
|
||||
.PHONY: build
|
||||
|
||||
down: ## stop and remove containers, networks, images, and volumes
|
||||
|
||||
@@ -39,6 +39,3 @@ LOGOUT_REDIRECT_URL=http://localhost:3000
|
||||
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS=["http://localhost:8083", "http://localhost:3000"]
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
|
||||
|
||||
AI_BASE_URL=https://openaiendpoint.com
|
||||
AI_API_KEY=password
|
||||
@@ -1,6 +1,5 @@
|
||||
"""API endpoints"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
@@ -8,8 +7,8 @@ from urllib.parse import urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import (
|
||||
OuterRef,
|
||||
Q,
|
||||
@@ -18,9 +17,6 @@ from django.db.models import (
|
||||
from django.http import Http404
|
||||
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
from rest_framework import (
|
||||
decorators,
|
||||
exceptions,
|
||||
@@ -362,80 +358,15 @@ class DocumentViewSet(
|
||||
try:
|
||||
# Add a trace that the user visited the document (this is needed to include
|
||||
# the document in the user's list view)
|
||||
models.LinkTrace.objects.create(
|
||||
models.LinkTrace.objects.update_or_create(
|
||||
document=instance,
|
||||
user=self.request.user,
|
||||
)
|
||||
except ValidationError:
|
||||
except IntegrityError:
|
||||
# The trace already exists, so we just pass without doing anything
|
||||
pass
|
||||
|
||||
return drf_response.Response(serializer.data)
|
||||
|
||||
@decorators.action(detail=True, methods=["post"], url_path="ai")
|
||||
def ai(self, request, *args, **kwargs):
|
||||
"""
|
||||
Process text using AI based on the specified action.
|
||||
"""
|
||||
if not request.user.is_authenticated:
|
||||
raise exceptions.PermissionDenied("Authentication required.")
|
||||
|
||||
action = request.data.get("action")
|
||||
text = request.data.get("text")
|
||||
|
||||
client = OpenAI(
|
||||
base_url=settings.AI_BASE_URL,
|
||||
api_key=settings.AI_API_KEY
|
||||
)
|
||||
|
||||
action_configs = {
|
||||
"prompt": {
|
||||
"system_content": 'Answer to the prompt. The output should be in markdown format. Send the data back in this json format: {"answer_prompt": "Your markdown answer"}. Do not give any other information.',
|
||||
"response_key": 'answer_prompt'
|
||||
},
|
||||
"correct": {
|
||||
"system_content": 'You are a text corrector. Only correct the grammar and spelling of the given markdown text. Keep the language and markdown formatting in which the text was originally written. Return only a JSON in the following format: {"corrected_text": "your corrected markdown text"}. Do not provide any other information.',
|
||||
"response_key": 'corrected_text'
|
||||
},
|
||||
"rephrase": {
|
||||
"system_content": 'You are a writer. Rephrase the given markdown text. Keep the language and markdown formatting in which the text was originally written. Return only a JSON in the following format: {"rephrased_text": "your rephrased markdown text"}. Do not provide any other information.',
|
||||
"response_key": 'rephrased_text'
|
||||
},
|
||||
"summarize": {
|
||||
"system_content": 'You are a writer. Summarize the given markdown text. Keep the language in which the text was originally written and use appropriate markdown formatting. Return only a JSON in the following format: {"summary": "your markdown summary"}. Do not provide any other information.',
|
||||
"response_key": 'summary'
|
||||
},
|
||||
"translate_en": {
|
||||
"system_content": 'You are an English translator. Translate the given markdown text to English, preserving the markdown formatting. Return only a JSON in the following format: {"text": "Your translated markdown text in English"}. Do not provide any other information.',
|
||||
"response_key": 'text'
|
||||
},
|
||||
"translate_de": {
|
||||
"system_content": 'You are a German translator. Translate the given markdown text to German, preserving the markdown formatting. Return only a JSON in the following format: {"text": "Your translated markdown text in German"}. Do not provide any other information.',
|
||||
"response_key": 'text'
|
||||
},
|
||||
"translate_fr": {
|
||||
"system_content": 'You are a French translator. Translate the given markdown text to French, preserving the markdown formatting. Return only a JSON in the following format: {"text": "Your translated markdown text in French"}. Do not provide any other information.',
|
||||
"response_key": 'text'
|
||||
}
|
||||
}
|
||||
|
||||
if action not in action_configs:
|
||||
return drf_response.Response({"error": "Invalid action"}, status=400)
|
||||
|
||||
config = action_configs[action]
|
||||
try:
|
||||
response = client.chat.completions.create(
|
||||
model="meta-llama/Meta-Llama-3.1-70B-Instruct",
|
||||
response_format={ "type": "json_object"},
|
||||
messages=[
|
||||
{"role": "system", "content": config["system_content"]},
|
||||
{"role": "user", "content": json.dumps({"mardown_input": text})},
|
||||
]
|
||||
)
|
||||
corrected_response = json.loads(response.choices[0].message.content)
|
||||
return drf_response.Response(corrected_response[config["response_key"]])
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
return drf_response.Response({"error": f"Error processing AI response: {str(e)}"}, status=500)
|
||||
|
||||
@decorators.action(detail=True, methods=["get"], url_path="versions")
|
||||
def versions_list(self, request, *args, **kwargs):
|
||||
@@ -444,7 +375,7 @@ class DocumentViewSet(
|
||||
to the document
|
||||
"""
|
||||
if not request.user.is_authenticated:
|
||||
raise exceptions.PermissionDenied("Authentication required.")
|
||||
return drf_response.Response([])
|
||||
|
||||
document = self.get_object()
|
||||
user = request.user
|
||||
|
||||
@@ -16,7 +16,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='document',
|
||||
name='link_reach',
|
||||
field=models.CharField(choices=[('restricted', 'Restricted'), ('authenticated', 'Authenticated'), ('public', 'Public')], default='authenticated', max_length=20),
|
||||
field=models.CharField(choices=[('restricted', 'Restricted'), ('authenticated', 'Authenticated'), ('public', 'Public')], default='restricted', max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='document',
|
||||
|
||||
@@ -324,7 +324,7 @@ class Document(BaseModel):
|
||||
link_reach = models.CharField(
|
||||
max_length=20,
|
||||
choices=LinkReachChoices.choices,
|
||||
default=LinkReachChoices.AUTHENTICATED,
|
||||
default=LinkReachChoices.RESTRICTED,
|
||||
)
|
||||
link_role = models.CharField(
|
||||
max_length=20, choices=LinkRoleChoices.choices, default=LinkRoleChoices.READER
|
||||
|
||||
@@ -29,8 +29,8 @@ def test_api_document_versions_list_anonymous(role, reach):
|
||||
|
||||
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/versions/")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Authentication required."}
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
|
||||
|
||||
@@ -5,7 +5,7 @@ Tests for Documents API endpoint in impress's core app: retrieve
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
from core import factories
|
||||
from core.api import serializers
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
@@ -94,38 +94,6 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
}
|
||||
assert (
|
||||
models.LinkTrace.objects.filter(document=document, user=user).exists() is True
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reach", ["public", "authenticated"])
|
||||
def test_api_documents_retrieve_authenticated_trace_twice(reach):
|
||||
"""
|
||||
Accessing a document several times should not raise any error even though the
|
||||
trace already exists for this document and user.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(link_reach=reach)
|
||||
assert (
|
||||
models.LinkTrace.objects.filter(document=document, user=user).exists() is False
|
||||
)
|
||||
|
||||
client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
)
|
||||
assert (
|
||||
models.LinkTrace.objects.filter(document=document, user=user).exists() is True
|
||||
)
|
||||
|
||||
# A second visit should not raise any error
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_api_documents_retrieve_authenticated_unrelated_restricted():
|
||||
|
||||
@@ -387,12 +387,6 @@ class Base(Configuration):
|
||||
ALLOW_LOGOUT_GET_METHOD = values.BooleanValue(
|
||||
default=True, environ_name="ALLOW_LOGOUT_GET_METHOD", environ_prefix=None
|
||||
)
|
||||
AI_BASE_URL = values.Value(
|
||||
None, environ_name="AI_BASE_URL", environ_prefix=None
|
||||
)
|
||||
AI_API_KEY = values.Value(
|
||||
None, environ_name="AI_API_KEY", environ_prefix=None
|
||||
)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@property
|
||||
|
||||
@@ -47,7 +47,6 @@ dependencies = [
|
||||
"jsonschema==4.23.0",
|
||||
"markdown==3.7",
|
||||
"nested-multipart-parser==1.5.0",
|
||||
"openai==1.44.1",
|
||||
"psycopg[binary]==3.2.1",
|
||||
"PyJWT==2.9.0",
|
||||
"pypandoc==1.13",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"test:ui::chromium": "yarn test:ui --project=chromium"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.47.0",
|
||||
"@playwright/test": "1.46.1",
|
||||
"@types/node": "*",
|
||||
"@types/pdf-parse": "1.1.4",
|
||||
"eslint-config-impress": "*",
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
||||
@@ -21,12 +21,12 @@
|
||||
"@gouvfr-lasuite/integration": "1.0.2",
|
||||
"@hocuspocus/provider": "2.13.5",
|
||||
"@openfun/cunningham-react": "2.9.4",
|
||||
"@tanstack/react-query": "5.55.4",
|
||||
"i18next": "23.15.1",
|
||||
"@tanstack/react-query": "5.53.2",
|
||||
"i18next": "23.14.0",
|
||||
"idb": "8.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.5.0",
|
||||
"next": "14.2.9",
|
||||
"next": "14.2.7",
|
||||
"react": "*",
|
||||
"react-aria-components": "1.3.3",
|
||||
"react-dom": "*",
|
||||
@@ -34,12 +34,11 @@
|
||||
"react-select": "5.8.0",
|
||||
"styled-components": "6.1.13",
|
||||
"yjs": "*",
|
||||
"y-protocols": "1.0.6",
|
||||
"zustand": "4.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@tanstack/react-query-devtools": "5.55.4",
|
||||
"@tanstack/react-query-devtools": "5.53.2",
|
||||
"@testing-library/dom": "10.4.0",
|
||||
"@testing-library/jest-dom": "6.5.0",
|
||||
"@testing-library/react": "16.0.1",
|
||||
@@ -62,7 +61,6 @@
|
||||
"stylelint-config-standard": "36.0.1",
|
||||
"stylelint-prettier": "5.0.2",
|
||||
"typescript": "*",
|
||||
"webpack": "5.94.0",
|
||||
"workbox-webpack-plugin": "7.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
|
||||
export type AIActions =
|
||||
| 'prompt'
|
||||
| 'rephrase'
|
||||
| 'summarize'
|
||||
| 'translate'
|
||||
| 'correct'
|
||||
| 'translate_fr'
|
||||
| 'translate_en'
|
||||
| 'translate_de';
|
||||
|
||||
export type DocAIParams = {
|
||||
docId: string;
|
||||
text: string;
|
||||
action: AIActions;
|
||||
};
|
||||
|
||||
export type DocAIResponse = string;
|
||||
|
||||
export const DocAI = async ({
|
||||
docId,
|
||||
...params
|
||||
}: DocAIParams): Promise<DocAIResponse> => {
|
||||
const response = await fetchAPI(`documents/${docId}/ai/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
...params,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError('Failed to get AI', await errorCauses(response));
|
||||
}
|
||||
|
||||
return response.json() as Promise<DocAIResponse>;
|
||||
};
|
||||
|
||||
export function useAIRewrite() {
|
||||
return useMutation<DocAIResponse, APIError, DocAIParams>({
|
||||
mutationFn: DocAI,
|
||||
});
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
import {
|
||||
useBlockNoteEditor,
|
||||
useComponentsContext,
|
||||
useSelectedBlocks,
|
||||
ComponentProps,
|
||||
} from '@blocknote/react';
|
||||
import { mergeRefs } from "@mantine/hooks";
|
||||
|
||||
import { ReactNode, useMemo, forwardRef, useRef, useCallback, useState, createContext } from 'react';
|
||||
|
||||
import {
|
||||
Menu as MantineMenu,
|
||||
} from "@mantine/core";
|
||||
|
||||
import {Box, Text } from '@/components';
|
||||
|
||||
import { Doc } from '../../doc-management';
|
||||
import { AIActions, useAIRewrite } from '../api/useAIRewrite';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface AIGroupButtonProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export function AIGroupButton({ doc }: AIGroupButtonProps) {
|
||||
const editor = useBlockNoteEditor();
|
||||
const Components = useComponentsContext();
|
||||
const selectedBlocks = useSelectedBlocks(editor);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const show = useMemo(() => {
|
||||
return !!selectedBlocks.find((block) => block.content !== undefined);
|
||||
}, [selectedBlocks]);
|
||||
|
||||
if (!show || !editor.isEditable || !Components) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Components.Generic.Menu.Root>
|
||||
<Components.Generic.Menu.Trigger>
|
||||
<Components.FormattingToolbar.Button
|
||||
className="bn-button"
|
||||
data-test="ai-actions"
|
||||
label="AI"
|
||||
mainTooltip={t('AI Actions')}
|
||||
>
|
||||
AI
|
||||
</Components.FormattingToolbar.Button>
|
||||
</Components.Generic.Menu.Trigger>
|
||||
<Components.Generic.Menu.Dropdown className="bn-menu-dropdown bn-drag-handle-menu">
|
||||
<AIMenuItem action="prompt" docId={doc.id} icon={<Text $isMaterialIcon $size="s">text_fields</Text>}>
|
||||
{t('Use as prompt')}
|
||||
</AIMenuItem>
|
||||
<AIMenuItem action="rephrase" docId={doc.id} icon={<Text $isMaterialIcon $size="s">refresh</Text>}>
|
||||
{t('Rephrase')}
|
||||
</AIMenuItem>
|
||||
<AIMenuItem action="summarize" docId={doc.id} icon={<Text $isMaterialIcon $size="s">summarize</Text>}>
|
||||
{t('Summarize')}
|
||||
</AIMenuItem>
|
||||
<AIMenuItem action="correct" docId={doc.id} icon={<Text $isMaterialIcon $size="s">check</Text>}>
|
||||
{t('Correct')}
|
||||
</AIMenuItem>
|
||||
<TranslateMenu docId={doc.id} />
|
||||
</Components.Generic.Menu.Dropdown>
|
||||
</Components.Generic.Menu.Root>
|
||||
);
|
||||
}
|
||||
|
||||
interface AIMenuItemProps {
|
||||
action: AIActions;
|
||||
docId: Doc['id'];
|
||||
children: ReactNode;
|
||||
icon: ReactNode;
|
||||
}
|
||||
|
||||
const AIMenuItem = ({ action, docId, children, icon }: AIMenuItemProps) => {
|
||||
const editor = useBlockNoteEditor();
|
||||
const Components = useComponentsContext()!;
|
||||
const { mutateAsync: requestAI, isPending } = useAIRewrite();
|
||||
|
||||
const handleAIAction = useCallback(async () => {
|
||||
const selectedBlocks = editor.getSelection()?.blocks;
|
||||
|
||||
if (!selectedBlocks || selectedBlocks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const markdown = await editor.blocksToMarkdownLossy(selectedBlocks);
|
||||
const newText = await requestAI({
|
||||
docId,
|
||||
text: markdown,
|
||||
action,
|
||||
});
|
||||
|
||||
const blockMarkdown = await editor.tryParseMarkdownToBlocks(newText);
|
||||
editor.replaceBlocks(selectedBlocks, blockMarkdown);
|
||||
}, [editor, requestAI, docId, action]);
|
||||
|
||||
return (
|
||||
<Components.Generic.Menu.Item
|
||||
closeMenuOnClick={false}
|
||||
icon={icon}
|
||||
onClick={handleAIAction}
|
||||
rightSection={isPending && <Box className="loader" />}
|
||||
>
|
||||
{children}
|
||||
</Components.Generic.Menu.Item>
|
||||
);
|
||||
};
|
||||
|
||||
interface TranslateMenuProps {
|
||||
docId: Doc['id'];
|
||||
}
|
||||
|
||||
const TranslateMenu = ({ docId }: TranslateMenuProps) => {
|
||||
const Components = useComponentsContext()!;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SubMenu position="right" sub={true} icon={<Text $isMaterialIcon $size="s">translate</Text>} close>
|
||||
<Components.Generic.Menu.Trigger sub={true}>
|
||||
<Components.Generic.Menu.Item subTrigger={true}>
|
||||
{t('Translate')}
|
||||
</Components.Generic.Menu.Item>
|
||||
</Components.Generic.Menu.Trigger>
|
||||
<Components.Generic.Menu.Dropdown className="bn-menu-dropdown bn-color-picker-dropdown">
|
||||
<AIMenuItem action="translate_en" docId={docId}>
|
||||
{t('English')}
|
||||
</AIMenuItem>
|
||||
<AIMenuItem action="translate_fr" docId={docId}>
|
||||
{t('French')}
|
||||
</AIMenuItem>
|
||||
<AIMenuItem action="translate_de" docId={docId}>
|
||||
{t('German')}
|
||||
</AIMenuItem>
|
||||
</Components.Generic.Menu.Dropdown>
|
||||
</SubMenu>
|
||||
);
|
||||
};
|
||||
|
||||
const SubMenuContext = createContext<
|
||||
| {
|
||||
onMenuMouseOver: () => void;
|
||||
onMenuMouseLeave: () => void;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
|
||||
const SubMenu = forwardRef<
|
||||
HTMLButtonElement,
|
||||
ComponentProps["Generic"]["Menu"]["Root"]
|
||||
>((props, ref) => {
|
||||
const {
|
||||
children,
|
||||
onOpenChange,
|
||||
position,
|
||||
icon,
|
||||
sub, // not used
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
const itemRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const menuCloseTimer = useRef<ReturnType<typeof setTimeout> | undefined>();
|
||||
|
||||
const mouseLeave = useCallback(() => {
|
||||
if (menuCloseTimer.current) {
|
||||
clearTimeout(menuCloseTimer.current);
|
||||
}
|
||||
menuCloseTimer.current = setTimeout(() => {
|
||||
setOpened(false);
|
||||
}, 250);
|
||||
}, []);
|
||||
|
||||
const mouseOver = useCallback(() => {
|
||||
if (menuCloseTimer.current) {
|
||||
clearTimeout(menuCloseTimer.current);
|
||||
}
|
||||
setOpened(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SubMenuContext.Provider
|
||||
value={{
|
||||
onMenuMouseOver: mouseOver,
|
||||
onMenuMouseLeave: mouseLeave,
|
||||
}}>
|
||||
<MantineMenu.Item
|
||||
className="bn-menu-item bn-mt-sub-menu-item"
|
||||
closeMenuOnClick={false}
|
||||
ref={mergeRefs(ref, itemRef)}
|
||||
onMouseOver={mouseOver}
|
||||
onMouseLeave={mouseLeave}
|
||||
leftSection={icon}>
|
||||
<MantineMenu
|
||||
portalProps={{
|
||||
target: itemRef.current
|
||||
? itemRef.current.parentElement!
|
||||
: undefined,
|
||||
}}
|
||||
middlewares={{ flip: true, shift: true, inline: false, size: true }}
|
||||
trigger={"hover"}
|
||||
opened={opened}
|
||||
onClose={() => onOpenChange?.(false)}
|
||||
onOpen={() => onOpenChange?.(true)}
|
||||
position={position}>
|
||||
{children}
|
||||
</MantineMenu>
|
||||
</MantineMenu.Item>
|
||||
</SubMenuContext.Provider>
|
||||
);
|
||||
});
|
||||
+1
-1
@@ -130,7 +130,7 @@ export const BlockNoteContent = ({
|
||||
editable={doc.abilities.partial_update && !isVersion}
|
||||
theme="light"
|
||||
>
|
||||
<BlockNoteToolbar doc={doc} />
|
||||
<BlockNoteToolbar />
|
||||
</BlockNoteView>
|
||||
</Box>
|
||||
);
|
||||
|
||||
+1
-13
@@ -16,25 +16,13 @@ import {
|
||||
import { forEach, isArray } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { Doc } from '../../doc-management';
|
||||
|
||||
// import { AIButton } from './AIButton';
|
||||
import { AIGroupButton } from './AIButton';
|
||||
|
||||
interface BlockNoteToolbarProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const BlockNoteToolbar = ({ doc }: BlockNoteToolbarProps) => {
|
||||
export const BlockNoteToolbar = () => {
|
||||
return (
|
||||
<FormattingToolbarController
|
||||
formattingToolbar={() => (
|
||||
<FormattingToolbar>
|
||||
<BlockTypeSelect key="blockTypeSelect" />
|
||||
|
||||
{/* Extra button to convert from markdown to json */}
|
||||
<AIGroupButton key="AIButton" doc={doc} />
|
||||
|
||||
{/* Extra button to convert from markdown to json */}
|
||||
<MarkdownButton key="customButton" />
|
||||
|
||||
|
||||
@@ -140,16 +140,7 @@
|
||||
"accessibility-dinum-services": "<strong>DINUM</strong> s'engage à rendre accessibles ses services numériques, conformément à l'article 47 de la loi n° 2005-102 du 11 février 2005.",
|
||||
"accessibility-form-defenseurdesdroits": "Écrire un message au<1>Défenseur des droits</1>",
|
||||
"accessibility-not-audit": "<strong>docs.numerique.gouv.fr</strong> n'est pas en conformité avec le RGAA 4.1. Le site n'a <strong>pas encore été audité.</strong>",
|
||||
"you have reported to the website manager a lack of accessibility that prevents you from accessing content or one of the services of the portal and you have not received a satisfactory response.": "vous avez signalé au responsable du site internet un défaut d'accessibilité qui vous empêche d'accéder à un contenu ou à un des services du portail et vous n'avez pas obtenu de réponse satisfaisante.",
|
||||
"AI Actions": "Actions IA",
|
||||
"Use as prompt": "Utiliser comme prompt",
|
||||
"Rephrase": "Reformuler",
|
||||
"Summarize": "Résumer",
|
||||
"Correct": "Corriger",
|
||||
"Translate": "Traduire",
|
||||
"English": "Anglais",
|
||||
"French": "Français",
|
||||
"German": "Allemand"
|
||||
"you have reported to the website manager a lack of accessibility that prevents you from accessing content or one of the services of the portal and you have not received a satisfactory response.": "vous avez signalé au responsable du site internet un défaut d'accessibilité qui vous empêche d'accéder à un contenu ou à un des services du portail et vous n'avez pas obtenu de réponse satisfaisante."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,17 +41,3 @@ main ::-webkit-scrollbar-thumb:hover,
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3498db;
|
||||
border-radius: 71%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@@ -25,18 +25,15 @@
|
||||
"i18n:test": "yarn I18N run test"
|
||||
},
|
||||
"resolutions": {
|
||||
"@blocknote/core": "0.15.7",
|
||||
"@blocknote/mantine": "0.15.7",
|
||||
"@blocknote/react": "0.15.7",
|
||||
"@types/node": "20.16.5",
|
||||
"@blocknote/core": "0.15.6",
|
||||
"@blocknote/mantine": "0.15.6",
|
||||
"@blocknote/react": "0.15.6",
|
||||
"@types/node": "20.16.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.5.0",
|
||||
"@typescript-eslint/parser": "8.5.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.57.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"typescript": "5.6.2",
|
||||
"yjs": "13.6.19"
|
||||
"typescript": "5.5.4",
|
||||
"yjs": "13.6.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,20 +6,19 @@
|
||||
"lint": "eslint --ext .js ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "14.2.9",
|
||||
"@next/eslint-plugin-next": "14.2.7",
|
||||
"@tanstack/eslint-plugin-query": "5.53.0",
|
||||
"@typescript-eslint/eslint-plugin": "*",
|
||||
"@typescript-eslint/parser": "*",
|
||||
"eslint": "*",
|
||||
"eslint-config-next": "14.2.9",
|
||||
"@typescript-eslint/eslint-plugin": "8.3.0",
|
||||
"@typescript-eslint/parser": "8.3.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-next": "14.2.7",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-import": "2.30.0",
|
||||
"eslint-plugin-jest": "28.8.3",
|
||||
"eslint-plugin-jsx-a11y": "6.10.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-jest": "28.8.2",
|
||||
"eslint-plugin-jsx-a11y": "6.9.0",
|
||||
"eslint-plugin-playwright": "1.6.2",
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"eslint-plugin-react": "7.35.2",
|
||||
"eslint-plugin-testing-library": "6.3.0",
|
||||
"prettier": "3.3.3"
|
||||
"eslint-plugin-react": "7.35.0",
|
||||
"eslint-plugin-testing-library": "6.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/node": "*",
|
||||
"eslint-config-impress": "*",
|
||||
"eslint-plugin-import": "2.30.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"i18next-parser": "9.0.2",
|
||||
"jest": "29.7.0",
|
||||
"ts-jest": "29.2.5",
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hocuspocus/server": "2.13.5",
|
||||
"y-protocols": "1.0.6"
|
||||
"@hocuspocus/server": "2.13.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "*",
|
||||
|
||||
+440
-494
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user