feat: improve drafts and diffs (#2534)

* feat: improve drafts and diffs

* feat: add scripts diff button

* fix: remove diff drawer in scripts/add

* fix: nits

* feat: diffs for flows and apps w/ deep comparison

* fix: flow preview buttons height

* fix: use ordered json stringify

* fix: code view

* fix: temp flow diffs

* fix: flow diffs merge conflict
This commit is contained in:
HugoCasa
2023-11-04 08:45:03 +01:00
committed by GitHub
parent 543fae77a7
commit 3bfc2c81d2
31 changed files with 1140 additions and 256 deletions
@@ -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'
})
}
</script>
+236 -43
View File
@@ -1,63 +1,256 @@
<script lang="ts">
import * as Diff from 'diff'
import { Button, Drawer, DrawerContent } from './common'
import { Alert, Button, Drawer, DrawerContent } from './common'
import { Loader2 } from 'lucide-svelte'
import DiffEditor from './DiffEditor.svelte'
import { scriptLangToEditorLang } from '$lib/scripts'
import Tabs from './common/tabs/Tabs.svelte'
import Tab from './common/tabs/Tab.svelte'
import { cloneDeep } from 'lodash'
import { cleanValueProperties, orderedJsonStringify, type Value } from '$lib/utils'
import type { Script } from '$lib/gen'
type DiffData = {
lang?: string
content?: string
metadata: string
}
let contentType: 'content' | 'metadata' | undefined = undefined
let diffType: 'draft' | 'deployed' | 'custom' | undefined = undefined
let diffViewer: Drawer
let diffContent: string | undefined = undefined
export let button: { text: string; onClick: () => void } | undefined = undefined
export let restoreDeployed: () => Promise<void> = async () => {}
export let restoreDraft: () => Promise<void> = async () => {}
let data:
| {
mode: 'normal'
deployed: DiffData | undefined
draft: DiffData | undefined
current: DiffData
path?: string
button?: { text: string; onClick: () => void }
}
| {
mode: 'simple'
title: string
original: DiffData | undefined
current: DiffData
button?: { text: string; onClick: () => void }
}
| undefined = undefined
export function openDrawer() {
diffContent = undefined
data = undefined
contentType = undefined
diffType = undefined
diffViewer.openDrawer()
}
export function setDiff(local: string, remote: string) {
let finalString = ''
for (const part of Diff.diffLines(local, remote)) {
if (part.removed) {
// print red if removed without newline
finalString += `<span class="text-red-600">${part.value}</span>`
} else if (part.added) {
// print green if added
finalString += `<span class="text-green-600">${part.value}</span>`
} else {
let lines = part.value.split('\n')
if (lines.length > 12) {
lines = lines.slice(0, 6)
lines.push('...')
lines = lines.concat(part.value.split('\n').slice(-6))
}
// print white if unchanged
finalString += `${lines.join('\n')}`
}
}
diffContent = finalString
export function closeDrawer() {
diffViewer.closeDrawer()
}
function prepareDiff(data: Value) {
const metadata = cloneDeep(cleanValueProperties(data))
const content = metadata['content']
if (metadata['content'] !== undefined) {
metadata['content'] = 'check content diff'
}
return {
lang: data.language ? scriptLangToEditorLang(data.language as Script.language) : undefined,
content,
metadata: orderedJsonStringify(metadata, 2)
}
}
export function setDiff(
diff:
| {
mode: 'normal'
deployed: Value
draft: Value | undefined
current: Value
defaultDiffType?: 'deployed' | 'draft'
button?: { text: string; onClick: () => void }
}
| {
mode: 'simple'
original: Value
current: Value
title: string
button?: { text: string; onClick: () => void }
}
) {
if (diff.mode === 'normal') {
const { deployed, draft, current, defaultDiffType, button } = diff
data = {
mode: 'normal',
deployed: !deployed.draft_only ? prepareDiff(deployed) : undefined,
draft: draft ? prepareDiff(draft) : undefined,
current: prepareDiff(current),
path: draft?.path || deployed?.path,
button
}
if (defaultDiffType && data[defaultDiffType]) {
diffType = defaultDiffType
} else if (data.deployed) {
diffType = 'deployed'
} else if (data.draft) {
diffType = 'draft'
}
} else {
const { original, current, title, button } = diff
data = {
title,
mode: 'simple',
original: prepareDiff(original),
current: prepareDiff(current),
button
}
diffType = 'custom'
}
}
function updateContentType(data_: typeof data, diffType_: typeof diffType) {
if (!data_) return
if (!diffType_) return
const dataType = diffType_ === 'custom' ? 'original' : diffType_
contentType =
data_[dataType]?.content !== data_.current.content
? 'content'
: data_[dataType]?.metadata !== data_.current.metadata
? 'metadata'
: undefined
}
$: updateContentType(data, diffType)
</script>
<Drawer bind:this={diffViewer} size="800px">
<Drawer bind:this={diffViewer} size="1200px">
<DrawerContent title="Diff" on:close={diffViewer.closeDrawer}>
{#if diffContent == undefined}
<Loader2 class="animate-spin" />
{:else}
<pre class="border bg-surface-secondary p-2 whitespace-pre-wrap"
><code>{@html diffContent}</code></pre
>
<div class="flex flex-row-reverse gap-2">
<div class="text-red-600">Removed</div>
<div class="text-green-600">Added</div></div
>
{/if}
<div class="flex flex-col gap-4 h-full">
{#if diffType && data}
<Tabs bind:selected={diffType} wrapperClass="shrink-0">
{#if data.mode === 'simple'}
<Tab value="custom">{data.title}</Tab>
{:else}
<Tab value="deployed" disabled={!data.deployed}
>{'Deployed <> Current'}{!data.deployed ? ' (no deployed version)' : ''}</Tab
>
<Tab value="draft" disabled={!data.draft}
>{'Latest saved draft <> Current'}{!data.draft ? ' (no draft)' : ''}</Tab
>
{/if}
</Tabs>
{/if}
{#if data?.mode === 'normal'}
{#if diffType === 'draft'}
<Button
size="xs"
color="light"
variant="border"
wrapperClasses="self-start"
on:click={restoreDraft}
disabled={orderedJsonStringify(data.draft) === orderedJsonStringify(data.current)}
>Restore to latest saved draft</Button
>
{:else if diffType === 'deployed'}
<Button
size="xs"
color="light"
variant="border"
wrapperClasses="self-start"
on:click={restoreDeployed}
disabled={!data.draft &&
orderedJsonStringify(data.deployed) === orderedJsonStringify(data.current)}
>
Restore to deployed{data.draft ? ' and discard draft' : ''}
</Button>
{/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}
<div class="flex flex-col h-full gap-4">
{#if data.current.content !== undefined}
<Tabs bind:selected={contentType}>
<Tab value="content" disabled={content === data.current.content}
>Content{content === data.current.content ? ' (no changes)' : ''}</Tab
>
<Tab value="metadata" disabled={metadata === data.current.metadata}
>Metadata{metadata === data.current.metadata ? ' (no changes)' : ''}</Tab
>
</Tabs>
{/if}
<div class="flex-1">
{#key diffType}
{#if contentType === 'content'}
<DiffEditor
automaticLayout
class="h-full"
defaultLang={lang}
defaultModifiedLang={data.current.lang}
defaultOriginal={content}
defaultModified={data.current.content}
readOnly
/>
{:else if contentType === 'metadata'}
<DiffEditor
automaticLayout
class="h-full"
defaultLang="json"
defaultOriginal={metadata}
defaultModified={data.current.metadata}
readOnly
/>
{/if}
{/key}
</div>
</div>
{:else}
<Alert title="No changes detected">
{#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}
</Alert>
{/if}
{:else}
<Loader2 class="animate-spin" />
{/if}
</div>
<svelte:fragment slot="actions">
{#if button}
{#if data?.button}
<Button
color="light"
on:click={() => {
button?.onClick()
diffViewer.closeDrawer()
}}>{button.text}</Button
if (data?.button) {
data.button.onClick()
diffViewer.closeDrawer()
}
}}>{data.button.text}</Button
>
{/if}
</svelte:fragment>
+24 -2
View File
@@ -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) {
+3 -5
View File
@@ -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()
+69 -9
View File
@@ -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<OpenFlow>
export let flowStateStore: Writable<FlowState>
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<void> {
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 @@
<svelte:window on:keydown={onKeyDown} />
<UnsavedConfirmationModal
{diffDrawer}
savedValue={savedFlow}
modifiedValue={{
...$flowStore,
path: $pathStore
}}
/>
{#key renderCount}
{#if !$userStore?.operator}
<FlowCopilotDrawer {getHubCompletions} {genFlow} bind:flowCopilotMode />
@@ -977,6 +1014,29 @@
renderCount += 1
}}
/>
<Button
color="light"
variant="border"
size="xs"
on:click={() => {
if (!savedFlow) {
return
}
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: savedFlow,
draft: savedFlow['draft'],
current: $flowStore
})
}}
disabled={!savedFlow}
>
<div class="flex flex-row gap-2 items-center">
<DiffIcon size={14} />
Diff
</div>
</Button>
<FlowCopilotStatus
{copilotLoading}
@@ -1,14 +1,26 @@
<script lang="ts">
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<string, any> = {}
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<void> {
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<void> {
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 @@
</script>
<svelte:window on:keydown={onKeyDown} />
<UnsavedConfirmationModal />
<UnsavedConfirmationModal {diffDrawer} savedValue={savedScript} modifiedValue={script} />
{#if !$userStore?.operator}
<Drawer placement="right" bind:open={metadataOpen} size="800px">
<DrawerContent noPadding title="Settings" on:close={() => (metadataOpen = false)}>
<!-- svelte-ignore a11y-autofocus -->
<Tabs bind:selected={selectedTab}>
<Tab value="metadata">Metadata</Tab>
<Tab value="runtime">Runtime</Tab>
@@ -781,6 +813,29 @@
{/if}
<div class="flex flex-row gap-x-1 lg:gap-x-2">
<Button
color="light"
variant="border"
size="xs"
on:click={() => {
if (!savedScript) {
return
}
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: savedScript,
draft: savedScript['draft'],
current: script
})
}}
disabled={!savedScript || !diffDrawer}
>
<div class="flex flex-row gap-2 items-center">
<DiffIcon size={14} />
Diff
</div>
</Button>
<Button
color="light"
variant="border"
@@ -810,7 +865,7 @@
size="xs"
startIcon={{ icon: faSave }}
on:click={() => editScript()}
dropdownItems={initialPath != '' ? computeDropdownItems : undefined}
dropdownItems={computeDropdownItems(initialPath)}
>
Deploy
</Button>
+3 -2
View File
@@ -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)
})
</script>
@@ -61,7 +62,7 @@
</button>
</div>
</div>
<div class="mt-2 flex flex-col gap-2 h-15">
<div class="mt-2 flex flex-col gap-2 h-15 items-center">
{#each actions as action, index (index)}
<Button
on:click={() => {
@@ -54,6 +54,7 @@
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
import { getTheme } from './componentsPanel/themeUtils'
import StylePanel from './settingsPanel/StylePanel.svelte'
import type DiffDrawer from '$lib/components/DiffDrawer.svelte'
export let app: App
export let path: string
@@ -61,6 +62,17 @@
export let summary: string
export let fromHub: boolean = false
export let versions: number[]
export let diffDrawer: DiffDrawer | undefined = undefined
export let savedApp:
| {
value: App
draft?: any
path: string
summary: string
policy: any
draft_only?: boolean
}
| undefined = undefined
migrateApp(app)
@@ -464,7 +476,15 @@
{#if !$userStore?.operator}
{#if $appStore}
<AppEditorHeader on:restore {versions} {policy} {fromHub} bind:this={appEditorHeader} />
<AppEditorHeader
on:restore
{versions}
{policy}
{fromHub}
bind:this={appEditorHeader}
{diffDrawer}
bind:savedApp
/>
{#if $mode === 'preview'}
<SplitPanesWrapper>
<div
@@ -12,7 +12,6 @@
UndoRedo
} from '$lib/components/common'
import Button from '$lib/components/common/button/Button.svelte'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
import DisplayResult from '$lib/components/DisplayResult.svelte'
import FlowProgressBar from '$lib/components/flows/FlowProgressBar.svelte'
import FlowStatusViewer from '$lib/components/FlowStatusViewer.svelte'
@@ -29,6 +28,7 @@
AlignHorizontalSpaceAround,
BellOff,
Bug,
DiffIcon,
Expand,
FileJson,
FileUp,
@@ -43,7 +43,13 @@
import { getContext } from 'svelte'
import { Icon } from 'svelte-awesome'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { classNames, copyToClipboard, truncateRev } from '../../../utils'
import {
classNames,
cleanValueProperties,
copyToClipboard,
orderedJsonStringify,
truncateRev
} from '../../../utils'
import type {
AppInput,
ConnectedAppInput,
@@ -52,7 +58,7 @@
StaticAppInput,
UserAppInput
} from '../inputType'
import type { AppEditorContext, AppViewerContext } from '../types'
import type { App, AppEditorContext, AppViewerContext } from '../types'
import { BG_PREFIX, allItems, toStatic } from '../utils'
import AppExportButton from './AppExportButton.svelte'
import AppInputs from './AppInputs.svelte'
@@ -73,6 +79,7 @@
import { MenuItem } from '@rgossiaux/svelte-headlessui'
import AppEditorTutorial from './AppEditorTutorial.svelte'
import AppTimeline from './AppTimeline.svelte'
import type DiffDrawer from '$lib/components/DiffDrawer.svelte'
async function hash(message) {
try {
@@ -94,6 +101,17 @@
export let policy: Policy
export let fromHub: boolean = false
export let versions: number[]
export let diffDrawer: DiffDrawer | undefined = undefined
export let savedApp:
| {
value: App
draft?: any
path: string
summary: string
policy: any
draft_only?: boolean
}
| undefined = undefined
const {
app,
@@ -207,7 +225,25 @@
policy
}
})
$dirtyStore = false
const app_w_draft = await AppService.getAppByPathWithDraft({
workspace: $workspaceStore!,
path
})
savedApp = {
summary: app_w_draft.summary,
value: app_w_draft.value,
path: app_w_draft.path,
policy: app_w_draft.policy,
draft_only: app_w_draft.draft_only,
draft: app_w_draft.draft
? {
summary: app_w_draft.summary,
value: app_w_draft.draft,
path: app_w_draft.path,
policy: app_w_draft.policy
}
: undefined
}
closeSaveDrawer()
sendUserToast('App deployed successfully')
localStorage.removeItem(`app-${path}`)
@@ -229,8 +265,26 @@
path: npath
}
})
const app_w_draft = await AppService.getAppByPathWithDraft({
workspace: $workspaceStore!,
path: npath
})
savedApp = {
summary: app_w_draft.summary,
value: app_w_draft.value,
path: app_w_draft.path,
policy: app_w_draft.policy,
draft_only: app_w_draft.draft_only,
draft: app_w_draft.draft
? {
summary: app_w_draft.summary,
value: app_w_draft.draft,
path: app_w_draft.path,
policy: app_w_draft.policy
}
: undefined
}
$dirtyStore = false
closeSaveDrawer()
sendUserToast('App deployed successfully')
if (appPath !== npath) {
@@ -268,12 +322,23 @@
$secondaryMenuLeftStore.isOpen = false
$secondaryMenuRightStore.isOpen = false
$dirtyStore = false
saveDrawerOpen = true
return
}
async function saveInitialDraft() {
if (savedApp) {
const draftOrDeployed = cleanValueProperties(savedApp.draft || savedApp)
const current = cleanValueProperties({
summary: $summary,
value: $app,
path: newPath || savedApp.draft?.path || savedApp.path,
policy
})
if (orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(current)) {
return
}
}
await computeTriggerables()
try {
await AppService.createApp({
@@ -294,8 +359,27 @@
value: $app!
}
})
const app_w_draft = await AppService.getAppByPathWithDraft({
workspace: $workspaceStore!,
path: newPath
})
savedApp = {
summary: app_w_draft.summary,
value: app_w_draft.value,
path: app_w_draft.path,
policy: app_w_draft.policy,
draft_only: app_w_draft.draft_only,
draft: app_w_draft.draft
? {
summary: app_w_draft.summary,
value: app_w_draft.draft,
path: app_w_draft.path,
policy: app_w_draft.policy
}
: undefined
}
draftDrawerOpen = false
$dirtyStore = false
goto(`/apps/edit/${newPath}`)
} catch (e) {
sendUserToast('Error saving initial draft', e)
@@ -304,7 +388,18 @@
}
async function saveDraft() {
$dirtyStore = false
if (savedApp) {
const draftOrDeployed = cleanValueProperties(savedApp.draft || savedApp)
const current = cleanValueProperties({
summary: $summary,
value: $app,
path: newPath || savedApp.draft?.path || savedApp.path,
policy
})
if (orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(current)) {
return
}
}
if ($page.params.path == undefined) {
draftDrawerOpen = true
return
@@ -312,7 +407,6 @@
loading.saveDraft = true
try {
await computeTriggerables()
$dirtyStore = false
let path = $page.params.path
await DraftService.createDraft({
workspace: $workspaceStore!,
@@ -322,6 +416,26 @@
value: $app!
}
})
const app_w_draft = await AppService.getAppByPathWithDraft({
workspace: $workspaceStore!,
path
})
savedApp = {
summary: app_w_draft.summary,
value: app_w_draft.value,
path: app_w_draft.path,
policy: app_w_draft.policy,
draft_only: app_w_draft.draft_only,
draft: app_w_draft.draft
? {
summary: app_w_draft.summary,
value: app_w_draft.draft,
path: app_w_draft.path,
policy: app_w_draft.policy
}
: undefined
}
sendUserToast('Draft saved')
localStorage.removeItem(`app-${path}`)
loading.saveDraft = false
@@ -439,7 +553,33 @@
action: () => {
inputsDrawerOpen = true
}
}
},
...(savedApp
? [
{
displayName: 'Diff',
icon: DiffIcon,
action: () => {
if (!savedApp) {
return
}
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: savedApp,
draft: savedApp.draft,
current: {
summary: $summary,
value: $app,
path: newPath || savedApp.draft?.path || savedApp.path,
policy
}
})
}
}
]
: [])
]
let appEditorTutorial: AppEditorTutorial | undefined = undefined
@@ -455,7 +595,16 @@
<TestJobLoader bind:this={testJobLoader} bind:isLoading={testIsLoading} bind:job />
<UnsavedConfirmationModal />
<UnsavedConfirmationModal
{diffDrawer}
savedValue={savedApp}
modifiedValue={{
summary: $summary,
value: $app,
path: newPath || savedApp?.draft?.path || savedApp?.path,
policy
}}
/>
{#if appPath == ''}
<Drawer bind:open={draftDrawerOpen} size="800px">
@@ -465,6 +614,7 @@
</Alert>
<h3>Summary</h3>
<div class="w-full pt-2">
<!-- svelte-ignore a11y-autofocus -->
<input
autofocus
type="text"
@@ -513,6 +663,7 @@
<DrawerContent title="Deploy" on:close={() => closeSaveDrawer()}>
<span class="text-secondary text-sm font-bold">Summary</span>
<div class="w-full pt-2">
<!-- svelte-ignore a11y-autofocus -->
<input
autofocus
type="text"
@@ -545,7 +696,45 @@
autofocus={false}
/>
<div slot="actions">
<div slot="actions" class="flex flex-row gap-4">
<Button
variant="border"
color="light"
disabled={!savedApp || savedApp.draft_only}
on:click={() => {
if (!savedApp) {
return
}
saveDrawerOpen = false
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: savedApp,
draft: savedApp.draft,
current: {
summary: $summary,
value: $app,
path: newPath || savedApp.draft?.path || savedApp.path,
policy
},
button: {
text: 'Looks good, deploy',
onClick: () => {
if (appPath == '') {
createApp(newPath)
} else {
updateApp(newPath)
}
}
}
})
}}
>
<div class="flex flex-row gap-2 items-center">
<DiffIcon size={14} />
Diff
</div>
</Button>
<Button
startIcon={{ icon: faSave }}
disabled={pathError != ''}
@@ -1,7 +1,6 @@
<script lang="ts">
import type { AppEditorContext, AppViewerContext } from '../../types'
import { getContext, onMount, tick } from 'svelte'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
import {
components as componentsRecord,
presets as presetsRecord,
@@ -35,8 +34,6 @@
function addComponent(appComponentType: TypedComponent['type']): string {
push(history, $app)
$dirtyStore = true
const id = insertNewGridItem(
$app,
appComponentFromType(appComponentType) as (id: string) => AppComponent,
@@ -56,8 +53,6 @@
push(history, $app)
$dirtyStore = true
const id = copyComponent($app, res.value.item, $focusedGrid, res.value.subgrids, [])
if (id) {
@@ -71,8 +66,6 @@
push(history, $app)
$dirtyStore = true
const id = insertNewGridItem(
$app,
appComponentFromType(preset.targetComponent, preset.configuration) as (
@@ -57,6 +57,7 @@
const handleHide = () => (hidden = !hidden)
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
on:click
on:keydown
@@ -1,32 +1,44 @@
<script lang="ts">
import ConfirmationModal from './ConfirmationModal.svelte'
import { beforeNavigate, goto } from '$app/navigation'
import { onDestroy } from 'svelte'
import { dirtyStore } from './dirtyStore'
import Button from '../button/Button.svelte'
import type DiffDrawer from '$lib/components/DiffDrawer.svelte'
import { cleanValueProperties, orderedJsonStringify, type Value } from '$lib/utils'
import { tick } from 'svelte'
let navigationState: { from: URL | undefined; to: URL | null; cancel: () => void } | undefined =
undefined
$: open = Boolean(navigationState)
export let savedValue: Value | undefined = undefined
export let modifiedValue: Value | undefined = undefined
export let diffDrawer: DiffDrawer | undefined = undefined
beforeNavigate((newNavigationState) => {
let bypassBeforeNavigate = false
let open = false
let goingTo: URL | undefined = undefined
beforeNavigate(async (newNavigationState) => {
if (
!navigationState &&
$dirtyStore &&
!bypassBeforeNavigate &&
newNavigationState.to &&
newNavigationState.to.url.pathname !== newNavigationState.from?.url.pathname
) {
navigationState = {
to: newNavigationState.to.url,
from: newNavigationState.from?.url,
cancel: newNavigationState.cancel
}
goingTo = newNavigationState.to.url
newNavigationState.cancel()
await tick() // make sure saved value is updated when clicking on save draft or deploy
if (savedValue && modifiedValue) {
const draftOrDeployed = cleanValueProperties(savedValue.draft || savedValue)
const current = cleanValueProperties(modifiedValue)
if (orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(current)) {
bypassBeforeNavigate = true
goto(goingTo)
} else {
open = true
}
} else {
open = true
}
} else if (bypassBeforeNavigate) {
bypassBeforeNavigate = false
}
})
onDestroy(() => {
$dirtyStore = false
})
</script>
<ConfirmationModal
@@ -34,20 +46,46 @@
title="Unsaved changes detected"
confirmationText="Discard changes"
on:canceled={() => {
if (navigationState) {
navigationState.cancel()
}
navigationState = undefined
open = false
}}
on:confirmed={() => {
if (navigationState?.to) {
goto(navigationState.to)
if (goingTo) {
bypassBeforeNavigate = true
goto(goingTo)
}
$dirtyStore = false
navigationState = undefined
}}
>
<div class="flex flex-col w-full space-y-4">
<span>Are you sure you want to discard the changes you have made? </span>
{#if savedValue && modifiedValue}
<Button
wrapperClasses="self-start"
color="light"
on:click={() => {
if (!savedValue || !modifiedValue) {
return
}
open = false
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: savedValue,
draft: savedValue.draft,
current: modifiedValue,
defaultDiffType: 'draft',
button: {
text: 'Leave anyway',
onClick: () => {
if (goingTo) {
bypassBeforeNavigate = true
goto(goingTo)
}
}
}
})
}}
>Show diff
</Button>
{/if}
</div>
</ConfirmationModal>
@@ -1,3 +0,0 @@
import { writable } from 'svelte/store'
export const dirtyStore = writable<boolean>(false)
@@ -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,
@@ -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 @@
<ManualPopover bind:this={copilotPopover}>
<Button
size="xs"
btnClasses={twMerge('mr-2', $currentStepStore !== undefined ? 'z-[901]' : '')}
btnClasses={$currentStepStore !== undefined ? 'z-[901]' : ''}
on:click={() => {
if (copilotLoading || ($currentStepStore !== undefined && $currentStepStore !== 'Input')) {
abortController?.abort()
@@ -116,7 +115,6 @@
<svelte:fragment slot="button">
<Button
size="xs"
btnClasses="mr-2"
startIcon={{
icon: faMagicWandSparkles
}}
@@ -52,7 +52,7 @@
startIcon={{ icon: faPlay }}
>
Test up to&nbsp;
<Badge baseClass="ml-1" small color="indigo">
<Badge baseClass="ml-1" small color="indigo" wrapperClass="max-h-[15px]">
{$selectedId}
</Badge>
</Button>
@@ -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 {
@@ -19,6 +19,7 @@
let shareModal: ShareModal
let moveDrawer: MoveDrawer
let deploymentDrawer: DeployWorkspaceDrawer
export let showCode: (path: string, summary: string) => void
</script>
{#key item.summary}
@@ -39,6 +40,7 @@
{moveDrawer}
{deploymentDrawer}
{depth}
{showCode}
/>
{:else if item.type == 'flow'}
<FlowRow
@@ -32,6 +32,9 @@
import { page } from '$app/stores'
import { setQuery } from '$lib/navigation'
import ContentSearch from '../ContentSearch.svelte'
import Drawer from '../common/drawer/Drawer.svelte'
import HighlightCode from '../HighlightCode.svelte'
import DrawerContent from '../common/drawer/DrawerContent.svelte'
import { groupItems } from './treeViewUtils'
import Item from './Item.svelte'
@@ -246,6 +249,18 @@
let treeView = false
let contentSearch: ContentSearch
let viewCodeDrawer: Drawer
let viewCodeTitle: string | undefined
let script: Script | undefined
async function showCode(path: string, summary: string) {
viewCodeTitle = summary || path
await viewCodeDrawer.openDrawer()
script = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
path
})
}
</script>
<SearchItems
@@ -256,6 +271,24 @@
{opts}
/>
<Drawer
bind:this={viewCodeDrawer}
on:close={() => {
setTimeout(() => {
viewCodeTitle = undefined
script = undefined
}, 300)
}}
>
<DrawerContent title={viewCodeTitle} on:close={viewCodeDrawer.closeDrawer}>
{#if script}
<HighlightCode language={script?.language} code={script?.content} />
{:else}
<Skeleton layout={[[40]]} />
{/if}
</DrawerContent>
</Drawer>
<ContentSearch bind:this={contentSearch} />
<CenteredPage>
<div class="flex flex-wrap gap-2 items-center justify-between w-full mt-2">
@@ -370,6 +403,7 @@
loadApps()
loadRawApps()
}}
{showCode}
/>
{/each}
{:else}
@@ -386,6 +420,7 @@
loadApps()
loadRawApps()
}}
{showCode}
/>
{/each}
{/if}
@@ -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
</script>
{#if item}
@@ -110,6 +111,15 @@
{/if}
</div>
{:else}
<Item {item} on:scriptChanged on:flowChanged on:appChanged on:rawAppChanged on:reload {depth} />
<Item
{item}
on:scriptChanged
on:flowChanged
on:appChanged
on:rawAppChanged
on:reload
{depth}
{showCode}
/>
{/if}
{/if}
@@ -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)
}
}
</script>
@@ -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,
@@ -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,
+4 -2
View File
@@ -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'
},
+31
View File
@@ -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)
}
@@ -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
</script>
{#if value}
@@ -1,15 +1,26 @@
<script lang="ts">
import AppEditor from '$lib/components/apps/editor/AppEditor.svelte'
import { AppService, AppWithLastVersion } from '$lib/gen'
import { AppService, AppWithLastVersion, DraftService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { page } from '$app/stores'
import { decodeState } from '$lib/utils'
import { cleanValueProperties, decodeState } from '$lib/utils'
import { goto } from '$app/navigation'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
import { sendUserToast, type ToastAction } from '$lib/toast'
import DiffDrawer from '$lib/components/DiffDrawer.svelte'
import type { App } from '$lib/components/apps/types'
import { cloneDeep } from 'lodash'
let app = undefined as (AppWithLastVersion & { draft_only?: boolean }) | undefined
let savedApp:
| {
value: App
draft?: any
path: string
summary: string
policy: any
draft_only?: boolean
}
| undefined = undefined
let redraw = 0
let path = $page.params.path
@@ -27,16 +38,52 @@
path,
workspace: $workspaceStore!
})
const app_w_draft_ = cloneDeep(app_w_draft)
savedApp = {
summary: app_w_draft_.summary,
value: app_w_draft_.value,
path: app_w_draft_.path,
policy: app_w_draft_.policy,
draft_only: app_w_draft_.draft_only,
draft: app_w_draft_.draft
? {
summary: app_w_draft_.summary,
value: app_w_draft_.draft,
path: app_w_draft_.path,
policy: app_w_draft_.policy
}
: undefined
}
if (stateLoadedFromUrl) {
const reloadAction = async () => {
stateLoadedFromUrl = undefined
await loadApp()
redraw++
}
const actions: ToastAction[] = []
if (stateLoadedFromUrl) {
actions.push({
label: 'Discard URL stored autosave and reload',
callback: reloadAction
})
const draftOrDeployed = cleanValueProperties(savedApp.draft || savedApp)
const urlScript = {
...draftOrDeployed,
value: stateLoadedFromUrl
}
actions.push({
label: 'Show diff',
callback: async () => {
stateLoadedFromUrl = undefined
await loadApp()
redraw++
diffDrawer.openDrawer()
diffDrawer.setDiff({
mode: 'simple',
original: draftOrDeployed,
current: urlScript,
title: `${savedApp?.draft ? 'Latest saved draft' : 'Deployed'} <> Autosave`,
button: { text: 'Discard autosave', onClick: reloadAction }
})
}
})
}
@@ -50,13 +97,33 @@
value: app_w_draft.draft
}
if (!app_w_draft.draft_only) {
sendUserToast('flow loaded from latest saved draft', false, [
const reloadAction = () => {
stateLoadedFromUrl = undefined
app = app_w_draft
redraw++
}
const deployed = cleanValueProperties(app_w_draft)
const draft = {
...deployed,
value: app.value
}
sendUserToast('app loaded from latest saved draft', false, [
{
label: 'Ignore draft and load from latest deployed version',
callback: () => {
stateLoadedFromUrl = undefined
app = app_w_draft
redraw++
label: 'Discard draft and load from latest deployed version',
callback: reloadAction
},
{
label: 'Show diff',
callback: async () => {
diffDrawer.openDrawer()
diffDrawer.setDiff({
mode: 'simple',
original: deployed,
current: draft,
title: 'Deployed <> Draft',
button: { text: 'Discard draft', onClick: reloadAction }
})
}
}
])
@@ -64,7 +131,6 @@
} else {
app = app_w_draft
}
$dirtyStore = false
}
$: {
@@ -72,8 +138,39 @@
loadApp()
}
}
async function restoreDraft() {
if (!savedApp) {
sendUserToast('Could not restore to draft', true)
return
}
diffDrawer.closeDrawer()
await loadApp()
redraw++
}
async function restoreDeployed() {
if (!savedApp) {
sendUserToast('Could not restore to deployed', true)
return
}
diffDrawer.closeDrawer()
if (savedApp.draft) {
await DraftService.deleteDraft({
workspace: $workspaceStore!,
kind: 'app',
path: savedApp.path
})
}
await loadApp()
redraw++
}
let diffDrawer: DiffDrawer
</script>
<DiffDrawer bind:this={diffDrawer} {restoreDeployed} {restoreDraft} />
{#key redraw}
{#if app}
<div class="h-screen">
@@ -88,6 +185,8 @@
app={app.value}
path={app.path}
policy={app.policy}
bind:savedApp
{diffDrawer}
/>
</div>
{/if}
@@ -1,8 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte'
import FlowBuilder from '$lib/components/FlowBuilder.svelte'
import type { FlowState } from '$lib/components/flows/flowState'
@@ -113,14 +111,11 @@
loadFlow()
$dirtyStore = true
let getSelectedId: (() => string) | undefined = undefined
let flowBuilder: FlowBuilder | undefined = undefined
</script>
<div id="monaco-widgets-root" class="monaco-editor" style="z-index: 1200;" />
<UnsavedConfirmationModal />
<FlowBuilder
on:saveInitial={(e) => {
@@ -1,23 +1,35 @@
<script lang="ts">
import { FlowService, type Flow } from '$lib/gen'
import { FlowService, type Flow, DraftService } from '$lib/gen'
import { page } from '$app/stores'
import FlowBuilder from '$lib/components/FlowBuilder.svelte'
import { workspaceStore } from '$lib/stores'
import { decodeArgs, decodeState, emptySchema } from '$lib/utils'
import {
cleanValueProperties,
decodeArgs,
decodeState,
emptySchema,
orderedJsonStringify
} from '$lib/utils'
import { initFlow } from '$lib/components/flows/flowStore'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
import { goto } from '$app/navigation'
import { writable } from 'svelte/store'
import type { FlowState } from '$lib/components/flows/flowState'
import { sendUserToast } from '$lib/toast'
import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte'
import DiffDrawer from '$lib/components/DiffDrawer.svelte'
import { cloneDeep } from 'lodash'
let nodraft = $page.url.searchParams.get('nodraft')
const initialState = nodraft ? undefined : localStorage.getItem(`flow-${$page.params.path}`)
let stateLoadedFromUrl = initialState != undefined ? decodeState(initialState) : undefined
const initialArgs = decodeArgs($page.url.searchParams.get('args') ?? undefined)
let savedFlow:
| (Flow & {
draft?: Flow | undefined
})
| undefined = undefined
if (nodraft) {
goto('?', { replaceState: true })
}
@@ -43,32 +55,82 @@
loading = true
let flow: Flow
if (stateLoadedFromUrl != undefined && stateLoadedFromUrl?.flow?.path == $page.params.path) {
sendUserToast('Flow loaded from browser storage', false, [
{
label: 'Discard browser stored autosave and reload',
callback: () => {
stateLoadedFromUrl = undefined
goto(`/flows/edit/${flow!.path}`)
loadFlow()
}
}
])
savedFlow = await FlowService.getFlowByPathWithDraft({
workspace: $workspaceStore!,
path: stateLoadedFromUrl.flow.path
})
const draftOrDeployed = cleanValueProperties(savedFlow?.draft || savedFlow)
const urlScript = cleanValueProperties(stateLoadedFromUrl.flow)
flow = stateLoadedFromUrl.flow
const reloadAction = () => {
stateLoadedFromUrl = undefined
goto(`/flows/edit/${flow!.path}`)
loadFlow()
}
if (orderedJsonStringify(draftOrDeployed) === orderedJsonStringify(urlScript)) {
reloadAction()
} else {
sendUserToast('Flow loaded from browser storage', 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: `${savedFlow?.draft ? 'Latest saved draft' : 'Deployed'} <> Autosave`,
button: { text: 'Discard autosave', onClick: reloadAction }
})
}
}
])
}
} else {
const flowWithDraft = await FlowService.getFlowByPathWithDraft({
workspace: $workspaceStore!,
path: $page.params.path
})
console.log(flowWithDraft)
savedFlow = cloneDeep(flowWithDraft)
if (flowWithDraft.draft != undefined && !nobackenddraft) {
flow = flowWithDraft.draft
if (!flowWithDraft.draft_only) {
const deployed = cleanValueProperties(flowWithDraft)
const draft = cleanValueProperties(flow)
const reloadAction = async () => {
stateLoadedFromUrl = undefined
await DraftService.deleteDraft({
workspace: $workspaceStore!,
kind: 'flow',
path: flow.path
})
nobackenddraft = true
loadFlow()
}
sendUserToast('flow loaded from latest saved draft', false, [
{
label: 'Ignore draft and load from latest deployed version',
callback: () => {
stateLoadedFromUrl = undefined
nobackenddraft = true
loadFlow()
label: 'Discard draft and load from latest deployed version',
callback: reloadAction
},
{
label: 'Show diff',
callback: async () => {
diffDrawer.openDrawer()
diffDrawer.setDiff({
mode: 'simple',
original: deployed,
current: draft,
title: 'Deployed <> Draft',
button: { text: 'Discard draft', onClick: reloadAction }
})
}
}
])
@@ -81,7 +143,6 @@
await initFlow(flow, flowStore, flowStateStore)
loading = false
selectedId = stateLoadedFromUrl?.selectedId ?? $page.url.searchParams.get('selected')
$dirtyStore = false
}
$: {
@@ -89,12 +150,40 @@
loadFlow()
}
}
let diffDrawer: DiffDrawer
async function restoreDraft() {
if (!savedFlow) {
sendUserToast('Could not restore to draft', true)
return
}
diffDrawer.closeDrawer()
goto(`/flows/edit/${savedFlow.path}`)
loadFlow()
}
async function restoreDeployed() {
if (!savedFlow) {
sendUserToast('Could not restore to deployed', true)
return
}
diffDrawer.closeDrawer()
if (savedFlow['draft']) {
await DraftService.deleteDraft({
workspace: $workspaceStore!,
kind: 'flow',
path: savedFlow.path
})
}
goto(`/flows/edit/${savedFlow.path}`)
loadFlow()
}
</script>
<div id="monaco-widgets-root" class="monaco-editor" style="z-index: 1200;" />
<UnsavedConfirmationModal />
<DiffDrawer bind:this={diffDrawer} {restoreDeployed} {restoreDraft} />
<FlowBuilder
on:deploy={(e) => {
goto(`/flows/get/${e.detail}?workspace=${$workspaceStore}`)
@@ -109,4 +198,6 @@
{selectedId}
{initialArgs}
{loading}
bind:savedFlow
{diffDrawer}
/>
@@ -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
</script>
<ScriptBuilder
@@ -1,13 +1,14 @@
<script lang="ts">
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<void> = async () => {}
let savedScript: NewScriptWithDraft | undefined = undefined
async function loadScript(): Promise<void> {
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()
}
</script>
<DiffDrawer bind:this={diffDrawer} button={{ text: 'Revert', onClick: reloadAction }} />
<DiffDrawer bind:this={diffDrawer} {restoreDraft} {restoreDeployed} />
{#if script}
<ScriptBuilder bind:this={scriptBuilder} {initialPath} {script} {initialArgs} />
<ScriptBuilder
bind:this={scriptBuilder}
{initialPath}
{script}
bind:savedScript
{initialArgs}
{diffDrawer}
/>
{/if}