feat: introduce draft for flows
This commit is contained in:
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)>,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <Kbd small>Ctrl</Kbd><Kbd small>S</Kbd>
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
loading={loadingDraft}
|
||||
size="xs"
|
||||
startIcon={{ icon: faSave }}
|
||||
on:click={() => saveDraft()}
|
||||
>
|
||||
Save draft <Kbd small>Ctrl</Kbd><Kbd small>S</Kbd>
|
||||
</Button>
|
||||
<Button
|
||||
loading={loadingSave}
|
||||
size="xs"
|
||||
startIcon={{ icon: faSave }}
|
||||
on:click={() => saveFlow()}
|
||||
>
|
||||
Deploy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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 →</Button
|
||||
}}><Plug size={16} /> →</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 <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 <Kbd small>{getModifierKey()}</Kbd><Kbd small>Enter</Kbd></Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ModulePreviewForm {pickableProperties} {mod} {schema} bind:args={stepArgs} />
|
||||
</Pane>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -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 <Kbd>{getModifierKey()}</Kbd>
|
||||
Save draft <Kbd>{getModifierKey()}</Kbd>
|
||||
<Kbd>S</Kbd>
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
loading={loadingSave}
|
||||
size="sm"
|
||||
startIcon={{ icon: faSave }}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
{:else if !draft_only}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
let size = 50
|
||||
let size = 40
|
||||
</script>
|
||||
|
||||
<div class="h-full overflow-hidden border-t">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
{`Mode: ${$propPickerConfig?.insertionMode}`}
|
||||
</Badge>
|
||||
{:else}
|
||||
<Badge large color="blue">← Edit or link an input</Badge>
|
||||
<Badge large color="blue">← Edit or connect an input</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user