Skip to content

Commit 71321a5

Browse files
committed
✨(front) add mail signatures
Allow to manage maildomain's signature from the administration. Then in the Message composer allow the user to select a signature then render a preview in the MessageEditor
1 parent 741f909 commit 71321a5

File tree

30 files changed

+1154
-178
lines changed

30 files changed

+1154
-178
lines changed

src/frontend/src/features/forms/components/message-editor/_index.scss renamed to src/frontend/src/features/blocknote/blocknote-view-field/_index.scss

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
1-
.message-editor {
2-
&--error {
3-
.message-editor {
1+
.composer-field {
2+
&.c__field--error {
3+
.composer-field-input {
44
border-color: var(--c--theme--colors--error);
55
}
66
}
7-
&--success {
8-
.message-editor {
7+
&.c__field--success {
8+
.composer-field-input {
99
border-color: var(--c--theme--colors--success);
1010
}
1111
}
1212
&--disabled {
1313
pointer-events: none;
14-
.message-editor-input {
14+
.composer-field-input {
1515
cursor: not-allowed;
1616
border-color: var(--c--theme--colors--greyscale-200);
1717
}
1818
}
1919
}
20-
.message-editor-input {
20+
.composer-field-input {
2121
border-radius: var(--c--components--forms-input--border-radius);
2222
border-width: var(--c--components--forms-input--border-width);
2323
border-color: var(--c--components--forms-input--border-color);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core";
2+
import { BlockNoteView } from "@blocknote/mantine";
3+
import { Field, FieldProps } from "@openfun/cunningham-react";
4+
import clsx from "clsx";
5+
import { PropsWithChildren } from "react";
6+
7+
type BlockNoteViewFieldProps<BSchema extends BlockSchema, ISchema extends InlineContentSchema, SSchema extends StyleSchema> = PropsWithChildren<FieldProps & {
8+
composerProps: Parameters<typeof BlockNoteView<BSchema, ISchema, SSchema>>[0];
9+
disabled?: boolean;
10+
}>
11+
export const BlockNoteViewField = <BSchema extends BlockSchema, ISchema extends InlineContentSchema, SSchema extends StyleSchema>({ composerProps, disabled = false, children, ...fieldProps }: BlockNoteViewFieldProps<BSchema, ISchema, SSchema>) => {
12+
return (
13+
<Field {...fieldProps} className={clsx(fieldProps?.className, "composer-field", { 'composer-field--disabled': disabled })}>
14+
<BlockNoteView
15+
theme="light"
16+
sideMenu={false}
17+
slashMenu={false}
18+
formattingToolbar={false}
19+
{...composerProps}
20+
className={clsx(composerProps.className, "composer-field-input")}
21+
editable={!disabled}
22+
>
23+
{children}
24+
</BlockNoteView>
25+
</Field>
26+
)
27+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { createReactInlineContentSpec } from "@blocknote/react";
2+
import React, { useMemo } from "react";
3+
import { useBlockNoteEditor, useComponentsContext } from "@blocknote/react";
4+
import { Icon, IconSize, Spinner } from "@gouvfr-lasuite/ui-kit";
5+
import { PlaceholdersRetrieve200 } from "@/features/api/gen";
6+
import { SignatureComposerBlockSchema, SignatureComposerInlineContentSchema, SignatureComposerStyleSchema } from "@/features/layouts/components/admin/modal-compose-signature/signature-composer";
7+
8+
type TemplateVariableSelectorProps = {
9+
variables: PlaceholdersRetrieve200;
10+
isLoading: boolean;
11+
}
12+
13+
export const TemplateVariableSelector = ({ variables, isLoading }: TemplateVariableSelectorProps) => {
14+
const editor = useBlockNoteEditor<SignatureComposerBlockSchema, SignatureComposerInlineContentSchema, SignatureComposerStyleSchema>();
15+
const Components = useComponentsContext()!;
16+
const variableItems = useMemo(() => {
17+
if (!variables) return [];
18+
return Object.entries(variables).map(([value, label]) => ({
19+
text: label,
20+
icon: null,
21+
isSelected: false,
22+
onClick: () => {
23+
editor.insertInlineContent([{ type: "template-variable", props: { label, value } }, " "]);
24+
}
25+
}));
26+
}, [variables]);
27+
28+
if (isLoading) {
29+
return (
30+
<Components.FormattingToolbar.Button icon={<Spinner size="sm" />} isDisabled={true} label="Loading variables..." mainTooltip="Loading variables..." />
31+
);
32+
}
33+
34+
if (!variables) {
35+
return null;
36+
}
37+
38+
return (
39+
<Components.FormattingToolbar.Select
40+
key={"templateVariableSelector"}
41+
items={[
42+
{
43+
text: "Variables",
44+
isSelected: true,
45+
isDisabled: true,
46+
icon: <Icon name="space_bar" size={IconSize.SMALL} />,
47+
onClick: () => {}
48+
},
49+
...variableItems,
50+
]}
51+
/>
52+
);
53+
}
54+
55+
56+
export const InlineTemplateVariable = createReactInlineContentSpec(
57+
{
58+
type: "template-variable",
59+
content: "none",
60+
draggable: true,
61+
propSchema: {
62+
value: { default: "" },
63+
label: { default: "" },
64+
},
65+
},
66+
{
67+
render: ({ inlineContent: { props } }) => {
68+
return (
69+
// TODO : Find a way to display variable name
70+
// and (de)serialize this inline content during export and parsing
71+
<span data-inline-type="template-variable">
72+
{`{${props.value}}`}
73+
</span>
74+
);
75+
},
76+
}
77+
);
78+
79+
80+
81+
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { createReactBlockSpec, useBlockNoteEditor, useComponentsContext, useEditorContentOrSelectionChange } from "@blocknote/react";
2+
import { Icon, IconSize, Spinner } from "@gouvfr-lasuite/ui-kit";
3+
import { useEffect, useState } from "react";
4+
import { Props } from "@blocknote/core";
5+
import { ReadOnlyMessageTemplate, useMessageTemplatesRenderRetrieve } from "@/features/api/gen";
6+
import { MessageComposerBlockSchema, MessageComposerInlineContentSchema, MessageComposerStyleSchema, PartialMessageComposerBlockSchema } from "@/features/forms/components/message-composer";
7+
8+
9+
type SignatureTemplateSelectorProps = {
10+
mailboxId?: string;
11+
templates?: ReadOnlyMessageTemplate[];
12+
isLoading?: boolean;
13+
}
14+
15+
/**
16+
* A BlockNote toolbar selector which allows the user to select a signature template from
17+
* all active signatures for a given mailbox.
18+
*/
19+
export const SignatureTemplateSelector = ({ mailboxId, templates = [], isLoading }: SignatureTemplateSelectorProps) => {
20+
const editor = useBlockNoteEditor<MessageComposerBlockSchema, MessageComposerInlineContentSchema, MessageComposerStyleSchema>();
21+
const Components = useComponentsContext()!;
22+
23+
// Tracks whether the text & background are both blue.
24+
const [isSelected, setIsSelected] = useState<string>();
25+
26+
// Updates state on content or selection change.
27+
useEditorContentOrSelectionChange(() => {
28+
const signatureBlock = editor.getBlock('signature');
29+
if (signatureBlock) {
30+
setIsSelected((signatureBlock.props as BlockSignatureConfigProps).templateId);
31+
}
32+
}, editor);
33+
34+
useEffect(() => {
35+
if(!isSelected) {
36+
const signatureBlock = editor.getBlock('signature');
37+
if (signatureBlock) {
38+
setIsSelected((signatureBlock.props as BlockSignatureConfigProps).templateId);
39+
}
40+
}
41+
}, []);
42+
43+
if (isLoading) {
44+
return <Spinner size="sm" />;
45+
}
46+
47+
if (templates.length === 0) {
48+
return null;
49+
}
50+
51+
return (
52+
<Components.FormattingToolbar.Select
53+
key={"templateVariableSelector"}
54+
items={[
55+
{
56+
text: "Signatures",
57+
isSelected: !isSelected,
58+
isDisabled: true,
59+
icon: <Icon name="content_copy" size={IconSize.SMALL} />,
60+
onClick: () => {}
61+
},
62+
...templates.map((template) => ({
63+
text: `Signature : ${template.name}`,
64+
isSelected: isSelected === template.id,
65+
icon: <Icon name="content_copy" size={IconSize.SMALL} />,
66+
onClick: () => {
67+
const signatureBlock = editor.getBlock('signature');
68+
if (signatureBlock) {
69+
editor.replaceBlocks(
70+
["signature"],
71+
[{ id: "signature", type: "signature", props: { templateId: template.id, mailboxId: mailboxId } }] as unknown as PartialMessageComposerBlockSchema[]
72+
);
73+
} else {
74+
editor.insertBlocks(
75+
[{ id: "signature", type: "signature", props: { templateId: template.id, mailboxId: mailboxId } }] as unknown as PartialMessageComposerBlockSchema[],
76+
editor.document[0].id,
77+
"after"
78+
);
79+
}
80+
81+
}
82+
})),
83+
]}
84+
/>
85+
);
86+
}
87+
88+
/**
89+
* A BlockNote custom block which displays a signature template.
90+
*/
91+
export const BlockSignature = createReactBlockSpec(
92+
{
93+
type: "signature",
94+
content: "none",
95+
isSelectable: false,
96+
isFileBlock: false,
97+
propSchema: {
98+
templateId: { default: "" },
99+
mailboxId: { default: "" },
100+
username: { default: "" },
101+
}
102+
},
103+
{
104+
render: ({ block : { props }}) => {
105+
// eslint-disable-next-line react-hooks/rules-of-hooks
106+
const { data: { data: preview = null } = {}, isLoading } = useMessageTemplatesRenderRetrieve(props.templateId, {
107+
request: {
108+
params: {
109+
mailbox_id: props.mailboxId
110+
}
111+
}
112+
});
113+
114+
if (isLoading) {
115+
return <Spinner size="sm" />;
116+
}
117+
118+
if (!preview?.html_body) {
119+
return null;
120+
}
121+
122+
return (
123+
<div dangerouslySetInnerHTML={{ __html: preview.html_body }} />
124+
)
125+
},
126+
toExternalHTML: ({ block : { props }}) => {
127+
// This will be parsed by the backend to insert the signature in the message body
128+
return <p>{`<SignatureID>${props.templateId}</SignatureID>`}</p>
129+
}
130+
}
131+
)
132+
export type BlockSignatureConfigProps = Props<typeof BlockSignature['config']["propSchema"]>;

src/frontend/src/features/forms/components/message-editor/toolbar.tsx renamed to src/frontend/src/features/blocknote/toolbar.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { BasicTextStyleButton, BlockTypeSelect, CreateLinkButton, FormattingToolbar } from "@blocknote/react";
22

3-
const MessageEditorToolbar = () => {
3+
type ToolbarProps = {
4+
children?: React.ReactNode;
5+
}
6+
export const Toolbar = ({ children }: ToolbarProps) => {
47
return (
58
<FormattingToolbar>
69
<BlockTypeSelect key={"blockTypeSelect"} />
@@ -20,13 +23,8 @@ const MessageEditorToolbar = () => {
2023
basicTextStyle={"strike"}
2124
key={"strikeStyleButton"}
2225
/>
23-
<BasicTextStyleButton
24-
key={"codeStyleButton"}
25-
basicTextStyle={"code"}
26-
/>
2726
<CreateLinkButton key={"createLinkButton"} />
27+
{children}
2828
</FormattingToolbar>
2929
)
3030
}
31-
32-
export default MessageEditorToolbar;

0 commit comments

Comments
 (0)