From f1ec0b322b212d1b820ebbfb4b28329a0b408761 Mon Sep 17 00:00:00 2001 From: Sachin Chaurasiya Date: Thu, 7 Dec 2023 16:22:35 +0530 Subject: [PATCH] Minor(#13715): add table support in block editor (#14290) * minor(#13715): add table support in block editor * fix code smell * address comment --- .../src/main/resources/ui/package.json | 6 +- .../assets/svg/ic-format-add-column-after.svg | 3 + .../assets/svg/ic-format-add-row-after.svg | 3 + .../assets/svg/ic-format-delete-column.svg | 5 + .../src/assets/svg/ic-format-delete-row.svg | 4 + .../ui/src/assets/svg/ic-format-table.svg | 1 + .../BlockEditor/BubbleMenu/BubbleMenu.tsx | 3 +- .../components/BlockEditor/EditorSlots.tsx | 8 +- .../BlockEditor/Extensions/index.ts | 28 ++++ .../Extensions/slash-command/items.ts | 16 +++ .../Extensions/slash-command/renderItems.ts | 6 +- .../BlockEditor/TableMenu/TableMenu.tsx | 135 ++++++++++++++++++ .../components/BlockEditor/block-editor.less | 33 ++++- .../src/main/resources/ui/yarn.lock | 25 +++- 14 files changed, 264 insertions(+), 12 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-add-column-after.svg create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-add-row-after.svg create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-delete-column.svg create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-delete-row.svg create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-table.svg create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/TableMenu/TableMenu.tsx diff --git a/openmetadata-ui/src/main/resources/ui/package.json b/openmetadata-ui/src/main/resources/ui/package.json index b290924fd32..54b9be229c1 100644 --- a/openmetadata-ui/src/main/resources/ui/package.json +++ b/openmetadata-ui/src/main/resources/ui/package.json @@ -54,12 +54,15 @@ "@okta/okta-auth-js": "^6.4.0", "@okta/okta-react": "^6.4.3", "@rjsf/core": "5.4.0", - "rapidoc": "9.3.4", "@rjsf/utils": "5.4.0", "@rjsf/validator-ajv8": "5.4.0", "@tiptap/core": "^2.1.7", "@tiptap/extension-link": "^2.1.7", "@tiptap/extension-placeholder": "^2.1.7", + "@tiptap/extension-table": "^2.1.13", + "@tiptap/extension-table-cell": "^2.1.13", + "@tiptap/extension-table-header": "^2.1.13", + "@tiptap/extension-table-row": "^2.1.13", "@tiptap/extension-task-item": "^2.1.7", "@tiptap/extension-task-list": "^2.1.7", "@tiptap/pm": "^2.1.7", @@ -105,6 +108,7 @@ "quill-emoji": "^0.2.0", "quill-mention": "^4.0.0", "quilljs-markdown": "^1.1.10", + "rapidoc": "9.3.4", "react": "^16.14.0", "react-awesome-query-builder": "5.1.2", "react-codemirror2": "^7.2.1", diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-add-column-after.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-add-column-after.svg new file mode 100644 index 00000000000..149988d6c13 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-add-column-after.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-add-row-after.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-add-row-after.svg new file mode 100644 index 00000000000..83d372fd18c --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-add-row-after.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-delete-column.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-delete-column.svg new file mode 100644 index 00000000000..e43c0cf72e4 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-delete-column.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-delete-row.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-delete-row.svg new file mode 100644 index 00000000000..f594d977b3d --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-delete-row.svg @@ -0,0 +1,4 @@ + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-table.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-table.svg new file mode 100644 index 00000000000..d737e19f8a6 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-format-table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BubbleMenu/BubbleMenu.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BubbleMenu/BubbleMenu.tsx index a2537c8fbfc..b49ca06cb66 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BubbleMenu/BubbleMenu.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/BubbleMenu/BubbleMenu.tsx @@ -115,7 +115,8 @@ const BubbleMenu: FC = ({ editor, toggleLink }) => { editor.isActive('image') || empty || isNodeSelection(selection) || - editor.isActive('link') + editor.isActive('link') || + editor.isActive('table') ) { return false; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/EditorSlots.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/EditorSlots.tsx index 8f568cdc4cc..176a02bcfd2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/EditorSlots.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/EditorSlots.tsx @@ -19,6 +19,7 @@ import BlockMenu from './BlockMenu/BlockMenu'; import BubbleMenu from './BubbleMenu/BubbleMenu'; import LinkModal, { LinkData } from './LinkModal/LinkModal'; import LinkPopup from './LinkPopup/LinkPopup'; +import TableMenu from './TableMenu/TableMenu'; interface EditorSlotsProps { editor: Editor | null; @@ -165,7 +166,12 @@ const EditorSlots = forwardRef( /> )} {menus} - {!isNil(editor) && } + {!isNil(editor) && ( + <> + + + + )} ); } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/index.ts b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/index.ts index f961fa0d2e0..c20b086768a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/index.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/index.ts @@ -12,6 +12,10 @@ */ import Placeholder from '@tiptap/extension-placeholder'; +import Table from '@tiptap/extension-table'; +import TableCell from '@tiptap/extension-table-cell'; +import TableHeader from '@tiptap/extension-table-header'; +import TableRow from '@tiptap/extension-table-row'; import TaskItem from '@tiptap/extension-task-item'; import TaskList from '@tiptap/extension-task-list'; import StarterKit from '@tiptap/starter-kit'; @@ -115,4 +119,28 @@ export const extensions = [ mode: 'deepest', }), Callout, + Table.configure({ + HTMLAttributes: { + class: 'om-table', + 'data-om-table': 'om-table', + }, + }), + TableRow.configure({ + HTMLAttributes: { + class: 'om-table-row', + 'data-om-table-row': 'om-table-row', + }, + }), + TableHeader.configure({ + HTMLAttributes: { + class: 'om-table-header', + 'data-om-table-header': 'om-table-header', + }, + }), + TableCell.configure({ + HTMLAttributes: { + class: 'om-table-cell', + 'data-om-table-cell': 'om-table-cell', + }, + }), ]; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/slash-command/items.ts b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/slash-command/items.ts index 18026dacea5..d14b87ce554 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/slash-command/items.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/slash-command/items.ts @@ -24,6 +24,7 @@ import TaskListIcon from '../../../../assets/img/ic-task-list.png'; import IconFormatCallout from '../../../../assets/svg/ic-format-callout.svg'; import CodeBlockImage from '../../../../assets/svg/ic-format-code-block.svg'; import IconFormatImage from '../../../../assets/svg/ic-format-image.svg'; +import IconTable from '../../../../assets/svg/ic-format-table.svg'; import MentionImage from '../../../../assets/svg/ic-mentions.svg'; export enum SuggestionItemType { @@ -216,6 +217,21 @@ export const getSuggestionItems = (props: { type: SuggestionItemType.ADVANCED_BLOCKS, imgSrc: IconFormatCallout, }, + { + title: 'Table', + description: 'Add tabular content', + searchTerms: ['table', 'row', 'column', 'tabular'], + command: ({ editor, range }) => { + editor + .chain() + .focus() + .deleteRange(range) + .insertTable({ rows: 3, cols: 3, withHeaderRow: true }) + .run(); + }, + type: SuggestionItemType.ADVANCED_BLOCKS, + imgSrc: IconTable, + }, ]; const filteredItems = suggestionItems.filter((item) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/slash-command/renderItems.ts b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/slash-command/renderItems.ts index ed6df87fe2d..ad2c59c65d5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/slash-command/renderItems.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/Extensions/slash-command/renderItems.ts @@ -30,7 +30,11 @@ const renderItems = () => { editor: props.editor, }); - if (!props.clientRect) { + if ( + !props.clientRect || + props.editor.isActive('table') || + props.editor.isActive('callout') + ) { return; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/TableMenu/TableMenu.tsx b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/TableMenu/TableMenu.tsx new file mode 100644 index 00000000000..ed2fdb5e284 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/TableMenu/TableMenu.tsx @@ -0,0 +1,135 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Editor } from '@tiptap/react'; +import { Button, Space, Tooltip } from 'antd'; +import React, { useCallback, useEffect, useRef } from 'react'; +import tippy, { Instance } from 'tippy.js'; +import { ReactComponent as IconDeleteTable } from '../../../assets/svg/ic-delete.svg'; +import { ReactComponent as IconAddColumnAfter } from '../../../assets/svg/ic-format-add-column-after.svg'; +import { ReactComponent as IconAddRowAfter } from '../../../assets/svg/ic-format-add-row-after.svg'; +import { ReactComponent as IconDeleteColumn } from '../../../assets/svg/ic-format-delete-column.svg'; +import { ReactComponent as IconDeleteRow } from '../../../assets/svg/ic-format-delete-row.svg'; + +interface TableMenuProps { + editor: Editor; +} + +const TableMenu = (props: TableMenuProps) => { + const { editor } = props; + const { view } = editor; + const menuRef = useRef(null); + const tableMenuPopup = useRef(null); + + const handleMouseDown = useCallback((event: MouseEvent) => { + const target = event.target as HTMLElement; + const table = target?.closest('[data-om-table]'); + + if (table?.contains(target)) { + tableMenuPopup.current?.setProps({ + getReferenceClientRect: () => table.getBoundingClientRect(), + }); + + tableMenuPopup.current?.show(); + } + }, []); + + useEffect(() => { + if (menuRef.current) { + menuRef.current.remove(); + menuRef.current.style.visibility = 'visible'; + + tableMenuPopup.current = tippy(view.dom, { + getReferenceClientRect: null, + content: menuRef.current, + appendTo: 'parent', + trigger: 'manual', + interactive: true, + arrow: false, + placement: 'top', + hideOnClick: true, + onShown: () => { + menuRef.current?.focus(); + }, + }); + } + + return () => { + tableMenuPopup.current?.destroy(); + tableMenuPopup.current = null; + }; + }, []); + + useEffect(() => { + document.addEventListener('mousedown', handleMouseDown); + + return () => { + document.removeEventListener('mousedown', handleMouseDown); + }; + }, [handleMouseDown]); + + return ( +
+ + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default TableMenu; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/block-editor.less b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/block-editor.less index 976a35c0bb5..c2572f389c1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/block-editor.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/BlockEditor/block-editor.less @@ -35,6 +35,17 @@ height: 0; pointer-events: none; } + .tiptap.ProseMirror-focused .om-table-header, + .tiptap.ProseMirror-focused .om-table-row, + .tiptap.ProseMirror-focused div[data-type='callout'] { + .is-node-empty.has-focus::before { + color: inherit; + content: none; + float: inherit; + height: 0; + pointer-events: inherit; + } + } .tiptap.ProseMirror { font-size: 14px; h1, @@ -88,7 +99,11 @@ table { border-collapse: collapse; - margin-left: 35px; + width: 100%; + table-layout: auto; + text-align: left; + margin-top: 2em; + margin-bottom: 2em; } th, @@ -96,6 +111,8 @@ border: 1px solid @border-color; margin: 0; padding: 8px 16px; + min-width: 200px; + padding: 0.5rem; } th { @@ -214,8 +231,8 @@ object-fit: cover; border-radius: 4px; background: white; - width: 46px; - height: 46px; + width: 32px; + height: 32px; box-shadow: rgba(15, 15, 15, 0.1) 0px 0px 0px 1px; } } @@ -424,3 +441,13 @@ .om-callout-node-content { align-self: center; } + +.table-menu { + padding: 4px 8px; + background-color: @white; + border: @global-border; + border-radius: @border-radius-base; + button { + padding: 0px; + } +} diff --git a/openmetadata-ui/src/main/resources/ui/yarn.lock b/openmetadata-ui/src/main/resources/ui/yarn.lock index d3d5fd23cf0..a269934df6e 100644 --- a/openmetadata-ui/src/main/resources/ui/yarn.lock +++ b/openmetadata-ui/src/main/resources/ui/yarn.lock @@ -3613,6 +3613,26 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.1.7.tgz#b7b7f49254f1de22416b1415ca88a2a20edd0627" integrity sha512-ONLXYnuZGM2EoGcxkyvJSDMBeAp7K6l83UXkK9TSj+VpEEDdeV7m8mJs8/vACJjJxD5HMN61+EPgU7VTEukQCA== +"@tiptap/extension-table-cell@^2.1.13": + version "2.1.13" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-cell/-/extension-table-cell-2.1.13.tgz#28efbc99480d53346200dcbf50cfb32bade180d1" + integrity sha512-30pyVt2PxGAk8jmsXKxDheql8K/xIRA9FiDo++kS2Kr6Y7I42/kNPQttJ2W+Q1JdRJvedNfQtziQfKWDRLLCNA== + +"@tiptap/extension-table-header@^2.1.13": + version "2.1.13" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-header/-/extension-table-header-2.1.13.tgz#8d64a0e5a6a5ea128708b866e56a0e04e34d7a5b" + integrity sha512-FwIV5iso5kmpu01QyvrPCjJqZfqxRTjtjMsDyut2uIgx9v5TXk0V5XvMWobx435ANIDJoGTYCMRlIqcgtyqwAQ== + +"@tiptap/extension-table-row@^2.1.13": + version "2.1.13" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table-row/-/extension-table-row-2.1.13.tgz#ef75d6de9c7695bbb90f745aabd72d327f161ac3" + integrity sha512-27Mb9/oYbiLd+/BUFMhQzRIqMd2Z5j1BZMYsktwtDG8vGdYVlaW257UVaoNR9TmiXyIzd3Dh1mOil8G35+HRHg== + +"@tiptap/extension-table@^2.1.13": + version "2.1.13" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-2.1.13.tgz#cfe3fc2665d12d2c946fc83b2cce9d1485ff29a0" + integrity sha512-yMWt2LqotOsWJhLwFNo8fyTwJNLPtnk+eCUxKLlMXP23mJ/lpF+jvTihhHVVic5GqV9vLYZFU2Tn+5k/Vd5P1w== + "@tiptap/extension-task-item@^2.1.7": version "2.1.7" resolved "https://registry.yarnpkg.com/@tiptap/extension-task-item/-/extension-task-item-2.1.7.tgz#384a55308f3524f36388560486a2508a4b3c5413" @@ -8197,11 +8217,6 @@ follow-redirects@^1.0.0, follow-redirects@^1.15.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== -follow-redirects@^1.15.0: - version "1.15.3" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" - integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== - for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"