From f2a0107dcd09d83fbbadf640573b1b7dc3216eb9 Mon Sep 17 00:00:00 2001 From: Guilhem Date: Mon, 3 Nov 2025 20:17:41 +0100 Subject: [PATCH] Add sticky note to saved flow data --- backend/windmill-api/openapi.yaml | 60 ++++++------ backend/windmill-api/src/flows.rs | 95 ++++++++++--------- backend/windmill-common/src/flows.rs | 23 +++++ backend/windmill-queue/src/jobs.rs | 1 + .../lib/components/flows/flowStore.svelte.ts | 19 +--- .../flows/map/FlowModuleSchemaMap.svelte | 7 +- frontend/src/lib/components/flows/types.ts | 2 + frontend/src/lib/components/flows/utils.ts | 12 +-- openflow.openapi.yaml | 51 ++++++++++ 9 files changed, 168 insertions(+), 102 deletions(-) diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 13a654513a..034c7d270e 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -572,12 +572,12 @@ paths: use_case: type: string responses: - '200': + "200": description: Onboarding data submitted successfully content: application/json: schema: - type: string + type: string /w/{workspace}/users/delete/{username}: delete: @@ -14963,8 +14963,7 @@ components: CreatedAfterQueue: name: created_after_queue - description: - filter on jobs created after X for jobs in the queue only + description: filter on jobs created after X for jobs in the queue only in: query schema: type: string @@ -14972,8 +14971,7 @@ components: CreatedBeforeQueue: name: created_before_queue - description: - filter on jobs created before X for jobs in the queue only + description: filter on jobs created before X for jobs in the queue only in: query schema: type: string @@ -15154,6 +15152,8 @@ components: $ref: "../../openflow.openapi.yaml#/components/schemas/FlowStatus" FlowStatusModule: $ref: "../../openflow.openapi.yaml#/components/schemas/FlowStatusModule" + FlowNote: + $ref: "../../openflow.openapi.yaml#/components/schemas/FlowNote" # -- INLINE END -- # Do not change line above @@ -16419,30 +16419,30 @@ components: ScriptLang: type: string enum: [ - python3, - deno, - go, - bash, - powershell, - postgresql, - mysql, - bigquery, - snowflake, - mssql, - oracledb, - graphql, - nativets, - bun, - php, - rust, - ansible, - csharp, - nu, - java, - ruby, - duckdb, - # for related places search: ADD_NEW_LANG - ] + python3, + deno, + go, + bash, + powershell, + postgresql, + mysql, + bigquery, + snowflake, + mssql, + oracledb, + graphql, + nativets, + bun, + php, + rust, + ansible, + csharp, + nu, + java, + ruby, + duckdb, + # for related places search: ADD_NEW_LANG + ] Preview: type: object diff --git a/backend/windmill-api/src/flows.rs b/backend/windmill-api/src/flows.rs index 0f4e8db038..13488ba220 100644 --- a/backend/windmill-api/src/flows.rs +++ b/backend/windmill-api/src/flows.rs @@ -782,8 +782,8 @@ async fn update_flow( sqlx::query!( " - UPDATE - flow + UPDATE + flow SET path = $1, summary = $2, @@ -799,7 +799,7 @@ async fn update_flow( schema = $9::text::json, edited_by = $10, edited_at = now() - WHERE + WHERE path = $11 AND workspace_id = $12", if is_new_path { flow_path } else { &nf.path }, nf.summary, @@ -823,8 +823,8 @@ async fn update_flow( if is_new_path { // if new path, must clone flow to new path and delete old flow for flow_version foreign key constraint sqlx::query!( - "INSERT INTO flow - (workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at) + "INSERT INTO flow + (workspace_id, path, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at) SELECT workspace_id, $1, summary, description, archived, extra_perms, dependency_job, draft_only, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at FROM flow WHERE path = $2 AND workspace_id = $3", @@ -1118,33 +1118,33 @@ async fn get_flow_by_path( let flow_o = if query.with_starred_info.unwrap_or(false) { sqlx::query_as::<_, FlowWithStarred>( r#" - SELECT - flow.workspace_id, - flow.path, - flow.lock_error_logs, - flow.summary, - flow.description, - flow.archived, - flow.extra_perms, - flow.draft_only, - flow.dedicated_worker, - flow.tag, - flow.ws_error_handler_muted, - flow.timeout, - flow.visible_to_runner_only, - flow.on_behalf_of_email, - flow_version.schema, - flow_version.value, - flow_version.created_at AS edited_at, + SELECT + flow.workspace_id, + flow.path, + flow.lock_error_logs, + flow.summary, + flow.description, + flow.archived, + flow.extra_perms, + flow.draft_only, + flow.dedicated_worker, + flow.tag, + flow.ws_error_handler_muted, + flow.timeout, + flow.visible_to_runner_only, + flow.on_behalf_of_email, + flow_version.schema, + flow_version.value, + flow_version.created_at AS edited_at, flow_version.created_by AS edited_by, favorite.path IS NOT NULL AS starred FROM flow LEFT JOIN favorite - ON favorite.favorite_kind = 'flow' - AND favorite.workspace_id = flow.workspace_id - AND favorite.path = flow.path + ON favorite.favorite_kind = 'flow' + AND favorite.workspace_id = flow.workspace_id + AND favorite.path = flow.path AND favorite.usr = $3 - LEFT JOIN flow_version + LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)] WHERE flow.path = $1 AND flow.workspace_id = $2 "#, @@ -1157,28 +1157,28 @@ async fn get_flow_by_path( } else { sqlx::query_as::<_, FlowWithStarred>( r#" - SELECT - flow.workspace_id, - flow.path, - flow.lock_error_logs, - flow.summary, - flow.description, - flow.archived, - flow.extra_perms, - flow.draft_only, - flow.dedicated_worker, - flow.tag, - flow.ws_error_handler_muted, - flow.timeout, - flow.visible_to_runner_only, - flow.on_behalf_of_email, - flow_version.schema, - flow_version.value, - flow_version.created_at AS edited_at, - flow_version.created_by AS edited_by, + SELECT + flow.workspace_id, + flow.path, + flow.lock_error_logs, + flow.summary, + flow.description, + flow.archived, + flow.extra_perms, + flow.draft_only, + flow.dedicated_worker, + flow.tag, + flow.ws_error_handler_muted, + flow.timeout, + flow.visible_to_runner_only, + flow.on_behalf_of_email, + flow_version.schema, + flow_version.value, + flow_version.created_at AS edited_at, + flow_version.created_by AS edited_by, NULL AS starred FROM flow - LEFT JOIN flow_version + LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)] WHERE flow.path = $1 AND flow.workspace_id = $2 "#, @@ -1636,6 +1636,7 @@ mod tests { chat_input_enabled: None, debounce_key: None, debounce_delay_s: None, + ui: None, }; let expect = serde_json::json!({ "modules": [ diff --git a/backend/windmill-common/src/flows.rs b/backend/windmill-common/src/flows.rs index cc47b0b981..58a69c8fb9 100644 --- a/backend/windmill-common/src/flows.rs +++ b/backend/windmill-common/src/flows.rs @@ -192,6 +192,29 @@ pub struct FlowValue { pub priority: Option, #[serde(skip_serializing_if = "Option::is_none")] pub chat_input_enabled: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub notes: Option>, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct FlowNote { + pub id: String, + pub text: String, + pub position: FlowNotePosition, + pub size: FlowNoteSize, + pub color: String, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct FlowNotePosition { + pub x: f64, + pub y: f64, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct FlowNoteSize { + pub width: f64, + pub height: f64, } impl FlowValue { diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index 64247efd82..1e68402e30 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -4149,6 +4149,7 @@ pub async fn push<'c, 'd>( skip_expr: None, preprocessor_module: None, chat_input_enabled: None, + notes: None, }; // this is a new flow being pushed, flow_status is set to flow_value: let flow_status: FlowStatus = FlowStatus::new(&flow_value); diff --git a/frontend/src/lib/components/flows/flowStore.svelte.ts b/frontend/src/lib/components/flows/flowStore.svelte.ts index e4d4d74b35..957e51b4fb 100644 --- a/frontend/src/lib/components/flows/flowStore.svelte.ts +++ b/frontend/src/lib/components/flows/flowStore.svelte.ts @@ -1,9 +1,8 @@ -import type { Flow } from '$lib/gen' +import type { Flow, OpenFlow } from '$lib/gen' import { writable } from 'svelte/store' import { initFlowState, type FlowState } from './flowState' import { sendUserToast } from '$lib/toast' import type { StateStore } from '$lib/utils' -import type { ExtendedOpenFlow } from './types' export type FlowMode = 'push' | 'pull' @@ -11,24 +10,16 @@ export const importFlowStore = writable(undefined) export async function initFlow( flow: Flow, - flowStore: StateStore, + flowStore: StateStore, flowStateStore: StateStore ) { await initFlowState(flow, flowStateStore) - // Initialize ExtendedOpenFlow with ui.notes field if not present - const extendedFlow: ExtendedOpenFlow = { - ...flow, - ui: { - notes: (flow as any).ui?.notes || [], - ...(flow as any).ui - } - } - flowStore.val = extendedFlow + flowStore.val = flow } export async function copyFirstStepSchema( flowState: FlowState, - flowStore: StateStore + flowStore: StateStore ): Promise { const firstModuleId = flowStore.val.value.modules[0]?.id @@ -49,7 +40,7 @@ export async function copyFirstStepSchema( return sendUserToast('No first step found', true) } -export async function getFirstStepSchema(flowState: FlowState, flow: ExtendedOpenFlow) { +export async function getFirstStepSchema(flowState: FlowState, flow: OpenFlow) { const firstModuleId = flow.value.modules[0]?.id if (!firstModuleId || !flowState[firstModuleId]) { diff --git a/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte b/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte index e2e03f0a60..e3ab7afe4f 100644 --- a/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte +++ b/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte @@ -426,12 +426,9 @@ maxHeight={minHeight} modules={flowStore.val.value.modules} {noteMode} - notes={flowStore.val.ui?.notes || []} + notes={flowStore.val.value?.notes || []} onNotesChange={(newNotes) => { - if (!flowStore.val.ui) { - flowStore.val.ui = {} - } - flowStore.val.ui.notes = newNotes + flowStore.val.value.notes = newNotes }} preprocessorModule={flowStore.val.value?.preprocessor_module} {selectedId} diff --git a/frontend/src/lib/components/flows/types.ts b/frontend/src/lib/components/flows/types.ts index 57485e6b84..3e84ed4a35 100644 --- a/frontend/src/lib/components/flows/types.ts +++ b/frontend/src/lib/components/flows/types.ts @@ -15,6 +15,7 @@ import type ResourceEditorDrawer from '../ResourceEditorDrawer.svelte' import type { ModulesTestStates } from '../modulesTest.svelte' import type { ButtonProp } from '$lib/components/DiffEditor.svelte' +// Type for flow notes stored in the UI field export type Note = { id: string text: string @@ -36,6 +37,7 @@ export type FlowInput = Record< } > +// Extended OpenFlow with additional properties not in the core spec export type ExtendedOpenFlow = OpenFlow & { tag?: string ws_error_handler_muted?: boolean diff --git a/frontend/src/lib/components/flows/utils.ts b/frontend/src/lib/components/flows/utils.ts index 2e04c2a17e..7d94bb335a 100644 --- a/frontend/src/lib/components/flows/utils.ts +++ b/frontend/src/lib/components/flows/utils.ts @@ -1,6 +1,5 @@ import { JobService, - type Flow, type FlowModule, type InputTransform, type Job, @@ -22,10 +21,11 @@ function create_context_function_template(eval_string: string, context: Record 0 - ? `let ${Object.keys(context).map((key) => ` ${key} = context['${key}']`)};` - : `` - } +${ + Object.keys(context).length > 0 + ? `let ${Object.keys(context).map((key) => ` ${key} = context['${key}']`)};` + : `` +} return ${eval_string} }` } @@ -100,7 +100,7 @@ export function cleanInputs(flow: OpenFlow | any): OpenFlow & { visible_to_runner_only?: boolean on_behalf_of_email?: string } { - const newFlow: Flow = JSON.parse(JSON.stringify(flow)) + const newFlow: OpenFlow = JSON.parse(JSON.stringify(flow)) newFlow.value.modules.forEach((mod) => { if (mod.value.type == 'rawscript' || mod.value.type == 'script') { Object.values(mod.value.input_transforms ?? {}).forEach((inp) => { diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index 34b1731352..519e08961b 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -69,6 +69,11 @@ components: chat_input_enabled: type: boolean description: Whether this flow accepts chat-style input + notes: + type: array + description: Sticky notes attached to the flow + items: + $ref: "#/components/schemas/FlowNote" required: - modules @@ -99,6 +104,52 @@ components: retry_if: $ref: "#/components/schemas/RetryIf" + FlowNote: + type: object + description: A sticky note attached to a flow for documentation and annotation + properties: + id: + type: string + description: Unique identifier for the note + text: + type: string + description: Content of the note + position: + type: object + description: Position of the note in the flow editor + properties: + x: + type: number + description: X coordinate + y: + type: number + description: Y coordinate + required: + - x + - y + size: + type: object + description: Size of the note in the flow editor + properties: + width: + type: number + description: Width in pixels + height: + type: number + description: Height in pixels + required: + - width + - height + color: + type: string + description: Color of the note (e.g., "yellow", "#ffff00") + required: + - id + - text + - position + - size + - color + RetryIf: type: object properties: