[id].tsx162 lines · main
1import { usePrevious } from '@uidotdev/usehooks'
2import { useParams } from 'common/hooks/useParams'
3import Link from 'next/link'
4import { useRouter } from 'next/router'
5import { useEffect } from 'react'
6import { Button } from 'ui'
7import { Admonition } from 'ui-patterns'
8
9import { SQLEditor } from '@/components/interfaces/SQLEditor/SQLEditor'
10import { generateSnippetTitle } from '@/components/interfaces/SQLEditor/SQLEditor.constants'
11import DefaultLayout from '@/components/layouts/DefaultLayout'
12import { EditorBaseLayout } from '@/components/layouts/editors/EditorBaseLayout'
13import { useEditorType } from '@/components/layouts/editors/EditorsLayout.hooks'
14import SQLEditorLayout from '@/components/layouts/SQLEditorLayout/SQLEditorLayout'
15import { SQLEditorMenu } from '@/components/layouts/SQLEditorLayout/SQLEditorMenu'
16import { useContentIdQuery } from '@/data/content/content-id-query'
17import { useDashboardHistory } from '@/hooks/misc/useDashboardHistory'
18import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
19import { IS_PLATFORM } from '@/lib/constants'
20import { SnippetWithContent, useSnippets, useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
21import { createTabId, useTabsStateSnapshot } from '@/state/tabs'
22import type { NextPageWithLayout } from '@/types'
23
24const SqlEditor: NextPageWithLayout = () => {
25 const router = useRouter()
26 const { id, ref, content, skip } = useParams()
27 const previousRoute = usePrevious(id)
28 const { data: project } = useSelectedProjectQuery()
29
30 const editor = useEditorType()
31 const tabs = useTabsStateSnapshot()
32 const snapV2 = useSqlEditorV2StateSnapshot()
33 const { history, setLastVisitedSnippet } = useDashboardHistory()
34
35 const allSnippets = useSnippets(ref!)
36 const snippet = allSnippets.find((x) => x.id === id)
37
38 const tabId = !!id ? tabs.openTabs.find((x) => x.endsWith(id)) : undefined
39
40 // [Joshen] May need to investigate separately, but occasionally addSnippet doesnt exist in
41 // the snapV2 valtio store for some reason hence why the added typeof check here
42 const canFetchContentBasedOnId = Boolean(
43 id !== 'new' && typeof snapV2.addSnippet === 'function' && !snippet?.isNotSavedInDatabaseYet
44 )
45 const { data, error, isError } = useContentIdQuery(
46 { projectRef: ref, id },
47 {
48 retry: false,
49 enabled: canFetchContentBasedOnId,
50 }
51 )
52
53 const snippetMissing =
54 isError && error.code === 404 && error.message.includes('Content not found')
55 const invalidId = isError && error.code === 400 && error.message.includes('Invalid uuid')
56
57 // [Joshen] Atm we suspect that replication lag is causing this to happen whereby a newly created snippet
58 // shows the "Unable to find snippet" error which blocks the whole UI
59 // Am opting to silently swallow this error, since the saves are still going through and we're scoping this behaviour
60 // behaviour down to a very specific use case too with all these conditionals
61 // More details: https://github.com/briven/briven/pull/39389
62 const snippetMissingImmediatelyAfterCreating =
63 !!snippet && snippetMissing && previousRoute === 'new' && 'isNotSavedInDatabaseYet' in snippet
64
65 useEffect(() => {
66 if (ref && data && project) {
67 // [Joshen] Check if snippet belongs to the current project
68 if (!IS_PLATFORM || data.project_id === project.id) {
69 snapV2.setSnippet(ref, data as unknown as SnippetWithContent)
70 } else {
71 setLastVisitedSnippet(undefined)
72 router.replace(`/project/${ref}/sql/new`)
73 }
74 }
75 // eslint-disable-next-line react-hooks/exhaustive-deps
76 }, [ref, data, project])
77
78 // Load the last visited snippet when landing on /new
79 useEffect(() => {
80 if (
81 id === 'new' &&
82 skip !== 'true' && // [Joshen] Skip flag implies to skip loading the last visited snippet
83 history.sql !== undefined &&
84 content === undefined
85 ) {
86 const snippet = allSnippets.find((snippet) => snippet.id === history.sql)
87 if (snippet !== undefined) router.replace(`/project/${ref}/sql/${history.sql}`)
88 }
89 // eslint-disable-next-line react-hooks/exhaustive-deps
90 }, [id, allSnippets, content])
91
92 // Watch for route changes
93 useEffect(() => {
94 if (!router.isReady || !id || id === 'new') return
95
96 const tabId = createTabId('sql', { id })
97 const snippet = allSnippets.find((x) => x.id === id)
98
99 tabs.addTab({
100 id: tabId,
101 type: 'sql',
102 label: snippet?.name || generateSnippetTitle(),
103 metadata: {
104 sqlId: id,
105 name: snippet?.name,
106 },
107 })
108 // eslint-disable-next-line react-hooks/exhaustive-deps
109 }, [router.isReady, id])
110
111 if ((snippetMissing || invalidId) && !snippetMissingImmediatelyAfterCreating) {
112 return (
113 <div className="flex items-center justify-center h-full">
114 <div className="w-[400px]">
115 <Admonition
116 type="default"
117 title={`Unable to find snippet with ID ${id}`}
118 description="This snippet doesn't exist in your project"
119 >
120 {!!tabId ? (
121 <Button
122 type="default"
123 className="mt-2"
124 onClick={() => {
125 tabs.handleTabClose({
126 id: tabId,
127 router,
128 editor,
129 onClearDashboardHistory: () => setLastVisitedSnippet(undefined),
130 })
131 }}
132 >
133 Close tab
134 </Button>
135 ) : (
136 <Button
137 asChild
138 type="default"
139 className="mt-2"
140 onClick={() => setLastVisitedSnippet(undefined)}
141 >
142 <Link href={`/project/${ref}/sql`}>Head back</Link>
143 </Button>
144 )}
145 </Admonition>
146 </div>
147 </div>
148 )
149 }
150
151 return <SQLEditor />
152}
153
154SqlEditor.getLayout = (page) => (
155 <DefaultLayout>
156 <EditorBaseLayout productMenu={<SQLEditorMenu />} product="SQL Editor">
157 <SQLEditorLayout>{page}</SQLEditorLayout>
158 </EditorBaseLayout>
159 </DefaultLayout>
160)
161
162export default SqlEditor