add undo redo

This commit is contained in:
Nate Kelley 2025-09-09 09:08:21 -06:00
parent 350b1eade9
commit 2f8e70aaec
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
9 changed files with 125 additions and 14 deletions

View File

@ -16,6 +16,7 @@ import { useListReportVersionDropdownItems } from '@/components/features/version
import { Button } from '@/components/ui/buttons';
import {
createDropdownItem,
createDropdownItems,
Dropdown,
DropdownContent,
type IDropdownItem,
@ -23,12 +24,19 @@ import {
} from '@/components/ui/dropdown';
import { Dots, History, PenSparkle, ShareRight, Star } from '@/components/ui/icons';
import { Star as StarFilled } from '@/components/ui/icons/NucleoIconFilled';
import { Download4, Refresh } from '@/components/ui/icons/NucleoIconOutlined';
import {
Download4,
DuplicatePlus,
Redo,
Refresh,
Undo,
} from '@/components/ui/icons/NucleoIconOutlined';
import { useStartChatFromAsset } from '@/context/BusterAssets/useStartChatFromAsset';
import { useBusterNotifications } from '@/context/BusterNotifications';
import { useGetChatId } from '@/context/Chats/useGetChatId';
import { useReportPageExport } from '@/context/Reports/useReportPageExport';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { useEditorContext } from '@/layouts/AssetContainer/ReportAssetContainer';
import { canEdit, getIsEffectiveOwner } from '@/lib/share';
export const ReportThreeDotMenu = React.memo(
@ -46,9 +54,10 @@ export const ReportThreeDotMenu = React.memo(
const saveToLibrary = useSaveToLibrary({ reportId });
const favoriteItem = useFavoriteReportSelectMenu({ reportId });
const versionHistory = useVersionHistorySelectMenu({ reportId });
const undoRedo = useUndoRedo();
const duplicateReport = useDuplicateReportSelectMenu({ reportId });
// const verificationItem = useReportVerificationSelectMenu(); // Hidden - not supported yet
const refreshReportItem = useRefreshReportSelectMenu({ reportId });
// const duplicateReportItem = useDuplicateReportSelectMenu();
const { dropdownItem: downloadPdfItem, exportPdfContainer } = useDownloadPdfSelectMenu({
reportId,
});
@ -68,11 +77,13 @@ export const ReportThreeDotMenu = React.memo(
saveToLibrary,
favoriteItem,
{ type: 'divider' },
...undoRedo,
{ type: 'divider' },
versionHistory,
// verificationItem, // Hidden - not supported yet
{ type: 'divider' },
isEditor && refreshReportItem,
// duplicateReportItem,
duplicateReport,
downloadPdfItem,
];
}, [
@ -293,7 +304,6 @@ const useReportVerificationSelectMenu = (): IDropdownItem => {
// Refresh report with latest data
const useRefreshReportSelectMenu = ({ reportId }: { reportId: string }): IDropdownItem => {
const navigate = useNavigate();
const { onCreateFileClick, loading: isPending } = useStartChatFromAsset({
assetId: reportId,
assetType: 'report',
@ -374,3 +384,42 @@ const useDownloadPdfSelectMenu = ({
};
}, [reportId, exportReportAsPDF, cancelExport, ExportContainer]);
};
const useUndoRedo = (): IDropdownItems => {
const { editor, setEditor } = useEditorContext();
return createDropdownItems([
{
label: 'Undo',
value: 'undo',
icon: <Undo />,
onClick: () => {
console.log('Undo');
console.log(editor);
// editor?.tf.undo();
},
},
{
label: 'Redo',
value: 'redo',
icon: <Redo />,
onClick: () => {
console.log('Redo');
// editor?.tf.redo();
},
},
]);
};
const useDuplicateReportSelectMenu = ({ reportId }: { reportId: string }): IDropdownItem => {
return useMemo(
() => ({
label: 'Duplicate',
value: 'duplicate-report',
icon: <DuplicatePlus />,
onClick: () => {
console.log('Duplicate report');
},
}),
[reportId]
);
};

View File

@ -99,7 +99,3 @@ export const EditorKit = ({
// Dnd
...DndKit({ containerRef }),
];
export type MyEditor = TPlateEditor<Value, ReturnType<typeof EditorKit>[number]>;
export const useEditor = () => useEditorRef<MyEditor>();

View File

@ -0,0 +1,5 @@
import type { Value } from 'platejs';
import type { TPlateEditor } from 'platejs/react';
import type { EditorKit } from './editor-kit';
export type BusterReportEditor = TPlateEditor<Value, ReturnType<typeof EditorKit>[number]>;

View File

@ -0,0 +1,4 @@
import { useEditorRef } from 'platejs/react';
import type { BusterReportEditor } from './types';
export const useEditor = () => useEditorRef<BusterReportEditor>();

View File

@ -3,7 +3,7 @@ import type { QueryClient } from '@tanstack/react-query';
import { Outlet } from '@tanstack/react-router';
import { z } from 'zod';
import { prefetchGetReport } from '@/api/buster_rest/reports';
import { ReportAssetContainer } from '../../../layouts/AssetContainer/ReportAssetContainer/ReportAssetContainer';
import { ReportAssetContainer } from '@/layouts/AssetContainer/ReportAssetContainer/ReportAssetContainer';
import { useGetReportParams } from '../../Reports/useGetReportParams';
export const validateSearch = z.object({

View File

@ -6,8 +6,10 @@ import { useTrackAndUpdateReportChanges } from '@/api/buster-electric/reports/ho
import DynamicReportEditor from '@/components/ui/report/DynamicReportEditor';
import type { IReportEditor } from '@/components/ui/report/ReportEditor';
import { ReportEditorSkeleton } from '@/components/ui/report/ReportEditorSkeleton';
import type { BusterReportEditor } from '@/components/ui/report/types';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { useMount } from '@/hooks/useMount';
import { useEditorContext } from '@/layouts/AssetContainer/ReportAssetContainer';
import { cn } from '@/lib/utils';
import { chatQueryKeys } from '../../api/query_keys/chat';
import { useGetCurrentMessageId, useIsStreamingMessage } from '../../context/Chats';
@ -23,6 +25,7 @@ export const ReportPageController: React.FC<{
}> = React.memo(
({ reportId, readOnly = false, className = '', onReady: onReadyProp, mode = 'default' }) => {
const { data: report } = useGetReport({ id: reportId, versionNumber: undefined });
const { setEditor } = useEditorContext();
const isStreamingMessage = useIsStreamingMessage();
const messageId = useGetCurrentMessageId();
@ -71,6 +74,11 @@ export const ReportPageController: React.FC<{
updateReport({ reportId, content });
});
const onReady = useMemoizedFn((editor: BusterReportEditor) => {
setEditor(editor);
onReadyProp?.(editor);
});
useTrackAndUpdateReportChanges({ reportId, subscribe: isStreamingMessage });
const containerRef = useRef<HTMLDivElement>(null);
@ -102,7 +110,7 @@ export const ReportPageController: React.FC<{
onValueChange={onChangeContent}
readOnly={readOnly || !report}
mode={mode}
onReady={onReadyProp}
onReady={onReady}
isStreaming={isStreamingMessage}
containerRef={containerRef}
preEditorChildren={

View File

@ -39,7 +39,7 @@ const LazyMetricStoreDevtools = !import.meta.env.SSR
// The actual devtools component implementation
const TanstackDevtoolsImpl: React.FC = React.memo(() => {
useMount(() => {
console.log('🐓 Rendering TanstackDevtoolsImpl');
if (import.meta.env.PROD) console.log('🐓 Rendering TanstackDevtoolsImpl');
});
const isServerOrSSR = isServer && import.meta.env.SSR;

View File

@ -5,10 +5,10 @@ import { useGetReport } from '@/api/buster_rest/reports';
import { CreateChatButton } from '@/components/features/AssetLayout/CreateChatButton';
import { ShareReportButton } from '@/components/features/buttons/ShareReportButton';
import { ClosePageButton } from '@/components/features/chat/ClosePageButton';
import { ReportThreeDotMenu } from '@/components/features/reports/ReportThreeDotMenu';
import { useIsChatMode, useIsFileMode } from '@/context/Chats/useMode';
import { useIsReportReadOnly } from '@/context/Reports/useIsReportReadOnly';
import { getIsEffectiveOwner } from '@/lib/share';
import { ReportThreeDotMenu } from '../../../components/features/reports/ReportThreeDotMenu';
import { FileButtonContainer } from '../FileButtonContainer';
import { HideButtonContainer } from '../HideButtonContainer';
@ -26,7 +26,7 @@ export const ReportContainerHeaderButtons: React.FC<ReportContainerHeaderButtons
const { isViewingOldVersion } = useIsReportReadOnly({
reportId: reportId || '',
});
const { error: reportError, data: permission } = useGetReport(
const { data: permission } = useGetReport(
{ id: reportId },
{ select: useCallback((x: GetReportResponse) => x.permission, []) }
);

View File

@ -1,9 +1,17 @@
import { useState } from 'react';
import { useCallback, useRef, useState, useTransition } from 'react';
import { createContext, useContextSelector } from 'use-context-selector';
import type { BusterReportEditor } from '@/components/ui/report/types';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
const useReportAssetContext = () => {
const [forceUpdate, startForceUpdate] = useTransition();
const [hasEditor, setHasEditor] = useState(false);
const [versionHistoryMode, setVersionHistoryMode] = useState<number | false>(false);
const editor = useRef<BusterReportEditor | null>(null);
const undo = useRef<(() => void) | null>(null);
const redo = useRef<(() => void) | null>(null);
console.log('hasEditor', editor.current, !!undo.current, !!redo.current, hasEditor, forceUpdate);
const openReportVersionHistoryMode = useMemoizedFn((versionNumber: number) => {
setVersionHistoryMode(versionNumber);
@ -13,10 +21,29 @@ const useReportAssetContext = () => {
setVersionHistoryMode(false);
});
const setEditor = useMemoizedFn((editor: BusterReportEditor) => {
if (!editor) {
return;
}
editor.current = editor;
undo.current = editor.undo;
redo.current = editor.redo;
startForceUpdate(() => {
setHasEditor(true);
console.log('setEditor2', editor.current);
console.log('editor.current', editor.current);
});
});
return {
openReportVersionHistoryMode,
closeVersionHistoryMode,
versionHistoryMode,
setEditor,
hasEditor,
editor,
};
};
@ -55,3 +82,25 @@ export const useReportVersionHistoryMode = () => {
closeVersionHistoryMode,
};
};
const stableSetEditorSelector = (x: ReturnType<typeof useReportAssetContext>) => x.setEditor;
const stableHasEditorSelector = (x: ReturnType<typeof useReportAssetContext>) => x.hasEditor;
export const useEditorContext = () => {
const hasEditor = useContextSelector(ReportAssetContext, stableHasEditorSelector);
const setEditor = useContextSelector(ReportAssetContext, stableSetEditorSelector);
const editor = useContextSelector(
ReportAssetContext,
useCallback((x) => x.editor, [hasEditor])
);
if (!setEditor) {
console.warn(
'ReportAssetContext is not defined. useEditorContext must be used within a ReportAssetContextProvider.'
);
}
return {
editor,
setEditor,
};
};