mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
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:
parent
0070fc10f8
commit
be3ceca0a3
12 changed files with 236 additions and 1 deletions
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
11
yarn.lock
11
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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue