Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 86c4ed2

Browse files
authored
[UI] save file change only on ctrl-s (SQLMesh#603)
1 parent 3f28eb8 commit 86c4ed2

21 files changed

Lines changed: 233 additions & 199 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ repos:
4242
- id: prettier
4343
name: prettier
4444
files: ^(web/client)
45-
entry: prettier --ignore-path web/client/.prettierignore
45+
entry: prettier --write --ignore-path web/client/.prettierignore
4646
exclude: ^(web/client/node_modules)
4747
require_serial: true
4848
language: node

web/client/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!DOCTYPE html>
1+
<!doctype html>
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />

web/client/src/context/plan.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const EnumPlanState = {
2424

2525
export const EnumPlanApplyType = {
2626
Virtual: 'virtual',
27-
Backfill: 'backfill'
27+
Backfill: 'backfill',
2828
} as const
2929

3030
export type PlanApplyType = KeyOf<typeof EnumPlanApplyType>

web/client/src/index.css

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@
66
.scrollbar--horizontal::-webkit-scrollbar {
77
height: 0.2rem;
88
}
9-
9+
1010
.scrollbar--vertical::-webkit-scrollbar {
1111
width: 0.2rem;
1212
}
13-
13+
1414
.scrollbar::-webkit-scrollbar-track {
1515
background-color: transparent;
1616
}
17-
17+
1818
.scrollbar::-webkit-scrollbar-thumb {
1919
background: var(--color-brand);
2020
border-radius: 1rem;
21-
}
21+
}
2222
}
2323

2424
@layer components {
@@ -277,4 +277,3 @@ html {
277277
format('opentype');
278278
font-weight: 900;
279279
}
280-

web/client/src/library/components/editor/CodeEditor.tsx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
22
import CodeMirror from '@uiw/react-codemirror'
33
import { python } from '@codemirror/lang-python'
44
import { StreamLanguage } from '@codemirror/language'
5-
5+
import { keymap } from '@codemirror/view'
66
import { yaml } from '@codemirror/legacy-modes/mode/yaml'
77
import { type Extension } from '@codemirror/state'
88
import { type Model } from '~/api/client'
@@ -24,37 +24,50 @@ export default function CodeEditor({
2424
dialect,
2525
dialects,
2626
onChange,
27+
saveChange,
2728
}: {
2829
file: ModelFile
2930
models: Map<string, Model>
3031
dialect?: string
3132
dialects?: string[]
3233
onChange: (value: string) => void
34+
saveChange: (value: string) => void
3335
}): JSX.Element {
3436
const { mode } = useColorScheme()
35-
const theme = mode === EnumColorScheme.Dark ? dracula : tomorrow
37+
const [SqlMeshDialect, SqlMeshDialectCleanUp] = useSqlMeshExtension(dialects)
3638

3739
const files = useStoreFileTree(s => s.files)
3840
const selectFile = useStoreFileTree(s => s.selectFile)
3941

4042
const [sqlDialectOptions, setSqlDialectOptions] = useState()
4143

42-
const [SqlMeshDialect, SqlMeshDialectCleanUp] =
43-
useSqlMeshExtension(dialects)
44-
4544
const extensions = useMemo(() => {
46-
const showSqlMeshDialect = file.extension === '.sql' && models != null && sqlDialectOptions != null
45+
const showSqlMeshDialect =
46+
file.extension === '.sql' && sqlDialectOptions != null
4747

4848
return [
49-
theme,
50-
models != null && HoverTooltip(models),
51-
models != null && events(models, files, selectFile),
52-
models != null && SqlMeshModel(models),
49+
mode === EnumColorScheme.Dark ? dracula : tomorrow,
50+
HoverTooltip(models),
51+
events(models, files, selectFile),
52+
SqlMeshModel(models),
5353
showSqlMeshDialect && SqlMeshDialect(models, file, sqlDialectOptions),
5454
file.extension === '.py' && python(),
5555
file.extension === '.yaml' && StreamLanguage.define(yaml),
56+
keymap.of([
57+
{
58+
mac: 'Cmd-s',
59+
win: 'Ctrl-s',
60+
linux: 'Ctrl-s',
61+
preventDefault: true,
62+
run() {
63+
saveChange(file.content)
64+
65+
return true
66+
},
67+
},
68+
]),
5669
].filter(Boolean) as Extension[]
57-
}, [file, models, sqlDialectOptions, theme])
70+
}, [file, models, sqlDialectOptions, mode])
5871

5972
const handleSqlGlotWorkerMessage = useCallback((e: MessageEvent): void => {
6073
if (e.data.topic === 'dialect') {

web/client/src/library/components/editor/Editor.tsx

Lines changed: 39 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,13 @@ import CodeEditor from './CodeEditor'
5656
import { type PropsComponent } from '~/main'
5757
import { sqlglotWorker } from './workers'
5858

59-
export const EnumEditorFileStatus = {
60-
Edit: 'edit',
61-
Editing: 'editing',
62-
Saving: 'saving',
63-
Saved: 'saved',
64-
} as const
65-
6659
export const EnumEditorTabs = {
6760
QueryPreview: 'queryPreview',
6861
Table: 'table',
6962
Terminal: 'terminal',
7063
} as const
7164

7265
export type EditorTabs = KeyOf<typeof EnumEditorTabs>
73-
export type EditorFileStatus = KeyOf<typeof EnumEditorFileStatus>
7466

7567
interface PropsEditor extends React.HTMLAttributes<HTMLElement> {
7668
environment: ModelEnvironment
@@ -110,9 +102,6 @@ export default function Editor({
110102
const tabTableContent = useStoreEditor(s => s.tabTableContent)
111103
const tabTerminalContent = useStoreEditor(s => s.tabTerminalContent)
112104

113-
const [fileStatus, setEditorFileStatus] = useState<EditorFileStatus>(
114-
EnumEditorFileStatus.Edit,
115-
)
116105
const [isSaved, setIsSaved] = useState(true)
117106
const [formEvaluate, setFormEvaluate] = useState<FormEvaluate>({
118107
start: toDateFormat(toDate(Date.now() - DAY)),
@@ -134,37 +123,29 @@ export default function Editor({
134123
const { data: fileData, refetch: getFileContent } = useApiFileByPath(
135124
activeFile.path,
136125
)
126+
const debouncedPlanRun = useCallback(debounceAsync(planRun, 1000, true), [
127+
planRun,
128+
])
129+
137130
const mutationSaveFile = useMutationApiSaveFile(client, {
138131
onSuccess(file: File) {
139132
setIsSaved(true)
140-
setEditorFileStatus(EnumEditorFileStatus.Edit)
141133

142134
if (file == null) return
143135

144-
activeFile.content = file.content ?? ''
136+
activeFile.updateContent(file.content)
145137

146138
setOpenedFiles(openedFiles)
139+
140+
void debouncedPlanRun()
147141
},
148142
onMutate() {
149143
setIsSaved(false)
150-
setEditorFileStatus(EnumEditorFileStatus.Saving)
151144
},
152145
})
153-
const debouncedPlanRun = useCallback(debounceAsync(planRun, 5000), [
154-
planRun,
155-
])
156-
const debouncedChange = useMemo(
157-
() =>
158-
debounce(
159-
onChange,
160-
() => {
161-
setEditorFileStatus(EnumEditorFileStatus.Editing)
162-
},
163-
() => {
164-
setEditorFileStatus(EnumEditorFileStatus.Edit)
165-
},
166-
1000,
167-
),
146+
147+
const debouncedSaveChange = useMemo(
148+
() => debounce(saveChange, 1000, true),
168149
[activeFile],
169150
)
170151

@@ -197,25 +178,26 @@ export default function Editor({
197178
}, [handleSqlGlotWorkerMessage])
198179

199180
useEffect(() => {
200-
if (activeFile.isSQLMeshModel || activeFile.isSQLMeshSeed) {
201-
void debouncedPlanRun()
202-
}
203-
204181
return () => {
182+
debouncedPlanRun.cancel()
183+
205184
apiCancelPlanRun(client)
206185
}
207-
}, [activeFile.content])
186+
}, [])
208187

209188
useEffect(() => {
210189
if (fileData == null) return
211190

212-
activeFile.content = fileData.content ?? ''
191+
activeFile.updateContent(fileData.content)
213192

214193
setOpenedFiles(openedFiles)
215194
}, [fileData])
216195

217196
useEffect(() => {
218-
if (isFalse(isStringEmptyOrNil(activeFile.path))) {
197+
if (
198+
isFalse(isStringEmptyOrNil(activeFile.path)) &&
199+
isStringEmptyOrNil(activeFile.content)
200+
) {
219201
void getFileContent()
220202
}
221203

@@ -257,19 +239,19 @@ export default function Editor({
257239
selectFile(new ModelFile())
258240
}
259241

260-
function onChange(value: string): void {
242+
function updateFileContent(value: string): void {
261243
if (activeFile.content === value) return
262244

263245
activeFile.content = value
264246

265-
if (activeFile.isLocal) {
266-
setOpenedFiles(openedFiles)
267-
} else {
268-
mutationSaveFile.mutate({
269-
path: activeFile.path,
270-
body: { content: value },
271-
})
272-
}
247+
setOpenedFiles(openedFiles)
248+
}
249+
250+
function saveChange(): void {
251+
mutationSaveFile.mutate({
252+
path: activeFile.path,
253+
body: { content: activeFile.content },
254+
})
273255
}
274256

275257
function sendQuery(): void {
@@ -314,7 +296,10 @@ export default function Editor({
314296
model: formEvaluate.model,
315297
})
316298
.then(updateTabs)
317-
.catch(console.log)
299+
.catch(error => {
300+
bucket.set(EnumEditorTabs.Terminal, error.message)
301+
setTabTerminalContent(bucket.get(EnumEditorTabs.Terminal))
302+
})
318303
}
319304
}
320305

@@ -345,10 +330,6 @@ export default function Editor({
345330
}
346331
}
347332

348-
function cleanUp(): void {
349-
setEditorFileStatus(EnumEditorFileStatus.Edit)
350-
}
351-
352333
// TODO: remove once we have a better way to determine if a file is a model
353334
const hasContentActiveFile = isFalse(isStringEmptyOrNil(activeFile.content))
354335
const shouldEvaluate =
@@ -363,7 +344,7 @@ export default function Editor({
363344
: [100, 0]
364345
const sizesMain =
365346
activeFile.isSQLMeshModel &&
366-
[tabTableContent, tabTerminalContent].some(isTrue)
347+
[tabTableContent, tabTerminalContent].some(Boolean)
367348
? [75, 25]
368349
: [100, 0]
369350

@@ -413,13 +394,15 @@ export default function Editor({
413394
<small className="text-xs">
414395
{file.isUntitled ? `SQL-${idx + 1}` : file.name}
415396
</small>
397+
{file.isChanged && (
398+
<small className="group-hover:hidden text-xs inline-block mx-2 w-2 h-2 bg-warning-500 rounded-full"></small>
399+
)}
416400
{openedFiles.size > 1 && (
417401
<XCircleIcon
418-
className="inline-block opacity-0 group-hover:opacity-100 text-neutral-600 dark:text-neutral-100 w-4 h-4 ml-2 cursor-pointer"
402+
className="hidden group-hover:inline-block text-neutral-600 dark:text-neutral-100 w-4 h-4 ml-2 cursor-pointer"
419403
onClick={(e: MouseEvent) => {
420404
e.stopPropagation()
421405

422-
cleanUp()
423406
closeEditorTab(file)
424407
}}
425408
/>
@@ -442,7 +425,8 @@ export default function Editor({
442425
<div className="flex flex-col h-full">
443426
{models != null && (
444427
<CodeEditor
445-
onChange={debouncedChange}
428+
onChange={updateFileContent}
429+
saveChange={debouncedSaveChange}
446430
models={models}
447431
file={activeFile}
448432
dialect={dialect}
@@ -641,7 +625,6 @@ export default function Editor({
641625
<EditorFooter
642626
activeFile={activeFile}
643627
isSaved={isSaved}
644-
fileStatus={fileStatus}
645628
dialects={dialects}
646629
dialect={dialect}
647630
setDialect={setDialect}
@@ -690,12 +673,10 @@ function EditorFooter({
690673
activeFile,
691674
isSaved,
692675
isValid,
693-
fileStatus,
694676
}: {
695677
activeFile: ModelFile
696678
isSaved: boolean
697679
isValid: boolean
698-
fileStatus: string
699680
dialects: Array<{ dialect_title: string; dialect_name: string }>
700681
dialect?: string
701682
setDialect: (dialect?: string) => void
@@ -711,14 +692,9 @@ function EditorFooter({
711692
<Indicator
712693
className="mr-2"
713694
text="Saved"
714-
ok={isSaved}
695+
ok={isFalse(activeFile.isChanged)}
715696
/>
716697
)}
717-
<Indicator
718-
className="mr-2"
719-
text="Status"
720-
value={fileStatus}
721-
/>
722698
<Indicator
723699
className="mr-2"
724700
text="Language"

0 commit comments

Comments
 (0)