From 8695bd85797026115a220db46b70b30546bb9ab6 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Wed, 6 Aug 2025 12:23:02 -0600 Subject: [PATCH] metric plugin add modal update --- .../features/modal/AddMetricModal.tsx | 32 +++++++--- .../components/ui/modal/InputSelectModal.tsx | 3 + .../ui/report/ReportEditor.stories.tsx | 10 +++- .../ui/report/config/addMenuItems.tsx | 3 +- .../elements/MetricElement/MetricElement.tsx | 33 ++--------- .../MetricElement/MetricPlaceholder.tsx | 59 +++++++++++++------ .../ui/report/elements/transforms.ts | 27 ++++----- .../ui/report/plugins/metric-plugin/index.ts | 1 + .../plugins/metric-plugin/insert-metric.ts | 22 +++++++ .../plugins/metric-plugin/metric-plugin.tsx | 42 ++++++------- 10 files changed, 139 insertions(+), 93 deletions(-) create mode 100644 apps/web/src/components/ui/report/plugins/metric-plugin/insert-metric.ts diff --git a/apps/web/src/components/features/modal/AddMetricModal.tsx b/apps/web/src/components/features/modal/AddMetricModal.tsx index ab255bebc..0f43765b2 100644 --- a/apps/web/src/components/features/modal/AddMetricModal.tsx +++ b/apps/web/src/components/features/modal/AddMetricModal.tsx @@ -13,10 +13,18 @@ export const AddMetricModal: React.FC<{ open: boolean; selectedMetrics: { id: string; name: string }[]; loading: boolean; + selectionMode?: 'single' | 'multiple'; onClose: () => void; onAddMetrics: (metrics: { id: string; name: string }[]) => Promise; }> = React.memo( - ({ open, selectedMetrics: selectedMetricsProp, loading, onAddMetrics, onClose }) => { + ({ + open, + selectionMode = 'multiple', + selectedMetrics: selectedMetricsProp, + loading, + onAddMetrics, + onClose + }) => { const [searchTerm, setSearchTerm] = useState(''); const [selectedMetrics, setSelectedMetrics] = useState<{ id: string; name: string }[]>([]); const debouncedSearchTerm = useDebounce(searchTerm, { wait: 175 }); @@ -69,7 +77,14 @@ export const AddMetricModal: React.FC<{ }); const onSelectChange = useMemoizedFn((items: string[]) => { - const itemsWithName = items.map((id) => { + // Handle single selection mode - only allow one item to be selected + let finalItems = items; + if (selectionMode === 'single' && items.length > 1) { + // Take only the last selected item (the most recent selection) + finalItems = [items[items.length - 1]]; + } + + const itemsWithName = finalItems.map((id) => { const item = rows.find((row) => row.id === id); return { id: id, @@ -117,15 +132,15 @@ export const AddMetricModal: React.FC<{ } if (hasRemovedItems) { - return 'Remove metrics'; + return selectionMode === 'single' ? 'Remove metric' : 'Remove metrics'; } if (hasAddedItems) { - return 'Add metrics'; + return selectionMode === 'single' ? 'Add metric' : 'Add metrics'; } return 'Update dashboard'; - }, [loading, removedMetricCount, addedMetricCount]); + }, [loading, removedMetricCount, addedMetricCount, selectionMode]); const primaryButtonTooltipText = useMemo(() => { if (loading) { @@ -156,7 +171,7 @@ export const AddMetricModal: React.FC<{ left: selectedMetrics.length > 0 ? ( ) : undefined, secondaryButton: { @@ -175,7 +190,9 @@ export const AddMetricModal: React.FC<{ primaryButtonTooltipText, primaryButtonText, isSelectedChanged, - handleAddAndRemoveMetrics + handleAddAndRemoveMetrics, + selectionMode, + onClose ]); useLayoutEffect(() => { @@ -197,6 +214,7 @@ export const AddMetricModal: React.FC<{ emptyState={emptyState} searchText={searchTerm} handleSearchChange={setSearchTerm} + showSelectAll={selectionMode === 'multiple'} /> ); } diff --git a/apps/web/src/components/ui/modal/InputSelectModal.tsx b/apps/web/src/components/ui/modal/InputSelectModal.tsx index 303aca5b3..40f249a14 100644 --- a/apps/web/src/components/ui/modal/InputSelectModal.tsx +++ b/apps/web/src/components/ui/modal/InputSelectModal.tsx @@ -15,6 +15,7 @@ export interface InputSelectModalProps extends Omit; selectedRowKeys: NonNullable; showHeader?: NonNullable; + showSelectAll?: BusterListProps['showSelectAll']; searchText: string; handleSearchChange: (searchText: string) => void; } @@ -30,6 +31,7 @@ function InputSelectModalBase({ searchText, handleSearchChange, showHeader = true, + showSelectAll = true, ...props }: InputSelectModalProps) { const memoizedHeader = useMemo(() => { @@ -75,6 +77,7 @@ function InputSelectModalBase({ selectedRowKeys={selectedRowKeys} useRowClickSelectChange={true} hideLastRowBorder + showSelectAll={showSelectAll} /> diff --git a/apps/web/src/components/ui/report/ReportEditor.stories.tsx b/apps/web/src/components/ui/report/ReportEditor.stories.tsx index 74b6f5cc7..77cf84a23 100644 --- a/apps/web/src/components/ui/report/ReportEditor.stories.tsx +++ b/apps/web/src/components/ui/report/ReportEditor.stories.tsx @@ -371,15 +371,21 @@ export const WithCustomKit: Story = { { type: 'metric', children: [{ text: '' }], - metricId: '123' + metricId: '' }, { - type: 'characterCounter', + type: 'characterCounter' as 'p', children: [{ text: 'This is my character counter' }] }, { type: 'p', children: [{ text: 'paragraph test' }] + }, + { + type: 'metric', + children: [{ text: '' }], + metricId: '1234', + caption: [{ text: 'This is a caption' }] } ], useFixedToolbarKit: true diff --git a/apps/web/src/components/ui/report/config/addMenuItems.tsx b/apps/web/src/components/ui/report/config/addMenuItems.tsx index 8559029a2..3d7899127 100644 --- a/apps/web/src/components/ui/report/config/addMenuItems.tsx +++ b/apps/web/src/components/ui/report/config/addMenuItems.tsx @@ -117,8 +117,7 @@ export const menuGroups: MenuGroup[] = [ label: NodeTypeLabels.metric.label, value: CUSTOM_KEYS.metric, onSelect: (editor, value) => { - alert('TODO: Add metric'); - // insertBlock(editor, value); + insertBlock(editor, CUSTOM_KEYS.metric); } } ] diff --git a/apps/web/src/components/ui/report/elements/MetricElement/MetricElement.tsx b/apps/web/src/components/ui/report/elements/MetricElement/MetricElement.tsx index 1d8d8ae63..4e86bdfb5 100644 --- a/apps/web/src/components/ui/report/elements/MetricElement/MetricElement.tsx +++ b/apps/web/src/components/ui/report/elements/MetricElement/MetricElement.tsx @@ -1,37 +1,22 @@ 'use client'; -import type { PluginConfig, TElement } from 'platejs'; -import { - PlateElement, - type PlateElementProps, - withHOC, - useFocused, - useSelected -} from 'platejs/react'; +import { PlateElement, type PlateElementProps, withHOC } from 'platejs/react'; import { ResizableProvider, useResizableValue } from '@platejs/resizable'; -import { cn } from '@/lib/utils'; import { MetricEmbedPlaceholder } from './MetricPlaceholder'; import { Caption, CaptionTextarea } from '../CaptionNode'; import { mediaResizeHandleVariants, Resizable, ResizeHandle } from '../ResizeHandle'; -import { useEffect, useState } from 'react'; -import { PlaceholderContainer } from '../PlaceholderContainer'; -import { MetricPlugin, type TMetricElement } from '../../plugins/metric-plugin'; +import { type TMetricElement } from '../../plugins/metric-plugin'; type MetricElementProps = PlateElementProps; export const MetricElement = withHOC( ResizableProvider, function MetricElement({ children, ...props }: MetricElementProps) { - const [openModal, setOpenModal] = useState(false); - const { openAddMetricModal } = props.editor.getPlugin(MetricPlugin).options; - - const metricId = ''; - const width = useResizableValue('width'); const align = 'center'; // Default align for metrics + const metricId = props.element.metricId; const { attributes, ...elementProps } = props; - const { plugin } = props; const content = metricId ? (
@@ -48,9 +33,9 @@ export const MetricElement = withHOC( /> {/* Metric content placeholder - replace with actual metric rendering */} - -
Metric: {metricId}
-
+
+
Metric: {metricId}
+
); - useEffect(() => { - if (openAddMetricModal) { - setOpenModal(true); - } - }, [openAddMetricModal]); - return ( { const [openModal, setOpenModal] = React.useState(false); + const editor = useEditorRef(); + const plugin = editor.getPlugin(MetricPlugin); const readOnly = useReadOnly(); const selected = useSelected(); const focused = useFocused(); - const anchorRef = React.useRef(null); + const element = useElement(); - const isOpenPopover = focused && selected && !readOnly; + // Use usePluginOption to make the component reactive to plugin option changes + const openMetricModal = usePluginOption(plugin, 'openMetricModal'); + + const isOpenModalFromPlugin = openMetricModal && !readOnly; const onOpenAddMetricModal = useMemoizedFn(() => { setOpenModal(true); + editor.tf.select(); }); const onCloseAddMetricModal = useMemoizedFn(() => { setOpenModal(false); + plugin.api.metric.closeAddMetricModal(); }); useEffect(() => { - if (isOpenPopover) { + if (isOpenModalFromPlugin) { onOpenAddMetricModal(); } - }, [isOpenPopover]); + }, [isOpenModalFromPlugin, onOpenAddMetricModal]); return ( <> -
+
@@ -51,7 +65,13 @@ export const MetricEmbedPlaceholder: React.FC = () => {
- + ); }; @@ -64,13 +84,17 @@ const EMPTY_SELECTED_METRICS: { const MemoizedAddMetricModal = React.memo( ({ openModal, + plugin, + editor, + element, onCloseAddMetricModal }: { openModal: boolean; onCloseAddMetricModal: () => void; + plugin: typeof MetricPlugin; + editor: PlateEditor; + element: TMetricElement; }) => { - const editor = useEditorRef(); - const element = useElement(); const { openInfoMessage } = useBusterNotifications(); const onAddMetric = useMemoizedFn(async (metrics: { id: string; name: string }[]) => { @@ -83,17 +107,15 @@ const MemoizedAddMetricModal = React.memo( openInfoMessage('Multiple metrics selected, only the first one will be used'); } - const plugin = editor.getPlugin(MetricPlugin); const at = editor.api.findPath(element); if (!at) { openInfoMessage('No metric element found'); + alert('No metric element found'); return; } - // editor.tf.setNodes({ metricId: selectedMetricId }, { at }); - - plugin.api.updateMetric(selectedMetricId); + plugin.api.metric.updateMetric(selectedMetricId, { at }); // Close the modal after successful selection onCloseAddMetricModal(); @@ -106,6 +128,7 @@ const MemoizedAddMetricModal = React.memo( selectedMetrics={EMPTY_SELECTED_METRICS} onClose={onCloseAddMetricModal} onAddMetrics={onAddMetric} + selectionMode="single" /> ); } diff --git a/apps/web/src/components/ui/report/elements/transforms.ts b/apps/web/src/components/ui/report/elements/transforms.ts index 0dff33c3c..9735c6cf0 100644 --- a/apps/web/src/components/ui/report/elements/transforms.ts +++ b/apps/web/src/components/ui/report/elements/transforms.ts @@ -11,7 +11,6 @@ import { insertEquation, insertInlineEquation } from '@platejs/math'; import { insertAudioPlaceholder, insertFilePlaceholder, - insertPlaceholder, insertImagePlaceholder, insertVideoPlaceholder } from '@platejs/media'; @@ -19,6 +18,8 @@ import { SuggestionPlugin } from '@platejs/suggestion/react'; import { TablePlugin } from '@platejs/table/react'; import { insertToc } from '@platejs/toc'; import { type NodeEntry, type Path, type TElement, KEYS, PathApi } from 'platejs'; +import { CUSTOM_KEYS } from '../config/keys'; +import { insertMetric } from '../plugins/metric-plugin'; const ACTION_THREE_COLUMNS = 'action_three_columns'; @@ -42,22 +43,13 @@ const insertBlockMap: Record void [KEYS.codeBlock]: (editor) => insertCodeBlock(editor, { select: true }), [KEYS.equation]: (editor) => insertEquation(editor, { select: true }), [KEYS.file]: (editor) => insertFilePlaceholder(editor, { select: true }), - [KEYS.img]: (editor) => { - insertImagePlaceholder(editor, { - select: true - }); - }, - [KEYS.mediaEmbed]: (editor) => { - editor.tf.insertNodes( - editor.api.create.block({ - type: KEYS.mediaEmbed - }), - { select: true } - ); - }, + [KEYS.img]: (editor) => insertImagePlaceholder(editor, { select: true }), + [KEYS.mediaEmbed]: (editor) => + editor.tf.insertNodes(editor.api.create.block({ type: KEYS.mediaEmbed }), { select: true }), [KEYS.table]: (editor) => editor.getTransforms(TablePlugin).insert.table({}, { select: true }), [KEYS.toc]: (editor) => insertToc(editor, { select: true }), - [KEYS.video]: (editor) => insertVideoPlaceholder(editor, { select: true }) + [KEYS.video]: (editor) => insertVideoPlaceholder(editor, { select: true }), + [CUSTOM_KEYS.metric]: (editor) => insertMetric(editor, { select: true }) }; const insertInlineMap: Record void> = { @@ -70,7 +62,10 @@ export const insertBlock = (editor: PlateEditor, type: string) => { editor.tf.withoutNormalizing(() => { const block = editor.api.block(); - if (!block) return; + if (!block) { + console.warn('No block found'); + return; + } if (type in insertBlockMap) { insertBlockMap[type](editor, type); } else { diff --git a/apps/web/src/components/ui/report/plugins/metric-plugin/index.ts b/apps/web/src/components/ui/report/plugins/metric-plugin/index.ts index 4d46b39f3..585b8c3fd 100644 --- a/apps/web/src/components/ui/report/plugins/metric-plugin/index.ts +++ b/apps/web/src/components/ui/report/plugins/metric-plugin/index.ts @@ -1 +1,2 @@ export * from './metric-plugin'; +export * from './insert-metric'; diff --git a/apps/web/src/components/ui/report/plugins/metric-plugin/insert-metric.ts b/apps/web/src/components/ui/report/plugins/metric-plugin/insert-metric.ts new file mode 100644 index 000000000..4936d5d6b --- /dev/null +++ b/apps/web/src/components/ui/report/plugins/metric-plugin/insert-metric.ts @@ -0,0 +1,22 @@ +import type { InsertNodesOptions } from 'platejs'; +import type { PlateEditor } from 'platejs/react'; +import { CUSTOM_KEYS } from '../../config/keys'; +import { MetricPlugin, type TMetricElement } from './metric-plugin'; + +export const insertMetric = (editor: PlateEditor, options?: InsertNodesOptions) => { + editor.tf.insertNode( + { + type: CUSTOM_KEYS.metric, + metricId: '', + children: [{ text: '' }] + }, + { select: true, ...options } + ); + + // Open the modal immediately after inserting the node + // Small timeout to ensure the node is properly inserted and rendered + setTimeout(() => { + const plugin = editor.getPlugin(MetricPlugin); + plugin.api.metric.openAddMetricModal(); + }, 50); +}; diff --git a/apps/web/src/components/ui/report/plugins/metric-plugin/metric-plugin.tsx b/apps/web/src/components/ui/report/plugins/metric-plugin/metric-plugin.tsx index 625a8910b..9e46a3ef8 100644 --- a/apps/web/src/components/ui/report/plugins/metric-plugin/metric-plugin.tsx +++ b/apps/web/src/components/ui/report/plugins/metric-plugin/metric-plugin.tsx @@ -1,16 +1,14 @@ import { createPlatePlugin } from 'platejs/react'; import { MetricElement } from '../../elements/MetricElement/MetricElement'; import { CUSTOM_KEYS } from '../../config/keys'; -import type { TElement } from 'platejs'; +import type { Path, SetNodesOptions, TElement } from 'platejs'; export type MetricPluginOptions = { - openAddMetricModal: boolean; + openMetricModal: boolean; }; export type MetricPluginApi = { - openAddMetricModal: () => void; - closeAddMetricModal: () => void; - updateMetric: (metricId: string) => void; + // the methods are defined in the extendApi function }; export type TMetricElement = TElement & { @@ -27,23 +25,25 @@ export const MetricPlugin = createPlatePlugin< >({ key: CUSTOM_KEYS.metric, options: { - openAddMetricModal: false + openMetricModal: false }, node: { type: CUSTOM_KEYS.metric, - isElement: true, - component: MetricElement + isElement: true } -}).extendApi(({ setOption, plugin, editor, tf, ...rest }) => { - return { - openAddMetricModal: () => { - setOption('openAddMetricModal', true); - }, - closeAddMetricModal: () => { - setOption('openAddMetricModal', false); - }, - updateMetric: (metricId: string) => { - tf.setNodes({ metricId }); - } - }; -}); +}) + .extendApi(({ setOption, plugin, editor, tf, ...rest }) => { + return { + openAddMetricModal: () => { + setOption('openMetricModal', true); + }, + closeAddMetricModal: () => { + setOption('openMetricModal', false); + }, + updateMetric: (metricId: string, options?: SetNodesOptions) => { + tf.setNodes({ metricId }, options); + console.log('updated metric', metricId); + } + }; + }) + .withComponent(MetricElement);