Add sticky note to saved flow data

This commit is contained in:
Guilhem
2025-11-04 13:37:51 +01:00
parent 3a4a60c225
commit f2a0107dcd
9 changed files with 168 additions and 102 deletions
+30 -30
View File
@@ -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
+48 -47
View File
@@ -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": [
+23
View File
@@ -192,6 +192,29 @@ pub struct FlowValue {
pub priority: Option<i16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub chat_input_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<Vec<FlowNote>>,
}
#[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 {
+1
View File
@@ -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);
@@ -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<Flow | undefined>(undefined)
export async function initFlow(
flow: Flow,
flowStore: StateStore<ExtendedOpenFlow>,
flowStore: StateStore<Flow>,
flowStateStore: StateStore<FlowState>
) {
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<ExtendedOpenFlow>
flowStore: StateStore<OpenFlow>
): Promise<void> {
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]) {
@@ -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}
@@ -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
+6 -6
View File
@@ -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<s
return `
return function (context) {
"use strict";
${Object.keys(context).length > 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) => {
+51
View File
@@ -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: