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