metric plugin add modal update

This commit is contained in:
Nate Kelley 2025-08-06 12:23:02 -06:00
parent a3f9dc8d13
commit 8695bd8579
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
10 changed files with 139 additions and 93 deletions

View File

@ -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<void>;
}> = 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 ? (
<Button variant="ghost" onClick={() => setSelectedMetrics([])}>
Clear selected
{selectionMode === 'single' ? 'Clear selection' : 'Clear selected'}
</Button>
) : 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'}
/>
);
}

View File

@ -15,6 +15,7 @@ export interface InputSelectModalProps<T = unknown> extends Omit<BorderedModalPr
onSelectChange: NonNullable<BusterListProps['onSelectChange']>;
selectedRowKeys: NonNullable<BusterListProps['selectedRowKeys']>;
showHeader?: NonNullable<BusterListProps['showHeader']>;
showSelectAll?: BusterListProps['showSelectAll'];
searchText: string;
handleSearchChange: (searchText: string) => void;
}
@ -30,6 +31,7 @@ function InputSelectModalBase<T = unknown>({
searchText,
handleSearchChange,
showHeader = true,
showSelectAll = true,
...props
}: InputSelectModalProps<T>) {
const memoizedHeader = useMemo(() => {
@ -75,6 +77,7 @@ function InputSelectModalBase<T = unknown>({
selectedRowKeys={selectedRowKeys}
useRowClickSelectChange={true}
hideLastRowBorder
showSelectAll={showSelectAll}
/>
</div>
</BorderedModal>

View File

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

View File

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

View File

@ -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<TMetricElement>;
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 ? (
<figure className="group relative m-0 w-full cursor-default" contentEditable={false}>
@ -48,9 +33,9 @@ export const MetricElement = withHOC(
/>
{/* Metric content placeholder - replace with actual metric rendering */}
<PlaceholderContainer>
<div className="text-sm text-gray-500">Metric: {metricId}</div>
</PlaceholderContainer>
<div className="min-h-40 rounded bg-red-100 p-4">
<div className="text-sm text-red-500">Metric: {metricId}</div>
</div>
<ResizeHandle
className={mediaResizeHandleVariants({ direction: 'right' })}
@ -66,12 +51,6 @@ export const MetricElement = withHOC(
<MetricEmbedPlaceholder />
);
useEffect(() => {
if (openAddMetricModal) {
setOpenModal(true);
}
}, [openAddMetricModal]);
return (
<PlateElement
className="rounded-md"

View File

@ -1,10 +1,15 @@
import { ASSET_ICONS } from '@/components/features/config/assetIcons';
import { PopoverAnchor, PopoverBase, PopoverContent } from '@/components/ui/popover';
import { useBusterNotifications } from '@/context/BusterNotifications';
import { useClickAway } from '@/hooks/useClickAway';
import { cn } from '@/lib/utils';
import { Text } from '@/components/ui/typography';
import { useEditorRef, useFocused, useReadOnly, useSelected, useElement } from 'platejs/react';
import {
useEditorRef,
useReadOnly,
useElement,
usePluginOption,
type PlateEditor,
useSelected,
useFocused
} from 'platejs/react';
import React, { useEffect } from 'react';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { AddMetricModal } from '@/components/features/modal/AddMetricModal';
@ -12,35 +17,44 @@ import { MetricPlugin, type TMetricElement } from '../../plugins/metric-plugin';
export const MetricEmbedPlaceholder: React.FC = () => {
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<HTMLDivElement>(null);
const element = useElement<TMetricElement>();
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 (
<>
<div className="media-embed py-2.5">
<div className={cn('metric-placeholder py-2.5')}>
<div
ref={anchorRef}
onClick={onOpenAddMetricModal}
className={cn(
'bg-muted hover:bg-primary/10 flex cursor-pointer items-center rounded-sm p-3 pr-9 select-none'
'bg-muted hover:bg-primary/10 flex cursor-pointer items-center rounded-sm p-3 pr-9 select-none',
{
'shadow-md': focused && selected
}
)}
contentEditable={false}>
<div className="text-muted-foreground/80 relative mr-3 flex [&_svg]:size-6">
@ -51,7 +65,13 @@ export const MetricEmbedPlaceholder: React.FC = () => {
</div>
</div>
<MemoizedAddMetricModal openModal={openModal} onCloseAddMetricModal={onCloseAddMetricModal} />
<MemoizedAddMetricModal
plugin={plugin}
editor={editor}
element={element}
openModal={openModal}
onCloseAddMetricModal={onCloseAddMetricModal}
/>
</>
);
};
@ -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<TMetricElement>();
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<TMetricElement>({ 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"
/>
);
}

View File

@ -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<string, (editor: PlateEditor, type: string) => 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<string, (editor: PlateEditor, type: string) => 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 {

View File

@ -1 +1,2 @@
export * from './metric-plugin';
export * from './insert-metric';

View File

@ -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<TMetricElement>(
{
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);
};

View File

@ -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<TMetricElement>({ metricId });
}
};
});
})
.extendApi(({ setOption, plugin, editor, tf, ...rest }) => {
return {
openAddMetricModal: () => {
setOption('openMetricModal', true);
},
closeAddMetricModal: () => {
setOption('openMetricModal', false);
},
updateMetric: (metricId: string, options?: SetNodesOptions<TMetricElement[]>) => {
tf.setNodes<TMetricElement>({ metricId }, options);
console.log('updated metric', metricId);
}
};
})
.withComponent(MetricElement);