Merge branch 'main' into glm/fix-flow-layout

This commit is contained in:
Guilhem
2026-03-04 14:39:23 +00:00
committed by GitHub
12 changed files with 374 additions and 174 deletions

View File

@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n CASE\n WHEN flow_version.id IS NOT NULL THEN\n flow_version.value -> 'flow_env' -> $3\n ELSE\n root_job.raw_flow -> 'flow_env' -> $3\n END AS \"flow_env: sqlx::types::Json<Box<RawValue>>\"\n FROM\n v2_job current_job\n JOIN\n v2_job root_job ON root_job.id = COALESCE(current_job.root_job, current_job.flow_innermost_root_job, current_job.parent_job, current_job.id)\n AND root_job.workspace_id = current_job.workspace_id\n LEFT JOIN\n flow_version ON flow_version.id = root_job.runnable_id\n AND flow_version.path = root_job.runnable_path\n AND flow_version.workspace_id = root_job.workspace_id\n WHERE\n current_job.id = $1 AND\n current_job.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "flow_env: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "2f53576c2ad58abc24617e911e486d7c4b9bdb1e8fb1f7725060990ef8984943"
}

View File

@@ -1,25 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n CASE\n WHEN flow_version.id IS NOT NULL THEN\n (flow_version.value -> 'flow_env' -> $3) #> $4\n ELSE\n (root_job.raw_flow -> 'flow_env' -> $3) #> $4\n END AS \"flow_env: sqlx::types::Json<Box<RawValue>>\"\n FROM\n v2_job current_job\n JOIN\n v2_job root_job ON root_job.id = COALESCE(current_job.root_job, current_job.flow_innermost_root_job, current_job.parent_job, current_job.id)\n AND root_job.workspace_id = current_job.workspace_id\n LEFT JOIN\n flow_version ON flow_version.id = root_job.runnable_id\n AND flow_version.path = root_job.runnable_path\n AND flow_version.workspace_id = root_job.workspace_id\n WHERE\n current_job.id = $1 AND\n current_job.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "flow_env: sqlx::types::Json<Box<RawValue>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text",
"TextArray"
]
},
"nullable": [
null
]
},
"hash": "c23bea7db9623a60683596b7d6e689e2c0100c1569436a01b207876aaa470154"
}

View File

@@ -8857,9 +8857,8 @@ paths:
type: boolean
flow_env:
type: object
description: Environment variables available to all steps
additionalProperties:
type: string
description: "Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource)."
additionalProperties: {}
priority:
type: number
description: Execution priority (higher numbers run first)
@@ -14644,9 +14643,8 @@ paths:
type: boolean
flow_env:
type: object
description: Environment variables available to all steps
additionalProperties:
type: string
description: "Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource)."
additionalProperties: {}
priority:
type: number
description: Execution priority (higher numbers run first)

View File

@@ -448,14 +448,15 @@ async fn get_flow_env_by_flow_job_id(
Path((w_id, flow_job_id, var_name)): Path<(String, Uuid, String)>,
Query(JsonPath { json_path, .. }): Query<JsonPath>,
) -> windmill_common::error::JsonResult<Box<JsonRawValue>> {
let flow_env = sqlx::query_scalar!(
// Fetch raw value (without json_path) to check for $var:/$res: references
let raw_value = sqlx::query_scalar!(
r#"
SELECT
CASE
WHEN flow_version.id IS NOT NULL THEN
(flow_version.value -> 'flow_env' -> $3) #> $4
flow_version.value -> 'flow_env' -> $3
ELSE
(root_job.raw_flow -> 'flow_env' -> $3) #> $4
root_job.raw_flow -> 'flow_env' -> $3
END AS "flow_env: sqlx::types::Json<Box<RawValue>>"
FROM
v2_job current_job
@@ -472,16 +473,86 @@ async fn get_flow_env_by_flow_job_id(
flow_job_id,
w_id,
var_name,
json_path
.as_ref()
.map(|x| x.split(".").collect::<Vec<_>>())
.unwrap_or_default() as Vec<&str>,
)
.fetch_optional(&db)
.await?
.map(|r| r.map(|x| x.0))
.flatten()
.unwrap_or_else(|| to_raw_value(&serde_json::Value::Null));
.and_then(|r| r.map(|x| x.0));
// Resolve $var:/$res: references if present
let resolved = if let Some(raw) = raw_value {
let raw_str = raw.get();
let db_authed = windmill_common::db::DbWithOptAuthed::<ApiAuthed>::from_authed(
&authed,
db.clone(),
None,
);
if let Some(path) = raw_str
.strip_prefix("\"$var:")
.and_then(|s| s.strip_suffix("\""))
{
match windmill_store::variables::get_value_internal(&db_authed, &w_id, path, false)
.await
{
Ok(val) => to_raw_value(&serde_json::Value::String(val)),
Err(e) => {
tracing::warn!("Failed to resolve flow_env variable $var:{path}: {e}");
raw
}
}
} else if let Some(path) = raw_str
.strip_prefix("\"$res:")
.and_then(|s| s.strip_suffix("\""))
{
match windmill_store::resources::get_resource_value_interpolated_internal(
&db_authed,
&w_id,
path,
Some(flow_job_id),
Some(&tokened.token),
false,
)
.await
{
Ok(Some(val)) => to_raw_value(&val),
Ok(None) => {
tracing::warn!(
"Failed to resolve flow_env resource $res:{path}: resource not found"
);
raw
}
Err(e) => {
tracing::warn!("Failed to resolve flow_env resource $res:{path}: {e}");
raw
}
}
} else {
raw
}
} else {
to_raw_value(&serde_json::Value::Null)
};
// Apply json_path navigation on the (possibly resolved) value
let flow_env = if let Some(ref jp) = json_path {
let mut value: serde_json::Value =
serde_json::from_str(resolved.get()).unwrap_or(serde_json::Value::Null);
for part in jp.split('.') {
value = match value {
serde_json::Value::Object(ref mut map) => {
map.remove(part).unwrap_or(serde_json::Value::Null)
}
serde_json::Value::Array(ref arr) => part
.parse::<usize>()
.ok()
.and_then(|i| arr.get(i).cloned())
.unwrap_or(serde_json::Value::Null),
_ => serde_json::Value::Null,
};
}
to_raw_value(&value)
} else {
resolved
};
log_job_view(
&db,

View File

@@ -11,7 +11,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use crate::common::{cached_result_path, get_root_job_id, save_in_cache};
use crate::common::{cached_result_path, get_root_job_id, save_in_cache, transform_json};
use crate::js_eval::{eval_timeout, IdContext};
use crate::worker_utils::get_tag_and_concurrency;
use crate::{
@@ -53,7 +53,7 @@ use windmill_common::runnable_settings::{
use windmill_common::scripts::{ScriptHash, ScriptRunnableSettingsInline};
use windmill_common::users::username_to_permissioned_as;
use windmill_common::utils::WarnAfterExt;
use windmill_common::worker::to_raw_value;
use windmill_common::worker::{to_raw_value, Connection};
use windmill_common::{
add_time, get_latest_flow_version_info_for_path, get_script_info_for_hash, FlowVersionInfo,
ScriptHashInfo, DB,
@@ -2245,6 +2245,35 @@ pub async fn handle_flow(
killpill_rx: &tokio::sync::broadcast::Receiver<()>,
) -> anyhow::Result<()> {
let flow = flow_data.value();
// Resolve $var: and $res: references in flow_env.
// We resolve into a separate variable to avoid cloning the entire FlowValue
// (which includes modules, failure_module, etc.) just to replace flow_env.
let resolved_env;
let flow_env = if let Some(ref env) = flow.flow_env {
match transform_json(
client,
&flow_job.workspace_id,
env,
&flow_job,
&Connection::Sql(db.clone()),
)
.await
{
Ok(Some(resolved)) => {
resolved_env = resolved;
Some(&resolved_env)
}
Ok(None) => flow.flow_env.as_ref(),
Err(e) => {
tracing::warn!("Failed to resolve flow_env references: {e}");
flow.flow_env.as_ref()
}
}
} else {
None
};
let status = flow_job
.parse_flow_status()
.with_context(|| "Unable to parse flow status")?;
@@ -2348,6 +2377,7 @@ pub async fn handle_flow(
flow_job,
status,
flow,
flow_env,
db,
client,
last_result.clone(),
@@ -2448,6 +2478,7 @@ async fn push_next_flow_job(
flow_job: Arc<MiniPulledJob>,
mut status: FlowStatus,
flow: &FlowValue,
flow_env: Option<&HashMap<String, Box<RawValue>>>,
db: &sqlx::Pool<sqlx::Postgres>,
client: &AuthedClient,
last_job_result: Option<Arc<Box<RawValue>>>,
@@ -2580,7 +2611,7 @@ async fn push_next_flow_job(
let skip = compute_bool_from_expr(
&skip_expr,
arc_flow_job_args.clone(),
flow.flow_env.as_ref(),
flow_env,
Arc::new(to_raw_value(&json!("{}"))),
None,
None,
@@ -2705,7 +2736,7 @@ async fn push_next_flow_job(
expr.to_string(),
context,
Some(arc_flow_job_args.clone()),
flow.flow_env.as_ref(),
flow_env,
None,
None,
None
@@ -2966,7 +2997,7 @@ async fn push_next_flow_job(
&input_transform,
arc_last_job_result.clone(),
Some(arc_flow_job_args.clone()),
flow.flow_env.as_ref(),
flow_env,
Some(client),
None,
)
@@ -3004,7 +3035,7 @@ async fn push_next_flow_job(
&status.retry,
arc_last_job_result.clone(),
arc_flow_job_args.clone(),
flow.flow_env.as_ref(),
flow_env,
Some(client),
)
.await?
@@ -3092,7 +3123,7 @@ async fn push_next_flow_job(
compute_bool_from_expr(
&skip_if.expr,
arc_flow_job_args.clone(),
flow.flow_env.as_ref(),
flow_env,
arc_last_job_result.clone(),
None,
Some(&idcontext),
@@ -3182,7 +3213,7 @@ async fn push_next_flow_job(
};
transform_input(
arc_flow_job_args.clone(),
flow.flow_env.as_ref(),
flow_env,
arc_last_job_result.clone(),
input_transforms,
resumes.clone(),
@@ -3209,7 +3240,7 @@ async fn push_next_flow_job(
let next_flow_transform = compute_next_flow_transform(
arc_flow_job_args.clone(),
arc_last_job_result.clone(),
flow.flow_env.as_ref(),
flow_env,
&flow_job,
&flow,
transform_context,
@@ -3373,7 +3404,7 @@ async fn push_next_flow_job(
let ctx = get_transform_context(&flow_job, "", &status);
let ti = transform_input(
Marc::new(args),
flow.flow_env.as_ref(),
flow_env,
arc_last_job_result.clone(),
input_transforms,
resumes.clone(),
@@ -3428,7 +3459,7 @@ async fn push_next_flow_job(
let ctx = get_transform_context(&flow_job, &previous_id, &status);
let ti = transform_input(
Marc::new(hm),
flow.flow_env.as_ref(),
flow_env,
arc_last_job_result.clone(),
input_transforms,
resumes.clone(),
@@ -3546,7 +3577,7 @@ async fn push_next_flow_job(
timeout_transform,
arc_last_job_result.clone(),
Some(arc_flow_job_args.clone()),
flow.flow_env.as_ref(),
flow_env,
Some(client),
Some(&ctx),
)
@@ -3625,7 +3656,7 @@ async fn push_next_flow_job(
parallelism_transform,
arc_last_job_result.clone(),
Some(arc_flow_job_args.clone()),
flow.flow_env.as_ref(),
flow_env,
Some(client),
Some(&ctx),
)
@@ -4461,7 +4492,7 @@ async fn compute_next_flow_transform(
let pred = compute_bool_from_expr(
&b.expr,
arc_flow_job_args.clone(),
flow.flow_env.as_ref(),
flow_env,
arc_last_job_result.clone(),
None,
Some(&idcontext),

View File

@@ -5,6 +5,8 @@
import { Button } from '$lib/components/common'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
import Tab from '$lib/components/common/tabs/Tab.svelte'
import Modal from '$lib/components/common/modal/Modal.svelte'
import { LayoutDashboard, Loader2, Plus, Code2 } from 'lucide-svelte'
import { importStore } from '../apps/store'
@@ -14,12 +16,21 @@
let pendingRaw: string = $state('')
let importType: 'yaml' | 'json' = $state('yaml')
let appKind: 'lowcode' | 'fullcode' = $state('lowcode')
let appTypeModalOpen = $state(false)
async function importRaw() {
$importStore = importType === 'yaml' ? YAML.parse(pendingRaw) : JSON.parse(pendingRaw)
await goto('/apps/add?nodraft=true')
const parsed = importType === 'yaml' ? YAML.parse(pendingRaw) : JSON.parse(pendingRaw)
if (appKind === 'fullcode') {
// Navigation to /apps_raw/add triggers a full page reload (for cross-origin isolation),
// so the in-memory importStore would be lost. Use sessionStorage instead.
sessionStorage.setItem('rawAppImport', JSON.stringify(parsed))
await goto('/apps_raw/add?nodraft=true')
} else {
$importStore = parsed
await goto('/apps/add?nodraft=true')
}
drawer?.closeDrawer?.()
}
@@ -51,17 +62,19 @@
variant="accent"
dropdownItems={[
{
label: 'Import low-code app from YAML',
label: 'Import low-code app',
onClick: () => {
drawer?.toggleDrawer?.()
appKind = 'lowcode'
importType = 'yaml'
drawer?.toggleDrawer?.()
}
},
{
label: 'Import low-code app from JSON',
label: 'Import full-code app',
onClick: () => {
appKind = 'fullcode'
importType = 'yaml'
drawer?.toggleDrawer?.()
importType = 'json'
}
}
]}
@@ -118,22 +131,32 @@
</div>
</Modal>
<!-- Raw JSON -->
<!-- Import Drawer -->
<Drawer bind:this={drawer} size="800px">
<DrawerContent
title={'Import low-code app from ' + (importType === 'yaml' ? 'YAML' : 'JSON')}
title={appKind === 'fullcode' ? 'Import full-code app' : 'Import low-code app'}
on:close={() => drawer?.toggleDrawer?.()}
>
{#await import('$lib/components/SimpleEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
bind:code={pendingRaw}
lang={importType}
class="h-full"
fixedOverflowWidgets={false}
/>
{/await}
<Tabs bind:selected={importType}>
<Tab value="yaml" label="YAML" />
<Tab value="json" label="JSON" />
{#snippet content()}
<div class="relative pt-2 h-full">
{#key importType}
{#await import('$lib/components/SimpleEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
bind:code={pendingRaw}
lang={importType}
class="h-full"
fixedOverflowWidgets={false}
/>
{/await}
{/key}
</div>
{/snippet}
</Tabs>
{#snippet actions()}
<Button size="sm" on:click={importRaw}>Import</Button>
{/snippet}

View File

@@ -5,17 +5,21 @@
import { writable } from 'svelte/store'
import type { FlowEditorContext } from '../types'
import { Button } from '$lib/components/common'
import { Plus, Trash2 } from 'lucide-svelte'
import { DollarSign, Plus, Trash2 } from 'lucide-svelte'
import FlowCard from '../common/FlowCard.svelte'
import JsonEditor from '$lib/components/JsonEditor.svelte'
import Label from '$lib/components/Label.svelte'
import Select from '$lib/components/select/Select.svelte'
import ItemPicker from '$lib/components/ItemPicker.svelte'
import ResourcePicker from '$lib/components/ResourcePicker.svelte'
import { VariableService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
interface Props {
noEditor: boolean
}
type EnvVarType = 'string' | 'json'
type EnvVarType = 'string' | 'json' | 'resource'
interface EnvVarEntry {
id: string
@@ -37,6 +41,9 @@
function determineValueType(value: any): EnvVarType {
if (typeof value === 'string') {
if (value.startsWith('$res:')) {
return 'resource'
}
try {
JSON.parse(value)
return value.trim().startsWith('{') ||
@@ -53,30 +60,51 @@
let flowEnvTypes = $state<Record<string, EnvVarType>>({})
const typeOptions = [
{ label: 'String', value: 'string' as EnvVarType },
{ label: 'JSON', value: 'json' as EnvVarType }
const typeOptions: { label: string; value: EnvVarType }[] = [
{ label: 'String', value: 'string' },
{ label: 'JSON', value: 'json' },
{ label: 'Resource', value: 'resource' }
]
// Track resource paths separately for bind:value with ResourcePicker
let resourcePaths = $state<Record<string, string | undefined>>({})
// Initialize resourcePaths from existing flow_env values
for (const [key, value] of Object.entries(flowStore.val.value.flow_env || {})) {
if (typeof value === 'string' && value.startsWith('$res:')) {
resourcePaths[key] = value.substring('$res:'.length)
}
}
// Initialize types for new keys and sync resourcePaths → flow_env
$effect(() => {
for (const [key, value] of flowEnvVarsMap.entries()) {
if (!flowEnvTypes[key]) {
flowEnvTypes[key] = determineValueType(value)
}
}
})
$effect(() => {
for (const [key, type] of Object.entries(flowEnvTypes)) {
if (flowStore.val.value.flow_env && key in flowStore.val.value.flow_env) {
const currentType = determineValueType(flowStore.val.value.flow_env[key])
if (currentType !== type) {
updateEnvType(key, type)
for (const [key, path] of Object.entries(resourcePaths)) {
if (flowStore.val.value.flow_env && flowEnvTypes[key] === 'resource') {
const newVal = '$res:' + (path || '')
if (flowStore.val.value.flow_env[key] !== newVal) {
flowStore.val.value.flow_env[key] = newVal
flowStore.val = flowStore.val
}
}
}
})
// Convert values when user changes the type dropdown
let prevTypes: Record<string, EnvVarType> = {}
$effect(() => {
for (const [key, type] of Object.entries(flowEnvTypes)) {
if (prevTypes[key] && prevTypes[key] !== type) {
updateEnvType(key, type)
}
prevTypes[key] = type
}
})
let flowEnvEntries = $derived(
Array.from(flowEnvVarsMap.entries()).map(([key, value]): EnvVarEntry => {
const stringValue = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
@@ -113,6 +141,7 @@
if (flowStore.val.value.flow_env && key in flowStore.val.value.flow_env) {
delete flowStore.val.value.flow_env[key]
delete flowEnvTypes[key]
delete resourcePaths[key]
flowStore.val = flowStore.val
}
}
@@ -150,32 +179,55 @@
flowStore.val.value.flow_env = newEnvVars
delete flowEnvTypes[oldKey]
flowEnvTypes[newKey] = type
// Move resource path if applicable
if (type === 'resource' && oldKey in resourcePaths) {
resourcePaths[newKey] = resourcePaths[oldKey]
delete resourcePaths[oldKey]
}
flowStore.val = flowStore.val
}
}
function updateEnvType(key: string, newType: EnvVarType) {
if (flowStore.val.value.flow_env && key in flowStore.val.value.flow_env) {
const currentValue = flowStore.val.value.flow_env[key]
const stringValue =
typeof currentValue === 'string' ? currentValue : JSON.stringify(currentValue, null, 2)
flowEnvTypes[key] = newType
if (newType === 'json') {
try {
const parsed = JSON.parse(stringValue)
flowStore.val.value.flow_env[key] = parsed
} catch {
flowStore.val.value.flow_env[key] = stringValue
if (newType === 'resource') {
flowStore.val.value.flow_env[key] = '$res:'
resourcePaths[key] = ''
} else if (newType === 'json') {
delete resourcePaths[key]
const currentValue = flowStore.val.value.flow_env[key]
if (typeof currentValue === 'string') {
try {
flowStore.val.value.flow_env[key] = JSON.parse(currentValue)
} catch {
// keep as string if not valid JSON
}
}
} else {
flowStore.val.value.flow_env[key] = stringValue
delete resourcePaths[key]
const currentValue = flowStore.val.value.flow_env[key]
if (typeof currentValue !== 'string') {
flowStore.val.value.flow_env[key] = JSON.stringify(currentValue, null, 2)
}
}
flowStore.val = flowStore.val
}
}
function setVarPath(key: string, path: string) {
if (flowStore.val.value.flow_env) {
flowStore.val.value.flow_env[key] = '$var:' + path
flowStore.val = flowStore.val
}
}
let variablePicker: ItemPicker | undefined = $state(undefined)
let pickForKey: string | undefined = $state(undefined)
setContext<PropPickerWrapperContext>('PropPickerWrapper', {
inputMatches: writable(undefined),
connectProp: () => {},
@@ -192,8 +244,8 @@
Flow envs can be referenced in any flow step input using the syntax{' '}
<code>flow_env.VARIABLE_NAME</code> or <code>flow_env["VARIABLE_NAME"]</code>. These
variables are available in the property picker and can be used in JavaScript expressions and
input bindings. You can choose between String or JSON types for each variable - JSON types
allow complex data structures.
input bindings. String values can link to workspace variables using the <DollarSign size={12}
class="inline" /> button. Resource type references workspace resources resolved at runtime.
</Alert>
{#if flowEnvEntries.length === 0}
@@ -246,10 +298,13 @@
{/if}
</div>
<div class="flex flex-col gap-1">
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="text-sm font-medium">Value</label>
{#if entry.type === 'json'}
<Label label="Value">
{#if entry.type === 'resource'}
<ResourcePicker
bind:value={resourcePaths[entry.key]}
disabled={noEditor}
/>
{:else if entry.type === 'json'}
<div class="w-full">
<JsonEditor
bind:code={entry.displayValue}
@@ -261,16 +316,43 @@
/>
</div>
{:else}
<input
type="text"
value={entry.displayValue}
oninput={(e) => updateEnvValue(entry.key, e.currentTarget.value, 'string')}
disabled={noEditor}
class="input w-full"
placeholder="Variable value"
/>
<div class="relative group w-full">
<input
type="text"
value={entry.displayValue}
oninput={(e) =>
updateEnvValue(entry.key, e.currentTarget.value, 'string')}
disabled={noEditor}
class="input w-full"
placeholder="Variable value"
/>
{#if !noEditor}
<Button
iconOnly
startIcon={{ icon: DollarSign }}
unifiedSize="sm"
onClick={() => {
pickForKey = entry.key
variablePicker?.openDrawer?.()
}}
wrapperClasses="opacity-0 group-hover:opacity-100 transition-opacity absolute right-2 top-1/2 -translate-y-1/2 bg-surface-input"
variant="subtle"
title="Insert a Variable"
/>
{/if}
</div>
{#if typeof entry.value === 'string' && entry.value.startsWith('$var:') && entry.value.length > 5}
<div class="text-2xs text-tertiary">
Linked to variable <a
href="/variables#{entry.value.slice(5)}"
target="_blank"
class="text-accent underline font-normal"
>{entry.value.slice(5)}</a
>
</div>
{/if}
{/if}
</div>
</Label>
</div>
{/each}
</div>
@@ -285,3 +367,20 @@
</div>
</FlowCard>
</div>
<ItemPicker
bind:this={variablePicker}
pickCallback={(path, _) => {
if (pickForKey) {
setVarPath(pickForKey, path)
pickForKey = undefined
}
}}
itemName="Variable"
extraField="path"
loadItems={async () =>
(await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({
name: x.path,
...x
}))}
/>

View File

@@ -27,7 +27,7 @@
import type { PickableProperties } from '../previousResults'
import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte'
import type { PropPickerContext } from '$lib/components/prop_picker'
import type { FlowEditorContext } from '../types'
interface Props {
pickableProperties: PickableProperties | undefined
@@ -67,9 +67,8 @@
const { flowPropPickerConfig } = getContext<PropPickerContext>('PropPickerContext')
flowPropPickerConfig.set(undefined)
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
let flow_env = $derived(pickableProperties?.flow_env || flowStore.val.value.flow_env)
setContext<PropPickerWrapperContext>('PropPickerWrapper', {
propPickerConfig,
inputMatches,
@@ -156,7 +155,6 @@
{extraResults}
{displayContext}
{error}
{flow_env}
previousId={pickableProperties?.previousId}
{pickableProperties}
allowCopy={!notSelectable && !$propPickerConfig}

View File

@@ -19,7 +19,6 @@
error?: boolean
allowCopy?: boolean
previousId?: string | undefined
flow_env?: Record<string, any> | undefined
result?: any | undefined
extraResults?: any
}
@@ -30,7 +29,6 @@
error = false,
allowCopy = false,
previousId = undefined,
flow_env = undefined,
result = undefined,
extraResults = undefined
}: Props = $props()
@@ -39,7 +37,6 @@
let resources: Record<string, any> = $state({})
let displayVariable = $state(false)
let displayResources = $state(false)
let displayFlowEnv = $state(false)
let allResultsCollapsed = $state(true)
let collapsableInitialState:
@@ -47,7 +44,6 @@
allResultsCollapsed: boolean
displayVariable: boolean
displayResources: boolean
displayFlowEnv: boolean
}
| undefined
@@ -139,7 +135,9 @@
resultByIdFiltered = {}
}
if (!$inputMatches?.some((match) => match.word === 'flow_env')) {
flowEnvFiltered = {}
if (search === EMPTY_STRING) {
flowEnvFiltered = pickableProperties.flow_env
}
}
if ($inputMatches?.length == 1) {
filteringFlowInputsOrResult = $inputMatches[0].value
@@ -185,8 +183,7 @@
collapsableInitialState = {
allResultsCollapsed,
displayVariable,
displayResources,
displayFlowEnv
displayResources
}
}
@@ -200,10 +197,6 @@
displayResources = true
return
}
if ($inputMatches[0].word === 'flow_env') {
displayFlowEnv = true
return
}
if ($inputMatches[0].word === 'results') {
allResultsCollapsed = false
return
@@ -214,8 +207,7 @@
if (!collapsableInitialState) {
return
}
;({ allResultsCollapsed, displayVariable, displayResources, displayFlowEnv } =
collapsableInitialState)
;({ allResultsCollapsed, displayVariable, displayResources } = collapsableInitialState)
collapsableInitialState = undefined
}
@@ -279,6 +271,18 @@
/>
</div>
{/if}
{#if flowEnvFiltered && Object.keys(flowEnvFiltered ?? {}).length > 0}
<span class={categoryTitleClasses}>Flow Env Variables</span>
<div class={categoryContentClasses}>
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
json={flowEnvFiltered}
prefix="flow_env"
on:select
/>
</div>
{/if}
{#if error}
<span class={categoryTitleClasses}>Error</span>
<div class={categoryContentClasses}>
@@ -445,45 +449,6 @@
{/if}
</div>
{/if}
{#if flow_env && Object.keys(flow_env).length > 0 && $inputMatches?.some((match) => match.word === 'flow_env')}
<div class="overflow-y-auto pb-2">
<span class="font-normal text-xs text-secondary">Flow Env Variables:</span>
{#if displayFlowEnv}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
displayFlowEnv = false
}}
wrapperClasses="inline-flex whitespace-nowrap w-fit"
btnClasses="font-mono h-4 text-2xs font-thin px-1 rounded-[0.275rem]">-</Button
>
<ObjectViewer
{allowCopy}
pureViewer={!$propPickerConfig}
rawKey={false}
json={flowEnvFiltered}
prefix="flow_env"
on:select
/>
{:else}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
displayFlowEnv = true
}}
wrapperClasses="inline-flex whitespace-nowrap w-fit"
btnClasses="font-normal text-2xs rounded-[0.275rem] h-4 px-1"
>
{'{...}'}
</Button>
{/if}
</div>
{/if}
{/if}
<!-- </div> -->
</Scrollable>

View File

@@ -41,10 +41,18 @@
const templateId = $page.url.searchParams.get('template_id')
const hubId = $page.url.searchParams.get('hub')
const importRaw = $importStore
// Check in-memory store first, then sessionStorage (used when full page reload occurs)
let importRaw = $importStore
if ($importStore) {
$importStore = undefined
}
if (!importRaw) {
const sessionData = sessionStorage.getItem('rawAppImport')
if (sessionData) {
sessionStorage.removeItem('rawAppImport')
importRaw = JSON.parse(sessionData)
}
}
const appState = nodraft || hubId ? undefined : localStorage.getItem('rawapp')
@@ -189,7 +197,7 @@
files: svelte5Template
}
]
let templatePicker = $state(nodraft != null)
let templatePicker = $state(nodraft != null && !importRaw)
let reloadCounter = $state(0)
// Modal state

View File

@@ -40,7 +40,8 @@
EyeOff,
Circle
} from 'lucide-svelte'
import { untrack } from 'svelte'
import { onMount, untrack } from 'svelte'
import { page } from '$app/stores'
type ListableVariableW = ListableVariable & { canWrite: boolean }
@@ -202,6 +203,14 @@
loadContextualVariables()
}, 5000)
}
onMount(() => {
let hash = $page.url.hash
if (hash.length > 1) {
let path = hash.slice(1)
variableEditor?.editVariable(path)
}
})
</script>
<DeployWorkspaceDrawer bind:this={deploymentDrawer} />

View File

@@ -96,9 +96,8 @@ components:
type: boolean
flow_env:
type: object
description: Environment variables available to all steps
additionalProperties:
type: string
description: "Environment variables available to all steps. Values can be strings, JSON values, or special references: '$var:path' (workspace variable) or '$res:path' (resource)."
additionalProperties: {}
priority:
type: number
description: Execution priority (higher numbers run first)