diff --git a/frontend/src/lib/components/DeployWorkspace.svelte b/frontend/src/lib/components/DeployWorkspace.svelte index a1fa199454..90bb859467 100644 --- a/frontend/src/lib/components/DeployWorkspace.svelte +++ b/frontend/src/lib/components/DeployWorkspace.svelte @@ -442,7 +442,8 @@ content: script.content, lock: script.lock, schema: script.schema, - summary: script.summary + summary: script.summary, + language: script.language } } else if (kind == 'app') { const app = await AppService.getAppByPath({ @@ -503,10 +504,15 @@ async function showDiff(kind: Kind, path: string) { diffDrawer.openDrawer() let values = await Promise.all([ - getValue(kind, path, $workspaceStore!), - getValue(kind, path, workspaceToDeployTo!) + getValue(kind, path, workspaceToDeployTo!), + getValue(kind, path, $workspaceStore!) ]) - diffDrawer.setDiff(JSON.stringify(values[0], null, 2), JSON.stringify(values[1], null, 2)) + diffDrawer.setDiff({ + mode: 'simple', + original: values[0], + current: values[1], + title: 'Staging/prod <> Dev' + }) } diff --git a/frontend/src/lib/components/DiffDrawer.svelte b/frontend/src/lib/components/DiffDrawer.svelte index 42857ec102..f83d2307ff 100644 --- a/frontend/src/lib/components/DiffDrawer.svelte +++ b/frontend/src/lib/components/DiffDrawer.svelte @@ -1,63 +1,256 @@ - + - {#if diffContent == undefined} - - {:else} -
{@html diffContent}
-
-
Removed
-
Added
- {/if} +
+ {#if diffType && data} + + {#if data.mode === 'simple'} + {data.title} + {:else} + {'Deployed <> Current'}{!data.deployed ? ' (no deployed version)' : ''} + {'Latest saved draft <> Current'}{!data.draft ? ' (no draft)' : ''} + {/if} + + {/if} + {#if data?.mode === 'normal'} + {#if diffType === 'draft'} + + {:else if diffType === 'deployed'} + + {/if} + {/if} + {#if data} + {#if contentType} + {@const content = + data.mode === 'normal' + ? diffType === 'draft' + ? data.draft?.content + : data.deployed?.content + : data.original?.content} + {@const metadata = + data.mode === 'normal' + ? diffType === 'draft' + ? data.draft?.metadata + : data.deployed?.metadata + : data.original?.metadata} + {@const lang = + data.mode === 'normal' + ? diffType === 'draft' + ? data.draft?.lang + : data.deployed?.lang + : data.original?.lang} +
+ {#if data.current.content !== undefined} + + Content{content === data.current.content ? ' (no changes)' : ''} + Metadata{metadata === data.current.metadata ? ' (no changes)' : ''} + + {/if} +
+ {#key diffType} + {#if contentType === 'content'} + + {:else if contentType === 'metadata'} + + {/if} + {/key} +
+
+ {:else} + + {#if diffType === 'draft'} + There are no differences between latest saved draft and current + {:else if diffType === 'deployed'} + There are no differences between deployed and current + {:else if diffType === 'custom'} + There are no differences + {/if} + + {/if} + {:else} + + {/if} +
- {#if button} + {#if data?.button} {data.button.text} {/if} diff --git a/frontend/src/lib/components/DiffEditor.svelte b/frontend/src/lib/components/DiffEditor.svelte index 3d04ea357b..5fc80691fe 100644 --- a/frontend/src/lib/components/DiffEditor.svelte +++ b/frontend/src/lib/components/DiffEditor.svelte @@ -15,6 +15,11 @@ export let automaticLayout = true export let fixedOverflowWidgets = true + export let defaultLang: string | undefined = undefined + export let defaultModifiedLang: string | undefined = undefined + export let defaultOriginal: string | undefined = undefined + export let defaultModified: string | undefined = undefined + export let readOnly = false let diffEditor: meditor.IStandaloneDiffEditor | undefined let diffDivEl: HTMLDivElement | null = null @@ -25,6 +30,7 @@ automaticLayout, renderSideBySide: editorWidth >= SIDE_BY_SIDE_MIN_WIDTH, originalEditable: false, + readOnly, minimap: { enabled: false }, @@ -35,15 +41,31 @@ autoDetectHighContrast: true, scrollbar: { alwaysConsumeMouseWheel: false } }) + if ( + defaultOriginal !== undefined && + defaultModified !== undefined && + defaultLang !== undefined + ) { + setupModel(defaultLang, defaultOriginal, defaultModified, defaultModifiedLang) + } } export function setupModel( - lang: 'typescript' | 'python' | 'go' | 'shell' | 'sql' | 'graphql' | 'javascript' | 'powershell' + lang: string, + original?: string, + modified?: string, + modifiedLang?: string ) { diffEditor?.setModel({ original: meditor.createModel('', lang), - modified: meditor.createModel('', lang) + modified: meditor.createModel('', modifiedLang ?? lang) }) + if (original) { + setOriginal(original) + } + if (modified) { + setModified(modified) + } } export function setOriginal(code: string) { diff --git a/frontend/src/lib/components/Editor.svelte b/frontend/src/lib/components/Editor.svelte index ca15480fee..e079438d06 100644 --- a/frontend/src/lib/components/Editor.svelte +++ b/frontend/src/lib/components/Editor.svelte @@ -40,7 +40,6 @@ } from '$lib/editorUtils' import type { Disposable } from 'vscode' import type { DocumentUri, MessageTransports } from 'vscode-languageclient' - import { dirtyStore } from './common/confirmationModal/dirtyStore' import { buildWorkerDefinition } from './build_workers' import { workspaceStore } from '$lib/stores' import { UserService } from '$lib/gen' @@ -224,11 +223,12 @@ } } - export function format() { + export async function format() { if (editor) { code = getCode() if (lang != 'shell') { - editor?.getAction('editor.action.formatDocument')?.run() + await editor?.getAction('editor.action.formatDocument')?.run() + code = getCode() } if (formatAction) { formatAction() @@ -916,8 +916,6 @@ let timeoutModel: NodeJS.Timeout | undefined = undefined editor.onDidChangeModelContent((event) => { - $dirtyStore = true - timeoutModel && clearTimeout(timeoutModel) timeoutModel = setTimeout(() => { let ncode = getCode() diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 85fd94ccac..e7feb7cf84 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -19,7 +19,13 @@ userStore, workspaceStore } from '$lib/stores' - import { encodeState, formatCron, sleep } from '$lib/utils' + import { + cleanValueProperties, + encodeState, + formatCron, + orderedJsonStringify, + sleep + } from '$lib/utils' import { sendUserToast } from '$lib/toast' import type { Drawer } from '$lib/components/common' @@ -28,7 +34,6 @@ import { writable, type Writable } from 'svelte/store' import CenteredPage from './CenteredPage.svelte' import { Badge, Button, Kbd, UndoRedo } from './common' - import { dirtyStore } from './common/confirmationModal/dirtyStore' import FlowEditor from './flows/FlowEditor.svelte' import ScriptEditorDrawer from './flows/content/ScriptEditorDrawer.svelte' import type { FlowState } from './flows/flowState' @@ -39,7 +44,7 @@ import { loadFlowSchedule, type Schedule } from './flows/scheduleUtils' import type { FlowEditorContext } from './flows/types' import { cleanInputs, emptyFlowModuleState } from './flows/utils' - import { Pen } from 'lucide-svelte' + import { DiffIcon, Pen } from 'lucide-svelte' import { createEventDispatcher } from 'svelte' import Awareness from './Awareness.svelte' import { getAllModules } from './flows/flowExplorer' @@ -60,6 +65,8 @@ import FlowTutorials from './FlowTutorials.svelte' import { ignoredTutorials } from './tutorials/ignoredTutorials' + import type DiffDrawer from './DiffDrawer.svelte' + import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte' export let initialPath: string = '' export let newFlow: boolean @@ -68,6 +75,12 @@ export let loading = false export let flowStore: Writable export let flowStateStore: Writable + export let savedFlow: + | (Flow & { + draft?: Flow | undefined + }) + | undefined = undefined + export let diffDrawer: DiffDrawer | undefined = undefined const dispatch = createEventDispatcher() @@ -96,11 +109,16 @@ let loadingDraft = false async function saveDraft(): Promise { + const flow = cleanInputs($flowStore) + if (savedFlow) { + const draftOrDeployed = cleanValueProperties(savedFlow.draft || savedFlow) + const current = cleanValueProperties(flow) + if (orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(current)) { + return + } + } loadingDraft = true try { - const flow = cleanInputs($flowStore) - - $dirtyStore = false localStorage.removeItem('flow') localStorage.removeItem(`flow-${$pathStore}`) @@ -127,8 +145,12 @@ value: flow } }) + + savedFlow = await FlowService.getFlowByPathWithDraft({ + workspace: $workspaceStore!, + path: newFlow ? $pathStore : initialPath + }) if (newFlow) { - $dirtyStore = false dispatch('saveInitial', $pathStore) } sendUserToast('Saved as draft') @@ -154,7 +176,6 @@ // loadingSave = false // del // return const { cron, timezone, args, enabled } = $scheduleStore - $dirtyStore = false if (newFlow) { localStorage.removeItem('flow') localStorage.removeItem(`flow-${$pathStore}`) @@ -169,6 +190,10 @@ ws_error_handler_muted: flow.ws_error_handler_muted } }) + savedFlow = await FlowService.getFlowByPathWithDraft({ + workspace: $workspaceStore!, + path: $pathStore + }) if (enabled) { await createSchedule($pathStore) } @@ -187,6 +212,10 @@ ws_error_handler_muted: flow.ws_error_handler_muted } }) + savedFlow = await FlowService.getFlowByPathWithDraft({ + workspace: $workspaceStore!, + path: $pathStore + }) const scheduleExists = await ScheduleService.existsSchedule({ workspace: $workspaceStore ?? '', path: $pathStore @@ -219,7 +248,6 @@ } } loadingSave = false - $dirtyStore = false dispatch('deploy', $pathStore) } catch (err) { sendUserToast(`The flow could not be saved: ${err.body}`, true) @@ -879,6 +907,15 @@ + + {#key renderCount} {#if !$userStore?.operator} @@ -977,6 +1014,29 @@ renderCount += 1 }} /> + - import { DraftService, NewScript, Script, ScriptService, WorkerService } from '$lib/gen' + import { + DraftService, + NewScript, + Script, + ScriptService, + WorkerService, + type NewScriptWithDraft + } from '$lib/gen' import { goto } from '$app/navigation' import { page } from '$app/stores' import { inferArgs } from '$lib/infer' import { initialCode } from '$lib/script_helpers' import { enterpriseLicense, userStore, workerTags, workspaceStore } from '$lib/stores' - import { emptySchema, encodeState, getModifierKey } from '$lib/utils' + import { + cleanValueProperties, + emptySchema, + encodeState, + getModifierKey, + orderedJsonStringify + } from '$lib/utils' import Path from './Path.svelte' import ScriptEditor from './ScriptEditor.svelte' - import { dirtyStore } from './common/confirmationModal/dirtyStore' import { Alert, Badge, Button, Drawer, Kbd, SecondsInput, Tab, TabContent, Tabs } from './common' import { faSave } from '@fortawesome/free-solid-svg-icons' import LanguageIcon from './common/languageIcons/LanguageIcon.svelte' @@ -22,6 +34,7 @@ Bug, CheckCircle, Code, + DiffIcon, ExternalLink, Loader2, Pen, @@ -43,6 +56,8 @@ import ScriptSchema from './ScriptSchema.svelte' import Section from './Section.svelte' import Label from './Label.svelte' + import type DiffDrawer from './DiffDrawer.svelte' + import { cloneDeep } from 'lodash' export let script: NewScript export let initialPath: string = '' @@ -50,6 +65,8 @@ export let initialArgs: Record = {} export let lockedLanguage = false export let showMeta: boolean = false + export let diffDrawer: DiffDrawer | undefined = undefined + export let savedScript: NewScriptWithDraft | undefined = undefined let metadataOpen = showMeta || @@ -162,7 +179,6 @@ async function editScript(): Promise { loadingSave = true try { - $dirtyStore = false localStorage.removeItem(script.path) script.schema = script.schema ?? emptySchema() @@ -196,6 +212,7 @@ priority: script.priority } }) + savedScript = cloneDeep(script) as NewScriptWithDraft history.replaceState(history.state, '', `/scripts/edit/${script.path}`) goto(`/scripts/get/${newHash}?workspace=${$workspaceStore}`) } catch (error) { @@ -205,9 +222,15 @@ } async function saveDraft(): Promise { + if (savedScript) { + const draftOrDeployed = cleanValueProperties(savedScript['draft'] || savedScript) + const current = cleanValueProperties(script) + if (orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(current)) { + return + } + } loadingDraft = true try { - $dirtyStore = false localStorage.removeItem(script.path) script.schema = script.schema ?? emptySchema() @@ -250,8 +273,13 @@ value: script } }) + + savedScript = await ScriptService.getScriptByPathWithDraft({ + workspace: $workspaceStore!, + path: script.path + }) + if (initialPath == '') { - $dirtyStore = false goto(`/scripts/edit/${script.path}`) } sendUserToast('Saved as draft') @@ -264,23 +292,26 @@ loadingDraft = false } - function computeDropdownItems() { - let dropdownItems: { label: string; onClick: () => void }[] = [ - { - label: 'Fork', - onClick: () => { - window.open(`/scripts/add?template=${initialPath}`) - } - }, - { - label: 'Exit & See details', - onClick: () => { - goto(`/scripts/get/${initialPath}?workspace=${$workspaceStore}`) - } - } - ] + function computeDropdownItems(initialPath: string) { + let dropdownItems: { label: string; onClick: () => void }[] = + initialPath != '' + ? [ + { + label: 'Fork', + onClick: () => { + window.open(`/scripts/add?template=${initialPath}`) + } + }, + { + label: 'Exit & See details', + onClick: () => { + goto(`/scripts/get/${initialPath}?workspace=${$workspaceStore}`) + } + } + ] + : [] - return dropdownItems + return dropdownItems.length > 0 ? dropdownItems : undefined } function onKeyDown(event: KeyboardEvent) { @@ -301,11 +332,12 @@ - + {#if !$userStore?.operator} (metadataOpen = false)}> + Metadata Runtime @@ -781,6 +813,29 @@ {/if}
+ diff --git a/frontend/src/lib/components/Toast.svelte b/frontend/src/lib/components/Toast.svelte index 5f4f2aeadb..3b1382ccf6 100644 --- a/frontend/src/lib/components/Toast.svelte +++ b/frontend/src/lib/components/Toast.svelte @@ -10,6 +10,7 @@ export let error: boolean = false export let actions: ToastAction[] = [] export let errorMessage: string | undefined = undefined + export let duration = 5000 function handleClose() { toast.pop(toastId) @@ -19,7 +20,7 @@ onMount(() => { setTimeout(() => { toast.pop(toastId) - }, 5000) + }, duration) }) @@ -61,7 +62,7 @@
-
+
{#each actions as action, index (index)} + {/if}
diff --git a/frontend/src/lib/components/common/confirmationModal/dirtyStore.ts b/frontend/src/lib/components/common/confirmationModal/dirtyStore.ts deleted file mode 100644 index 0429a9df43..0000000000 --- a/frontend/src/lib/components/common/confirmationModal/dirtyStore.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { writable } from 'svelte/store' - -export const dirtyStore = writable(false) diff --git a/frontend/src/lib/components/common/table/ScriptRow.svelte b/frontend/src/lib/components/common/table/ScriptRow.svelte index dc5bbda9c5..5dfd6e6153 100644 --- a/frontend/src/lib/components/common/table/ScriptRow.svelte +++ b/frontend/src/lib/components/common/table/ScriptRow.svelte @@ -11,6 +11,7 @@ import { faArchive, faCalendarAlt, + faCode, faCodeFork, faCopy, faEdit, @@ -37,6 +38,7 @@ export let deploymentDrawer: DeployWorkspaceDrawer export let deleteConfirmedCallback: (() => void) | undefined export let errorHandlerMuted: boolean + export let showCode: (path: string, summary: string) => void export let depth: number = 0 let { @@ -151,6 +153,13 @@ let owner = isOwner(path, $userStore, $workspaceStore) if (draft_only) { return [ + { + displayName: 'View code', + icon: faCode, + action: () => { + showCode(script.path, script.summary) + } + }, { displayName: 'Delete', icon: faTrashAlt, @@ -169,6 +178,13 @@ ] } return [ + { + displayName: 'View code', + icon: faCode, + action: () => { + showCode(script.path, script.summary) + } + }, { displayName: 'Duplicate/Fork', icon: faCodeFork, diff --git a/frontend/src/lib/components/copilot/FlowCopilotStatus.svelte b/frontend/src/lib/components/copilot/FlowCopilotStatus.svelte index e5a1427dda..d807b1712e 100644 --- a/frontend/src/lib/components/copilot/FlowCopilotStatus.svelte +++ b/frontend/src/lib/components/copilot/FlowCopilotStatus.svelte @@ -11,7 +11,6 @@ import type { FlowModule } from '$lib/gen' import type { FlowEditorContext } from '../flows/types' import { ExternalLink } from 'lucide-svelte' - import { twMerge } from 'tailwind-merge' export let copilotLoading: boolean export let copilotStatus: string @@ -35,7 +34,7 @@ diff --git a/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte b/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte index c749346c0f..c19abf7b16 100644 --- a/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte +++ b/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte @@ -277,7 +277,6 @@ on:move={async ({ detail }) => { if (!$moving || $moving.module.id !== detail.module.id) { if (detail.module && detail.modules) { - console.log('MOVE+') $moving = { module: detail.module, modules: detail.modules } } } else { diff --git a/frontend/src/lib/components/home/Item.svelte b/frontend/src/lib/components/home/Item.svelte index c2d75a66e2..110cdf5f0b 100644 --- a/frontend/src/lib/components/home/Item.svelte +++ b/frontend/src/lib/components/home/Item.svelte @@ -19,6 +19,7 @@ let shareModal: ShareModal let moveDrawer: MoveDrawer let deploymentDrawer: DeployWorkspaceDrawer + export let showCode: (path: string, summary: string) => void {#key item.summary} @@ -39,6 +40,7 @@ {moveDrawer} {deploymentDrawer} {depth} + {showCode} /> {:else if item.type == 'flow'} + { + setTimeout(() => { + viewCodeTitle = undefined + script = undefined + }, 300) + }} +> + + {#if script} + + {:else} + + {/if} + + +
@@ -370,6 +403,7 @@ loadApps() loadRawApps() }} + {showCode} /> {/each} {:else} @@ -386,6 +420,7 @@ loadApps() loadRawApps() }} + {showCode} /> {/each} {/if} diff --git a/frontend/src/lib/components/home/TreeView.svelte b/frontend/src/lib/components/home/TreeView.svelte index 2b386de10a..ecf834511c 100644 --- a/frontend/src/lib/components/home/TreeView.svelte +++ b/frontend/src/lib/components/home/TreeView.svelte @@ -14,6 +14,7 @@ const isUser = (i: any): i is UserItem => i && 'username' in i let opened: boolean = true + export let showCode: (path: string, summary: string) => void {#if item} @@ -110,6 +111,15 @@ {/if}
{:else} - + {/if} {/if} diff --git a/frontend/src/lib/components/sidebar/WorkspaceMenu.svelte b/frontend/src/lib/components/sidebar/WorkspaceMenu.svelte index 246b398bd4..c977a89cd5 100644 --- a/frontend/src/lib/components/sidebar/WorkspaceMenu.svelte +++ b/frontend/src/lib/components/sidebar/WorkspaceMenu.svelte @@ -6,7 +6,6 @@ import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons' import { Icon } from 'svelte-awesome' import { goto } from '$app/navigation' - import { dirtyStore } from '../common/confirmationModal/dirtyStore' import { page } from '$app/stores' import { switchWorkspace } from '$lib/storeUtils' import MultiplayerMenu from './MultiplayerMenu.svelte' @@ -16,42 +15,26 @@ export let isCollapsed: boolean = false - function waitForNextUpdate(store) { - return new Promise((resolve) => { - let firstEmission = true - store.subscribe((value) => { - if (firstEmission) { - firstEmission = false - return - } - resolve(value) - }) - }) - } - async function toggleSwitchWorkspace(id: string) { if ($workspaceStore === id) { return } - const editPages = ['/scripts/edit/', '/flows/edit/', '/apps/edit/'] + const editPages = [ + '/scripts/edit/', + '/flows/edit/', + '/apps/edit/', + '/scripts/get/', + '/flows/get/', + '/apps/get/' + ] const isOnEditPage = editPages.some((editPage) => $page.route.id?.includes(editPage) ?? false) - // Check if we have unsaved changes - const wasDirty = $dirtyStore - // Try to go to the home page - - // If we weren't dirty, we can directly switch workspaces - if (!wasDirty && !isOnEditPage) { + if (!isOnEditPage) { switchWorkspace(id) } else { await goto('/') - - const isStillDirty = await waitForNextUpdate(dirtyStore) - - if (!isStillDirty) { - switchWorkspace(id) - } + switchWorkspace(id) } } diff --git a/frontend/src/lib/components/tutorials/app/AppTutorial.svelte b/frontend/src/lib/components/tutorials/app/AppTutorial.svelte index b3dc895902..4f7e5160b8 100644 --- a/frontend/src/lib/components/tutorials/app/AppTutorial.svelte +++ b/frontend/src/lib/components/tutorials/app/AppTutorial.svelte @@ -2,7 +2,6 @@ import { insertNewGridItem, appComponentFromType } from '$lib/components/apps/editor/appUtils' import type { AppComponent, TypedComponent } from '$lib/components/apps/editor/component' import type { AppViewerContext, AppEditorContext } from '$lib/components/apps/types' - import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore' import { push } from '$lib/history' import { getContext } from 'svelte' import Tutorial from '../Tutorial.svelte' @@ -31,8 +30,6 @@ function addComponent(appComponentType: TypedComponent['type']): void { push(history, $app) - $dirtyStore = true - const id = insertNewGridItem( $app, appComponentFromType(appComponentType) as (id: string) => AppComponent, diff --git a/frontend/src/lib/components/tutorials/app/ConnectionTutorial.svelte b/frontend/src/lib/components/tutorials/app/ConnectionTutorial.svelte index 15ec4acb8c..019e2b5335 100644 --- a/frontend/src/lib/components/tutorials/app/ConnectionTutorial.svelte +++ b/frontend/src/lib/components/tutorials/app/ConnectionTutorial.svelte @@ -2,7 +2,6 @@ import { insertNewGridItem, appComponentFromType } from '$lib/components/apps/editor/appUtils' import type { AppComponent } from '$lib/components/apps/editor/component' import type { AppViewerContext, AppEditorContext } from '$lib/components/apps/types' - import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore' import { push } from '$lib/history' import { getContext } from 'svelte' import Tutorial from '../Tutorial.svelte' @@ -24,8 +23,6 @@ function addComponent(): void { push(history, $app) - $dirtyStore = true - const id = insertNewGridItem( $app, appComponentFromType('textcomponent') as (id: string) => AppComponent, diff --git a/frontend/src/lib/toast.ts b/frontend/src/lib/toast.ts index d2ce0d33d3..6f37988d87 100644 --- a/frontend/src/lib/toast.ts +++ b/frontend/src/lib/toast.ts @@ -10,7 +10,8 @@ export function sendUserToast( message: string, error: boolean = false, actions: ToastAction[] = [], - errorMessage: string | undefined = undefined + errorMessage: string | undefined = undefined, + duration: number = 5000 ): void { toast.push({ component: { @@ -19,7 +20,8 @@ export function sendUserToast( message, error, actions, - errorMessage + errorMessage, + duration }, sendIdTo: 'toastId' }, diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 78e8c52a18..a7c50bb56f 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -10,6 +10,7 @@ import { deepEqual } from 'fast-equals' import type { UserExt } from './stores' import { sendUserToast } from './toast' +import type { Script } from './gen' export { sendUserToast } export function validateUsername(username: string): string { @@ -660,3 +661,33 @@ export function roughSizeOfObject(object: object | string) { } return bytes } + +export type Value = { + language?: Script.language + content?: string + path?: string + draft_only?: boolean + value?: any + draft?: Value + [key: string]: any +} + +export function cleanValueProperties(obj: Value) { + if (typeof obj !== 'object') { + return obj + } else { + let newObj: any = {} + for (const key of Object.keys(obj)) { + if (key !== 'draft' && key !== 'draft_only' && key !== 'parent_hash' && obj[key]) { + newObj[key] = obj[key] + } + } + return newObj + } +} + +export function orderedJsonStringify(obj: any, space?: string | number) { + const allKeys = new Set() + JSON.stringify(obj, (key, value) => (allKeys.add(key), value)) + return JSON.stringify(obj, (Array.from(allKeys) as string[]).sort(), space) +} diff --git a/frontend/src/routes/(root)/(logged)/apps/add/+page.svelte b/frontend/src/routes/(root)/(logged)/apps/add/+page.svelte index 27fc1b0a73..851c1a19e9 100644 --- a/frontend/src/routes/(root)/(logged)/apps/add/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps/add/+page.svelte @@ -5,7 +5,6 @@ import { AppService, Policy } from '$lib/gen' import { page } from '$app/stores' import { decodeState } from '$lib/utils' - import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore' import { userStore, workspaceStore } from '$lib/stores' import type { App } from '$lib/components/apps/types' import { goto } from '$app/navigation' @@ -105,8 +104,6 @@ value = decodeState(state) } } - - $dirtyStore = false {#if value} diff --git a/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte index ff95bf7ff4..74fbe899b1 100644 --- a/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps/edit/[...path]/+page.svelte @@ -1,15 +1,26 @@ + + {#key redraw} {#if app}
@@ -88,6 +185,8 @@ app={app.value} path={app.path} policy={app.policy} + bind:savedApp + {diffDrawer} />
{/if} diff --git a/frontend/src/routes/(root)/(logged)/flows/add/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/add/+page.svelte index c39d523201..037a4dab3a 100644 --- a/frontend/src/routes/(root)/(logged)/flows/add/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/add/+page.svelte @@ -1,8 +1,6 @@
- { diff --git a/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte index e6db75faaf..4b86ac03fd 100644 --- a/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/edit/[...path]/+page.svelte @@ -1,23 +1,35 @@
- - + { goto(`/flows/get/${e.detail}?workspace=${$workspaceStore}`) @@ -109,4 +198,6 @@ {selectedId} {initialArgs} {loading} + bind:savedFlow + {diffDrawer} /> diff --git a/frontend/src/routes/(root)/(logged)/scripts/add/+page.svelte b/frontend/src/routes/(root)/(logged)/scripts/add/+page.svelte index b8d846f8b8..a7dd46d9aa 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/add/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/add/+page.svelte @@ -6,7 +6,6 @@ import ScriptBuilder from '$lib/components/ScriptBuilder.svelte' import type { Schema } from '$lib/common' import { decodeState, emptySchema } from '$lib/utils' - import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore' // Default let schema: Schema = emptySchema() @@ -71,7 +70,6 @@ loadTemplate() } } - $dirtyStore = true - import { ScriptService, NewScript } from '$lib/gen' + import { ScriptService, NewScript, type NewScriptWithDraft, DraftService } from '$lib/gen' import { page } from '$app/stores' import { runFormStore, workspaceStore } from '$lib/stores' import ScriptBuilder from '$lib/components/ScriptBuilder.svelte' - import { decodeState } from '$lib/utils' + import { decodeState, cleanValueProperties, orderedJsonStringify } from '$lib/utils' import { goto } from '$app/navigation' import { sendUserToast } from '$lib/toast' import DiffDrawer from '$lib/components/DiffDrawer.svelte' + import { cloneDeep } from 'lodash' const initialState = $page.url.hash != '' ? $page.url.hash.slice(1) : undefined let initialArgs = {} @@ -27,71 +28,97 @@ let scriptBuilder: ScriptBuilder | undefined = undefined - let reloadAction: () => void = () => {} + let reloadAction: () => Promise = async () => {} + + let savedScript: NewScriptWithDraft | undefined = undefined async function loadScript(): Promise { if (scriptLoadedFromUrl != undefined && scriptLoadedFromUrl.path == $page.params.path) { script = scriptLoadedFromUrl - reloadAction = () => { + reloadAction = async () => { scriptLoadedFromUrl = undefined goto(`/scripts/edit/${script!.path}`) loadScript() } - sendUserToast('Script loaded from latest autosave stored in the URL', false, [ - { - label: 'Discard browser stored autosave and reload', - callback: reloadAction - }, - { - label: 'Show diff', - callback: async () => { - diffDrawer.openDrawer() - let remoteContent = await ScriptService.getScriptByPathWithDraft({ - workspace: $workspaceStore!, - path: script!.path - }) - diffDrawer.setDiff( - remoteContent?.draft?.content ?? remoteContent.content, - scriptLoadedFromUrl.content - ) - } + + async function compareAutosave() { + savedScript = await ScriptService.getScriptByPathWithDraft({ + workspace: $workspaceStore!, + path: script!.path + }) + + const draftOrDeployed = cleanValueProperties(savedScript?.draft || savedScript) + const urlScript = cleanValueProperties(scriptLoadedFromUrl) + if (orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(urlScript)) { + reloadAction() + } else { + sendUserToast('Script loaded from latest autosave stored in the URL', false, [ + { + label: 'Discard browser stored autosave and reload', + callback: reloadAction + }, + { + label: 'Show diff', + callback: async () => { + diffDrawer.openDrawer() + diffDrawer.setDiff({ + mode: 'simple', + original: draftOrDeployed, + current: urlScript, + title: `${savedScript?.draft ? 'Latest saved draft' : 'Deployed'} <> Autosave`, + button: { text: 'Discard autosave', onClick: reloadAction } + }) + } + } + ]) } - ]) + } + compareAutosave() } else { if (hash) { const scriptByHash = await ScriptService.getScriptByHash({ workspace: $workspaceStore!, hash }) + savedScript = cloneDeep(scriptByHash) as NewScriptWithDraft script = { ...scriptByHash, parent_hash: hash, lock: undefined } } else { const scriptWithDraft = await ScriptService.getScriptByPathWithDraft({ workspace: $workspaceStore!, path: $page.params.path }) + savedScript = cloneDeep(scriptWithDraft) if (scriptWithDraft.draft != undefined) { script = scriptWithDraft.draft if (!scriptWithDraft.draft_only) { - reloadAction = () => { + reloadAction = async () => { scriptLoadedFromUrl = undefined - hash = scriptWithDraft.hash + await DraftService.deleteDraft({ + workspace: $workspaceStore!, + kind: 'script', + path: script!.path + }) goto(`/scripts/edit/${script!.path}`) loadScript() } + const deployed = cleanValueProperties(scriptWithDraft) + const draft = cleanValueProperties(script) sendUserToast('Script loaded from latest saved draft', false, [ { - label: 'Ignore draft and load from latest deployed version', + label: 'Discard draft and load from latest deployed version', callback: reloadAction }, { label: 'Show diff', callback: async () => { diffDrawer.openDrawer() - let remoteContent = await ScriptService.getScriptByPath({ - workspace: $workspaceStore!, - path: script!.path + diffDrawer.setDiff({ + mode: 'simple', + original: deployed, + current: draft, + title: 'Deployed <> Draft', + button: { text: 'Discard draft', onClick: reloadAction } }) - diffDrawer.setDiff(remoteContent.content, script?.content ?? '') } } ]) @@ -128,9 +155,43 @@ } let diffDrawer: DiffDrawer + + async function restoreDraft() { + if (!savedScript) { + sendUserToast('Could not restore to draft', true) + return + } + diffDrawer.closeDrawer() + goto(`/scripts/edit/${savedScript.path}`) + loadScript() + } + + async function restoreDeployed() { + if (!savedScript) { + sendUserToast('Could not restore to deployed', true) + return + } + diffDrawer.closeDrawer() + if (savedScript['draft']) { + await DraftService.deleteDraft({ + workspace: $workspaceStore!, + kind: 'script', + path: savedScript.path + }) + } + goto(`/scripts/edit/${savedScript.path}`) + loadScript() + } - + {#if script} - + {/if}