mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 08:01:35 +00:00
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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
+63
-25
@@ -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
|
||||
<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,
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user