mirror of
https://github.com/suitenumerique/docs.git
synced 2026-04-26 01:25:05 +02:00
Compare commits
2 Commits
hack2025/a
...
hack2025/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30382c24fc | ||
|
|
d952815932 |
@@ -26,9 +26,9 @@ readme = "README.md"
|
|||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"beautifulsoup4==4.13.4",
|
"beautifulsoup4==4.13.4",
|
||||||
"boto3==1.38.23",
|
"boto3==1.38.27",
|
||||||
"Brotli==1.1.0",
|
"Brotli==1.1.0",
|
||||||
"celery[redis]==5.5.2",
|
"celery[redis]==5.5.3",
|
||||||
"django-configurations==2.5.1",
|
"django-configurations==2.5.1",
|
||||||
"django-cors-headers==4.7.0",
|
"django-cors-headers==4.7.0",
|
||||||
"django-countries==7.6.1",
|
"django-countries==7.6.1",
|
||||||
@@ -46,12 +46,12 @@ dependencies = [
|
|||||||
"easy_thumbnails==2.10",
|
"easy_thumbnails==2.10",
|
||||||
"factory_boy==3.3.3",
|
"factory_boy==3.3.3",
|
||||||
"gunicorn==23.0.0",
|
"gunicorn==23.0.0",
|
||||||
"jsonschema==4.23.0",
|
"jsonschema==4.24.0",
|
||||||
"lxml==5.4.0",
|
"lxml==5.4.0",
|
||||||
"markdown==3.8",
|
"markdown==3.8",
|
||||||
"mozilla-django-oidc==4.0.1",
|
"mozilla-django-oidc==4.0.1",
|
||||||
"nested-multipart-parser==1.5.0",
|
"nested-multipart-parser==1.5.0",
|
||||||
"openai==1.82.0",
|
"openai==1.82.1",
|
||||||
"psycopg[binary]==3.2.9",
|
"psycopg[binary]==3.2.9",
|
||||||
"pycrdt==0.12.20",
|
"pycrdt==0.12.20",
|
||||||
"PyJWT==2.10.1",
|
"PyJWT==2.10.1",
|
||||||
@@ -72,10 +72,10 @@ dependencies = [
|
|||||||
dev = [
|
dev = [
|
||||||
"django-extensions==4.1",
|
"django-extensions==4.1",
|
||||||
"django-test-migrations==1.5.0",
|
"django-test-migrations==1.5.0",
|
||||||
"drf-spectacular-sidecar==2025.5.1",
|
"drf-spectacular-sidecar==2025.6.1",
|
||||||
"freezegun==1.5.2",
|
"freezegun==1.5.2",
|
||||||
"ipdb==0.13.13",
|
"ipdb==0.13.13",
|
||||||
"ipython==9.2.0",
|
"ipython==9.3.0",
|
||||||
"pyfakefs==5.8.0",
|
"pyfakefs==5.8.0",
|
||||||
"pylint-django==2.6.1",
|
"pylint-django==2.6.1",
|
||||||
"pylint==3.3.7",
|
"pylint==3.3.7",
|
||||||
@@ -83,10 +83,10 @@ dev = [
|
|||||||
"pytest-django==4.11.1",
|
"pytest-django==4.11.1",
|
||||||
"pytest==8.3.5",
|
"pytest==8.3.5",
|
||||||
"pytest-icdiff==0.9",
|
"pytest-icdiff==0.9",
|
||||||
"pytest-xdist==3.6.1",
|
"pytest-xdist==3.7.0",
|
||||||
"responses==0.25.7",
|
"responses==0.25.7",
|
||||||
"ruff==0.11.11",
|
"ruff==0.11.12",
|
||||||
"types-requests==2.32.0.20250515",
|
"types-requests==2.32.0.20250602",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import {
|
||||||
|
BlockNoteSchema,
|
||||||
|
PartialBlock,
|
||||||
|
defaultBlockSpecs,
|
||||||
|
} from '@blocknote/core';
|
||||||
|
import { ServerBlockNoteEditor } from '@blocknote/server-util';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
|
import { logger, toBase64 } from '@/utils';
|
||||||
|
|
||||||
|
import { CalloutBlock, DividerBlock } from './custom-blocks';
|
||||||
|
|
||||||
|
const blockNoteSchema = BlockNoteSchema.create({
|
||||||
|
blockSpecs: {
|
||||||
|
...defaultBlockSpecs,
|
||||||
|
callout: CalloutBlock,
|
||||||
|
divider: DividerBlock,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type DocsBlockSchema = typeof blockNoteSchema.blockSchema;
|
||||||
|
type DocsInlineContentSchema = typeof blockNoteSchema.inlineContentSchema;
|
||||||
|
type DocsStyleSchema = typeof blockNoteSchema.styleSchema;
|
||||||
|
|
||||||
|
interface ConversionRequest {
|
||||||
|
blocks: PartialBlock<DocsBlockSchema>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConversionResponse {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorResponse {
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertBlocksHandler = (
|
||||||
|
req: Request<
|
||||||
|
object,
|
||||||
|
ConversionResponse | ErrorResponse,
|
||||||
|
ConversionRequest,
|
||||||
|
object
|
||||||
|
>,
|
||||||
|
res: Response<ConversionResponse | ErrorResponse>,
|
||||||
|
) => {
|
||||||
|
const blocks = req.body?.blocks;
|
||||||
|
if (!blocks) {
|
||||||
|
res.status(400).json({ error: 'Invalid request: missing content' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a server editor with custom block schema
|
||||||
|
const editor = ServerBlockNoteEditor.create<
|
||||||
|
DocsBlockSchema,
|
||||||
|
DocsInlineContentSchema,
|
||||||
|
DocsStyleSchema
|
||||||
|
>({
|
||||||
|
schema: blockNoteSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a Yjs Document from blocks, and encode it as a base64 string
|
||||||
|
const yDocument = editor.blocksToYDoc(blocks, 'document-store');
|
||||||
|
const content = toBase64(Y.encodeStateAsUpdate(yDocument));
|
||||||
|
|
||||||
|
res.status(200).json({ content });
|
||||||
|
} catch (e) {
|
||||||
|
logger('conversion failed:', e);
|
||||||
|
res.status(500).json({ error: String(e) });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
|
import { defaultProps, insertOrUpdateBlock } from '@blocknote/core';
|
||||||
|
import { BlockTypeSelectItem, createReactBlockSpec } from '@blocknote/react';
|
||||||
|
import { TFunction } from 'i18next';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
|
import { Box, BoxButton, Icon } from '@/components';
|
||||||
|
|
||||||
|
import { DocsBlockNoteEditor } from '../../types';
|
||||||
|
import { EmojiPicker } from '../EmojiPicker';
|
||||||
|
|
||||||
|
const calloutCustom = [
|
||||||
|
{
|
||||||
|
name: 'Callout',
|
||||||
|
id: 'callout',
|
||||||
|
emojis: [
|
||||||
|
'bulb',
|
||||||
|
'point_right',
|
||||||
|
'point_up',
|
||||||
|
'ok_hand',
|
||||||
|
'key',
|
||||||
|
'construction',
|
||||||
|
'warning',
|
||||||
|
'fire',
|
||||||
|
'pushpin',
|
||||||
|
'scissors',
|
||||||
|
'question',
|
||||||
|
'no_entry',
|
||||||
|
'no_entry_sign',
|
||||||
|
'alarm_clock',
|
||||||
|
'phone',
|
||||||
|
'rotating_light',
|
||||||
|
'recycle',
|
||||||
|
'white_check_mark',
|
||||||
|
'lock',
|
||||||
|
'paperclip',
|
||||||
|
'book',
|
||||||
|
'speaking_head_in_silhouette',
|
||||||
|
'arrow_right',
|
||||||
|
'loudspeaker',
|
||||||
|
'hammer_and_wrench',
|
||||||
|
'gear',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const calloutCategories = [
|
||||||
|
'callout',
|
||||||
|
'people',
|
||||||
|
'nature',
|
||||||
|
'foods',
|
||||||
|
'activity',
|
||||||
|
'places',
|
||||||
|
'flags',
|
||||||
|
'objects',
|
||||||
|
'symbols',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CalloutBlock = createReactBlockSpec(
|
||||||
|
{
|
||||||
|
type: 'callout',
|
||||||
|
propSchema: {
|
||||||
|
textAlignment: defaultProps.textAlignment,
|
||||||
|
backgroundColor: defaultProps.backgroundColor,
|
||||||
|
emoji: { default: '💡' },
|
||||||
|
},
|
||||||
|
content: 'inline',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
render: ({ block, editor, contentRef }) => {
|
||||||
|
const [openEmojiPicker, setOpenEmojiPicker] = useState(false);
|
||||||
|
|
||||||
|
const toggleEmojiPicker = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setOpenEmojiPicker(!openEmojiPicker);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickOutside = () => setOpenEmojiPicker(false);
|
||||||
|
|
||||||
|
const onEmojiSelect = ({ native }: { native: string }) => {
|
||||||
|
editor.updateBlock(block, { props: { emoji: native } });
|
||||||
|
setOpenEmojiPicker(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Temporary: sets a yellow background color to a callout block when added by
|
||||||
|
// the user, while keeping the colors menu on the drag handler usable for
|
||||||
|
// this custom block.
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!block.content.length &&
|
||||||
|
block.props.backgroundColor === 'default'
|
||||||
|
) {
|
||||||
|
editor.updateBlock(block, { props: { backgroundColor: 'yellow' } });
|
||||||
|
}
|
||||||
|
}, [block, editor]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
$padding="1rem"
|
||||||
|
$gap="0.625rem"
|
||||||
|
style={{
|
||||||
|
flexGrow: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BoxButton
|
||||||
|
contentEditable={false}
|
||||||
|
onClick={toggleEmojiPicker}
|
||||||
|
$css={css`
|
||||||
|
font-size: 1.125rem;
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
$align="center"
|
||||||
|
$height="28px"
|
||||||
|
$width="28px"
|
||||||
|
$radius="4px"
|
||||||
|
>
|
||||||
|
{block.props.emoji}
|
||||||
|
</BoxButton>
|
||||||
|
|
||||||
|
{openEmojiPicker && (
|
||||||
|
<EmojiPicker
|
||||||
|
categories={calloutCategories}
|
||||||
|
custom={calloutCustom}
|
||||||
|
onClickOutside={onClickOutside}
|
||||||
|
onEmojiSelect={onEmojiSelect}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Box as="p" className="inline-content" ref={contentRef} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getCalloutReactSlashMenuItems = (
|
||||||
|
editor: DocsBlockNoteEditor,
|
||||||
|
t: TFunction<'translation', undefined>,
|
||||||
|
group: string,
|
||||||
|
) => [
|
||||||
|
{
|
||||||
|
title: t('Callout'),
|
||||||
|
onItemClick: () => {
|
||||||
|
insertOrUpdateBlock(editor, {
|
||||||
|
type: 'callout',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
aliases: ['callout', 'encadré', 'hervorhebung', 'benadrukken'],
|
||||||
|
group,
|
||||||
|
icon: <Icon iconName="lightbulb" $size="18px" />,
|
||||||
|
subtext: t('Add a callout block'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getCalloutFormattingToolbarItems = (
|
||||||
|
t: TFunction<'translation', undefined>,
|
||||||
|
): BlockTypeSelectItem => ({
|
||||||
|
name: t('Callout'),
|
||||||
|
type: 'callout',
|
||||||
|
icon: () => <Icon iconName="lightbulb" $size="16px" />,
|
||||||
|
isSelected: (block) => block.type === 'callout',
|
||||||
|
});
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { insertOrUpdateBlock } from '@blocknote/core';
|
||||||
|
import { createReactBlockSpec } from '@blocknote/react';
|
||||||
|
import { TFunction } from 'i18next';
|
||||||
|
|
||||||
|
import { Box, Icon } from '@/components';
|
||||||
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
|
|
||||||
|
import { DocsBlockNoteEditor } from '../../types';
|
||||||
|
|
||||||
|
export const DividerBlock = createReactBlockSpec(
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
propSchema: {},
|
||||||
|
content: 'none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
render: () => {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const { colorsTokens } = useCunninghamTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
as="hr"
|
||||||
|
$width="100%"
|
||||||
|
$background={colorsTokens['greyscale-300']}
|
||||||
|
$margin="1rem 0"
|
||||||
|
$css={`border: 1px solid ${colorsTokens['greyscale-300']};`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getDividerReactSlashMenuItems = (
|
||||||
|
editor: DocsBlockNoteEditor,
|
||||||
|
t: TFunction<'translation', undefined>,
|
||||||
|
group: string,
|
||||||
|
) => [
|
||||||
|
{
|
||||||
|
title: t('Divider'),
|
||||||
|
onItemClick: () => {
|
||||||
|
insertOrUpdateBlock(editor, {
|
||||||
|
type: 'divider',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
aliases: ['divider', 'hr', 'horizontal rule', 'line', 'separator'],
|
||||||
|
group,
|
||||||
|
icon: <Icon iconName="remove" $size="18px" />,
|
||||||
|
subtext: t('Add a horizontal line'),
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './CalloutBlock';
|
||||||
|
export * from './DividerBlock';
|
||||||
Reference in New Issue
Block a user