diff --git a/studio/components/to-be-cleaned/SqlEditor/TabSqlQuery.js b/studio/components/to-be-cleaned/SqlEditor/TabSqlQuery.js index 3fbdfcd0174..2b46001a02b 100644 --- a/studio/components/to-be-cleaned/SqlEditor/TabSqlQuery.js +++ b/studio/components/to-be-cleaned/SqlEditor/TabSqlQuery.js @@ -78,6 +78,7 @@ const TabSqlQuery = observer(() => {
@@ -88,7 +89,7 @@ const TabSqlQuery = observer(() => { }) export default TabSqlQuery -const MonacoEditor = ({ error, updateSqlSnippet }) => { +const MonacoEditor = ({ error, updateSqlSnippet, setUpdatingRequired }) => { const sqlEditorStore = useSqlStore() const editorRef = useRef(null) const monacoRef = useRef(null) @@ -173,6 +174,10 @@ const MonacoEditor = ({ error, updateSqlSnippet }) => { // this is so any SQL run will be whatever is currently in monaco editor sqlEditorStore.activeTab.setQuery(value) + // inform the content store that the SQL has changed and needs to be persisted + // this is so we can block the tab being closed if an update is required + setUpdatingRequired?.() + // debounce changes debounceUpdateSqlSnippet(value) } diff --git a/studio/pages/_app.tsx b/studio/pages/_app.tsx index 3d3cfcbf0c0..d9a8d280b26 100644 --- a/studio/pages/_app.tsx +++ b/studio/pages/_app.tsx @@ -52,6 +52,37 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) { handleEmailVerificationError() }, []) + const getSavingState = () => rootStore.content.savingState + + // prompt the user if they try and leave with unsaved content store changes + useEffect(() => { + const warningText = 'You have unsaved changes - are you sure you wish to leave this page?' + + const handleWindowClose = (e: BeforeUnloadEvent) => { + const savingState = getSavingState() + + const unsavedChanges = + savingState === 'UPDATING_REQUIRED' || + savingState === 'UPDATING' || + savingState === 'UPDATING_FAILED' + + if (!unsavedChanges) { + return + } + + e.preventDefault() + + return (e.returnValue = warningText) + } + + window.addEventListener('beforeunload', handleWindowClose) + + return () => { + window.removeEventListener('beforeunload', handleWindowClose) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + const getLayout = Component.getLayout ?? ((page) => page) return ( diff --git a/studio/stores/project/ProjectContentStore.ts b/studio/stores/project/ProjectContentStore.ts index 3298980145b..13d35d18297 100644 --- a/studio/stores/project/ProjectContentStore.ts +++ b/studio/stores/project/ProjectContentStore.ts @@ -15,7 +15,13 @@ export interface IProjectContentStore { isLoading: boolean isInitialized: boolean isLoaded: boolean - savingState: 'IDLE' | 'CREATING' | 'CREATING_FAILED' | 'UPDATING' | 'UPDATING_FAILED' + savingState: + | 'IDLE' + | 'CREATING' + | 'CREATING_FAILED' + | 'UPDATING_REQUIRED' + | 'UPDATING' + | 'UPDATING_FAILED' error: any recentLogSqlSnippets: LogSqlSnippets.Content[] @@ -81,6 +87,7 @@ export interface IProjectContentStore { del(id: any): Promise<{ data: boolean; error: unknown }> delOptimistically(id: string): { data: boolean; error: null } + setUpdatingRequired(): void } export default class ProjectContentStore implements IProjectContentStore { @@ -102,7 +109,13 @@ export default class ProjectContentStore implements IProjectContentStore { recentLogSqlSnippets: LogSqlSnippets.Content[] = [] state = this.STATES.INITIAL - savingState: 'IDLE' | 'CREATING' | 'CREATING_FAILED' | 'UPDATING' | 'UPDATING_FAILED' + savingState: + | 'IDLE' + | 'CREATING' + | 'CREATING_FAILED' + | 'UPDATING_REQUIRED' + | 'UPDATING' + | 'UPDATING_FAILED' error = null constructor(rootStore: IRootStore, options: { projectRef: string }) { @@ -354,6 +367,12 @@ export default class ProjectContentStore implements IProjectContentStore { return { data: true, error: null } } + setUpdatingRequired() { + if (this.savingState === 'IDLE' || this.savingState === 'UPDATING_FAILED') { + this.savingState = 'UPDATING_REQUIRED' + } + } + setProjectRef(ref?: string) { if (ref) { this.projectRef = ref