diff --git a/packages/twenty-emails/src/utils/email-renderer/nodes/bullet-list.tsx b/packages/twenty-emails/src/utils/email-renderer/nodes/bullet-list.tsx
new file mode 100644
index 0000000000..a1dcf88579
--- /dev/null
+++ b/packages/twenty-emails/src/utils/email-renderer/nodes/bullet-list.tsx
@@ -0,0 +1,16 @@
+import { type JSONContent } from '@tiptap/core';
+import { type ReactNode } from 'react';
+import { mappedNodeContent } from 'src/utils/email-renderer/renderers/render-node';
+
+export const bulletList = (node: JSONContent): ReactNode => {
+ return (
+
+ {mappedNodeContent(node)}
+
+ );
+};
diff --git a/packages/twenty-emails/src/utils/email-renderer/nodes/list-item.tsx b/packages/twenty-emails/src/utils/email-renderer/nodes/list-item.tsx
new file mode 100644
index 0000000000..1eeace2566
--- /dev/null
+++ b/packages/twenty-emails/src/utils/email-renderer/nodes/list-item.tsx
@@ -0,0 +1,16 @@
+import { type JSONContent } from '@tiptap/core';
+import { type ReactNode } from 'react';
+import { mappedNodeContent } from 'src/utils/email-renderer/renderers/render-node';
+
+export const listItem = (node: JSONContent): ReactNode => {
+ return (
+
+ {mappedNodeContent(node)}
+
+ );
+};
diff --git a/packages/twenty-emails/src/utils/email-renderer/nodes/ordered-list.tsx b/packages/twenty-emails/src/utils/email-renderer/nodes/ordered-list.tsx
new file mode 100644
index 0000000000..27dc8d461a
--- /dev/null
+++ b/packages/twenty-emails/src/utils/email-renderer/nodes/ordered-list.tsx
@@ -0,0 +1,16 @@
+import { type JSONContent } from '@tiptap/core';
+import { type ReactNode } from 'react';
+import { mappedNodeContent } from 'src/utils/email-renderer/renderers/render-node';
+
+export const orderedList = (node: JSONContent): ReactNode => {
+ return (
+
+ {mappedNodeContent(node)}
+
+ );
+};
diff --git a/packages/twenty-emails/src/utils/email-renderer/renderers/render-node.tsx b/packages/twenty-emails/src/utils/email-renderer/renderers/render-node.tsx
index 0b71d24a6b..2954d3c42f 100644
--- a/packages/twenty-emails/src/utils/email-renderer/renderers/render-node.tsx
+++ b/packages/twenty-emails/src/utils/email-renderer/renderers/render-node.tsx
@@ -6,6 +6,9 @@ import { image } from '../nodes/image';
import { paragraph } from '../nodes/paragraph';
import { text } from '../nodes/text';
import { variableTag } from '../nodes/variable-tag';
+import { bulletList } from '../nodes/bullet-list';
+import { listItem } from '../nodes/list-item';
+import { orderedList } from '../nodes/ordered-list';
const NODE_RENDERERS = {
[TIPTAP_NODE_TYPES.PARAGRAPH]: paragraph,
@@ -13,6 +16,9 @@ const NODE_RENDERERS = {
[TIPTAP_NODE_TYPES.HEADING]: heading,
[TIPTAP_NODE_TYPES.VARIABLE_TAG]: variableTag,
[TIPTAP_NODE_TYPES.IMAGE]: image,
+ [TIPTAP_NODE_TYPES.BULLET_LIST]: bulletList,
+ [TIPTAP_NODE_TYPES.ORDERED_LIST]: orderedList,
+ [TIPTAP_NODE_TYPES.LIST_ITEM]: listItem,
};
const renderNode = (node: JSONContent): ReactNode => {
diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json
index 22dd02d827..10ab402a81 100644
--- a/packages/twenty-front/package.json
+++ b/packages/twenty-front/package.json
@@ -63,6 +63,7 @@
"@tiptap/extension-image": "^3.4.4",
"@tiptap/extension-italic": "^3.4.2",
"@tiptap/extension-link": "^3.4.2",
+ "@tiptap/extension-list": "3.4.2",
"@tiptap/extension-paragraph": "^3.4.2",
"@tiptap/extension-strike": "^3.4.2",
"@tiptap/extension-text": "^3.4.2",
diff --git a/packages/twenty-front/src/modules/advanced-text-editor/components/AdvancedTextEditor.tsx b/packages/twenty-front/src/modules/advanced-text-editor/components/AdvancedTextEditor.tsx
index da7e9f889d..34fe7eb610 100644
--- a/packages/twenty-front/src/modules/advanced-text-editor/components/AdvancedTextEditor.tsx
+++ b/packages/twenty-front/src/modules/advanced-text-editor/components/AdvancedTextEditor.tsx
@@ -66,6 +66,11 @@ const StyledEditorContainer = styled.div<{
h3 {
font-size: 24px;
}
+
+ li {
+ margin-bottom: ${({ theme }) => theme.spacing(2)};
+ line-height: 1.5;
+ }
}
.ProseMirror-focused {
diff --git a/packages/twenty-front/src/modules/advanced-text-editor/components/TextBubbleMenu.tsx b/packages/twenty-front/src/modules/advanced-text-editor/components/TextBubbleMenu.tsx
index 872d15a154..3aedcb9220 100644
--- a/packages/twenty-front/src/modules/advanced-text-editor/components/TextBubbleMenu.tsx
+++ b/packages/twenty-front/src/modules/advanced-text-editor/components/TextBubbleMenu.tsx
@@ -9,6 +9,8 @@ import { BubbleMenu } from '@tiptap/react/menus';
import {
IconBold,
IconItalic,
+ IconList,
+ IconListNumbers,
IconStrikethrough,
IconUnderline,
} from 'twenty-ui/display';
@@ -51,6 +53,16 @@ export const TextBubbleMenu = ({ editor }: TextBubbleMenuProps) => {
onClick: () => editor.chain().focus().toggleStrike().run(),
isActive: state.isStrike,
},
+ {
+ Icon: IconList,
+ onClick: () => editor.chain().focus().wrapInList('bulletList').run(),
+ isActive: state.isBulletList,
+ },
+ {
+ Icon: IconListNumbers,
+ onClick: () => editor.chain().focus().wrapInList('orderedList').run(),
+ isActive: state.isOrderedList,
+ },
];
const handleShouldShow = () => {
diff --git a/packages/twenty-front/src/modules/advanced-text-editor/components/__stories__/AdvancedTextEditor.stories.tsx b/packages/twenty-front/src/modules/advanced-text-editor/components/__stories__/AdvancedTextEditor.stories.tsx
index 914592d367..ae549fbd3f 100644
--- a/packages/twenty-front/src/modules/advanced-text-editor/components/__stories__/AdvancedTextEditor.stories.tsx
+++ b/packages/twenty-front/src/modules/advanced-text-editor/components/__stories__/AdvancedTextEditor.stories.tsx
@@ -1,7 +1,8 @@
import { AdvancedTextEditor } from '@/advanced-text-editor/components/AdvancedTextEditor';
import { useAdvancedTextEditor } from '@/advanced-text-editor/hooks/useAdvancedTextEditor';
import { type Meta, type StoryObj } from '@storybook/react';
-import { fn } from '@storybook/test';
+import { expect, fn, userEvent } from '@storybook/test';
+import { isDefined } from 'twenty-shared/utils';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
@@ -293,3 +294,147 @@ export const CustomSize: Story = {
placeholder: 'This editor has custom dimensions...',
},
};
+
+export const WithLists: Story = {
+ args: {
+ defaultValue: JSON.stringify({
+ type: 'doc',
+ content: [
+ {
+ type: 'heading',
+ attrs: { level: 2 },
+ content: [{ type: 'text', text: 'Project Requirements' }],
+ },
+ {
+ type: 'bulletList',
+ content: [
+ {
+ type: 'listItem',
+ content: [
+ {
+ type: 'paragraph',
+ content: [
+ { type: 'text', text: 'User authentication system' },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'listItem',
+ content: [
+ {
+ type: 'paragraph',
+ content: [{ type: 'text', text: 'Database integration' }],
+ },
+ ],
+ },
+ {
+ type: 'listItem',
+ content: [
+ {
+ type: 'paragraph',
+ content: [{ type: 'text', text: 'API endpoints' }],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'heading',
+ attrs: { level: 2 },
+ content: [{ type: 'text', text: 'Implementation Steps' }],
+ },
+ {
+ type: 'orderedList',
+ content: [
+ {
+ type: 'listItem',
+ content: [
+ {
+ type: 'paragraph',
+ content: [
+ { type: 'text', text: 'Set up development environment' },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'listItem',
+ content: [
+ {
+ type: 'paragraph',
+ content: [{ type: 'text', text: 'Create database schema' }],
+ },
+ ],
+ },
+ {
+ type: 'listItem',
+ content: [
+ {
+ type: 'paragraph',
+ content: [{ type: 'text', text: 'Implement authentication' }],
+ },
+ ],
+ },
+ {
+ type: 'listItem',
+ content: [
+ {
+ type: 'paragraph',
+ content: [{ type: 'text', text: 'Build API endpoints' }],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ }),
+ },
+ play: async ({ canvasElement, step }) => {
+ await step('Verify bullet list is rendered', async () => {
+ const listItems = canvasElement.querySelectorAll('ul li');
+ expect(listItems.length).toBeGreaterThan(0);
+
+ const firstItem = listItems[0];
+ expect(firstItem).toBeInTheDocument();
+ expect(firstItem).toHaveTextContent(/User authentication system/i);
+ });
+
+ await step('Verify ordered list is rendered', async () => {
+ const orderedListItems = canvasElement.querySelectorAll('ol li');
+ expect(orderedListItems.length).toBeGreaterThan(0);
+
+ const firstOrderedItem = orderedListItems[0];
+ expect(firstOrderedItem).toBeInTheDocument();
+ expect(firstOrderedItem).toHaveTextContent(
+ /Set up development environment/i,
+ );
+ });
+
+ await step('Test list interaction', async () => {
+ const editorContent = canvasElement.querySelector('.tiptap');
+ expect(editorContent).toBeInTheDocument();
+
+ const firstListItem = canvasElement.querySelector('ul li');
+ if (isDefined(firstListItem)) {
+ await userEvent.click(firstListItem);
+
+ expect(editorContent).toHaveFocus();
+ }
+ });
+
+ await step('Verify list structure', async () => {
+ const bulletList = canvasElement.querySelector('ul');
+ expect(bulletList).toBeInTheDocument();
+
+ const orderedList = canvasElement.querySelector('ol');
+ expect(orderedList).toBeInTheDocument();
+
+ const bulletListItems = canvasElement.querySelectorAll('ul li');
+ expect(bulletListItems.length).toBe(3);
+
+ const orderedListItems = canvasElement.querySelectorAll('ol li');
+ expect(orderedListItems.length).toBe(4);
+ });
+ },
+};
diff --git a/packages/twenty-front/src/modules/advanced-text-editor/hooks/useAdvancedTextEditor.ts b/packages/twenty-front/src/modules/advanced-text-editor/hooks/useAdvancedTextEditor.ts
index b552c5fa5f..221d711136 100644
--- a/packages/twenty-front/src/modules/advanced-text-editor/hooks/useAdvancedTextEditor.ts
+++ b/packages/twenty-front/src/modules/advanced-text-editor/hooks/useAdvancedTextEditor.ts
@@ -8,6 +8,7 @@ import { HardBreak } from '@tiptap/extension-hard-break';
import { Heading } from '@tiptap/extension-heading';
import { Italic } from '@tiptap/extension-italic';
import { Link } from '@tiptap/extension-link';
+import { ListKit } from '@tiptap/extension-list';
import { Paragraph } from '@tiptap/extension-paragraph';
import { Strike } from '@tiptap/extension-strike';
import { Text } from '@tiptap/extension-text';
@@ -66,6 +67,7 @@ export const useAdvancedTextEditor = (
}),
ResizableImage,
Dropcursor,
+ ListKit,
UploadImageExtension.configure({
onImageUpload,
onImageUploadError,
diff --git a/packages/twenty-front/src/modules/advanced-text-editor/hooks/useTextBubbleState.ts b/packages/twenty-front/src/modules/advanced-text-editor/hooks/useTextBubbleState.ts
index 5c456a30fe..3cdcdc14fb 100644
--- a/packages/twenty-front/src/modules/advanced-text-editor/hooks/useTextBubbleState.ts
+++ b/packages/twenty-front/src/modules/advanced-text-editor/hooks/useTextBubbleState.ts
@@ -12,6 +12,8 @@ export const useTextBubbleState = (editor: Editor) => {
isUnderline: ctx.editor.isActive('underline'),
isLink: ctx.editor.isActive('link'),
linkHref: ctx.editor.getAttributes('link').href || '',
+ isBulletList: ctx.editor.isActive('bulletList'),
+ isOrderedList: ctx.editor.isActive('orderedList'),
};
},
});
diff --git a/packages/twenty-shared/src/utils/tiptap/tiptap-marks.ts b/packages/twenty-shared/src/utils/tiptap/tiptap-marks.ts
index a236f74511..970359fbb0 100644
--- a/packages/twenty-shared/src/utils/tiptap/tiptap-marks.ts
+++ b/packages/twenty-shared/src/utils/tiptap/tiptap-marks.ts
@@ -14,6 +14,9 @@ export const TIPTAP_NODE_TYPES = {
HEADING: 'heading',
VARIABLE_TAG: 'variableTag',
IMAGE: 'image',
+ BULLET_LIST: 'bulletList',
+ ORDERED_LIST: 'orderedList',
+ LIST_ITEM: 'listItem',
} as const;
export type TipTapMarkType =
diff --git a/yarn.lock b/yarn.lock
index 619331165e..b4d3b77faa 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -21592,6 +21592,16 @@ __metadata:
languageName: node
linkType: hard
+"@tiptap/extension-list@npm:3.4.2":
+ version: 3.4.2
+ resolution: "@tiptap/extension-list@npm:3.4.2"
+ peerDependencies:
+ "@tiptap/core": ^3.4.2
+ "@tiptap/pm": ^3.4.2
+ checksum: 10c0/6090c5758411201dce90a0bf26e4d766cc74113a3284274d9903580d5295f2eaf7a682ba3e921111e6304fd86c42aead0ac23369296c709038aa15febdb8a900
+ languageName: node
+ linkType: hard
+
"@tiptap/extension-paragraph@npm:^2.11.5":
version: 2.12.0
resolution: "@tiptap/extension-paragraph@npm:2.12.0"
@@ -52576,6 +52586,7 @@ __metadata:
"@tiptap/extension-image": "npm:^3.4.4"
"@tiptap/extension-italic": "npm:^3.4.2"
"@tiptap/extension-link": "npm:^3.4.2"
+ "@tiptap/extension-list": "npm:3.4.2"
"@tiptap/extension-paragraph": "npm:^3.4.2"
"@tiptap/extension-strike": "npm:^3.4.2"
"@tiptap/extension-text": "npm:^3.4.2"