diff --git a/web-common/src/components/editor/YAMLEditor.spec.ts b/web-common/src/components/editor/YAMLEditor.spec.ts index 4f79688d15e..ca203002147 100644 --- a/web-common/src/components/editor/YAMLEditor.spec.ts +++ b/web-common/src/components/editor/YAMLEditor.spec.ts @@ -38,7 +38,7 @@ describe("YAMLEditor.svelte", () => { expect(getLines(container)).toHaveLength(1); const onUpdate = vi.fn(); - component?.$on("update", onUpdate); + component?.$on("save", onUpdate); const content = "foo: 10\nbar: 20\nfoo: 10\nbar: 20"; diff --git a/web-common/src/components/editor/__stories__/YAMLEditor.stories.svelte b/web-common/src/components/editor/__stories__/YAMLEditor.stories.svelte index 648890685f0..ee7baec93ae 100644 --- a/web-common/src/components/editor/__stories__/YAMLEditor.stories.svelte +++ b/web-common/src/components/editor/__stories__/YAMLEditor.stories.svelte @@ -2,9 +2,9 @@ import type { EditorView } from "@codemirror/view"; import { Meta, Story, Template } from "@storybook/addon-svelte-csf"; import Button from "../../button/Button.svelte"; + import YAMLEditor from "../YAMLEditor.svelte"; import { setLineStatuses } from "../line-status"; import type { LineStatus } from "../line-status/state"; - import YAMLEditor from "../YAMLEditor.svelte"; let content = `name: this is the name values: @@ -52,7 +52,7 @@ values: key="key" {content} bind:view - on:update={(event) => { + on:save={(event) => { // Often, you want to debounce the update to parent content. // Here, we have no such requirement. content = event.detail; diff --git a/web-common/src/components/editor/dispatch-events/index.ts b/web-common/src/components/editor/dispatch-events/index.ts index 97b1d81170b..029587accfd 100644 --- a/web-common/src/components/editor/dispatch-events/index.ts +++ b/web-common/src/components/editor/dispatch-events/index.ts @@ -21,7 +21,7 @@ export function bindEditorEventsToDispatcher( * The viewUpdate can be used to look at transactions at the parent component level. */ if (whenFocused && !viewUpdate.view.hasFocus) return; - dispatch("update", { + dispatch("save", { content: viewUpdate.view.state.doc.toString(), viewUpdate, } as UpdateDetails); diff --git a/web-common/src/features/charts/editor/ChartsEditor.svelte b/web-common/src/features/charts/editor/ChartsEditor.svelte index 67bdb3c3751..767e468c49c 100644 --- a/web-common/src/features/charts/editor/ChartsEditor.svelte +++ b/web-common/src/features/charts/editor/ChartsEditor.svelte @@ -59,6 +59,6 @@ content={yaml} extensions={[customYAMLwithJSONandSQL]} key={filePath} - on:update={(e) => debounceUpdateChartContent(e.detail.content)} + on:save={(e) => debounceUpdateChartContent(e.detail.content)} /> diff --git a/web-common/src/features/custom-dashboards/CustomDashboardEditor.svelte b/web-common/src/features/custom-dashboards/CustomDashboardEditor.svelte index 7e96d427f7d..c4cc4003851 100644 --- a/web-common/src/features/custom-dashboards/CustomDashboardEditor.svelte +++ b/web-common/src/features/custom-dashboards/CustomDashboardEditor.svelte @@ -2,9 +2,9 @@ import type { EditorView } from "@codemirror/view"; import YAMLEditor from "@rilldata/web-common/components/editor/YAMLEditor.svelte"; import { debounce } from "@rilldata/web-common/lib/create-debouncer"; + import { V1ParseError } from "@rilldata/web-common/runtime-client"; import { createEventDispatcher } from "svelte"; import ChartsEditorContainer from "../charts/editor/ChartsEditorContainer.svelte"; - import { V1ParseError } from "@rilldata/web-common/runtime-client"; const dispatch = createEventDispatcher(); @@ -30,6 +30,6 @@ content={yaml} key={filePath} whenFocused - on:update={(e) => debounceUpdateChartContent(e.detail.content)} + on:save={(e) => debounceUpdateChartContent(e.detail.content)} /> diff --git a/web-common/src/features/editor/Editor.svelte b/web-common/src/features/editor/Editor.svelte index a3c4f45ed6e..99f5ba60e9c 100644 --- a/web-common/src/features/editor/Editor.svelte +++ b/web-common/src/features/editor/Editor.svelte @@ -2,20 +2,32 @@ import type { Extension } from "@codemirror/state"; import { EditorState } from "@codemirror/state"; import { EditorView } from "@codemirror/view"; + import Button from "@rilldata/web-common/components/button/Button.svelte"; + import Label from "@rilldata/web-common/components/forms/Label.svelte"; + import Switch from "@rilldata/web-common/components/forms/Switch.svelte"; + import Check from "@rilldata/web-common/components/icons/Check.svelte"; + import UndoIcon from "@rilldata/web-common/components/icons/UndoIcon.svelte"; import { createEventDispatcher, onMount } from "svelte"; import { bindEditorEventsToDispatcher } from "../../components/editor/dispatch-events"; import { base } from "../../components/editor/presets/base"; + import { debounce } from "../../lib/create-debouncer"; + import { FILE_SAVE_DEBOUNCE_TIME } from "./config"; const dispatch = createEventDispatcher(); export let blob: string; export let latest: string; export let extensions: Extension[] = []; + export let autoSave: boolean; + export let disableAutoSave: boolean; + export let hasUnsavedChanges: boolean; let editor: EditorView; let container: HTMLElement; $: latest = blob; + $: updateEditorContents(latest); + $: if (editor) updateEditorExtensions(extensions); onMount(() => { editor = new EditorView({ @@ -34,6 +46,30 @@ }); }); + function updateEditorExtensions(newExtensions: Extension[]) { + editor.setState( + EditorState.create({ + doc: blob, + extensions: [ + // establish a basic editor + base(), + // any extensions passed as props + ...newExtensions, + EditorView.updateListener.of((v) => { + if (v.focusChanged && v.view.hasFocus) { + dispatch("receive-focus"); + } + if (v.docChanged) { + latest = v.state.doc.toString(); + + if (!disableAutoSave && autoSave) debounceSave(); + } + }), + ], + }), + ); + } + function updateEditorContents(newContent: string) { if (editor && !editor.hasFocus) { // NOTE: when changing files, we still want to update the editor @@ -50,26 +86,82 @@ } } - function updateEditorExtensions(newExtensions: Extension[]) { - editor.setState( - EditorState.create({ - doc: blob, - extensions: [ - // any extensions passed as props - ...newExtensions, - // establish a basic editor - base(), - // this will catch certain events and dispatch them to the parent - bindEditorEventsToDispatcher(dispatch), - ], - }), - ); + function handleKeydown(e: KeyboardEvent) { + if (e.key === "s" && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + save(); + } } - // reactive statements to dynamically update the editor when inputs change - $: updateEditorContents(latest); + function save() { + dispatch("save"); + } - $: if (editor) updateEditorExtensions(extensions); + const debounceSave = debounce(save, FILE_SAVE_DEBOUNCE_TIME); + + function revertContent() { + dispatch("revert"); + } -
+