Multiplayer deploy collision warning (#4533)

* feat: Multiplayer deploy collision warning

Show warning with diff viewer if 2+ users try to deploy flow/script/app at the same time

In other words, if user A and user B started editing same flow/script/app and user A deployed it,
user B will get warning if they try to deploy as well.

* Add warning for scripts

* Add `deployedBy` to Apps

* Format

* Fix advanced deployment on scripts

* Write comments and cleanup

* feat(frontend): unify all triggers UX and simplify flow settings (#4259)

* feat(frontend): added list of triggers in the flow graph

* feat(frontend): added list of triggers in the flow graph

* feat(frontend): clean up

* feat(frontend): improve UX

* feat(frontend): triggers

* feat(frontend): triggers

* feat(frontend): done

* feat(frontend): fix trigger when position when a preprocessor is presetn

* Glm/rework flow settings v2 (#4497)

* fat(frontend): simplify flow settings menu

* improve scroll

* changing mute toggle

* Add advanced settings badge

* Add nord theme colors

* Add bage for advanced options

* fix minor issue

* fix minor issue

* Add triggers menu to flow settings

* Add quick trigger access

* remove triggers in flow settings

* fix minor issue

* Move triggers settings to flow right panel

* polishing

* fix unset store

* remove save up to for triggers

* fix padding

* reset default tag color

* remove custom select component

* revert path change

* revert section modif

* Revert unused feature

---------

Co-authored-by: Guilhem <guilhem@mbp-de-windmill.home>

* Connect top bar cron to schedules settings

* Turn copilot into node

* fix copilot placement

* remove useless import

* fix center copilot

* fix binding

* remove copilot on top of preprocessor

* render copilot node on condition

* quickfix

* remove copilot node

* fix minor issues

* fix route count update

* fix schedule sync

* harmonize colors

* fix alignment and add edges

* recenter node summary

* fix schedules sync

* Add id title

* all

* all

* all

* iteration

* all

* all

* done

* fix

* more fixes

---------

Co-authored-by: Guilhem <guilhemlemouel@gmail.com>
Co-authored-by: Guilhem <guilhem@mbp-de-windmill.home>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>

* Update ScriptBuilder.svelte

* Remove `onMount` for flows

* Use version instead of last_updated_at in flows

* Use only versions for apps

* Fetch latest data in Diffs in Apps

* Optimize with (script/flow/app)GetLatestVersion

Create several new endpoints, that returns just latest version without rest of the history

* Sync Diffs with deployed

* Improve Diff's data

* Use `getFlowLatestVersion`

---------

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
Co-authored-by: Guilhem <guilhemlemouel@gmail.com>
Co-authored-by: Guilhem <guilhem@mbp-de-windmill.home>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
This commit is contained in:
pyranota
2024-10-17 15:14:48 +00:00
committed by GitHub
parent 658a9345a2
commit ce80d6b07b
9 changed files with 445 additions and 27 deletions

View File

@@ -4092,6 +4092,23 @@ paths:
items:
$ref: "#/components/schemas/ScriptHistory"
/w/{workspace}/scripts/get_latest_version/{path}:
get:
summary: get scripts's latest version (hash)
operationId: getScriptLatestVersion
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
tags:
- script
responses:
"200":
description: Script version/hash
content:
application/json:
schema:
$ref: "#/components/schemas/ScriptHistory"
/w/{workspace}/scripts/history_update/h/{hash}/p/{path}:
post:
summary: update history of a script
@@ -4580,6 +4597,23 @@ paths:
items:
$ref: "#/components/schemas/FlowVersion"
/w/{workspace}/flows/get_latest_version/{path}:
get:
summary: get flow's latest version
operationId: getFlowLatestVersion
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
tags:
- flow
responses:
"200":
description: Flow version
content:
application/json:
schema:
$ref: "#/components/schemas/FlowVersion"
/w/{workspace}/flows/get/v/{version}/p/{path}:
get:
summary: get flow version
@@ -5134,6 +5168,23 @@ paths:
items:
$ref: "#/components/schemas/AppHistory"
/w/{workspace}/apps/get_latest_version/{path}:
get:
summary: get apps's latest version
operationId: getAppLatestVersion
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
tags:
- app
responses:
"200":
description: App version
content:
application/json:
schema:
$ref: "#/components/schemas/AppHistory"
/w/{workspace}/apps/history_update/a/{id}/v/{version}:
post:
summary: update app history

View File

@@ -62,6 +62,7 @@ pub fn workspaced_service() -> Router {
.route("/delete/*path", delete(delete_app))
.route("/create", post(create_app))
.route("/history/p/*path", get(get_app_history))
.route("/get_latest_version/*path", get(get_latest_version))
.route("/history_update/a/:id/v/:version", post(update_app_history))
}
@@ -427,6 +428,30 @@ async fn get_app_history(
return Ok(Json(result));
}
async fn get_latest_version(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<AppHistory> {
let mut tx = user_db.begin(&authed).await?;
let row = sqlx::query!(
"SELECT a.id as app_id, av.id as version_id, dm.deployment_msg as deployment_msg
FROM app a LEFT JOIN app_version av ON a.id = av.app_id LEFT JOIN deployment_metadata dm ON av.id = dm.app_version
WHERE a.workspace_id = $1 AND a.path = $2
ORDER BY created_at DESC",
w_id,
path.to_path(),
).fetch_one(&mut *tx).await?;
tx.commit().await?;
let result = AppHistory {
app_id: row.app_id,
version: row.version_id,
deployment_msg: row.deployment_msg,
};
return Ok(Json(result));
}
async fn update_app_history(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,

View File

@@ -63,6 +63,7 @@ pub fn workspaced_service() -> Router {
.route("/exists/*path", get(exists_flow_by_path))
.route("/list_paths", get(list_paths))
.route("/history/p/*path", get(get_flow_history))
.route("/get_latest_version/*path", get(get_latest_version))
.route(
"/history_update/v/:version/p/*path",
post(update_flow_history),
@@ -538,6 +539,30 @@ async fn get_flow_history(
Ok(Json(flows))
}
async fn get_latest_version(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<FlowVersion> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
let version = sqlx::query_as!(
FlowVersion,
"SELECT flow_version.id, flow_version.created_at, deployment_metadata.deployment_msg FROM flow_version
LEFT JOIN deployment_metadata ON flow_version.id = deployment_metadata.flow_version
WHERE flow_version.path = $1 AND flow_version.workspace_id = $2
ORDER BY flow_version.created_at DESC",
path,
w_id
)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(version))
}
async fn get_flow_version(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,

View File

@@ -152,6 +152,7 @@ pub fn workspaced_service() -> Router {
post(toggle_workspace_error_handler),
)
.route("/history/p/*path", get(get_script_history))
.route("/get_latest_version/*path", get(get_latest_version))
.route(
"/history_update/h/:hash/p/*path",
post(update_script_history),
@@ -948,6 +949,31 @@ async fn get_script_history(
return Ok(Json(result));
}
async fn get_latest_version(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<ScriptHistory> {
let mut tx = user_db.begin(&authed).await?;
let row = sqlx::query!(
"SELECT s.hash as hash, dm.deployment_msg as deployment_msg
FROM script s LEFT JOIN deployment_metadata dm ON s.hash = dm.script_hash
WHERE s.workspace_id = $1 AND s.path = $2
ORDER by created_at DESC",
w_id,
path.to_path(),
)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;
let result = ScriptHistory {
script_hash: ScriptHash(row.hash),
deployment_msg: row.deployment_msg, //
};
return Ok(Json(result));
}
async fn update_script_history(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,

View File

@@ -25,10 +25,12 @@
encodeState,
formatCron,
orderedJsonStringify,
sleep
sleep,
type Value
} from '$lib/utils'
import { sendUserToast } from '$lib/toast'
import { Drawer } from '$lib/components/common'
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
import { setContext, tick, type ComponentType } from 'svelte'
import { writable, type Writable } from 'svelte/store'
@@ -102,9 +104,29 @@
export let disableAi: boolean = false
export let disabledFlowInputs = false
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined
export let version: number | undefined = undefined
// Used by multiplayer deploy collision warning
let deployedValue: Value | undefined = undefined // Value to diff against
let deployedBy: string | undefined = undefined // Author
let confirmCallback: () => void = () => {} // What happens when user clicks `override` in warning
let open: boolean = false // Is confirmation modal open
$: setContext('customUi', customUi)
let onLatest = true
async function compareVersions() {
if (version === undefined) {
return
}
const flowVersion = await FlowService.getFlowLatestVersion({
workspace: $workspaceStore!,
path: $pathStore
})
onLatest = version === flowVersion.id
}
const dispatch = createEventDispatcher()
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(savedPrimarySchedule)
@@ -253,6 +275,46 @@
)
}
async function handleSaveFlow(deploymentMsg?: string) {
await compareVersions();
if (onLatest) {
// Handle directly
await saveFlow(deploymentMsg)
} else {
// We need it for diff
await syncWithDeployed()
// Handle through confirmation modal
confirmCallback = async () => {
await saveFlow(deploymentMsg)
}
// Open confirmation modal
open = true
}
}
async function syncWithDeployed(){
const flow = await FlowService.getFlowByPath({
workspace: $workspaceStore!,
path: $pathStore,
withStarredInfo: true
})
deployedValue = {
...flow,
starred: undefined,
id: undefined,
edited_at: undefined,
edited_by: undefined,
workspace_id: undefined,
archived: undefined,
same_worker: undefined,
visible_to_runner_only: undefined,
ws_error_handler_muted: undefined,
}
deployedBy = flow.edited_by
}
async function saveFlow(deploymentMsg?: string): Promise<void> {
loadingSave = true
try {
@@ -1122,6 +1184,15 @@
<slot />
<DeployOverrideConfirmationModal
bind:deployedBy
bind:confirmCallback
bind:open
{diffDrawer}
bind:deployedValue
currentValue={$flowStore}
/>
{#key renderCount}
{#if !$userStore?.operator}
<FlowCopilotDrawer {getHubCompletions} {genFlow} bind:flowCopilotMode />
@@ -1286,14 +1357,17 @@
color="light"
variant="border"
size="xs"
on:click={() => {
on:click={async () => {
if (!savedFlow) {
return
}
await syncWithDeployed()
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: savedFlow,
deployed: deployedValue ?? savedFlow,
draft: savedFlow['draft'],
current: { ...$flowStore, path: $pathStore }
})
@@ -1335,7 +1409,9 @@
loading={loadingSave}
size="xs"
startIcon={{ icon: Save }}
on:click={() => saveFlow()}
on:click={async () => {
await handleSaveFlow()
}}
dropdownItems={!newFlow ? dropdownItems : undefined}
>
Deploy
@@ -1346,16 +1422,16 @@
type="text"
placeholder="Deployment message"
bind:value={deploymentMsg}
on:keydown={(e) => {
on:keydown={async (e) => {
if (e.key === 'Enter') {
saveFlow(deploymentMsg)
await handleSaveFlow(deploymentMsg)
}
}}
bind:this={msgInput}
/>
<Button
size="xs"
on:click={() => saveFlow(deploymentMsg)}
on:click={async () => await handleSaveFlow(deploymentMsg)}
endIcon={{ icon: CornerDownLeft }}
loading={loadingSave}
>

View File

@@ -17,7 +17,10 @@
emptyString,
encodeState,
formatCron,
orderedJsonStringify
orderedJsonStringify,
type Value
} from '$lib/utils'
import Path from './Path.svelte'
import ScriptEditor from './ScriptEditor.svelte'
@@ -63,6 +66,7 @@
import CustomPopover from './CustomPopover.svelte'
import Summary from './Summary.svelte'
import type { ScriptBuilderWhitelabelCustomUi } from './custom_ui'
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
import TriggersEditor from './triggers/TriggersEditor.svelte'
import type { ScheduleTrigger, TriggerContext } from './triggers'
@@ -83,6 +87,12 @@
export let customUi: ScriptBuilderWhitelabelCustomUi = {}
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined
let deployedValue: Value | undefined = undefined // Value to diff against
let deployedBy: string | undefined = undefined // Author
let confirmCallback: () => void = () => {} // What happens when user clicks `override` in warning
let open: boolean = false // Is confirmation modal open
let metadataOpen =
!neverShowMeta &&
(showMeta ||
@@ -241,7 +251,61 @@
}
}
async function editScript(stay: boolean, deploymentMsg?: string): Promise<void> {
async function handleEditScript(stay: boolean, deployMsg?: string): Promise<void>{
// Fetch latest version and fetch entire script after if needed
let actual_parent_hash = (await ScriptService.getScriptLatestVersion({
workspace: $workspaceStore!,
path: script.path,
})).script_hash;
// Usually when we create new script, we put current hash as a parent_hash
// But if we specify parent_hash that is already used, than we get error
// In order to fix it we make sure that client's understanding of parent_hash
// is aligns with understanding of backend.
if (script.parent_hash == actual_parent_hash) {
// Handle directly
await editScript(stay, actual_parent_hash, deployMsg);
} else {
// Fetch entire script, since we need it to show Diff
await syncWithDeployed()
// Handle through confirmation modal
confirmCallback = async () => {
open = false
await editScript(stay, actual_parent_hash, deployMsg);
}
// Open confirmation modal
open = true
}
}
async function syncWithDeployed(){
const latestScript = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
path: script.path,
withStarredInfo: true
});
deployedValue = {
...latestScript,
starred: undefined,
workspace_id: undefined,
archived: undefined,
created_at: undefined,
created_by: undefined,
deleted: undefined,
extra_perms: undefined,
is_template: undefined,
lock: undefined,
lock_error_logs: undefined,
parent_hashes: undefined,
};
deployedBy = latestScript.created_by;
}
async function editScript(stay: boolean, parentHash: string, deploymentMsg?: string, ): Promise<void> {
loadingSave = true
try {
try {
@@ -265,7 +329,7 @@
summary: script.summary,
description: script.description ?? '',
content: script.content,
parent_hash: script.parent_hash,
parent_hash: parentHash,
schema: script.schema,
is_template: script.is_template,
language: script.language,
@@ -472,7 +536,7 @@
{
label: 'Deploy & Stay here',
onClick: () => {
editScript(true)
handleEditScript(true)
}
},
{
@@ -530,6 +594,14 @@
<svelte:window on:keydown={onKeyDown} />
<slot />
<DeployOverrideConfirmationModal
bind:deployedBy
bind:confirmCallback
bind:open
{diffDrawer}
bind:deployedValue
currentValue={script}
/>
{#if !$userStore?.operator}
<Drawer
placement="right"
@@ -1154,14 +1226,16 @@
color="light"
variant="border"
size="xs"
on:click={() => {
on:click={async () => {
if (!savedScript) {
return
}
await syncWithDeployed()
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: savedScript,
deployed: deployedValue ?? savedScript,
draft: savedScript['draft'],
current: script
})
@@ -1206,7 +1280,7 @@
size="xs"
disabled={!fullyLoaded}
startIcon={{ icon: Save }}
on:click={() => editScript(false)}
on:click={() => handleEditScript(false)}
dropdownItems={computeDropdownItems(initialPath)}
>
Deploy
@@ -1220,13 +1294,13 @@
bind:this={msgInput}
on:keydown={(e) => {
if (e.key === 'Enter') {
editScript(false, deploymentMsg)
handleEditScript(false, deploymentMsg)
}
}}
/>
<Button
size="xs"
on:click={() => editScript(false, deploymentMsg)}
on:click={() => handleEditScript(false, deploymentMsg)}
endIcon={{ icon: CornerDownLeft }}
loading={loadingSave}
>

View File

@@ -40,7 +40,8 @@
cleanValueProperties,
copyToClipboard,
truncateRev,
orderedJsonStringify
orderedJsonStringify,
type Value
} from '../../../utils'
import type {
AppInput,
@@ -85,6 +86,7 @@
import Summary from '$lib/components/Summary.svelte'
import ToggleEnable from '$lib/components/common/toggleButton-v2/ToggleEnable.svelte'
import HideButton from './settingsPanel/HideButton.svelte'
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
async function hash(message) {
try {
@@ -121,6 +123,11 @@
export let rightPanelHidden: boolean = false
export let bottomPanelHidden: boolean = false
let deployedValue: Value | undefined = undefined // Value to diff against
let deployedBy: string | undefined = undefined // Author
let confirmCallback: () => void = () => {} // What happens when user clicks `override` in warning
let open: boolean = false // Is confirmation modal open
const {
app,
summary,
@@ -360,6 +367,50 @@
}
}
async function handleUpdateApp(npath: string) {
// We have to make sure there is no updates when we clicked the button
await compareVersions();
if (onLatest) {
// Handle directly
await updateApp(npath)
} else {
// There is onLatest, but we need more information while deploying
// We need it to show diff
// Handle through confirmation modal
await syncWithDeployed();
confirmCallback = async () => {
open = false
await updateApp(npath)
}
// Open confirmation modal
open = true
}
}
async function syncWithDeployed() {
const deployedApp = await AppService.getAppByPath({
workspace: $workspaceStore!,
path: appPath,
withStarredInfo: true
})
deployedBy = deployedApp.created_by
// Strip off extra information
deployedValue = {
...deployedApp,
starred: undefined,
id: undefined,
created_at: undefined,
created_by: undefined,
versions: undefined,
extra_perms: undefined //
}
}
async function updateApp(npath: string) {
await computeTriggerables()
await AppService.updateApp({
@@ -577,11 +628,11 @@
if (version === undefined) {
return
}
const appHistory = await AppService.getAppHistoryByPath({
const appVersion = await AppService.getAppLatestVersion({
workspace: $workspaceStore!,
path: appPath
})
onLatest = version === appHistory[0]?.version
onLatest = version === appVersion.version
}
$: saveDrawerOpen && compareVersions()
@@ -706,14 +757,18 @@
{
displayName: 'Diff',
icon: DiffIcon,
action: () => {
action: async () => {
if (!savedApp) {
return
}
// deployedValue should be syncronized when we open Diff
await syncWithDeployed();
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: savedApp,
deployed: deployedValue ?? savedApp,
draft: savedApp.draft,
current: {
summary: $summary,
@@ -766,6 +821,20 @@
}}
/>
<DeployOverrideConfirmationModal
bind:deployedBy
bind:confirmCallback
bind:open
{diffDrawer}
bind:deployedValue
currentValue={{
summary: $summary,
value: $app,
path: newPath || savedApp?.draft?.path || savedApp?.path,
policy
}}
/>
{#if appPath == ''}
<Drawer bind:open={draftDrawerOpen} size="800px">
<DrawerContent title="Initial draft save" on:close={() => closeDraftDrawer()}>
@@ -823,8 +892,8 @@
<Drawer bind:open={saveDrawerOpen} size="800px">
<DrawerContent title="Deploy" on:close={() => closeSaveDrawer()}>
{#if !onLatest}
<Alert title="You're not on the latest app version" type="warning">
By deploying, you may overwrite changes made by other users.
<Alert title="You're not on the latest app version. " type="warning">
By deploying, you may overwrite changes made by other users. Press 'Deploy' to see diff.
</Alert>
<div class="py-2" />
{/if}
@@ -880,15 +949,18 @@
variant="border"
color="light"
disabled={!savedApp || savedApp.draft_only}
on:click={() => {
on:click={async () => {
if (!savedApp) {
return
}
// deployedValue should be syncronized when we open Diff
await syncWithDeployed();
saveDrawerOpen = false
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'normal',
deployed: savedApp,
deployed: deployedValue ?? savedApp,
draft: savedApp.draft,
current: {
summary: $summary,
@@ -902,7 +974,7 @@
if (appPath == '') {
createApp(newPath)
} else {
updateApp(newPath)
handleUpdateApp(newPath)
}
}
}
@@ -921,7 +993,7 @@
if (appPath == '') {
createApp(newPath)
} else {
updateApp(newPath)
handleUpdateApp(newPath)
}
}}
>

View File

@@ -0,0 +1,53 @@
<script lang="ts">
import ConfirmationModal from './ConfirmationModal.svelte'
import Button from '../button/Button.svelte'
import type DiffDrawer from '$lib/components/DiffDrawer.svelte'
import { type Value } from '$lib/utils'
export let deployedValue: Value | undefined = undefined
export let currentValue: Value | undefined = undefined
export let diffDrawer: DiffDrawer | undefined = undefined
export let confirmCallback: () => void
export let deployedBy : string | undefined = undefined
export let open = false
</script>
<ConfirmationModal
{open}
title={"New version deployed by " + deployedBy}
confirmationText="Override"
on:canceled={() => {
open = false
}}
on:confirmed={() => confirmCallback()}
>
<div class="flex flex-col w-full space-y-4">
<span>There was deployed new version while you were editing this one.</span>
{#if diffDrawer}
<Button
wrapperClasses="self-start"
color="light"
variant="border"
size="xs"
on:click={() => {
if (!deployedValue || !currentValue) {
return
}
open = false
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'simple',
original: deployedValue,
current: currentValue,
title: 'Deployed <> Current',
button: {
text: 'Override anyway',
onClick: () => confirmCallback()
}
})
}}
>Show diff
</Button>
{/if}
</div>
</ConfirmationModal>

View File

@@ -15,6 +15,7 @@
import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte'
import type { ScheduleTrigger } from '$lib/components/triggers'
let version: undefined | number = undefined;
let nodraft = $page.url.searchParams.get('nodraft')
const initialState = nodraft ? undefined : localStorage.getItem(`flow-${$page.params.path}`)
let stateLoadedFromUrl = initialState != undefined ? decodeState(initialState) : undefined
@@ -65,6 +66,13 @@
let flow: Flow
let statePath = stateLoadedFromUrl?.path
if (stateLoadedFromUrl != undefined && statePath == $page.params.path) {
// Currently there is no way to get version of flow with flow.
// So we have to request it here
version = (await FlowService.getFlowLatestVersion({
workspace: $workspaceStore!,
path: statePath
})).id;
savedFlow = await FlowService.getFlowByPathWithDraft({
workspace: $workspaceStore!,
path: statePath
@@ -103,6 +111,13 @@
])
}
} else {
// Currently there is no way to get version of flow with flow.
// So we have to request it here
version = (await FlowService.getFlowLatestVersion({
workspace: $workspaceStore!,
path: $page.params.path
})).id;
const flowWithDraft = await FlowService.getFlowByPathWithDraft({
workspace: $workspaceStore!,
path: $page.params.path
@@ -230,6 +245,7 @@
bind:savedFlow
{diffDrawer}
{savedPrimarySchedule}
bind:version
>
<UnsavedConfirmationModal {diffDrawer} savedValue={savedFlow} modifiedValue={$flowStore} />
</FlowBuilder>