Merge pull request #683 from buster-so/cursor/add-floating-toolbar-for-metric-element-1f54

Add floating toolbar for metric element
This commit is contained in:
Nate Kelley 2025-08-09 15:32:27 -06:00 committed by GitHub
commit 09f69bbd45
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 118 additions and 14 deletions

View File

@ -312,6 +312,11 @@ export const NodeTypeLabels = {
keyboard: undefined,
keywords: []
},
editMetric: {
label: 'Edit metric',
keyboard: undefined,
keywords: []
},
caption: {
label: 'Caption',
keyboard: undefined,

View File

@ -5,6 +5,8 @@ import { useChatLayoutContextSelector } from '@/layouts/ChatLayout';
import { assetParamsToRoute } from '@/lib/assets/assetParamsToRoute';
import React, { useMemo, useRef } from 'react';
import { useMetricContentThreeDotMenuItems } from './useMetricContentThreeDotMenuItems';
import { useFocused, useSelected } from 'platejs/react';
import { cn } from '@/lib/utils';
export const MetricContent = React.memo(
({
@ -22,6 +24,8 @@ export const MetricContent = React.memo(
const reportId = useChatLayoutContextSelector((x) => x.reportId) || '';
const reportVersionNumber = useChatLayoutContextSelector((x) => x.reportVersionNumber);
const ref = useRef<HTMLDivElement>(null);
const isSelected = useSelected();
const isFocused = useFocused();
const [inViewport] = useInViewport(ref, {
threshold: 0.33
@ -72,6 +76,9 @@ export const MetricContent = React.memo(
return (
<MetricCard
className={cn('transition-all duration-200', {
'ring-ring ring-1 ring-offset-3': isSelected && isFocused
})}
ref={ref}
metricLink={link}
animate={!isExportMode}

View File

@ -12,6 +12,7 @@ import {
} from 'platejs/react';
import { ResizableProvider, useResizableValue } from '@platejs/resizable';
import { MetricEmbedPlaceholder } from './MetricPlaceholder';
import { MetricToolbar } from './MetricToolbar';
import { Caption, CaptionTextarea } from '../CaptionNode';
import { mediaResizeHandleVariants, Resizable, ResizeHandle } from '../ResizeHandle';
import { type TMetricElement } from '../../plugins/metric-kit';
@ -33,14 +34,16 @@ export const MetricElement = withHOC(
const mode = props.editor.getOption(GlobalVariablePlugin, 'mode');
const content = metricId ? (
<MetricResizeContainer>
<MetricContent
metricId={metricId}
metricVersionNumber={metricVersionNumber}
readOnly={readOnly}
isExportMode={mode === 'export'}
/>
</MetricResizeContainer>
<MetricToolbar selectedMetricId={metricId}>
<MetricResizeContainer>
<MetricContent
metricId={metricId}
metricVersionNumber={metricVersionNumber}
readOnly={readOnly}
isExportMode={mode === 'export'}
/>
</MetricResizeContainer>
</MetricToolbar>
) : (
<MetricEmbedPlaceholder />
);

View File

@ -0,0 +1,95 @@
import * as React from 'react';
import {
useEditorRef,
useEditorSelector,
useElement,
useReadOnly,
useRemoveNodeButton,
useSelected
} from 'platejs/react';
import { Button } from '@/components/ui/buttons';
import { PopoverBase, PopoverAnchor, PopoverContent } from '@/components/ui/popover';
import { NodeTypeIcons } from '../../config/icons';
import { NodeTypeLabels } from '../../config/labels';
import { AddMetricModal } from '@/components/features/modal/AddMetricModal';
import { MetricPlugin, type TMetricElement } from '../../plugins/metric-kit';
import { Separator } from '@/components/ui/separator';
import { CaptionButton } from '../CaptionNode';
export function MetricToolbar({
children,
selectedMetricId
}: {
children: React.ReactNode;
selectedMetricId?: string;
}) {
const editor = useEditorRef();
const readOnly = useReadOnly();
const selected = useSelected();
const selectionCollapsed = useEditorSelector((ed) => !ed.api.isExpanded(), []);
const isOpen = !readOnly && selected && selectionCollapsed;
const element = useElement<TMetricElement>();
const plugin = editor.getPlugin(MetricPlugin);
const { props: removeButtonProps } = useRemoveNodeButton({ element });
const [openEditModal, setOpenEditModal] = React.useState(false);
const preselectedMetrics = React.useMemo(() => {
return selectedMetricId ? [{ id: selectedMetricId, name: '' }] : [];
}, [selectedMetricId]);
const onOpenEdit = React.useCallback(() => {
editor.tf.select();
setOpenEditModal(true);
}, [editor]);
const onCloseEdit = React.useCallback(() => {
setOpenEditModal(false);
}, []);
const handleAddMetrics = React.useCallback(
async (metrics: { id: string; name: string }[]) => {
const id = metrics?.[0]?.id;
const at = editor.api.findPath(element);
if (!id || !at) return onCloseEdit();
plugin.api.metric.updateMetric(id, { at });
onCloseEdit();
},
[editor, element, onCloseEdit, plugin.api.metric]
);
return (
<PopoverBase open={isOpen} modal={false}>
<PopoverAnchor>{children}</PopoverAnchor>
<PopoverContent className="w-auto p-1" onOpenAutoFocus={(e) => e.preventDefault()}>
<div className="box-content flex items-center">
<Button onClick={onOpenEdit} variant="ghost">
{NodeTypeLabels.editMetric?.label ?? 'Edit metric'}
</Button>
<CaptionButton variant="ghost">{NodeTypeLabels.caption.label}</CaptionButton>
<Separator orientation="vertical" className="mx-1 h-6" />
<Button prefix={<NodeTypeIcons.trash />} variant="ghost" {...removeButtonProps}></Button>
</div>
</PopoverContent>
<AddMetricModal
open={openEditModal}
loading={false}
selectedMetrics={preselectedMetrics}
onClose={onCloseEdit}
onAddMetrics={handleAddMetrics}
selectionMode="single"
saveButtonText="Update metric"
/>
</PopoverBase>
);
}

View File

@ -17,5 +17,3 @@ export const downloadFile = async (url: string, filename: string) => {
};
export default downloadFile;

View File

@ -29,5 +29,3 @@ export const exportToImage = async ({
};
export default exportToImage;

View File

@ -54,5 +54,3 @@ export const getCanvas = async (editor: PlateEditor) => {
};
export default getCanvas;