mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 16:02:19 +00:00
feat: dynamic select in flow (#6315)
* update * add dyn select for flows * nits * fix typo * done * update .sqlx * better * nits * fix * nits * nits * fix * improvements * update --------- Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow (\n workspace_id, path, summary, description,\n dependency_job, lock_error_logs, draft_only, tag,\n dedicated_worker, visible_to_runner_only, on_behalf_of_email,\n value, schema, edited_by, edited_at\n ) VALUES (\n $1, $2, $3, $4,\n NULL, '', $5, $6,\n $7, $8, $9,\n $10, $11::text::json, $12, now()\n )",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "081dc94a7d0fdaade77cfb593a025d8c48d7eab3dbb30ca0b43fb1ef45d8d8bd"
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE flow SET path = $1, summary = $2, description = $3,dependency_job = NULL, lock_error_logs = '', draft_only = NULL, tag = $4, dedicated_worker = $5, visible_to_runner_only = $6, on_behalf_of_email = $7, value = $8, schema = $9::text::json, edited_by = $10, edited_at = now()\n WHERE path = $11 AND workspace_id = $12",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2b9607ed838c8c62eb0f2856420389f7be648f52edbb875ff52c96219ed3ba84"
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n flow \n SET\n path = $1,\n summary = $2,\n description = $3,\n dependency_job = NULL,\n lock_error_logs = '',\n draft_only = NULL,\n tag = $4,\n dedicated_worker = $5,\n visible_to_runner_only = $6,\n on_behalf_of_email = $7,\n value = $8,\n schema = $9::text::json,\n edited_by = $10,\n edited_at = now()\n WHERE \n path = $11 AND workspace_id = $12",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d2acc380c45df31f741bbd420c2f4a5b962f7803def9ecd6e33d2aec6abce772"
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow (workspace_id, path, summary, description, dependency_job, lock_error_logs, draft_only, tag, dedicated_worker, visible_to_runner_only, on_behalf_of_email, value, schema, edited_by, edited_at) \n VALUES ($1, $2, $3, $4, NULL, '', $5, $6, $7, $8, $9, $10, $11::text::json, $12, now())",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e4f1ee1568ce3c186b569421c6c8a3039f73d04fc53c67c70e67371f06416ef3"
|
||||
}
|
||||
@@ -16732,6 +16732,7 @@ components:
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
|
||||
|
||||
FlowMetadata:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -409,11 +409,18 @@ async fn create_flow(
|
||||
check_schedule_conflict(&mut tx, &w_id, &nf.path).await?;
|
||||
|
||||
let schema_str = nf.schema.and_then(|x| serde_json::to_string(&x.0).ok());
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO flow (workspace_id, path, summary, description, \
|
||||
dependency_job, lock_error_logs, draft_only, tag, dedicated_worker, visible_to_runner_only, on_behalf_of_email, value, schema, edited_by, edited_at)
|
||||
VALUES ($1, $2, $3, $4, NULL, '', $5, $6, $7, $8, $9, $10, $11::text::json, $12, now())",
|
||||
r#"INSERT INTO flow (
|
||||
workspace_id, path, summary, description,
|
||||
dependency_job, lock_error_logs, draft_only, tag,
|
||||
dedicated_worker, visible_to_runner_only, on_behalf_of_email,
|
||||
value, schema, edited_by, edited_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4,
|
||||
NULL, '', $5, $6,
|
||||
$7, $8, $9,
|
||||
$10, $11::text::json, $12, now()
|
||||
)"#,
|
||||
w_id,
|
||||
nf.path,
|
||||
nf.summary,
|
||||
@@ -744,11 +751,27 @@ async fn update_flow(
|
||||
let schema_str = schema.and_then(|x| serde_json::to_string(&x).ok());
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE flow SET path = $1, summary = $2, description = $3,\
|
||||
dependency_job = NULL, lock_error_logs = '', draft_only = NULL, tag = $4, dedicated_worker = $5, visible_to_runner_only = $6, on_behalf_of_email = $7, \
|
||||
value = $8, schema = $9::text::json, edited_by = $10, edited_at = now()
|
||||
WHERE path = $11 AND workspace_id = $12",
|
||||
if is_new_path { flow_path } else { &nf.path }, // if new path, do not rename directly (to avoid flow_version foreign key constraint)
|
||||
"
|
||||
UPDATE
|
||||
flow
|
||||
SET
|
||||
path = $1,
|
||||
summary = $2,
|
||||
description = $3,
|
||||
dependency_job = NULL,
|
||||
lock_error_logs = '',
|
||||
draft_only = NULL,
|
||||
tag = $4,
|
||||
dedicated_worker = $5,
|
||||
visible_to_runner_only = $6,
|
||||
on_behalf_of_email = $7,
|
||||
value = $8,
|
||||
schema = $9::text::json,
|
||||
edited_by = $10,
|
||||
edited_at = now()
|
||||
WHERE
|
||||
path = $11 AND workspace_id = $12",
|
||||
if is_new_path { flow_path } else { &nf.path },
|
||||
nf.summary,
|
||||
nf.description.unwrap_or_else(String::new),
|
||||
nf.tag,
|
||||
@@ -761,12 +784,15 @@ async fn update_flow(
|
||||
},
|
||||
nf.value,
|
||||
schema_str,
|
||||
&authed.username,
|
||||
authed.username,
|
||||
flow_path,
|
||||
w_id,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await.map_err(|e| error::Error::internal_err(format!("Error updating flow due to flow update: {e:#}")))?;
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error::Error::internal_err(format!("Error updating flow due to flow update: {e:#}"))
|
||||
})?;
|
||||
|
||||
if is_new_path {
|
||||
// if new path, must clone flow to new path and delete old flow for flow_version foreign key constraint
|
||||
@@ -1047,36 +1073,80 @@ async fn get_flow_by_path(
|
||||
let path = path.to_path();
|
||||
check_scopes(&authed, || format!("flows:read:{}", path))?;
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let flow_o = if query.with_starred_info.unwrap_or(false) {
|
||||
sqlx::query_as::<_, FlowWithStarred>(
|
||||
"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
|
||||
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,
|
||||
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
|
||||
AND favorite.usr = $3
|
||||
LEFT JOIN flow_version ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
|
||||
WHERE flow.path = $1 AND flow.workspace_id = $2"
|
||||
AND favorite.workspace_id = flow.workspace_id
|
||||
AND favorite.path = flow.path
|
||||
AND favorite.usr = $3
|
||||
LEFT JOIN flow_version
|
||||
ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
|
||||
WHERE flow.path = $1 AND flow.workspace_id = $2
|
||||
"#,
|
||||
)
|
||||
.bind(path)
|
||||
.bind(w_id)
|
||||
.bind(&authed.username)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
.bind(path)
|
||||
.bind(w_id)
|
||||
.bind(&authed.username)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as::<_, FlowWithStarred>(
|
||||
"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 ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
|
||||
WHERE flow.path = $1 AND flow.workspace_id = $2"
|
||||
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,
|
||||
NULL AS starred
|
||||
FROM flow
|
||||
LEFT JOIN flow_version
|
||||
ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
|
||||
WHERE flow.path = $1 AND flow.workspace_id = $2
|
||||
"#,
|
||||
)
|
||||
.bind(path)
|
||||
.bind(w_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
.bind(path)
|
||||
.bind(w_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
};
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
let flow = not_found_if_none(flow_o, "Flow", path)?;
|
||||
@@ -1115,20 +1185,36 @@ async fn get_flow_by_path_w_draft(
|
||||
let path = path.to_path();
|
||||
check_scopes(&authed, || format!("flows:read:{}", path))?;
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let flow_o = sqlx::query_as::<_, FlowWDraft>(
|
||||
"SELECT flow.path, flow.summary, flow.description, flow_version.schema, flow_version.value, flow.extra_perms, flow.draft_only, flow.ws_error_handler_muted, flow.dedicated_worker, draft.value as draft, flow.tag, flow.visible_to_runner_only, flow.on_behalf_of_email
|
||||
FROM flow
|
||||
"SELECT
|
||||
flow.path,
|
||||
flow.summary,
|
||||
flow.description,
|
||||
flow_version.schema,
|
||||
flow_version.value,
|
||||
flow.extra_perms,
|
||||
flow.draft_only,
|
||||
flow.ws_error_handler_muted,
|
||||
flow.dedicated_worker,
|
||||
draft.value AS draft,
|
||||
flow.tag,
|
||||
flow.visible_to_runner_only,
|
||||
flow.on_behalf_of_email
|
||||
FROM flow
|
||||
LEFT JOIN draft
|
||||
ON flow.path = draft.path AND draft.workspace_id = $2 AND draft.typ = 'flow'
|
||||
LEFT JOIN flow_version
|
||||
ON flow.path = draft.path
|
||||
AND draft.workspace_id = $2
|
||||
AND draft.typ = 'flow'
|
||||
LEFT JOIN flow_version
|
||||
ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
|
||||
WHERE flow.path = $1 AND flow.workspace_id = $2",
|
||||
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)?;
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::{
|
||||
DB,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, sqlx::FromRow)]
|
||||
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Flow {
|
||||
pub workspace_id: String,
|
||||
pub path: String,
|
||||
@@ -91,7 +91,7 @@ pub struct ListableFlow {
|
||||
pub deployment_msg: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, sqlx::FromRow)]
|
||||
#[derive(Debug, Deserialize, sqlx::FromRow)]
|
||||
pub struct NewFlow {
|
||||
pub path: String,
|
||||
pub summary: String,
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
setInputCat as computeInputCat,
|
||||
debounce,
|
||||
emptyString,
|
||||
getSchemaFromProperties
|
||||
getSchemaFromProperties,
|
||||
type DynamicSelect
|
||||
} from '$lib/utils'
|
||||
import { DollarSign, Plus, X, Check, Loader2, ExternalLink } from 'lucide-svelte'
|
||||
import { createEventDispatcher, onDestroy, onMount, tick, untrack } from 'svelte'
|
||||
@@ -35,7 +36,6 @@
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import DynSelect from './DynSelect.svelte'
|
||||
import type { Script } from '$lib/gen'
|
||||
import type { SchemaDiff } from '$lib/components/schema/schemaUtils.svelte'
|
||||
import type { ComponentCustomCSS } from './apps/types'
|
||||
import MultiSelect from './select/MultiSelect.svelte'
|
||||
@@ -94,10 +94,7 @@
|
||||
orderEditable?: boolean
|
||||
shouldDispatchChanges?: boolean
|
||||
noDefaultOnSelectFirst?: boolean
|
||||
helperScript?:
|
||||
| { type: 'inline'; path?: string; lang: Script['language']; code: string }
|
||||
| { type: 'hash'; hash: string }
|
||||
| undefined
|
||||
helperScript?: DynamicSelect.HelperScript
|
||||
otherArgs?: Record<string, any>
|
||||
lightHeader?: boolean
|
||||
diffStatus?: SchemaDiff | undefined
|
||||
@@ -866,7 +863,7 @@
|
||||
{:else if inputCat == 'dynselect'}
|
||||
<DynSelect
|
||||
name={label}
|
||||
args={otherArgs}
|
||||
{otherArgs}
|
||||
{helperScript}
|
||||
bind:value
|
||||
entrypoint={format?.substring('dynselect_'.length) ?? ''}
|
||||
|
||||
@@ -15,33 +15,31 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Script } from '$lib/gen'
|
||||
import { usePromise } from '$lib/svelte5Utils.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import JobLoader, { type Callbacks } from './JobLoader.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { type DynamicSelect } from '$lib/utils'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { untrack } from 'svelte'
|
||||
import { readFieldsRecursively } from '$lib/utils'
|
||||
|
||||
interface Props {
|
||||
value?: any
|
||||
helperScript?:
|
||||
| { type: 'inline'; path?: string; lang: Script['language']; code: string }
|
||||
| { type: 'hash'; hash: string }
|
||||
helperScript?: DynamicSelect.HelperScript
|
||||
entrypoint: string
|
||||
args?: Record<string, any>
|
||||
otherArgs?: Record<string, any>
|
||||
name: string
|
||||
}
|
||||
|
||||
let { value = $bindable(), helperScript, entrypoint, args: _args, name }: Props = $props()
|
||||
let { value = $bindable(), helperScript, entrypoint, otherArgs: otherArgs }: Props = $props()
|
||||
|
||||
let args = $state(structuredClone($state.snapshot(_args)))
|
||||
$effect(() => {
|
||||
readFieldsRecursively(_args, { excludeField: [name] })
|
||||
untrack(() => !deepEqual(args, _args) && (args = $state.snapshot(_args)))
|
||||
})
|
||||
let resultJobLoader: JobLoader | undefined = $state()
|
||||
let _items = usePromise(getItemsFromOptions, { clearValueOnRefresh: false })
|
||||
let items = $derived(_items.value)
|
||||
|
||||
let filterText: string = $state('')
|
||||
let open: boolean = $state(false)
|
||||
|
||||
async function getItemsFromOptions() {
|
||||
return new Promise<{ label: string; value: any }[]>((resolve, reject) => {
|
||||
@@ -61,15 +59,14 @@
|
||||
return
|
||||
}
|
||||
if (result.length == 0) resolve([])
|
||||
|
||||
if (result.every((x) => typeof x == 'string')) {
|
||||
result = result.map((x) => ({ label: x, value: x }))
|
||||
} else if (result.find((x) => validSelectObject(x) != undefined)) {
|
||||
reject(validSelectObject(result.find((x) => validSelectObject(x) != undefined)))
|
||||
} else {
|
||||
if (filterText != undefined && filterText != '')
|
||||
result = result.filter((x) => x['label'].includes(filterText))
|
||||
resolve(result)
|
||||
return
|
||||
}
|
||||
resolve(result)
|
||||
},
|
||||
cancel: () => reject(),
|
||||
doneError({ id, error }) {
|
||||
@@ -81,7 +78,7 @@
|
||||
helperScript?.path ?? 'NO_PATH',
|
||||
helperScript.code,
|
||||
helperScript.lang,
|
||||
{ ...args, filterText, _ENTRYPOINT_OVERRIDE: entrypoint },
|
||||
{ ...otherArgs, filterText, _ENTRYPOINT_OVERRIDE: entrypoint },
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
@@ -89,22 +86,42 @@
|
||||
)
|
||||
: resultJobLoader?.runScriptByHash(
|
||||
helperScript?.hash ?? 'NO_HASH',
|
||||
{ ...args, filterText, _ENTRYPOINT_OVERRIDE: entrypoint },
|
||||
{ ...otherArgs, filterText, _ENTRYPOINT_OVERRIDE: entrypoint },
|
||||
cb
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
let _items = usePromise(getItemsFromOptions)
|
||||
let items = $derived(_items.value)
|
||||
let neverLoaded = $state(true)
|
||||
|
||||
$effect(() => {
|
||||
;[args, name, entrypoint, helperScript, filterText]
|
||||
untrack(() => _items.refresh())
|
||||
if (_items.value && value) {
|
||||
if (!_items.value.find((x) => x.value == value)) {
|
||||
value = undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let resultJobLoader: JobLoader | undefined = $state()
|
||||
let filterText: string = $state('')
|
||||
let open: boolean = $state(false)
|
||||
let lastArgs = $state.snapshot(otherArgs)
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined = $state()
|
||||
let nargs = $state($state.snapshot(otherArgs))
|
||||
$effect(() => {
|
||||
otherArgs
|
||||
untrack(() => clearTimeout(timeout))
|
||||
timeout = setTimeout(() => {
|
||||
nargs = $state.snapshot(otherArgs)
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
;[filterText, entrypoint, helperScript]
|
||||
if (resultJobLoader && (open || neverLoaded || !deepEqual(lastArgs, nargs))) {
|
||||
neverLoaded = false
|
||||
lastArgs = $state.snapshot(otherArgs)
|
||||
_items.refresh()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if helperScript}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
const bubble = createBubbler()
|
||||
import type { Schema } from '$lib/common'
|
||||
import { VariableService } from '$lib/gen'
|
||||
import { VariableService, type ScriptLang } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Button } from './common'
|
||||
import ItemPicker from './ItemPicker.svelte'
|
||||
@@ -20,13 +20,15 @@
|
||||
import Label from './Label.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import { DynamicSelect, emptyString } from '$lib/utils'
|
||||
import Popover from './meltComponents/Popover.svelte'
|
||||
import SchemaFormDnd from './schema/SchemaFormDND.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { tweened } from 'svelte/motion'
|
||||
import type { SchemaDiff } from '$lib/components/schema/schemaUtils.svelte'
|
||||
import type { EditableSchemaFormUi } from '$lib/components/custom_ui'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import Editor from './Editor.svelte'
|
||||
|
||||
// export let openEditTab: () => void = () => {}
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -64,6 +66,9 @@
|
||||
customUi?: EditableSchemaFormUi | undefined
|
||||
pannelExtraButtonWidth?: number
|
||||
class?: string
|
||||
dynSelectCode?: string | undefined
|
||||
dynSelectLang?: ScriptLang | undefined
|
||||
showDynSelectOpt?: boolean
|
||||
openEditTab?: import('svelte').Snippet
|
||||
addProperty?: import('svelte').Snippet
|
||||
runButton?: import('svelte').Snippet
|
||||
@@ -96,6 +101,9 @@
|
||||
customUi = undefined,
|
||||
pannelExtraButtonWidth = 0,
|
||||
class: clazz = '',
|
||||
dynSelectCode = $bindable(),
|
||||
dynSelectLang = $bindable(),
|
||||
showDynSelectOpt = false,
|
||||
openEditTab,
|
||||
addProperty,
|
||||
runButton,
|
||||
@@ -106,6 +114,24 @@
|
||||
if (args == undefined) {
|
||||
args = {}
|
||||
}
|
||||
if (dynSelectLang === undefined) {
|
||||
dynSelectLang = schema?.['x-windmill-dyn-select-lang'] || 'bun'
|
||||
}
|
||||
if (dynSelectCode === undefined) {
|
||||
dynSelectCode = schema?.['x-windmill-dyn-select-code'] || ''
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (schema && dynSelectCode !== undefined && dynSelectLang !== undefined) {
|
||||
if (dynSelectCode && dynSelectCode.trim()) {
|
||||
schema['x-windmill-dyn-select-code'] = dynSelectCode.trim()
|
||||
schema['x-windmill-dyn-select-lang'] = dynSelectLang
|
||||
} else {
|
||||
delete schema['x-windmill-dyn-select-code']
|
||||
delete schema['x-windmill-dyn-select-lang']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export function setDefaults() {
|
||||
@@ -183,9 +209,11 @@
|
||||
? property.type
|
||||
: property.format === 'resource-s3_object'
|
||||
? 'S3'
|
||||
: property.oneOf && property.oneOf.length >= 2
|
||||
? 'oneOf'
|
||||
: 'object'
|
||||
: property.format?.startsWith('dynselect-')
|
||||
? 'dynselect'
|
||||
: property.oneOf && property.oneOf.length >= 2
|
||||
? 'oneOf'
|
||||
: 'object'
|
||||
}
|
||||
|
||||
export function openField(key: string) {
|
||||
@@ -248,7 +276,7 @@
|
||||
let schemaString: string = $state(JSON.stringify(schema, null, '\t'))
|
||||
let error: string | undefined = $state(undefined)
|
||||
let editor: SimpleEditor | undefined = $state(undefined)
|
||||
|
||||
let dynamicSelectEditor: Editor | undefined = $state(undefined)
|
||||
export function updateJson() {
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
editor?.setCode(schemaString)
|
||||
@@ -297,6 +325,45 @@
|
||||
$effect(() => {
|
||||
!!editTab ? openEditTabFn() : closeEditTab()
|
||||
})
|
||||
|
||||
let dynSelectFunctions = $derived(
|
||||
Object.entries(schema?.properties ?? {})
|
||||
.filter(([_, property]) => {
|
||||
const props = property as any
|
||||
return (
|
||||
props.type === 'object' &&
|
||||
(props.format?.startsWith('dynselect-') || props.format?.startsWith('dynselect_'))
|
||||
)
|
||||
})
|
||||
.map(([fieldName, _]) => fieldName.replace(/\s+/g, '_'))
|
||||
)
|
||||
|
||||
let typeOptions = [
|
||||
['String', 'string'],
|
||||
['Number', 'number'],
|
||||
['Integer', 'integer'],
|
||||
['Object', 'object'],
|
||||
['OneOf', 'oneOf'],
|
||||
['Array', 'array'],
|
||||
['Boolean', 'boolean'],
|
||||
['S3 Object', 'S3']
|
||||
]
|
||||
if (showDynSelectOpt) {
|
||||
typeOptions.push(['DynSelect', 'dynselect'])
|
||||
}
|
||||
|
||||
function initDynSelectFn(lang: ScriptLang) {
|
||||
const generateFn = DynamicSelect.getGenerateTemplateFn(lang)
|
||||
return Object.entries(schema?.properties ?? {})
|
||||
.map(([functionName]) => generateFn(functionName))
|
||||
.join('')
|
||||
}
|
||||
|
||||
function updateDynSelectCode(functionName: string, lang: ScriptLang = 'bun') {
|
||||
const generateFn = DynamicSelect.getGenerateTemplateFn(lang)
|
||||
const code = generateFn(functionName)
|
||||
dynSelectCode = dynSelectCode ? dynSelectCode.concat(code) : code
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full">
|
||||
@@ -355,6 +422,11 @@
|
||||
}
|
||||
tick().then(() => dispatch('change', schema))
|
||||
}}
|
||||
helperScript={{
|
||||
type: 'inline',
|
||||
code: dynSelectCode!,
|
||||
lang: dynSelectLang!
|
||||
}}
|
||||
prettifyHeader={isAppInput}
|
||||
disabled={!!previewSchema}
|
||||
{diff}
|
||||
@@ -369,6 +441,57 @@
|
||||
/>
|
||||
|
||||
{@render runButton?.()}
|
||||
|
||||
<div class="h-full">
|
||||
{#if dynSelectFunctions.length > 0}
|
||||
<Section
|
||||
label="Dynamic select functions"
|
||||
collapsable={true}
|
||||
collapsed={false}
|
||||
class="text-sm"
|
||||
>
|
||||
<div class="flex flex-col gap-2 h-full">
|
||||
{#if dynSelectFunctions.length > 0}
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 p-3 rounded-md">
|
||||
<div class="text-sm font-medium text-blue-800 dark:text-blue-200 mb-2">
|
||||
Expected Functions for Dynamic Select Fields:
|
||||
</div>
|
||||
<ul class="text-xs text-blue-700 dark:text-blue-300 space-y-1">
|
||||
{#each dynSelectFunctions as functionName}
|
||||
<li class="font-mono bg-blue-100 dark:bg-blue-800/30 px-2 py-1 rounded">
|
||||
{functionName}()
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
<ToggleButtonGroup
|
||||
bind:selected={dynSelectLang}
|
||||
on:selected={({ detail }) => {
|
||||
dynSelectCode = initDynSelectFn(detail)
|
||||
}}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value="bun" label="Typescript (Bun)" {item} />
|
||||
<ToggleButton value="python3" label="Python" {item} />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
{#key dynSelectLang}
|
||||
<div class="border w-full h-full">
|
||||
<Editor
|
||||
bind:this={dynamicSelectEditor}
|
||||
class="flex flex-1 grow h-80 w-full"
|
||||
scriptLang={dynSelectLang}
|
||||
useWebsockets={false}
|
||||
automaticLayout
|
||||
bind:code={dynSelectCode}
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Pane>
|
||||
@@ -530,6 +653,7 @@
|
||||
(v) => {
|
||||
const isS3 = v == 'S3'
|
||||
const isOneOf = v == 'oneOf'
|
||||
const isDynSelect = v == 'dynselect'
|
||||
|
||||
const emptyProperty = {
|
||||
contentEncoding: undefined,
|
||||
@@ -555,6 +679,15 @@
|
||||
type: 'object',
|
||||
format: 'resource-s3_object'
|
||||
}
|
||||
} else if (isDynSelect) {
|
||||
const functionName = argName.replace(/\s+/g, '_')
|
||||
schema.properties[argName] = {
|
||||
...emptyProperty,
|
||||
type: 'object',
|
||||
format: 'dynselect-' + functionName
|
||||
}
|
||||
updateDynSelectCode(argName, dynSelectLang)
|
||||
dynamicSelectEditor?.setCode(dynSelectCode || '')
|
||||
} else if (isOneOf) {
|
||||
schema.properties[argName] = {
|
||||
...emptyProperty,
|
||||
@@ -604,7 +737,7 @@
|
||||
}}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
{#each [['String', 'string'], ['Number', 'number'], ['Integer', 'integer'], ['Object', 'object'], ['OneOf', 'oneOf'], ['Array', 'array'], ['Boolean', 'boolean'], ['S3 Object', 'S3']] as x}
|
||||
{#each typeOptions as x}
|
||||
<ToggleButton value={x[1]} label={x[0]} {item} />
|
||||
{/each}
|
||||
{/snippet}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { stopPropagation } from 'svelte/legacy'
|
||||
|
||||
import { type Job, JobService, type RestartedFrom, type OpenFlow } from '$lib/gen'
|
||||
import { type Job, JobService, type RestartedFrom, type OpenFlow, type ScriptLang } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Badge, Button } from './common'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
@@ -504,6 +504,14 @@
|
||||
savedArgs = previewArgs.val
|
||||
}}
|
||||
bind:isValid
|
||||
helperScript={flowStore.val.schema?.['x-windmill-dyn-select-code'] &&
|
||||
flowStore.val.schema?.['x-windmill-dyn-select-lang']
|
||||
? {
|
||||
type: 'inline',
|
||||
code: flowStore.val.schema['x-windmill-dyn-select-code'] as string,
|
||||
lang: flowStore.val.schema['x-windmill-dyn-select-lang'] as ScriptLang
|
||||
}
|
||||
: undefined}
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
@@ -571,7 +579,7 @@
|
||||
bind:rightColumnSelect
|
||||
bind:isOwner
|
||||
{render}
|
||||
customUi={customUi}
|
||||
{customUi}
|
||||
/>
|
||||
{:else}
|
||||
<div class="italic text-tertiary h-full grow"> Flow status will be displayed here </div>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte'
|
||||
import InputSelectedBadge from './schema/InputSelectedBadge.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import { type ScriptLang } from '$lib/gen'
|
||||
|
||||
let reloadArgs = $state(0)
|
||||
let jsonEditor: JsonInputs | undefined = $state(undefined)
|
||||
@@ -261,7 +262,14 @@
|
||||
type: 'hash',
|
||||
hash: runnable.hash
|
||||
}
|
||||
: undefined}
|
||||
: runnable.schema?.['x-windmill-dyn-select-code'] &&
|
||||
runnable.schema?.['x-windmill-dyn-select-lang']
|
||||
? {
|
||||
type: 'inline',
|
||||
code: runnable.schema['x-windmill-dyn-select-code'] as string,
|
||||
lang: runnable.schema['x-windmill-dyn-select-lang'] as ScriptLang
|
||||
}
|
||||
: undefined}
|
||||
prettifyHeader
|
||||
{noVariablePicker}
|
||||
{autofocus}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
const bubble = createBubbler()
|
||||
import type { Schema } from '$lib/common'
|
||||
import { VariableService, type Script } from '$lib/gen'
|
||||
import { VariableService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { allTrue, computeShow } from '$lib/utils'
|
||||
import { allTrue, computeShow, type DynamicSelect } from '$lib/utils'
|
||||
import { Button } from './common'
|
||||
import ItemPicker from './ItemPicker.svelte'
|
||||
import VariableEditor from './VariableEditor.svelte'
|
||||
@@ -44,10 +44,7 @@
|
||||
onlyMaskPassword?: boolean
|
||||
dndConfig?: DndOptions | undefined
|
||||
items?: { id: string; value: string }[] | undefined
|
||||
helperScript?:
|
||||
| { type: 'inline'; path?: string; lang: Script['language']; code: string }
|
||||
| { type: 'hash'; hash: string }
|
||||
| undefined
|
||||
helperScript?: DynamicSelect.HelperScript
|
||||
lightHeader?: boolean
|
||||
diff?: Record<string, SchemaDiff>
|
||||
nestedParent?: { label: string; nestedParent: any | undefined } | undefined
|
||||
@@ -321,7 +318,7 @@
|
||||
title={formerProperty?.title}
|
||||
placeholder={formerProperty?.placeholder}
|
||||
orderEditable={dndConfig != undefined}
|
||||
otherArgs={args}
|
||||
otherArgs={{ ...args, [argName]: undefined }}
|
||||
{helperScript}
|
||||
{lightHeader}
|
||||
hideNested={typeof diff[argName].diff === 'object'}
|
||||
@@ -394,7 +391,7 @@
|
||||
title={schema.properties[argName].title}
|
||||
placeholder={schema.properties[argName].placeholder}
|
||||
orderEditable={dndConfig != undefined}
|
||||
otherArgs={args}
|
||||
otherArgs={{ ...args, [argName]: undefined }}
|
||||
{helperScript}
|
||||
{lightHeader}
|
||||
diffStatus={diff[argName] ?? undefined}
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
import CaptureTable from '$lib/components/triggers/CaptureTable.svelte'
|
||||
import { isObjectTooBig } from '$lib/utils'
|
||||
import { refreshStateStore } from '$lib/svelte5Utils.svelte'
|
||||
import type { ScriptLang } from '$lib/gen'
|
||||
|
||||
interface Props {
|
||||
noEditor: boolean
|
||||
@@ -50,7 +51,6 @@
|
||||
}
|
||||
|
||||
let { noEditor, disabled, onTestFlow }: Props = $props()
|
||||
|
||||
const {
|
||||
flowStore,
|
||||
previewArgs,
|
||||
@@ -76,6 +76,8 @@
|
||||
let editableSchemaForm: EditableSchemaForm | undefined = $state(undefined)
|
||||
let savedPreviewArgs: Record<string, any> | undefined = $state(undefined)
|
||||
let isValid = $state(true)
|
||||
let dynSelectCode: string | undefined = $state(undefined)
|
||||
let dynSelectLang: ScriptLang | undefined = $state(undefined)
|
||||
|
||||
function updateEditPanelSize(size: number | undefined) {
|
||||
if (!$flowInputEditorState) return
|
||||
@@ -366,6 +368,7 @@
|
||||
on:delete={(e) => {
|
||||
addPropertyV2?.handleDeleteArgument([e.detail])
|
||||
}}
|
||||
showDynSelectOpt
|
||||
displayWebhookWarning
|
||||
editTab={$flowInputEditorState?.selectedTab}
|
||||
{previewSchema}
|
||||
@@ -406,6 +409,8 @@
|
||||
resetArgs()
|
||||
}}
|
||||
bind:isValid
|
||||
bind:dynSelectCode
|
||||
bind:dynSelectLang
|
||||
>
|
||||
{#snippet openEditTab()}
|
||||
<div class={twMerge('flex flex-row divide-x', ButtonType.ColorVariants.blue.divider)}>
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
let initialObjectSelected = $state(
|
||||
Object.keys(properties ?? {}).length == 0 ? 'resource' : 'custom-object'
|
||||
)
|
||||
let isDynSelect = $derived(format?.startsWith('dynselect-') ?? false)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -284,7 +285,7 @@
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{:else if type === 'object' && format !== 'resource-s3_object'}
|
||||
{:else if type === 'object' && format !== 'resource-s3_object' && !isDynSelect}
|
||||
<Tabs
|
||||
bind:selected={initialObjectSelected}
|
||||
on:selected={(e) => {
|
||||
@@ -325,7 +326,7 @@
|
||||
</Tabs>
|
||||
{/if}
|
||||
|
||||
{#if !(type === 'object' && oneOf && oneOf.length >= 2) && !(type == 'object' && initialObjectSelected == 'custom-object')}
|
||||
{#if !(type === 'object' && oneOf && oneOf.length >= 2) && !(type == 'object' && initialObjectSelected == 'custom-object') && !isDynSelect}
|
||||
<Label label="Default">
|
||||
<ArgInput
|
||||
noDefaultOnSelectFirst
|
||||
|
||||
@@ -265,8 +265,6 @@
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if type == 'object' && format?.startsWith('dynselect-')}
|
||||
<div class="text-tertiary text-xs">No settings available for Dynamic Select</div>
|
||||
{:else if type == 'object' && !format?.startsWith('resource-') && !isFlowInput && !isAppInput}
|
||||
<div class="border">
|
||||
<EditableSchemaForm
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import type { Schema } from '$lib/common'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import type { SchemaDiff } from '$lib/components/schema/schemaUtils.svelte'
|
||||
import type { DynamicSelect } from '$lib/utils'
|
||||
interface Props {
|
||||
dndType?: string | undefined
|
||||
schema: Schema
|
||||
@@ -22,6 +23,7 @@
|
||||
nestedClasses?: string
|
||||
isValid?: boolean
|
||||
noVariablePicker?: boolean
|
||||
helperScript?: DynamicSelect.HelperScript
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -36,6 +38,7 @@
|
||||
nestedParent = undefined,
|
||||
disableDnd = false,
|
||||
shouldDispatchChanges = false,
|
||||
helperScript = undefined,
|
||||
diff = {},
|
||||
nestedClasses = '',
|
||||
isValid = $bindable(true),
|
||||
@@ -114,6 +117,7 @@
|
||||
{onlyMaskPassword}
|
||||
{disablePortal}
|
||||
{disabled}
|
||||
{helperScript}
|
||||
bind:schema
|
||||
dndConfig={disableDnd
|
||||
? undefined
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
onCreateItem?: (value: string) => void
|
||||
} = $props()
|
||||
|
||||
let disabled = $derived(_disabled || loading)
|
||||
let disabled = $derived(_disabled || (loading && !value))
|
||||
|
||||
let inputEl: HTMLInputElement | undefined = $state()
|
||||
|
||||
@@ -121,12 +121,14 @@
|
||||
{disabled}
|
||||
type="text"
|
||||
bind:value={() => filterText, (v) => (filterText = v)}
|
||||
placeholder={loading ? 'Loading...' : (valueEntry?.label ?? getLabel({ value }) ?? placeholder)}
|
||||
placeholder={loading && !value
|
||||
? 'Loading...'
|
||||
: (valueEntry?.label ?? getLabel({ value }) ?? placeholder)}
|
||||
style={containerStyle}
|
||||
class={twMerge(
|
||||
'!bg-surface text-ellipsis',
|
||||
open ? '' : 'cursor-pointer',
|
||||
value && !loading ? '!placeholder-primary' : '',
|
||||
!loading ? '!placeholder-primary' : '',
|
||||
(clearable || RightIcon) && !disabled && value ? '!pr-8' : '',
|
||||
inputClass ?? ''
|
||||
)}
|
||||
|
||||
@@ -172,9 +172,9 @@ export function displayDate(
|
||||
}
|
||||
const dateChoices: Intl.DateTimeFormatOptions = displayDate
|
||||
? {
|
||||
day: 'numeric',
|
||||
month: 'numeric'
|
||||
}
|
||||
day: 'numeric',
|
||||
month: 'numeric'
|
||||
}
|
||||
: {}
|
||||
return date.toLocaleString(undefined, {
|
||||
...timeChoices,
|
||||
@@ -593,6 +593,47 @@ export type InputCat =
|
||||
| 'oneOf'
|
||||
| 'dynselect'
|
||||
|
||||
export namespace DynamicSelect {
|
||||
export type HelperScript =
|
||||
| { type: 'inline'; path?: string; lang: Script['language']; code: string }
|
||||
| { type: 'hash'; hash: string }
|
||||
|
||||
export const generatePythonFnTemplate = (functionName: string): string => {
|
||||
return `
|
||||
def ${functionName}():
|
||||
return [
|
||||
{ "label": "Foo", "value": "foo" },
|
||||
{ "label": "Bar", "value": "bar" }
|
||||
]
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
export const generateJsFnTemplate = (functionName: string): string => {
|
||||
return `
|
||||
// you can use filterText to filter the results from the backend
|
||||
// you can refer to other args directly as parameters (e.g. foobar: string)
|
||||
export function ${functionName}(filterText: string) {
|
||||
return [
|
||||
{ label: 'Foo', value: 'foo' },
|
||||
{ label: 'Bar', value: 'bar' }
|
||||
];
|
||||
}
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
export const generateDefaultTemplateFn = (functionName: string, lang: ScriptLang): string => {
|
||||
return lang === 'bun'
|
||||
? generateJsFnTemplate(functionName)
|
||||
: generatePythonFnTemplate(functionName)
|
||||
}
|
||||
|
||||
export const getGenerateTemplateFn = (lang: ScriptLang) => {
|
||||
return lang === 'bun' ? generateJsFnTemplate : generatePythonFnTemplate
|
||||
}
|
||||
}
|
||||
|
||||
export function setInputCat(
|
||||
type: string | undefined,
|
||||
format: string | undefined,
|
||||
@@ -608,7 +649,10 @@ export function setInputCat(
|
||||
return 'list'
|
||||
} else if (type == 'object' && format?.startsWith('resource')) {
|
||||
return 'resource-object'
|
||||
} else if (type == 'object' && format?.startsWith('dynselect-')) {
|
||||
} else if (
|
||||
type == 'object' &&
|
||||
(format?.startsWith('dynselect-') || format?.startsWith('dynselect_'))
|
||||
) {
|
||||
return 'dynselect'
|
||||
} else if (!type || type == 'object' || type == 'array') {
|
||||
return 'object'
|
||||
@@ -981,7 +1025,7 @@ export async function tryEvery({
|
||||
try {
|
||||
await tryCode()
|
||||
break
|
||||
} catch (err) {}
|
||||
} catch (err) { }
|
||||
i++
|
||||
}
|
||||
if (i >= times) {
|
||||
@@ -1248,7 +1292,7 @@ export function conditionalMelt(node: HTMLElement, meltItem: AnyMeltElement | un
|
||||
if (meltItem) {
|
||||
return meltItem(node)
|
||||
}
|
||||
return { destroy: () => {} }
|
||||
return { destroy: () => { } }
|
||||
}
|
||||
|
||||
export type Item = {
|
||||
@@ -1453,9 +1497,9 @@ export type S3Uri = `s3://${string}/${string}`
|
||||
export type S3Object =
|
||||
| S3Uri
|
||||
| {
|
||||
s3: string
|
||||
storage?: string
|
||||
}
|
||||
s3: string
|
||||
storage?: string
|
||||
}
|
||||
|
||||
export function parseS3Object(s3Object: S3Object): { s3: string; storage?: string } {
|
||||
if (typeof s3Object === 'object') return s3Object
|
||||
|
||||
Reference in New Issue
Block a user