mirror of https://github.com/buster-so/buster.git
metric plugin add modal update
This commit is contained in:
parent
a3f9dc8d13
commit
8695bd8579
|
@ -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'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export * from './metric-plugin';
|
||||
export * from './insert-metric';
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue