feat: introduce draft for flows

This commit is contained in:
Ruben Fiszel
2023-05-01 09:31:15 +02:00
parent bda06ac0e5
commit a1966427e8
24 changed files with 354 additions and 158 deletions

View File

@@ -2660,6 +2660,24 @@
},
"query": "SELECT policy from app WHERE path = $1 AND workspace_id = $2"
},
"6e8182e167b09a0f9800d5e5988d4dd027c49ad6e14d2665dfdf93c46cb54434": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text",
"Text",
"Jsonb",
"Varchar",
"Text"
]
}
},
"query": "INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, schema, dependency_job, draft_only) VALUES ($1, $2, $3, $4, $5, $6, now(), $7::text::json, NULL, true)"
},
"701f215eb14ba67a79afea15d7effc0dd394ba6c4a72c95d4560c5a377015d4e": {
"describe": {
"columns": [],
@@ -2693,25 +2711,6 @@
},
"query": "SELECT EXISTS(SELECT 1 FROM usr WHERE workspace_id = $1 AND username = $2)"
},
"72098030cab635723a9cecf8b3b1448e69a8afd68342850ef6376352d2897723": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text",
"Jsonb",
"Varchar",
"Text",
"Text",
"Text"
]
}
},
"query": "UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, edited_at = now(), schema = $6::text::json, dependency_job = NULL WHERE path = $7 AND workspace_id = $8"
},
"7226d470c04e58fcab92f3edac21956d2a10e5ed6b594747b22682f05a60a341": {
"describe": {
"columns": [
@@ -3596,24 +3595,6 @@
},
"query": "SELECT count(path) FROM flow WHERE path LIKE 'f/' || $1 || '%' AND archived IS false AND workspace_id = $2"
},
"8da6ff304d199401ee8ee9a3de1d51477c04f9abc1b6255d84c5b1fc58267c59": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text",
"Text",
"Jsonb",
"Varchar",
"Text"
]
}
},
"query": "INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, schema, dependency_job) VALUES ($1, $2, $3, $4, $5, $6, now(), $7::text::json, NULL)"
},
"8ede6fb740b145b3a8320adb789500870c7a8ec807a7b156b9ff7a15791b78f8": {
"describe": {
"columns": [],
@@ -4217,6 +4198,25 @@
},
"query": "INSERT INTO app\n (workspace_id, path, summary, policy, versions)\n VALUES ($1, $2, $3, $4, '{}') RETURNING id"
},
"9e103fb8405089e361d0528d34e166b3098cc0f7abecbef3b498cca86e74a285": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text",
"Jsonb",
"Varchar",
"Text",
"Text",
"Text"
]
}
},
"query": "UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, edited_at = now(), schema = $6::text::json, dependency_job = NULL, draft_only = NULL WHERE path = $7 AND workspace_id = $8"
},
"a17260a1f1ee02e786690994d98c84ddf81e2eeb883f895c9cfc47e144d422cb": {
"describe": {
"columns": [

View File

@@ -2712,7 +2712,14 @@ paths:
schema:
type: array
items:
$ref: "#/components/schemas/Flow"
allOf:
- $ref: "#/components/schemas/Flow"
- type: object
properties:
has_draft:
type: boolean
draft_only:
type: boolean
/w/{workspace}/flows/get/{path}:
get:
@@ -2731,6 +2738,30 @@ paths:
schema:
$ref: "#/components/schemas/Flow"
/w/{workspace}/flows/get/draft/{path}:
get:
summary: get flow by path with draft
operationId: getFlowByPathWithDraft
tags:
- flow
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
responses:
"200":
description: flow details with draft
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Flow"
- type: object
properties:
draft:
$ref: "#/components/schemas/Flow"
/w/{workspace}/flows/exists/{path}:
get:
summary: exists flow by path
@@ -6125,6 +6156,8 @@ components:
type: boolean
starred:
type: boolean
draft_only:
type: boolean
required:
- path
- edited_by

View File

@@ -19,7 +19,7 @@ use axum::{
Json, Router,
};
use hyper::StatusCode;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use sql_builder::prelude::*;
use sql_builder::SqlBuilder;
use sqlx::{Postgres, Transaction};
@@ -29,6 +29,7 @@ use windmill_common::{
flows::{Flow, ListFlowQuery, ListableFlow, NewFlow},
jobs::JobPayload,
schedule::Schedule,
scripts::Schema,
utils::{
http_get_from_hub, list_elems_from_hub, not_found_if_none, paginate, Pagination, StripPath,
},
@@ -43,6 +44,7 @@ pub fn workspaced_service() -> Router {
.route("/archive/*path", post(archive_flow_by_path))
.route("/delete/*path", delete(delete_flow_by_path))
.route("/get/*path", get(get_flow_by_path))
.route("/get/draft/*path", get(get_flow_by_path_w_draft))
.route("/exists/*path", get(exists_flow_by_path))
.route("/list_paths", get(list_paths))
}
@@ -73,6 +75,8 @@ async fn list_flows(
"archived",
"extra_perms",
"favorite.path IS NOT NULL as starred",
"draft.path IS NOT NULL as has_draft",
"draft_only"
])
.left()
.join("favorite")
@@ -80,6 +84,11 @@ async fn list_flows(
"favorite.favorite_kind = 'flow' AND favorite.workspace_id = o.workspace_id AND favorite.path = o.path AND favorite.usr = ?"
.bind(&authed.username),
)
.left()
.join("draft")
.on(
"draft.path = o.path AND draft.workspace_id = o.workspace_id AND draft.typ = 'flow'"
)
.order_desc("favorite.path IS NOT NULL")
.order_by("edited_at", lq.order_desc.unwrap_or(true))
.and_where("o.workspace_id = ?".bind(&w_id))
@@ -194,7 +203,7 @@ async fn create_flow(
sqlx::query!(
"INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, \
schema, dependency_job) VALUES ($1, $2, $3, $4, $5, $6, now(), $7::text::json, NULL)",
schema, dependency_job, draft_only) VALUES ($1, $2, $3, $4, $5, $6, now(), $7::text::json, NULL, true)",
w_id,
nf.path,
nf.summary,
@@ -206,6 +215,14 @@ async fn create_flow(
.execute(&mut tx)
.await?;
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'flow'",
nf.path,
&w_id
)
.execute(&mut tx)
.await?;
audit_log(
&mut tx,
&authed.username,
@@ -310,7 +327,7 @@ async fn update_flow(
let old_dep_job = not_found_if_none(old_dep_job, "Flow", flow_path)?;
sqlx::query!(
"UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, \
edited_at = now(), schema = $6::text::json, dependency_job = NULL WHERE path = $7 AND workspace_id = $8",
edited_at = now(), schema = $6::text::json, dependency_job = NULL, draft_only = NULL WHERE path = $7 AND workspace_id = $8",
nf.path,
nf.summary,
nf.description,
@@ -364,7 +381,7 @@ async fn update_flow(
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'flow'",
nf.path,
flow_path,
&w_id
)
.execute(&mut tx)
@@ -454,6 +471,43 @@ async fn get_flow_by_path(
Ok(Json(flow))
}
#[derive(Serialize, sqlx::FromRow)]
pub struct FlowWDraft {
pub path: String,
pub summary: String,
pub description: String,
pub schema: Option<Schema>,
pub value: serde_json::Value,
pub extra_perms: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
}
async fn get_flow_by_path_w_draft(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<FlowWDraft> {
let path = path.to_path();
let mut tx = user_db.begin(&authed).await?;
let flow_o = sqlx::query_as::<_, FlowWDraft>(
"SELECT flow.path, flow.summary, flow,description, flow.schema, flow.value, flow.extra_perms, flow.draft_only, draft.value as draft FROM flow LEFT JOIN draft ON
flow.path = draft.path AND flow.workspace_id = draft.workspace_id AND draft.typ = 'flow'
WHERE flow.path = $1 AND flow.workspace_id = $2",
)
.bind(path)
.bind(w_id)
.fetch_optional(&mut tx)
.await?;
tx.commit().await?;
let flow = not_found_if_none(flow_o, "Flow", path)?;
Ok(Json(flow))
}
async fn exists_flow_by_path(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,

View File

@@ -65,8 +65,10 @@ pub struct ScriptWDraft {
pub language: ScriptLang,
pub kind: ScriptKind,
pub tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft: Option<serde_json::Value>,
pub schema: Option<Schema>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
}
@@ -386,15 +388,15 @@ async fn create_script(
.execute(&mut tx)
.await?;
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script'",
ns.path,
&w_id
)
.execute(&mut tx)
.await?;
if let Some(p_path) = parent_hashes_and_perms.as_ref().map(|x| x.p_path.clone()) {
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script'",
p_path,
&w_id
)
.execute(&mut tx)
.await?;
let schedulables = sqlx::query_as!(
Schedule,
"UPDATE schedule SET script_path = $1 WHERE script_path = $2 AND workspace_id = $3 AND is_flow IS false RETURNING *",
@@ -412,6 +414,14 @@ async fn create_script(
tx = push_scheduled_job(tx, schedule).await?;
}
}
} else {
sqlx::query!(
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script'",
ns.path,
&w_id
)
.execute(&mut tx)
.await?;
}
if p_hashes.is_some() && !p_hashes.unwrap().is_empty() {

View File

@@ -33,6 +33,8 @@ pub struct Flow {
pub archived: bool,
pub schema: Option<Schema>,
pub extra_perms: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
}
#[derive(Serialize)]
@@ -47,6 +49,9 @@ pub struct ListableFlow {
pub archived: bool,
pub extra_perms: serde_json::Value,
pub starred: bool,
pub has_draft: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
}
#[derive(Deserialize)]

View File

@@ -137,6 +137,7 @@ pub struct Script {
pub language: ScriptLang,
pub kind: ScriptKind,
pub tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub draft_only: Option<bool>,
}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { FlowService, ScheduleService, type Flow, type FlowModule } from '$lib/gen'
import { FlowService, ScheduleService, type Flow, type FlowModule, DraftService } from '$lib/gen'
import { initHistory, redo, undo } from '$lib/history'
import { userStore, workspaceStore } from '$lib/stores'
import { encodeState, formatCron, loadHubScripts, sendUserToast } from '$lib/utils'
@@ -20,6 +20,7 @@
import type { FlowEditorContext } from './flows/types'
import { cleanInputs } from './flows/utils'
import { Pen } from 'lucide-svelte'
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
export let initialPath: string = ''
export let selectedId: string | undefined
@@ -50,8 +51,43 @@
}
let loadingSave = false
let loadingDraft = false
async function saveFlow(leave: boolean): Promise<void> {
async function saveDraft(): Promise<void> {
loadingDraft = true
try {
const flow = cleanInputs($flowStore)
$dirtyStore = false
localStorage.removeItem('flow')
if (initialPath == '') {
await FlowService.createFlow({
workspace: $workspaceStore!,
requestBody: {
path: flow.path,
summary: flow.summary,
description: flow.description ?? '',
value: flow.value,
schema: flow.schema
}
})
}
await DraftService.createDraft({
workspace: $workspaceStore!,
requestBody: { path: initialPath == '' ? flow.path : initialPath, typ: 'flow', value: flow }
})
if (initialPath == '') {
$dirtyStore = false
goto(`/flows/edit/${flow.path}`)
}
sendUserToast('Saved as draft')
} catch (error) {
sendUserToast(`Error while saving the flow as a draft: ${error.body || error.message}`, true)
}
loadingDraft = false
}
async function saveFlow(): Promise<void> {
loadingSave = true
try {
const flow = cleanInputs($flowStore)
@@ -117,12 +153,7 @@
}
}
loadingSave = false
if (leave) {
goto(`/flows/get/${$flowStore.path}?workspace=${$workspaceStore}`)
} else if (initialPath !== $flowStore.path) {
initialPath = $flowStore.path
goto(`/flows/edit/${$flowStore.path}?workspace=${$workspaceStore}`)
}
goto(`/flows/get/${$flowStore.path}?workspace=${$workspaceStore}`)
} catch (err) {
sendUserToast(`The flow could not be saved: ${err.body}`, true)
loadingSave = false
@@ -133,11 +164,11 @@
$: {
if ($flowStore || $selectedIdStore) {
saveDraft()
saveSessionDraft()
}
}
function saveDraft() {
function saveSessionDraft() {
timeout && clearTimeout(timeout)
timeout = setTimeout(() => {
try {
@@ -231,7 +262,7 @@
break
case 's':
if (event.ctrlKey || event.metaKey) {
saveFlow(false)
saveDraft()
event.preventDefault()
}
break
@@ -269,8 +300,8 @@
onClick: () => void
}> = [
{
label: 'Save and exit',
onClick: () => saveFlow(true)
label: 'Exit & see details',
onClick: () => goto(`/flows/get/${$flowStore.path}?workspace=${$workspaceStore}`)
}
]
@@ -284,6 +315,8 @@
<svelte:window on:keydown={onKeyDown} />
<UnsavedConfirmationModal />
{#if !$userStore?.operator}
<ScriptEditorDrawer bind:this={$scriptEditorDrawer} />
@@ -360,18 +393,22 @@
<FlowImportExportMenu />
<FlowPreviewButtons />
<div class="center-center">
<Button
title="Ctrl/Cmd + S"
loading={loadingSave}
size="xs"
startIcon={{ icon: faSave }}
on:click={() => saveFlow(false)}
{dropdownItems}
>
Save&nbsp;<Kbd small>Ctrl</Kbd><Kbd small>S</Kbd>
</Button>
</div>
<Button
loading={loadingDraft}
size="xs"
startIcon={{ icon: faSave }}
on:click={() => saveDraft()}
>
Save draft&nbsp;<Kbd small>Ctrl</Kbd><Kbd small>S</Kbd>
</Button>
<Button
loading={loadingSave}
size="xs"
startIcon={{ icon: faSave }}
on:click={() => saveFlow()}
>
Deploy
</Button>
</div>
</div>

View File

@@ -130,7 +130,7 @@
<Button
variant="contained"
startIcon={{ icon: isRunning ? faRefresh : faPlay }}
color="blue"
color="dark"
size="sm"
btnClasses="w-full max-w-lg"
on:click={() => runPreview($previewArgs)}

View File

@@ -16,6 +16,7 @@
import type { InputTransform } from '$lib/gen'
import TemplateEditor from './TemplateEditor.svelte'
import { setInputCat as computeInputCat } from '$lib/utils'
import { Plug } from 'lucide-svelte'
export let schema: Schema
export let arg: InputTransform | any
@@ -221,14 +222,14 @@
<Button
title="Connect to another node's output"
variant="border"
color="dark"
color="light"
size="xs2"
on:click={() => {
focusProp(argName, 'connect', (path) => {
connectProperty(path)
return true
})
}}>Link &rightarrow;</Button
}}><Plug size={16} /> &rightarrow;</Button
>
</div>
</div>

View File

@@ -80,16 +80,18 @@
>
{/if}
{#if testIsLoading}
<Button on:click={testJobLoader?.cancelJob} btnClasses="w-full" color="red" size="sm">
<Loader2 size={16} class="animate-spin mr-1" />
Cancel
</Button>
{:else}
<Button btnClasses="w-full truncate" size="sm" on:click={() => runTest(stepArgs)}
>Run&nbsp;<Kbd small>{getModifierKey()}</Kbd><Kbd small>Enter</Kbd></Button
>
{/if}
<div class="w-full justify-center flex">
{#if testIsLoading}
<Button size="sm" on:click={testJobLoader?.cancelJob} btnClasses="w-full" color="red">
<Loader2 size={16} class="animate-spin mr-1" />
Cancel
</Button>
{:else}
<Button color="dark" btnClasses="truncate" size="sm" on:click={() => runTest(stepArgs)}
>Run&nbsp;<Kbd small>{getModifierKey()}</Kbd><Kbd small>Enter</Kbd></Button
>
{/if}
</div>
<ModulePreviewForm {pickableProperties} {mod} {schema} bind:args={stepArgs} />
</Pane>

View File

@@ -52,7 +52,7 @@
}
</script>
<div class="w-full pt-4">
<div class="w-full pt-2">
{#if keys.length > 0}
{#each keys as argName, i (argName)}
{#if Object.keys(schema.properties ?? {}).includes(argName)}

View File

@@ -312,8 +312,8 @@
<label class="block grow w-48">
<span class="text-gray-700 text-sm"
>Folder <Tooltip
documentationLink="https://docs.windmill.dev/docs/core_concepts/groups_and_folders"
>Read and write permissions are given to groups and users at the folder level and
documentationLink="https://docs.windmill.dev/docs/core_concepts/groups_and_folders"
>Read and write permissions are given to groups and users at the folder level and
shared by all items inside the folder.</Tooltip
></span
>

View File

@@ -190,7 +190,11 @@
}
await DraftService.createDraft({
workspace: $workspaceStore!,
requestBody: { path: script.path, typ: 'script', value: script }
requestBody: {
path: initialPath == '' ? script.path : initialPath,
typ: 'script',
value: script
}
})
if (initialPath == '') {
$dirtyStore = false
@@ -428,17 +432,15 @@
Customise
</Button>
<Button
color="dark"
loading={loadingDraft}
size="sm"
startIcon={{ icon: faSave }}
on:click={() => saveDraft()}
>
Save as draft&nbsp;<Kbd>{getModifierKey()}</Kbd>
Save draft&nbsp;<Kbd>{getModifierKey()}</Kbd>
<Kbd>S</Kbd>
</Button>
<Button
color="dark"
loading={loadingSave}
size="sm"
startIcon={{ icon: faSave }}

View File

@@ -183,7 +183,7 @@
</Pane>
<Pane size={40} minSize={10}>
<div class="flex flex-col h-full">
<div class="px-2 w-full border-b py-1">
<div class="flex justify-center pt-1">
{#if testIsLoading}
<Button on:click={testJobLoader?.cancelJob} btnClasses="w-full" color="red" size="xs">
<WindmillIcon
@@ -197,6 +197,7 @@
</Button>
{:else}
<Button
color="dark"
on:click={runTest}
btnClasses="w-full"
size="xs"

View File

@@ -14,7 +14,9 @@
editor as meditor,
Uri as mUri,
languages,
Range
Range,
KeyMod,
KeyCode
} from 'monaco-editor/esm/vs/editor/editor.api'
import 'monaco-editor/esm/vs/basic-languages/typescript/typescript.contribution'
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
@@ -451,6 +453,12 @@
lineDecorationsWidth: 14
})
editor.onDidFocusEditorText(() => {
dispatch('focus')
editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyS, function () {})
})
const stdLib = { content: libStdContent, filePath: 'es5.d.ts' }
if (extraLib != '') {
languages.typescript.javascriptDefaults.setExtraLibs([

View File

@@ -24,15 +24,16 @@
import Badge from '../badge/Badge.svelte'
import Button from '../button/Button.svelte'
import Row from './Row.svelte'
import DraftBadge from '$lib/components/DraftBadge.svelte'
export let flow: Flow & { canWrite: boolean }
export let flow: Flow & { has_draft?: boolean; draft_only?: boolean; canWrite: boolean }
export let marked: string | undefined
export let starred: boolean
export let shareModal: ShareModal
export let moveDrawer: MoveDrawer
export let deleteConfirmedCallback: (() => void) | undefined
let { summary, path, extra_perms, canWrite, workspace_id, archived } = flow
let { summary, path, extra_perms, canWrite, workspace_id, archived, draft_only, has_draft } = flow
const dispatch = createEventDispatcher()
@@ -72,9 +73,11 @@
{summary}
{starred}
on:change
canFavorite={!draft_only}
>
<svelte:fragment slot="badges">
<SharedBadge {canWrite} extraPerms={extra_perms} />
<DraftBadge {has_draft} {draft_only} />
{#if archived}
<Badge color="red" baseClass="border">archived</Badge>
@@ -110,31 +113,52 @@
{/if}
{/if}
<Button
href="/flows/get/{path}?workspace={$workspaceStore}"
color="light"
variant="border"
size="xs"
spacingSize="md"
startIcon={{ icon: faEye }}
>
Detail
</Button>
<Button
href="/flows/run/{path}"
color="dark"
size="xs"
spacingSize="md"
endIcon={{ icon: faPlay }}
>
Run
</Button>
{#if !draft_only}
<Button
href="/flows/get/{path}?workspace={$workspaceStore}"
color="light"
variant="border"
size="xs"
spacingSize="md"
startIcon={{ icon: faEye }}
>
Detail
</Button>
<Button
href="/flows/run/{path}"
color="dark"
size="xs"
spacingSize="md"
endIcon={{ icon: faPlay }}
>
Run
</Button>
{/if}
</span>
<Dropdown
placement="bottom-end"
dropdownItems={() => {
let owner = isOwner(path, $userStore, $workspaceStore)
if (draft_only) {
return [
{
displayName: 'Delete',
icon: faTrashAlt,
action: (event) => {
if (event?.shiftKey) {
deleteFlow(path)
} else {
deleteConfirmedCallback = () => {
deleteFlow(path)
}
}
},
type: 'delete',
disabled: !owner
}
]
}
return [
{
displayName: 'View flow',

View File

@@ -122,7 +122,7 @@
Edit
</Button>
</div>
{:else}
{:else if !draft_only}
<div>
<Button
color="light"

View File

@@ -12,7 +12,7 @@
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
let size = 50
let size = 40
</script>
<div class="h-full overflow-hidden border-t">

View File

@@ -67,7 +67,7 @@
</span>
</label>
<Slider text="How to trigger flows?">
<div class="text-sm text-gray-600 border p-4">
<div class="text-sm text-gray-600 border p-4 mb-20">
On-demand:
<ul class="pt-4">
<li>

View File

@@ -24,7 +24,7 @@
on:click={() => jsonViewerDrawer.toggleDrawer()}
>
<Icon data={faFileExport} scale={0.6} class="inline mr-2" />
Export JSON
JSON
</Button>
<Drawer bind:this={jsonViewerDrawer} size="800px">

View File

@@ -33,16 +33,6 @@
$selectedId?.includes('branch')
</script>
<Button
size="xs"
on:click={() => {
previewMode = 'whole'
previewOpen = !previewOpen
}}
startIcon={{ icon: faPlay }}
>
Test flow
</Button>
{#if !upToDisabled}
<Button
size="xs"
@@ -62,6 +52,18 @@
</Button>
{/if}
<Button
color="dark"
size="xs"
on:click={() => {
previewMode = 'whole'
previewOpen = !previewOpen
}}
startIcon={{ icon: faPlay }}
>
Test flow
</Button>
<Drawer bind:open={previewOpen} alwaysOpen size="75%">
<FlowPreviewContent
open={previewOpen}

View File

@@ -71,7 +71,7 @@
{`Mode: ${$propPickerConfig?.insertionMode}`}
</Badge>
{:else}
<Badge large color="blue">&leftarrow; Edit or link an input</Badge>
<Badge large color="blue">&leftarrow; Edit or connect an input</Badge>
{/if}
</div>
{/if}

View File

@@ -36,29 +36,45 @@
let selectedId: string = 'settings-metadata'
let nobackenddraft = false
async function loadFlow(): Promise<void> {
loading = true
let flow: Flow
if (stateLoadedFromUrl != undefined && stateLoadedFromUrl?.flow?.path == $page.params.path) {
sendUserToast('Flow restored from draft', false, [
sendUserToast('Flow restored from ephemeral autosave', false, [
{
label: 'Restore last saved version instead',
label: 'Discard autosave and reload',
callback: () => {
FlowService.getFlowByPath({
workspace: $workspaceStore!,
path: $page.params.path
}).then((flow) => {
$flowStore = flow
})
stateLoadedFromUrl = undefined
goto(`/flows/edit/${flow!.path}`)
loadFlow()
}
}
])
flow = stateLoadedFromUrl.flow
} else {
flow = await FlowService.getFlowByPath({
const flowWithDraft = await FlowService.getFlowByPathWithDraft({
workspace: $workspaceStore!,
path: $page.params.path
})
if (flowWithDraft.draft != undefined && !nobackenddraft) {
flow = flowWithDraft.draft
if (!flowWithDraft.draft_only) {
sendUserToast('flow loaded from latest saved draft', false, [
{
label: 'Ignore draft and load from latest deployed version',
callback: () => {
stateLoadedFromUrl = undefined
nobackenddraft = true
goto(`/flows/edit/${flow!.path}`)
loadFlow()
}
}
])
}
} else {
flow = flowWithDraft
}
}
await initFlow(flow, flowStore, flowStateStore)

View File

@@ -28,9 +28,9 @@
async function loadScript(): Promise<void> {
if (scriptLoadedFromUrl != undefined && scriptLoadedFromUrl.path == $page.params.path) {
script = scriptLoadedFromUrl
sendUserToast('Script loaded from latest state stored in the URL', false, [
sendUserToast('Script loaded from latest autosave stored in the URL', false, [
{
label: 'Discard state and reload',
label: 'Discard autosave and reload',
callback: () => {
scriptLoadedFromUrl = undefined
goto(`/scripts/edit/${script!.path}`)
@@ -52,23 +52,23 @@
})
if (scriptWithDraft.draft != undefined) {
script = scriptWithDraft.draft
if (!scriptWithDraft.draft_only) {
sendUserToast('Script loaded from latest saved draft', false, [
{
label: 'Ignore draft and load from latest deployed version',
callback: () => {
scriptLoadedFromUrl = undefined
hash = scriptWithDraft.hash
console.log(hash)
goto(`/scripts/edit/${script!.path}`)
loadScript()
}
}
])
}
} else {
script = scriptWithDraft
}
if (!scriptWithDraft.draft_only) {
sendUserToast('Script loaded from latest saved draft', false, [
{
label: 'Ignore draft and load from latest deployed version',
callback: () => {
scriptLoadedFromUrl = undefined
hash = scriptWithDraft.hash
console.log(hash)
goto(`/scripts/edit/${script!.path}`)
loadScript()
}
}
])
}
topHash = scriptWithDraft.hash
}
}