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.
This commit is contained in:
Harshit Singh 2025-10-29 20:31:54 +05:30 committed by GitHub
parent 0070fc10f8
commit be3ceca0a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 236 additions and 1 deletions

View file

@ -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 (
<ul
style={{
marginBottom: '8px',
lineHeight: '1.5',
}}
>
{mappedNodeContent(node)}
</ul>
);
};

View file

@ -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 (
<li
style={{
marginBottom: '8px',
lineHeight: '1.5',
}}
>
{mappedNodeContent(node)}
</li>
);
};

View file

@ -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 (
<ol
style={{
marginBottom: '8px',
lineHeight: '1.5',
}}
>
{mappedNodeContent(node)}
</ol>
);
};

View file

@ -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 => {

View file

@ -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",

View file

@ -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 {

View file

@ -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 = () => {

View file

@ -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);
});
},
};

View file

@ -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,

View file

@ -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'),
};
},
});

View file

@ -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 =

View file

@ -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"