From be3ceca0a37f593301f700dd4652ff71391751de Mon Sep 17 00:00:00 2001 From: Harshit Singh <73997189+harshit078@users.noreply.github.com> Date: Wed, 29 Oct 2025 20:31:54 +0530 Subject: [PATCH] Add listkit to tiptap extension in workflow node (#15363) ## Description - This PR address issue - https://github.com/twentyhq/core-team-issues/issues/1768 - Added listkit bundle from tiptap which includes BulletList, orderedList, ListItem and ListKeymap in one single import - This bundle also includes keyboards shortcuts - `Cmd + Shift + 7` and `Cmd + Shift + 8` for ordered and bullet list ## Visual Appearance https://github.com/user-attachments/assets/7eff1233-8503-4854-bad2-2521898bc568 ## Why this Approach - our current version of tiptap is 3.4.2 while the latest is 3.8.0 hence installing these versions manually would install the latest version of 3.8.0. The issue when downgrading to 3.4.2 was that Version 3.8.0 of `@tiptap/extension-list` requires `renderNestedMarkdownContent` from @tiptap/core but our `@tiptap/core` version 3.4.2 doesn't export this function. --- .../email-renderer/nodes/bullet-list.tsx | 16 ++ .../utils/email-renderer/nodes/list-item.tsx | 16 ++ .../email-renderer/nodes/ordered-list.tsx | 16 ++ .../email-renderer/renderers/render-node.tsx | 6 + packages/twenty-front/package.json | 1 + .../components/AdvancedTextEditor.tsx | 5 + .../components/TextBubbleMenu.tsx | 12 ++ .../AdvancedTextEditor.stories.tsx | 147 +++++++++++++++++- .../hooks/useAdvancedTextEditor.ts | 2 + .../hooks/useTextBubbleState.ts | 2 + .../src/utils/tiptap/tiptap-marks.ts | 3 + yarn.lock | 11 ++ 12 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 packages/twenty-emails/src/utils/email-renderer/nodes/bullet-list.tsx create mode 100644 packages/twenty-emails/src/utils/email-renderer/nodes/list-item.tsx create mode 100644 packages/twenty-emails/src/utils/email-renderer/nodes/ordered-list.tsx 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 ( + + ); +}; 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"