Compare commits

..

10 Commits

Author SHA1 Message Date
Anthony LC 3eff663a78 ♻️(frontend) adapt doc visibility to new api
We updated the way we handle the visibility of a doc
in the backend. Now we use a new api to update
the visibility (documents/{id}/link-configuration/)
of a doc. We adapted the frontend to use this new api.
We changed the types to reflect the new api and
to keep the same logic.
2024-09-11 10:23:06 +02:00
Anthony LC feabab030d fixup! (models/api) add link access reach and role 2024-09-10 16:07:43 +02:00
Samuel Paccoud - DINUM 4002a049eb (models) allow null titles on documents
We want to make it as fast as possible to create a new document.
We should not have any modal asking the title before creating the
document but rather show an "untitle document" title and let the
owner set it on the already created document.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM c7d8211aac (api) allow forcing ID when creating a document via API endpoint
We need to be able to force the ID when creating a document via
the API endpoint. This is usefull for documents that are created
offline as synchronization is achieved by replaying stacked requests.

We do it via the serializer, making sure that we don't override an
existing document.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM 25a24eaf12 🔥(api) remove possibility to force document id on creation
This feature poses security issues in the way it is implemented.
We decide to remove it while clarifying the use case.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM 9adecf93ec (api) allow updating link configuration for a document
We open a specific endpoint to update documents link configuration
because it makes it more secure and simple to limit access rights
to administrators/owners whereas other document fields like title
and content can be edited by anonymous or authenticated users with
much less access rights.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM 6a3a07db31 🐛(api) fix randomly failing test on document list ordering via API
The test was randomly failing because postgresql and python sorting
was not 100% consistent e.g "treatment" vs "treat them" were not
ordered the same.

Comparing each field value insteat of relying on "sort" solves the
issue and makes the test simpler.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM 79b56ccd4e (models/api) add link access reach and role
Link access was either public or private and was only allowing readers.

This commit makes link access more powerful:
- link reach can be private (users need to obtain specific access by
  document's administrators), restricted (any authenticated user) or
  public (anybody including anonymous users)
- link role can be reader or editor.

It is thus now possible to give editor access to an anonymous user or
any authenticated user.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM 611d77f3bb 🔥(compose) remove docker compose version
The version is now automatically guessed by Docker Compose. This
commit will fix the warning raised about the uselessness of this
setting.
2024-09-10 16:02:25 +02:00
Anthony LC de0acedc0a 🛂(backend) stop to list public doc to everyone
Everybody could see the full list of public docs.
Now only members can see their public docs.
They can still access to any specific public doc.
2024-09-10 16:02:25 +02:00
24 changed files with 478 additions and 955 deletions
+2 -9
View File
@@ -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
-1
View File
@@ -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
-3
View File
@@ -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
+4 -73
View File
@@ -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',
+1 -1
View File
@@ -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():
-6
View File
@@ -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
-1
View File
@@ -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",
+1 -1
View File
@@ -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
View File
@@ -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.
+4 -6
View File
@@ -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>
);
});
@@ -130,7 +130,7 @@ export const BlockNoteContent = ({
editable={doc.abilities.partial_update && !isVersion}
theme="light"
>
<BlockNoteToolbar doc={doc} />
<BlockNoteToolbar />
</BlockNoteView>
</Box>
);
@@ -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); }
}
+6 -9
View File
@@ -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"
}
}
+1 -1
View File
@@ -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",
+1 -2
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff