diff --git a/web/src/components/features/sidebars/SidebarUserFooter/SidebarUserFooter.tsx b/web/src/components/features/sidebars/SidebarUserFooter/SidebarUserFooter.tsx index 7de13ef88..17b9e8789 100644 --- a/web/src/components/features/sidebars/SidebarUserFooter/SidebarUserFooter.tsx +++ b/web/src/components/features/sidebars/SidebarUserFooter/SidebarUserFooter.tsx @@ -20,14 +20,13 @@ import { useSignOut } from '@/components/features/auth/SignOutHandler'; export const SidebarUserFooter: React.FC<{}> = () => { const user = useUserConfigContextSelector((x) => x.user); + const handleSignOut = useSignOut(); if (!user) return null; const { name, email } = user; if (!name || !email) return null; - const handleSignOut = useSignOut(); - return (
diff --git a/web/src/context/Dashboards/useIsDashboardChanged.tsx b/web/src/context/Dashboards/useIsDashboardChanged.tsx index 75d97b7a3..93f8df728 100644 --- a/web/src/context/Dashboards/useIsDashboardChanged.tsx +++ b/web/src/context/Dashboards/useIsDashboardChanged.tsx @@ -8,7 +8,7 @@ import { compareObjectsByKeys } from '@/lib/objects'; import { useMemo } from 'react'; import { create } from 'mutative'; -export const useIsDashboardChanged = ({ dashboardId }: { dashboardId: string }) => { +export const useIsDashboardChanged = ({ dashboardId }: { dashboardId: string | undefined }) => { const queryClient = useQueryClient(); const originalDashboard = useOriginalDashboardStore((x) => x.getOriginalDashboard(dashboardId)); @@ -26,7 +26,7 @@ export const useIsDashboardChanged = ({ dashboardId }: { dashboardId: string }) const onResetDashboardToOriginal = useMemoizedFn(() => { const options = dashboardQueryKeys.dashboardGetDashboard( - dashboardId, + dashboardId || '', originalDashboard?.version_number || null ); const currentDashboard = queryClient.getQueryData(options.queryKey); diff --git a/web/src/context/Dashboards/useOriginalDashboardStore.tsx b/web/src/context/Dashboards/useOriginalDashboardStore.tsx index 39f42bff3..5a8585e95 100644 --- a/web/src/context/Dashboards/useOriginalDashboardStore.tsx +++ b/web/src/context/Dashboards/useOriginalDashboardStore.tsx @@ -8,7 +8,7 @@ type OriginalDashboardStore = { originalDashboards: Record; bulkAddOriginalDashboards: (dashboards: Record) => void; setOriginalDashboard: (dashboard: BusterDashboard) => void; - getOriginalDashboard: (dashboardId: string) => BusterDashboard | undefined; + getOriginalDashboard: (dashboardId: string | undefined) => BusterDashboard | undefined; removeOriginalDashboard: (dashboardId: string) => void; }; @@ -28,7 +28,8 @@ export const useOriginalDashboardStore = create((set, ge [dashboard.id]: dashboard } })), - getOriginalDashboard: (dashboardId: string) => get().originalDashboards[dashboardId], + getOriginalDashboard: (dashboardId: string | undefined) => + dashboardId ? get().originalDashboards[dashboardId] : undefined, removeOriginalDashboard: (dashboardId: string) => set((state) => { const { [dashboardId]: removed, ...rest } = state.originalDashboards; diff --git a/web/src/context/Metrics/useIsMetricChanged.tsx b/web/src/context/Metrics/useIsMetricChanged.tsx index e98cd22ab..e157fa8c0 100644 --- a/web/src/context/Metrics/useIsMetricChanged.tsx +++ b/web/src/context/Metrics/useIsMetricChanged.tsx @@ -1,13 +1,12 @@ -import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; import { useOriginalMetricStore } from './useOriginalMetricStore'; import { useMemoizedFn } from '@/hooks'; import { metricsQueryKeys } from '@/api/query_keys/metric'; import { useGetMetric } from '@/api/buster_rest/metrics'; import { compareObjectsByKeys } from '@/lib/objects'; import { useMemo } from 'react'; -import last from 'lodash/last'; -export const useIsMetricChanged = ({ metricId }: { metricId: string }) => { +export const useIsMetricChanged = ({ metricId }: { metricId: string | undefined }) => { const queryClient = useQueryClient(); const originalMetric = useOriginalMetricStore((x) => x.getOriginalMetric(metricId)); @@ -31,7 +30,7 @@ export const useIsMetricChanged = ({ metricId }: { metricId: string }) => { const onResetMetricToOriginal = useMemoizedFn(() => { const options = metricsQueryKeys.metricsGetMetric( - metricId, + metricId || '', originalMetric?.version_number || null ); if (originalMetric) { diff --git a/web/src/context/Metrics/useOriginalMetricStore.tsx b/web/src/context/Metrics/useOriginalMetricStore.tsx index 1c31b5b9e..deab53b8b 100644 --- a/web/src/context/Metrics/useOriginalMetricStore.tsx +++ b/web/src/context/Metrics/useOriginalMetricStore.tsx @@ -8,7 +8,7 @@ type OriginalMetricStore = { originalMetrics: Record; bulkAddOriginalMetrics: (metrics: Record) => void; setOriginalMetric: (metric: IBusterMetric) => void; - getOriginalMetric: (metricId: string) => IBusterMetric | undefined; + getOriginalMetric: (metricId: string | undefined) => IBusterMetric | undefined; removeOriginalMetric: (metricId: string) => void; }; @@ -28,7 +28,8 @@ export const useOriginalMetricStore = create((set, get) => [metric.id]: metric } })), - getOriginalMetric: (metricId: string) => get().originalMetrics[metricId], + getOriginalMetric: (metricId: string | undefined) => + metricId ? get().originalMetrics[metricId] : undefined, removeOriginalMetric: (metricId: string) => set((state) => { const { [metricId]: removed, ...rest } = state.originalMetrics; diff --git a/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardSavePopup.tsx b/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardSaveFilePopup.tsx similarity index 72% rename from web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardSavePopup.tsx rename to web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardSaveFilePopup.tsx index 2a7ff508b..82ca9a0cb 100644 --- a/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardSavePopup.tsx +++ b/web/src/controllers/DashboardController/DashboardViewDashboardController/DashboardSaveFilePopup.tsx @@ -1,17 +1,17 @@ import { useGetDashboard, useUpdateDashboard } from '@/api/buster_rest/dashboards'; import { SaveResetFilePopup } from '@/components/features/popups/SaveResetFilePopup'; -import { useIsDashboardChanged } from '@/context/Dashboards'; import { useMemoizedFn } from '@/hooks'; import { useChatLayoutContextSelector } from '@/layouts/ChatLayout'; +import { useChatIndividualContextSelector } from '@/layouts/ChatLayout/ChatContext'; import React from 'react'; -export const DashboardSavePopup: React.FC<{ dashboardId: string }> = React.memo( +export const DashboardSaveFilePopup: React.FC<{ dashboardId: string }> = React.memo( ({ dashboardId }) => { + const onResetToOriginal = useChatIndividualContextSelector((x) => x.onResetToOriginal); + const isFileChanged = useChatIndividualContextSelector((x) => x.isFileChanged); const chatId = useChatLayoutContextSelector((x) => x.chatId); const { data: dashboardResponse } = useGetDashboard({ id: dashboardId }); - const { isDashboardChanged, onResetDashboardToOriginal } = useIsDashboardChanged({ - dashboardId - }); + const { mutateAsync: onSaveDashboard, isPending: isSaving } = useUpdateDashboard({ saveToServer: true, updateOnSave: true, @@ -30,8 +30,8 @@ export const DashboardSavePopup: React.FC<{ dashboardId: string }> = React.memo( return ( {!isVersionHistoryMode && !isViewingOldVersion && ( - + )}
diff --git a/web/src/controllers/MetricController/MetricViewChart/MetricSaveFilePopup.tsx b/web/src/controllers/MetricController/MetricViewChart/MetricSaveFilePopup.tsx index a90a7fbc6..aff9927de 100644 --- a/web/src/controllers/MetricController/MetricViewChart/MetricSaveFilePopup.tsx +++ b/web/src/controllers/MetricController/MetricViewChart/MetricSaveFilePopup.tsx @@ -1,16 +1,17 @@ import { SaveResetFilePopup } from '@/components/features/popups/SaveResetFilePopup'; -import { useIsMetricChanged } from '@/context/Metrics/useIsMetricChanged'; import { useUpdateMetricChart } from '@/context/Metrics/useUpdateMetricChart'; +import { useChatIndividualContextSelector } from '@/layouts/ChatLayout/ChatContext'; import React from 'react'; export const MetricSaveFilePopup: React.FC<{ metricId: string }> = React.memo(({ metricId }) => { - const { isMetricChanged, onResetMetricToOriginal } = useIsMetricChanged({ metricId }); + const onResetToOriginal = useChatIndividualContextSelector((x) => x.onResetToOriginal); + const isFileChanged = useChatIndividualContextSelector((x) => x.isFileChanged); const { onSaveMetricToServer, isSaving } = useUpdateMetricChart({ metricId }); return ( state.onStartChatFromFile); const onStopChatContext = useBusterNewChatContextSelector((state) => state.onStopChat); const currentMessageId = useChatIndividualContextSelector((x) => x.currentMessageId); + const isFileChanged = useChatIndividualContextSelector((x) => x.isFileChanged); + const onResetToOriginal = useChatIndividualContextSelector((x) => x.onResetToOriginal); + const { openConfirmModal } = useBusterNotifications(); const flow: FlowType = useMemo(() => { if (hasChat) return 'followup-chat'; @@ -43,40 +48,65 @@ export const useChatInputFlow = ({ return; } - switch (flow) { - case 'followup-chat': - await onFollowUpChat({ prompt: inputValue, chatId }); - break; + const method = async () => { + switch (flow) { + case 'followup-chat': + await onFollowUpChat({ prompt: inputValue, chatId }); + break; - case 'followup-metric': - await onStartChatFromFile({ - prompt: inputValue, - fileId: selectedFileId!, - fileType: 'metric' - }); - break; - case 'followup-dashboard': - await onStartChatFromFile({ - prompt: inputValue, - fileId: selectedFileId!, - fileType: 'dashboard' - }); - break; + case 'followup-metric': + await onStartChatFromFile({ + prompt: inputValue, + fileId: selectedFileId!, + fileType: 'metric' + }); + break; + case 'followup-dashboard': + await onStartChatFromFile({ + prompt: inputValue, + fileId: selectedFileId!, + fileType: 'dashboard' + }); + break; - case 'new': - await onStartNewChat({ prompt: inputValue }); - break; + case 'new': + await onStartNewChat({ prompt: inputValue }); + break; - default: - const _exhaustiveCheck: never = flow; - return _exhaustiveCheck; + default: + const _exhaustiveCheck: never = flow; + return _exhaustiveCheck; + } + + setInputValue(''); + + setTimeout(() => { + textAreaRef.current?.focus(); + }, 50); + }; + + if (!isFileChanged) { + return method(); } - setInputValue(''); + await openConfirmModal({ + title: 'Unsaved changes', + content: 'Looks like you have unsaved changes. Do you want to save them before continuing?', + primaryButtonProps: { + text: 'Reset to original' + }, + cancelButtonProps: { + text: 'Continue' + }, + onOk: async () => { + onResetToOriginal(); - setTimeout(() => { - textAreaRef.current?.focus(); - }, 50); + return await method(); + }, + onCancel: async () => { + return await method(); + } + }); }); const onStopChat = useMemoizedFn(() => { diff --git a/web/src/layouts/ChatLayout/ChatContext/ChatContext.tsx b/web/src/layouts/ChatLayout/ChatContext/ChatContext.tsx index 4659b9f83..8bd84ca01 100644 --- a/web/src/layouts/ChatLayout/ChatContext/ChatContext.tsx +++ b/web/src/layouts/ChatLayout/ChatContext/ChatContext.tsx @@ -7,15 +7,14 @@ import { useQueries } from '@tanstack/react-query'; import { queryKeys } from '@/api/query_keys'; import { IBusterChatMessage } from '@/api/asset_interfaces/chat'; import { useChatLayoutContextSelector } from '..'; +import { useIsFileChanged } from './useIsFileChanged'; const useChatIndividualContext = ({ chatId, - selectedFile, - onSetSelectedFile + selectedFile }: { chatId?: string; selectedFile: SelectedFile | null; - onSetSelectedFile: (file: SelectedFile) => void; }) => { const selectedFileId = selectedFile?.id; const selectedFileType = selectedFile?.type; @@ -53,6 +52,11 @@ const useChatIndividualContext = ({ chatId }); + const { isFileChanged, onResetToOriginal } = useIsFileChanged({ + selectedFileId, + selectedFileType + }); + return React.useMemo( () => ({ hasChat, @@ -63,7 +67,9 @@ const useChatIndividualContext = ({ selectedFileType, chatMessageIds, chatId, - isStreamingMessage + isStreamingMessage, + isFileChanged, + onResetToOriginal }), [ hasChat, @@ -74,7 +80,9 @@ const useChatIndividualContext = ({ chatTitle, selectedFileType, chatMessageIds, - chatId + chatId, + isFileChanged, + onResetToOriginal ] ); }; @@ -86,11 +94,9 @@ const IndividualChatContext = createContext) => { const chatId = useChatLayoutContextSelector((x) => x.chatId); const selectedFile = useChatLayoutContextSelector((x) => x.selectedFile); - const onSetSelectedFile = useChatLayoutContextSelector((x) => x.onSetSelectedFile); const useChatContextValue = useChatIndividualContext({ chatId, - selectedFile, - onSetSelectedFile + selectedFile }); return ( diff --git a/web/src/layouts/ChatLayout/ChatContext/useIsFileChanged.ts b/web/src/layouts/ChatLayout/ChatContext/useIsFileChanged.ts new file mode 100644 index 000000000..c3fbe0d51 --- /dev/null +++ b/web/src/layouts/ChatLayout/ChatContext/useIsFileChanged.ts @@ -0,0 +1,41 @@ +import { useIsMetricChanged } from '@/context/Metrics'; +import { SelectedFile } from '../interfaces'; +import { useIsDashboardChanged } from '@/context/Dashboards'; +import { useMemo } from 'react'; + +type UseIsFileChangeReturn = { + isFileChanged: boolean; + onResetToOriginal: () => void; +}; + +type UseIsFileChangeParams = { + selectedFileId: SelectedFile['id'] | undefined; + selectedFileType: SelectedFile['type'] | undefined; +}; + +export const useIsFileChanged = ({ + selectedFileId, + selectedFileType +}: UseIsFileChangeParams): UseIsFileChangeReturn => { + const { isMetricChanged, onResetMetricToOriginal } = useIsMetricChanged({ + metricId: selectedFileType === 'metric' ? selectedFileId : undefined + }); + + const { isDashboardChanged, onResetDashboardToOriginal } = useIsDashboardChanged({ + dashboardId: selectedFileType === 'dashboard' ? selectedFileId : undefined + }); + + return useMemo(() => { + if (selectedFileType === 'metric') + return { + isFileChanged: isMetricChanged, + onResetToOriginal: onResetMetricToOriginal + }; + if (selectedFileType === 'dashboard') + return { + isFileChanged: isDashboardChanged, + onResetToOriginal: onResetDashboardToOriginal + }; + return { isFileChanged: false, onResetToOriginal: () => {} }; + }, [isMetricChanged, isDashboardChanged, onResetMetricToOriginal, onResetDashboardToOriginal]); +};