Compare commits

..

14 Commits

Author SHA1 Message Date
Ruben Fiszel
b6da492d1b chore(main): release 1.657.0 (#8368)
* chore(main): release 1.657.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-03-14 04:52:06 +00:00
Ruben Fiszel
87215193ca system promps generate metadata 2026-03-14 04:51:54 +00:00
Alexander Petric
5df37fb0db feat: add datatable config support to CLI settings sync and backend export (#8024)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-14 04:44:11 +00:00
Ruben Fiszel
6fa2543647 chore(main): release 1.656.0 (#8346) 2026-03-13 22:32:57 +00:00
hugocasa
c431053a1e fix(frontend): prevent duplicate and reserved agent tool names (#8367)
* fix(frontend): prevent duplicate and reserved agent tool names

Extend tool name validation to detect duplicates within an agent step
and reserved names (like 'preprocessor', 'failure'). Show specific error
messages in the editor panel and red styling in the graph view.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(frontend): remove duplicate banner for agent tool name errors

The inline per-tool error messages are sufficient — the panel-level
banner was redundant and showed a double error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-13 22:30:53 +00:00
Ruben Fiszel
a079dd500f i pkg 2026-03-13 22:21:28 +00:00
centdix
9d2c439e2a fix: resource drawer opening behind dialog in chat mode (#8328)
* fix: resource drawer opening behind dialog in chat mode

Integrate Modal into the Disposable z-index stacking system so drawers
opened from within a modal (e.g. "Add a new resource") correctly appear
above the dialog instead of behind it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resource drawer opening behind dialog in chat mode

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: simplify minZIndex tracking by removing unnecessary refcount

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use map-based minZIndex tracking and conditional chat elevation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use plain object instead of Map for reactive minZIndex tracking

$state(new Map()) is not deeply reactive in Svelte 5 — only plain
objects and arrays are proxied. Replaced with Record<string, number>
so that property assignments properly trigger $derived updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 20:33:16 +00:00
hugocasa
fb12b31df0 fix(frontend): improve native mode alert message and fix workspaced tag detection (#8361)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 20:32:29 +00:00
Pyra
51933be3ca fix(cli): suppress verbose lock generation messages in generate-metadata (#8357)
* fix(cli): suppress verbose lock generation messages in generate-metadata

Pass noStaleMessage flag through to updateRawAppRunnables and
updateAppInlineScripts to suppress verbose "Generating lock for..."
messages when running generate-metadata command. Also fixes a stray
`}` character in a template literal.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(cli): show updated inline scripts in generate-metadata output

Display inline script names that were relocked when processing flows
and apps in the generate-metadata command output. For example:

  [4/5] app    u/admin/test__raw_app: a, b, c

This provides visibility into what work was done without verbose
per-script logging that clutters the output.

- Add AppLocksResult and FlowLocksResult types to track updated scripts
- Update internal functions to return lists of updated script names
- Display script names inline with progress in generate-metadata

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-13 20:32:08 +00:00
Pyra
404ae09d42 fix(cli): normalize path separators in generate-metadata folder filter for Windows (#8358)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-13 20:31:44 +00:00
Diego Imbert
e0e78442b7 Support T | T[] in debounce (#8340)
* Detect union types in TS

* display union type arguments

* Handle single values at accumulation time

* nit propagate otyp

* Python support

* npm package update
2026-03-13 20:31:25 +00:00
hugocasa
0d31c35f3e fix(frontend): filter webhook/email tokens by scope instead of label (#8363)
The backend already filters tokens by scope matching the script/flow
path. Remove the redundant client-side label prefix filter so that all
tokens with matching scopes are shown, not just those with a specific
label convention.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 20:31:06 +00:00
Pyra
060687b1fa fix(cli): exclude raw app backend files from script metadata generation (#8362)
Files inside .raw_app/backend/ were incorrectly being processed by
`script generate-metadata` and `generate-metadata --skip-flows --skip-apps`
because the filter only checked isFlowPath and isAppPath, but not isRawAppPath.

This caused backend runnables to be treated as standalone scripts, creating
incorrect .script.yaml files at wrong locations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-13 20:30:23 +00:00
HugoCasa
8301d86800 docs: rewrite Code Navigation section with MUST for outline/body and condensed limitations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:45:06 +01:00
55 changed files with 1219 additions and 350 deletions

View File

@@ -66,7 +66,7 @@ jobs:
with:
bun-version: 1.3.10
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
node-version: "20"

View File

@@ -57,7 +57,7 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
node-version: "20"
- uses: astral-sh/setup-uv@v6.2.1

View File

@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "20"
@@ -70,7 +70,7 @@ jobs:
cache-workspaces: backend
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "20"
@@ -124,7 +124,7 @@ jobs:
cache-workspaces: backend
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: "20"

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v5
with:
node-version: 24
cache: "npm"

View File

@@ -129,7 +129,7 @@ jobs:
with:
deno-version: v2.x
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
node-version: "20"

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v3
with:
node-version: "20.x"
registry-url: "https://registry.npmjs.org"
@@ -21,7 +21,7 @@ jobs:
runs-on: ubicloud-standard-8
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
- uses: actions/setup-node@v3
with:
node-version: "20.x"
registry-url: "https://registry.npmjs.org"

View File

@@ -15,7 +15,7 @@ jobs:
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: '20'

View File

@@ -1,5 +1,36 @@
# Changelog
## [1.657.0](https://github.com/windmill-labs/windmill/compare/v1.656.0...v1.657.0) (2026-03-14)
### Features
* add datatable config support to CLI settings sync and backend export ([#8024](https://github.com/windmill-labs/windmill/issues/8024)) ([5df37fb](https://github.com/windmill-labs/windmill/commit/5df37fb0dbf9190a430f066cf2d3c48914782e53))
## [1.656.0](https://github.com/windmill-labs/windmill/compare/v1.655.0...v1.656.0) (2026-03-13)
### Features
* add GitHub Enterprise Server (GHES) support for GitHub App git sync ([#8344](https://github.com/windmill-labs/windmill/issues/8344)) ([2e430c4](https://github.com/windmill-labs/windmill/commit/2e430c4c0b8540df7b6997434a7a9f9134858026))
* **cli:** add unified generate-metadata command ([#8335](https://github.com/windmill-labs/windmill/issues/8335)) ([4c2c165](https://github.com/windmill-labs/windmill/commit/4c2c165a5b757bd5f2f49074bb290407bce3b2fb))
### Bug Fixes
* **ci:** add NODE_AUTH_TOKEN for npm publish authentication ([2a8e276](https://github.com/windmill-labs/windmill/commit/2a8e276b6d2761bb2798b6bc5f8d90ab34fbb403))
* **ci:** remove provenance flag and use NPM_TOKEN for npm publish ([44dd3ee](https://github.com/windmill-labs/windmill/commit/44dd3ee8cd05d288828d1d46c84cbcdf40f8fa78))
* **cli:** exclude raw app backend files from script metadata generation ([#8362](https://github.com/windmill-labs/windmill/issues/8362)) ([060687b](https://github.com/windmill-labs/windmill/commit/060687b1fa6b627a7b06fbdc4b3f4eb0b63411c0))
* **cli:** normalize path separators in generate-metadata folder filter for Windows ([#8358](https://github.com/windmill-labs/windmill/issues/8358)) ([404ae09](https://github.com/windmill-labs/windmill/commit/404ae09d429fb545610ba17d747e1903c542d4a3))
* **cli:** suppress verbose lock generation messages in generate-metadata ([#8357](https://github.com/windmill-labs/windmill/issues/8357)) ([51933be](https://github.com/windmill-labs/windmill/commit/51933be3cabd853960d384cd358c7bcaef6bfa86))
* **frontend:** collapse flow topbar buttons to icon-only in narrow panes ([#8322](https://github.com/windmill-labs/windmill/issues/8322)) ([b585dee](https://github.com/windmill-labs/windmill/commit/b585dee64dfd63d20812ca969b17ff9ee9989493))
* **frontend:** filter webhook/email tokens by scope instead of label ([#8363](https://github.com/windmill-labs/windmill/issues/8363)) ([0d31c35](https://github.com/windmill-labs/windmill/commit/0d31c35f3e12d637c757a95fe350294002cbf640))
* **frontend:** improve native mode alert message and fix workspaced tag detection ([#8361](https://github.com/windmill-labs/windmill/issues/8361)) ([fb12b31](https://github.com/windmill-labs/windmill/commit/fb12b31df081b2f1ac63becea6e6538ca80f8c46))
* **frontend:** prevent duplicate and reserved agent tool names ([#8367](https://github.com/windmill-labs/windmill/issues/8367)) ([c431053](https://github.com/windmill-labs/windmill/commit/c431053a1e24ef29cd551a86de4d013fd7f158be))
* graceful shutdown instead of panic on job completion channel failure ([#8345](https://github.com/windmill-labs/windmill/issues/8345)) ([724d135](https://github.com/windmill-labs/windmill/commit/724d1350d070fcf078034a52166d3048fb74e6f3))
* Linked resources and vars not triggering both sync jobs on delete ([#8342](https://github.com/windmill-labs/windmill/issues/8342)) ([8e3b8bd](https://github.com/windmill-labs/windmill/commit/8e3b8bdfd2ded9652bc7e876c6bcd0ac2cfae148))
* lower default indexer memory/batch settings to prevent OOM ([#8347](https://github.com/windmill-labs/windmill/issues/8347)) ([d9d45cf](https://github.com/windmill-labs/windmill/commit/d9d45cf2f9235b0e7118d0fc97ccdc0776ca9726))
## [1.655.0](https://github.com/windmill-labs/windmill/compare/v1.654.0...v1.655.0) (2026-03-12)

View File

@@ -4,7 +4,7 @@ Open-source platform for internal tools, workflows, API integrations, background
## Workflow
1. **Understand**: Before coding, use `wm-ts-nav` to explore (see Code Navigation below). Use `outline` to understand file structure, `body` to read specific symbols, `def`/`callers`/`callees` to trace code. Read `docs/` for domain context.
1. **Understand**: Before coding, explore the codebase (see Code Navigation below). Use `outline` to understand file structure, `body` to read specific symbols, `def`/`callers`/`callees` to trace code, `Grep` to find usages. Read `docs/` for domain context.
2. **Plan**: For non-trivial changes, use plan mode. For large features, break into reviewable stages
3. **Execute**: Follow coding patterns from skills (`rust-backend`, `svelte-frontend`)
4. **Validate**: After every change, run the appropriate checks per `docs/validation.md`
@@ -52,11 +52,9 @@ let { my_prop = $bindable(default_value) }: { my_prop?: string } = $props()
## Code Navigation
`wm-ts-nav` is an AST-aware code navigator. Use **Grep** for regex/pattern search. Use **wm-ts-nav** for structural queries — it skips comments/strings and understands symbol boundaries.
`wm-ts-nav` is an AST-aware code navigator. Use **wm-ts-nav** for structural queries — it skips comments/strings and understands symbol boundaries.
**Prefer wm-ts-nav over Read** to save context window:
- `outline <file>` instead of reading a full file — understand structure first, then `body` or Read for specifics
- `body "X"` instead of reading a full file to see one function/struct
**MUST use `outline` before `Read`** on unfamiliar files — a 500-line file costs ~500 lines of context, while `outline` costs ~20. Then **MUST use `body "X"`** instead of reading a full file to see one function/struct. Use `Read` with offset/limit only when you need surrounding context that `body` doesn't capture.
- `refs "X" --caller` instead of reading files to find which function contains each reference
- `callers "X"` / `callees "X"` for call-graph questions
@@ -73,20 +71,14 @@ $NAV --root backend callers "X" # who calls X?
$NAV --root backend callees "X" # what does X call?
```
**Limitations** — syntax-level analysis, no type inference:
- Import paths are stored literally — `crate::X` and `super::X` pointing to the same type won't be linked
- Re-export chains (`pub use`) aren't followed — refs through different re-export paths won't connect
- Trait methods can't be resolved to their trait definition
- Nested `use` trees (`use foo::{bar::{A, B}, baz::C}`) aren't parsed correctly
- Glob imports (`use foo::*`) — refs won't show import origin
- Macro-generated symbols (e.g. `sqlx::FromRow`) — invisible to tree-sitter
- Single-char identifiers — intentionally filtered out of refs
**Limitations** — syntax-level analysis, no type inference. Use **Grep** instead when completeness matters (finding all usages, exhaustiveness checks):
- `refs`/`callers`/`callees` can't follow re-exports, glob imports, or different import paths to the same symbol
- Trait impls, macro-generated symbols (`sqlx::FromRow`), and namespace member access (`ns.X`) are invisible
- `callees` shows all identifiers in a function body, not just actual calls
- `import * as ns` namespace imports — member accesses through `ns.X` aren't resolved
## Core Principles
- **Use `outline`/`body` to explore, then `Read` with offset/limit from the results before editing** — avoid reading full files
- **MUST `outline` before `Read`** on unfamiliar files — then `body` or `Read` with offset/limit for specifics
- Search for existing code to reuse before writing new code
- Follow established patterns in the codebase
- Keep changes focused — don't refactor beyond what's asked

198
backend/Cargo.lock generated
View File

@@ -169,9 +169,9 @@ dependencies = [
[[package]]
name = "anstream"
version = "0.6.21"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -184,15 +184,15 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
@@ -1860,9 +1860,9 @@ dependencies = [
[[package]]
name = "bon"
version = "3.9.0"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d13a61f2963b88eef9c1be03df65d42f6996dfeac1054870d950fcf66686f83"
checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe"
dependencies = [
"bon-macros",
"rustversion",
@@ -1870,9 +1870,9 @@ dependencies = [
[[package]]
name = "bon-macros"
version = "3.9.0"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d314cc62af2b6b0c65780555abb4d02a03dd3b799cd42419044f0c38d99738c0"
checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c"
dependencies = [
"darling 0.23.0",
"ident_case",
@@ -2208,9 +2208,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.56"
version = "1.2.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -2323,9 +2323,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.60"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
dependencies = [
"clap_builder",
"clap_derive",
@@ -2333,9 +2333,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.60"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
@@ -2345,9 +2345,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.55"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -2357,9 +2357,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "1.0.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "clipboard-win"
@@ -2418,9 +2418,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colorchoice"
version = "1.0.4"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "combine"
@@ -14697,9 +14697,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.22"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
@@ -15741,7 +15741,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-nats",
@@ -15808,7 +15808,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15821,7 +15821,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"argon2",
@@ -15962,7 +15962,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15985,7 +15985,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15998,7 +15998,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16024,7 +16024,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -16034,7 +16034,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16051,7 +16051,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"base64 0.22.1",
@@ -16074,7 +16074,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16097,7 +16097,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16113,7 +16113,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16133,7 +16133,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16153,7 +16153,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16167,7 +16167,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-nats",
@@ -16195,7 +16195,7 @@ dependencies = [
[[package]]
name = "windmill-api-jobs"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16220,7 +16220,7 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"flate2",
@@ -16238,7 +16238,7 @@ dependencies = [
[[package]]
name = "windmill-api-openapi"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16259,7 +16259,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16279,7 +16279,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16309,7 +16309,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16336,7 +16336,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"lazy_static",
"serde",
@@ -16348,7 +16348,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"argon2",
"axum 0.7.9",
@@ -16371,7 +16371,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16385,7 +16385,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16416,7 +16416,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"chrono",
"lazy_static",
@@ -16430,7 +16430,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16449,7 +16449,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"aes-gcm",
"anyhow",
@@ -16548,7 +16548,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -16567,7 +16567,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"regex",
"serde",
@@ -16582,7 +16582,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -16606,7 +16606,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"futures",
@@ -16623,7 +16623,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -16639,7 +16639,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16660,7 +16660,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -16691,7 +16691,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-oauth2",
@@ -16715,7 +16715,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-stream",
@@ -16749,7 +16749,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"futures",
@@ -16767,7 +16767,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -16776,7 +16776,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16788,7 +16788,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16800,7 +16800,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"gosyn",
@@ -16812,7 +16812,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16824,7 +16824,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"serde_json",
@@ -16836,7 +16836,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -16847,7 +16847,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16858,7 +16858,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16870,7 +16870,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-asset"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -16881,7 +16881,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -16905,7 +16905,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16919,7 +16919,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -16936,7 +16936,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16950,7 +16950,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql-asset"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"serde",
@@ -16962,7 +16962,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -16980,7 +16980,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts-asset"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
@@ -16996,7 +16996,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wac"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -17012,7 +17012,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"serde",
@@ -17023,7 +17023,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17060,7 +17060,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"const_format",
@@ -17098,7 +17098,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"getrandom 0.3.4",
"wasm-bindgen",
@@ -17109,7 +17109,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -17138,7 +17138,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -17161,7 +17161,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17194,7 +17194,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17214,7 +17214,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17248,7 +17248,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17283,7 +17283,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17306,7 +17306,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17330,7 +17330,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-nats",
@@ -17354,7 +17354,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17389,7 +17389,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17417,7 +17417,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-trait",
@@ -17440,7 +17440,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"bitflags 2.9.4",
@@ -17458,7 +17458,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"anyhow",
"async-once-cell",
@@ -17564,7 +17564,7 @@ dependencies = [
[[package]]
name = "windmill-worker-volumes"
version = "1.655.0"
version = "1.657.0"
dependencies = [
"bytes",
"futures",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.655.0"
version = "1.657.0"
authors.workspace = true
edition.workspace = true
@@ -82,7 +82,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.655.0"
version = "1.657.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"

View File

@@ -25,6 +25,50 @@ pub mod pydantic_parser;
const FUNCTION_CALL: &str = "<function call>";
/// Get the simple type name from an expression (e.g. `str`, `int`).
fn simple_type_name(e: &Expr) -> Option<&str> {
match e {
Expr::Name(ExprName { id, .. }) => Some(id.as_ref()),
_ => None,
}
}
/// If `e` is `list[T]` or `List[T]`, return the inner expression `T`.
fn list_elem_expr(e: &Expr) -> Option<&Expr> {
match e {
Expr::Subscript(x) => match x.value.as_ref() {
Expr::Name(ExprName { id, .. }) if id == "list" || id == "List" => {
Some(x.slice.as_ref())
}
_ => None,
},
_ => None,
}
}
/// Detect `T | list[T]` or `list[T] | T` union patterns.
/// Returns the original type string (e.g. "str | list[str]") for use as `otyp`.
fn detect_py_union_array_otyp(e: &Expr) -> Option<String> {
let Expr::BinOp(x) = e else { return None };
// T | list[T]
if let (Some(scalar), Some(elem)) = (simple_type_name(&x.left), list_elem_expr(&x.right)) {
if let Some(elem_name) = simple_type_name(elem) {
if scalar == elem_name {
return Some(format!("{} | list[{}]", scalar, elem_name));
}
}
}
// list[T] | T
if let (Some(elem), Some(scalar)) = (list_elem_expr(&x.left), simple_type_name(&x.right)) {
if let Some(elem_name) = simple_type_name(elem) {
if scalar == elem_name {
return Some(format!("list[{}] | {}", elem_name, scalar));
}
}
}
None
}
/// Cheap string-based check to see if code might contain Pydantic models or dataclasses.
/// Returns true if we should do full AST parsing for type detection, false otherwise.
/// This avoids expensive parsing for the common case where scripts don't use these features.
@@ -390,8 +434,19 @@ pub fn parse_python_signature(
_ => {}
}
// Detect T | list[T] union types and set otyp for
// debounce accumulation support. Falls back to docstring
// description if no union array pattern is found.
let union_otyp = params.args[i]
.as_arg()
.annotation
.as_ref()
.and_then(|ann| detect_py_union_array_otyp(ann.as_ref()));
Arg {
otyp: metadata.descriptions.get(&arg_name).map(|d| d.to_string()),
otyp: union_otyp.or_else(|| {
metadata.descriptions.get(&arg_name).map(|d| d.to_string())
}),
name: arg_name,
typ,
has_default: has_default || default.is_some(),
@@ -441,6 +496,9 @@ fn parse_expr(
Expr::Constant(ExprConstant { value: Constant::None, .. })
) {
(parse_expr(&x.left, enums, module).0, true)
} else if detect_py_union_array_otyp(e.as_ref()).is_some() {
// T | list[T] — parsed type is Unknown; otyp is set separately
(Typ::Unknown, false)
} else {
(Typ::Unknown, false)
}
@@ -1046,6 +1104,33 @@ def main(a: str, b: Optional[str], c: str | None): return
Ok(())
}
#[test]
fn test_parse_python_union_array_type() -> anyhow::Result<()> {
let code = r#"
def main(items: str | list[str], numbers: list[int] | int, plain: str):
pass
"#;
let result = parse_python_signature(code, None, false)?;
assert_eq!(result.args.len(), 3);
// str | list[str] → otyp set, typ Unknown
assert_eq!(result.args[0].name, "items");
assert_eq!(result.args[0].otyp, Some("str | list[str]".to_string()));
assert_eq!(result.args[0].typ, Typ::Unknown);
// list[int] | int → otyp set, typ Unknown
assert_eq!(result.args[1].name, "numbers");
assert_eq!(result.args[1].otyp, Some("list[int] | int".to_string()));
assert_eq!(result.args[1].typ, Typ::Unknown);
// plain str → no otyp
assert_eq!(result.args[2].name, "plain");
assert_eq!(result.args[2].otyp, None);
assert_eq!(result.args[2].typ, Typ::Str(None));
Ok(())
}
#[test]
fn test_parse_python_sig_enum() -> anyhow::Result<()> {
let code = r#"

View File

@@ -363,8 +363,12 @@ fn parse_param(
let r = match param.pat {
Pat::Ident(ident) => {
let (name, typ, nullable) = binding_ident_to_arg(symbol_table, type_resolver, &ident);
let otyp = ident
.type_ann
.as_ref()
.and_then(|ta| detect_union_array_otyp(&ta.type_ann));
Ok(Arg {
otyp: None,
otyp,
name,
typ,
default: None,
@@ -374,13 +378,21 @@ fn parse_param(
}
// Pat::Object(ObjectPat { ... }) = todo!()
Pat::Assign(AssignPat { left, right, .. }) => {
let (name, mut typ, _nullable) = match *left {
Pat::Ident(ident) => binding_ident_to_arg(symbol_table, type_resolver, &ident),
let (name, mut typ, _nullable, otyp) = match *left {
Pat::Ident(ident) => {
let otyp = ident
.type_ann
.as_ref()
.and_then(|ta| detect_union_array_otyp(&ta.type_ann));
let (name, typ, nullable) =
binding_ident_to_arg(symbol_table, type_resolver, &ident);
(name, typ, nullable, otyp)
}
Pat::Object(ObjectPat { type_ann, .. }) => {
let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, &type_ann);
*counter += 1;
let name = format!("anon{}", counter);
(name, typ, nullable)
(name, typ, nullable, None)
}
_ => {
return Err(anyhow::anyhow!(
@@ -416,7 +428,7 @@ fn parse_param(
if typ == Typ::Unknown && dflt.is_some() {
typ = json_to_typ(dflt.as_ref().unwrap(), false);
}
Ok(Arg { otyp: None, name, typ, default: dflt, has_default: true, oidx: None })
Ok(Arg { otyp, name, typ, default: dflt, has_default: true, oidx: None })
}
Pat::Object(ObjectPat { type_ann, .. }) => {
let (typ, nullable) = eval_type_ann(symbol_table, type_resolver, &type_ann);
@@ -961,6 +973,75 @@ fn one_of_properties(
.collect()
}
fn ts_type_to_string(ts_type: &TsType) -> Option<String> {
match ts_type {
TsType::TsKeywordType(t) => Some(
match t.kind {
TsKeywordTypeKind::TsStringKeyword => "string",
TsKeywordTypeKind::TsNumberKeyword => "number",
TsKeywordTypeKind::TsBooleanKeyword => "boolean",
TsKeywordTypeKind::TsObjectKeyword => "object",
TsKeywordTypeKind::TsBigIntKeyword => "bigint",
TsKeywordTypeKind::TsAnyKeyword => "any",
_ => return None,
}
.to_string(),
),
TsType::TsTypeRef(TsTypeRef { type_name, .. }) => match type_name {
TsEntityName::Ident(Ident { sym, .. }) => Some(sym.to_string()),
_ => None,
},
_ => None,
}
}
fn get_array_elem_type(ts_type: &TsType) -> Option<&TsType> {
match ts_type {
TsType::TsArrayType(TsArrayType { elem_type, .. }) => Some(elem_type),
_ => None,
}
}
/// Detects union types of the form `T | T[]` or `T[] | T` and returns
/// the original type string (e.g. "string | string[]").
fn detect_union_array_otyp(ts_type: &TsType) -> Option<String> {
let TsType::TsUnionOrIntersectionType(TsUnionOrIntersectionType::TsUnionType(TsUnionType {
types,
..
})) = ts_type
else {
return None;
};
if types.len() != 2 {
return None;
}
// Check pattern: T | T[]
if let (Some(scalar_name), Some(array_elem)) =
(ts_type_to_string(&types[0]), get_array_elem_type(&types[1]))
{
if let Some(elem_name) = ts_type_to_string(array_elem) {
if scalar_name == elem_name {
return Some(format!("{} | {}[]", scalar_name, elem_name));
}
}
}
// Check pattern: T[] | T
if let (Some(array_elem), Some(scalar_name)) =
(get_array_elem_type(&types[0]), ts_type_to_string(&types[1]))
{
if let Some(elem_name) = ts_type_to_string(array_elem) {
if scalar_name == elem_name {
return Some(format!("{}[] | {}", elem_name, scalar_name));
}
}
}
None
}
fn find_undefined(types: &Vec<Box<TsType>>) -> Option<usize> {
types.into_iter().position(|x| match **x {
TsType::TsKeywordType(TsKeywordType { kind, .. }) => {

View File

@@ -646,6 +646,55 @@ mod tests {
);
}
#[test]
fn test_parse_union_array_type() {
let code = r#"
export async function main(
items: string | string[],
numbers: number[] | number,
plain: string
) {
return { items, numbers, plain };
}
"#;
let sig = parse_deno_signature(code, false, false, None).unwrap();
assert_eq!(
sig,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
name: "items".to_string(),
otyp: Some("string | string[]".to_string()),
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None,
},
Arg {
name: "numbers".to_string(),
otyp: Some("number[] | number".to_string()),
typ: Typ::Unknown,
default: None,
has_default: false,
oidx: None,
},
Arg {
name: "plain".to_string(),
otyp: None,
typ: Typ::Str(None),
default: None,
has_default: false,
oidx: None,
},
],
no_main_func: Some(false),
has_preprocessor: Some(false),
}
);
}
#[test]
fn test_parse_invalid_typescript() {
let code = r#"

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.655.0
version: 1.657.0
title: Windmill API
contact:

View File

@@ -283,6 +283,8 @@ struct SimplifiedSettings {
#[serde(skip_serializing_if = "Option::is_none")]
operator_settings: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
datatable: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
slack_team_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
slack_name: Option<String>,
@@ -323,6 +325,8 @@ struct SimplifiedSettingsLegacy {
#[serde(skip_serializing_if = "Option::is_none")]
operator_settings: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
datatable: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
slack_team_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
slack_name: Option<String>,
@@ -347,6 +351,7 @@ struct SettingsRow {
mute_critical_alerts: Option<bool>,
color: Option<String>,
operator_settings: Option<serde_json::Value>,
datatable: Option<Value>,
slack_team_id: Option<String>,
slack_name: Option<String>,
slack_command_script: Option<String>,
@@ -955,6 +960,7 @@ pub(crate) async fn tarball_workspace(
mute_critical_alerts,
color,
operator_settings,
datatable,
slack_team_id,
slack_name,
slack_command_script
@@ -983,6 +989,7 @@ pub(crate) async fn tarball_workspace(
mute_critical_alerts: row.mute_critical_alerts,
color: row.color.clone(),
operator_settings: row.operator_settings.clone(),
datatable: row.datatable.clone(),
slack_team_id: row.slack_team_id.clone(),
slack_name: row.slack_name.clone(),
slack_command_script: row.slack_command_script.clone(),
@@ -1045,6 +1052,7 @@ pub(crate) async fn tarball_workspace(
mute_critical_alerts: row.mute_critical_alerts,
color: row.color,
operator_settings: row.operator_settings,
datatable: row.datatable,
slack_team_id: row.slack_team_id,
slack_name: row.slack_name,
slack_command_script: row.slack_command_script,

View File

@@ -3030,8 +3030,16 @@ impl PulledJobResult {
if let Some(s) = str_o.as_ref() {
match serde_json::from_str::<Vec<Box<RawValue>>>(s) {
Ok(ref mut vec) => accumulated_arg.append(vec),
Err(e) => {
return Err(error::Error::ArgumentErr(format!("cannot consolidate arguments of non-list type. Type provided for argument `{arg_name_to_accumulate}` is not a list\nUnwrapped Error: {e}")));
Err(_) => {
// Value is not an array — wrap the scalar into a
// single-element array. This supports union types
// like T | T[] where the caller may pass a bare T.
match RawValue::from_string(s.to_string()) {
Ok(raw) => accumulated_arg.push(raw),
Err(e) => {
return Err(error::Error::ArgumentErr(format!("cannot consolidate argument `{arg_name_to_accumulate}`: value is neither a valid list nor a valid JSON value\nUnwrapped Error: {e}")));
}
}
}
}
}

View File

@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.655.0";
export const VERSION = "v1.657.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({

View File

@@ -93,7 +93,16 @@ async function generateAppHash(
}
/**
* Updates locks for inline scripts in an app
* Result of generating app locks, including which scripts were updated
*/
export interface AppLocksResult {
path: string;
updatedScripts: string[];
}
/**
* Updates locks for inline scripts in an app.
* Returns the path if dry-run, or AppLocksResult with updated scripts if actual update occurred.
*/
export async function generateAppLocksInternal(
appFolder: string,
@@ -105,7 +114,7 @@ export async function generateAppLocksInternal(
},
justUpdateMetadataLock?: boolean,
noStaleMessage?: boolean
): Promise<string | void> {
): Promise<string | AppLocksResult | void> {
if (appFolder.endsWith(SEP)) {
appFolder = appFolder.substring(0, appFolder.length - 1);
}
@@ -167,6 +176,8 @@ export async function generateAppLocksInternal(
);
}
let updatedScripts: string[] = [];
if (!justUpdateMetadataLock) {
const changedScripts = [];
// Find hashes that do not correspond to previous hashes
@@ -201,13 +212,14 @@ export async function generateAppLocksInternal(
replaceInlineScripts(runnables, runnablesPath + SEP, false);
// Update the app runnables with new locks (writes to separate files)
await updateRawAppRunnables(
updatedScripts = await updateRawAppRunnables(
workspace,
runnables,
remote_path,
appFolder,
filteredDeps,
opts.defaultTs
opts.defaultTs,
noStaleMessage
);
// Note: updateRawAppRunnables now writes each runnable to its own file
} else {
@@ -217,14 +229,17 @@ export async function generateAppLocksInternal(
replaceInlineScripts(normalAppFile.value, appFolder + SEP, false);
// Update the app value with new locks
normalAppFile.value = await updateAppInlineScripts(
const result = await updateAppInlineScripts(
workspace,
normalAppFile.value,
remote_path,
appFolder,
filteredDeps,
opts.defaultTs
opts.defaultTs,
noStaleMessage
);
normalAppFile.value = result.value;
updatedScripts = result.updatedScripts;
// Write the updated app file (only for normal apps, raw apps use separate files)
writeIfChanged(
@@ -251,6 +266,8 @@ export async function generateAppLocksInternal(
if (!noStaleMessage) {
log.info(colors.green(`App ${remote_path} lockfiles updated`));
}
return { path: remote_path, updatedScripts };
}
/**
@@ -340,6 +357,7 @@ async function traverseAndProcessInlineScripts(
* Updates locks for all runnables in a raw app, generating locks inline script by inline script.
* Writes each runnable to its own YAML file in the backend folder (new format).
* Also writes content and lock files to the runnables folder.
* Returns the list of runnable IDs that had their locks updated.
*/
async function updateRawAppRunnables(
workspace: Workspace,
@@ -347,8 +365,10 @@ async function updateRawAppRunnables(
remotePath: string,
appFolder: string,
rawDeps?: Record<string, string>,
defaultTs: "bun" | "deno" = "bun"
): Promise<void> {
defaultTs: "bun" | "deno" = "bun",
noStaleMessage?: boolean
): Promise<string[]> {
const updatedRunnables: string[] = [];
const runnablesFolder = path.join(appFolder, APP_BACKEND_FOLDER);
// Ensure runnables folder exists
@@ -414,12 +434,11 @@ async function updateRawAppRunnables(
continue;
}
log.info(
colors.gray(
`Generating lock for runnable ${runnableId} (${language})
}`
)
);
if (!noStaleMessage) {
log.info(
colors.gray(`Generating lock for runnable ${runnableId} (${language})`)
);
}
try {
const lock = await generateInlineScriptLock(
@@ -459,11 +478,15 @@ async function updateRawAppRunnables(
// Write the runnable to its own YAML file
writeRunnableToBackend(runnablesFolder, runnableId, simplifiedRunnable);
log.info(
colors.gray(
` Written ${runnableId}.yaml, ${basePath}${ext}${lock ? ` and ${basePath}lock` : ""}`
)
);
updatedRunnables.push(runnableId);
if (!noStaleMessage) {
log.info(
colors.gray(
` Written ${runnableId}.yaml, ${basePath}${ext}${lock ? ` and ${basePath}lock` : ""}`
)
);
}
} catch (error: any) {
log.error(
colors.red(
@@ -474,11 +497,14 @@ async function updateRawAppRunnables(
writeRunnableToBackend(runnablesFolder, runnableId, runnable);
}
}
return updatedRunnables;
}
/**
* Updates locks for all inline scripts in a normal app, similar to updateRawAppRunnables
* but for the app.value structure instead of app.runnables
* but for the app.value structure instead of app.runnables.
* Returns a tuple of [updated app value, list of script names that were updated].
*/
async function updateAppInlineScripts(
workspace: Workspace,
@@ -486,9 +512,11 @@ async function updateAppInlineScripts(
remotePath: string,
appFolder: string,
rawDeps?: Record<string, string>,
defaultTs: "bun" | "deno" = "bun"
): Promise<any> {
defaultTs: "bun" | "deno" = "bun",
noStaleMessage?: boolean
): Promise<{ value: any; updatedScripts: string[] }> {
const pathAssigner = newPathAssigner(defaultTs, { skipInlineScriptSuffix: getNonDottedPaths() });
const updatedScripts: string[] = [];
const processor: InlineScriptProcessor = async (inlineScript, context) => {
const language = inlineScript.language as SupportedLanguage;
@@ -518,13 +546,15 @@ async function updateAppInlineScripts(
try {
let lock: string | undefined;
if (language !== "frontend") {
log.info(
colors.gray(
`Generating lock for inline script "${scriptName}" at ${context.path.join(
"."
)} (${language})`
)
);
if (!noStaleMessage) {
log.info(
colors.gray(
`Generating lock for inline script "${scriptName}" at ${context.path.join(
"."
)} (${language})`
)
);
}
lock = await generateInlineScriptLock(
workspace,
@@ -553,11 +583,18 @@ async function updateAppInlineScripts(
const inlineLockRef =
lock && lock !== "" ? `!inline ${basePath}lock` : "";
log.info(
colors.gray(
` Written ${basePath}${ext}${lock ? ` and ${basePath}lock` : ""}`
)
);
if (!noStaleMessage) {
log.info(
colors.gray(
` Written ${basePath}${ext}${lock ? ` and ${basePath}lock` : ""}`
)
);
}
// Track that this script was updated (only for non-frontend scripts that needed locks)
if (language !== "frontend") {
updatedScripts.push(scriptName);
}
return {
...inlineScript,
@@ -577,7 +614,8 @@ async function updateAppInlineScripts(
}
};
return await traverseAndProcessInlineScripts(appValue, processor);
const updatedValue = await traverseAndProcessInlineScripts(appValue, processor);
return { value: updatedValue, updatedScripts };
}
/**

View File

@@ -51,6 +51,14 @@ async function generateFlowHash(
}
return { ...hashes, [TOP_HASH]: await generateHash(JSON.stringify(hashes)) };
}
/**
* Result of generating flow locks, including which scripts were updated
*/
export interface FlowLocksResult {
path: string;
updatedScripts: string[];
}
export async function generateFlowLockInternal(
folder: string,
dryRun: boolean,
@@ -60,7 +68,7 @@ export async function generateFlowLockInternal(
},
justUpdateMetadataLock?: boolean,
noStaleMessage?: boolean
): Promise<string | void> {
): Promise<string | FlowLocksResult | void> {
if (folder.endsWith(SEP)) {
folder = folder.substring(0, folder.length - 1);
}
@@ -109,8 +117,9 @@ export async function generateFlowLockInternal(
}
let changedScripts: string[] = [];
if (!justUpdateMetadataLock) {
const changedScripts = [];
//find hashes that do not correspond to previous hashes
for (const [path, hash] of Object.entries(hashes)) {
if (path == TOP_HASH) {
@@ -185,6 +194,13 @@ export async function generateFlowLockInternal(
if (!noStaleMessage) {
log.info(colors.green(`Flow ${remote_path} lockfiles updated`));
}
// Return the list of updated scripts (extract just the filename from the path)
const updatedScripts = changedScripts.map(p => {
const parts = p.split(SEP);
return parts[parts.length - 1].replace(/\.[^.]+$/, ""); // Remove extension
});
return { path: remote_path, updatedScripts };
}
/**

View File

@@ -11,15 +11,15 @@ import {
generateScriptMetadataInternal,
getRawWorkspaceDependencies,
} from "../../utils/metadata.ts";
import { generateFlowLockInternal } from "../flow/flow_metadata.ts";
import { generateAppLocksInternal, getAppFolders } from "../app/app_metadata.ts";
import { generateFlowLockInternal, FlowLocksResult } from "../flow/flow_metadata.ts";
import { generateAppLocksInternal, getAppFolders, AppLocksResult } from "../app/app_metadata.ts";
import {
elementsToMap,
FSFSElement,
ignoreF,
} from "../sync/sync.ts";
import { exts } from "../script/script.ts";
import { isFlowPath, isAppPath } from "../../utils/resource_folders.ts";
import { isFlowPath, isAppPath, isRawAppPath } from "../../utils/resource_folders.ts";
import { listSyncCodebases } from "../../utils/codebase.ts";
interface StaleItem {
@@ -82,7 +82,8 @@ async function generateMetadata(
(!isD && !exts.some((ext) => p.endsWith(ext))) ||
ignore(p, isD) ||
isFlowPath(p) ||
isAppPath(p)
isAppPath(p) ||
isRawAppPath(p)
);
},
false,
@@ -192,12 +193,17 @@ async function generateMetadata(
// === Filter by folder if specified ===
let filteredItems = staleItems;
if (folder) {
// Strip trailing separator to match deprecated flow/app handler behavior
// (see generateFlowLockInternal line 64-66, generateAppLocksInternal line 109-110)
if (folder.endsWith(SEP)) {
// Normalize to forward slashes (Windows users may use backslashes)
folder = folder.replaceAll("\\", "/");
// Strip trailing slash to match deprecated flow/app handler behavior
if (folder.endsWith("/")) {
folder = folder.substring(0, folder.length - 1);
}
filteredItems = staleItems.filter((item) => item.folder === folder || item.folder.startsWith(folder + SEP));
// Normalize item.folder for comparison (Windows file paths use backslashes)
filteredItems = staleItems.filter((item) => {
const normalizedFolder = item.folder.replaceAll("\\", "/");
return normalizedFolder === folder || normalizedFolder.startsWith(folder + "/");
});
}
// === Show stale items and confirm ===
@@ -280,21 +286,23 @@ async function generateMetadata(
// Process flows
for (const item of flows) {
current++;
log.info(`${formatProgress(current)} flow ${colors.cyan(item.path)}`);
await generateFlowLockInternal(
const result = await generateFlowLockInternal(
item.folder,
false, // dryRun
workspace,
opts,
false,
true // noStaleMessage - we handle output
);
) as FlowLocksResult | void;
const scriptsInfo = result?.updatedScripts?.length
? `: ${colors.gray(result.updatedScripts.join(", "))}`
: "";
log.info(`${formatProgress(current)} flow ${colors.cyan(item.path)}${scriptsInfo}`);
}
// Process apps
for (const item of apps) {
current++;
log.info(`${formatProgress(current)} app ${colors.cyan(item.path)}`);
await generateAppLocksInternal(
const result = await generateAppLocksInternal(
item.folder,
item.isRawApp!, // rawApp
false, // dryRun
@@ -302,7 +310,11 @@ async function generateMetadata(
opts,
false,
true // noStaleMessage - we handle output
);
) as AppLocksResult | void;
const scriptsInfo = result?.updatedScripts?.length
? `: ${colors.gray(result.updatedScripts.join(", "))}`
: "";
log.info(`${formatProgress(current)} app ${colors.cyan(item.path)}${scriptsInfo}`);
}
log.info("");

View File

@@ -58,6 +58,7 @@ import {
isFlowInlineScriptPath as isFlowInlineScriptPathInternal,
isFlowPath,
isAppPath,
isRawAppPath,
} from "../../utils/resource_folders.ts";
export interface ScriptFile {
@@ -1027,7 +1028,8 @@ export async function generateMetadata(
(!isD && !exts.some((ext) => p.endsWith(ext))) ||
ignore(p, isD) ||
isFlowPath(p) ||
isAppPath(p)
isAppPath(p) ||
isRawAppPath(p)
);
},
false,

View File

@@ -53,6 +53,7 @@ export interface SimplifiedSettings {
mute_critical_alerts?: boolean;
color?: string;
operator_settings?: any;
datatable?: any;
slack_team_id?: string;
slack_name?: string;
slack_command_script?: string;
@@ -100,6 +101,7 @@ export function migrateToGroupedFormat(settings: any): SimplifiedSettings {
if (settings.mute_critical_alerts !== undefined) result.mute_critical_alerts = settings.mute_critical_alerts;
if (settings.color !== undefined) result.color = settings.color;
if (settings.operator_settings !== undefined) result.operator_settings = settings.operator_settings;
if (settings.datatable !== undefined) result.datatable = settings.datatable;
if (settings.slack_team_id !== undefined) result.slack_team_id = settings.slack_team_id;
if (settings.slack_name !== undefined) result.slack_name = settings.slack_name;
if (settings.slack_command_script !== undefined) result.slack_command_script = settings.slack_command_script;
@@ -192,6 +194,7 @@ export async function pushWorkspaceSettings(
mute_critical_alerts: remoteSettings.mute_critical_alerts,
color: remoteSettings.color,
operator_settings: remoteSettings.operator_settings,
datatable: remoteSettings.datatable,
slack_team_id: remoteSettings.slack_team_id,
slack_name: remoteSettings.slack_name,
slack_command_script: remoteSettings.slack_command_script,
@@ -382,6 +385,14 @@ export async function pushWorkspaceSettings(
});
}
if (!deepEqual(localSettings.datatable, settings.datatable)) {
log.debug(`Updating datatable config...`);
await wmill.editDataTableConfig({
workspace,
requestBody: { settings: localSettings.datatable ?? { datatables: {} } },
});
}
if (localSettings.slack_command_script != settings.slack_command_script) {
log.debug(`Updating slack command script...`);
await wmill.editSlackCommand({

View File

@@ -5072,6 +5072,23 @@ folder related commands
- \`folder add-missing\` - create default folder.meta.yaml for all subdirectories of f/ that are missing one
- \`-y, --yes\` - skip confirmation prompt
### generate-metadata
Generate metadata (locks, schemas) for all scripts, flows, and apps
**Arguments:** \`[folder:string]\`
**Options:**
- \`--yes\` - Skip confirmation prompt
- \`--dry-run\` - Show what would be updated without making changes
- \`--lock-only\` - Re-generate only the lock files
- \`--schema-only\` - Re-generate only script schemas (skips flows and apps)
- \`--skip-scripts\` - Skip processing scripts
- \`--skip-flows\` - Skip processing flows
- \`--skip-apps\` - Skip processing apps
- \`-i --includes <patterns:file[]>\` - Comma separated patterns to specify which files to include
- \`-e --excludes <patterns:file[]>\` - Comma separated patterns to specify which files to exclude
### gitsync-settings
Manage git-sync settings between local wmill.yaml and Windmill backend

View File

@@ -68,7 +68,7 @@ export {
workspaceAdd,
};
export const VERSION = "1.655.0";
export const VERSION = "1.657.0";
// Re-exported from constants.ts to maintain backwards compatibility
export { WM_FORK_PREFIX } from "./core/constants.ts";

View File

@@ -50,26 +50,25 @@ export class LockfileGenerationError extends Error {
}
}
export async function generateAllMetadata() {}
export async function getRawWorkspaceDependencies(): Promise<Record<string, string>> {
const rawWorkspaceDeps: Record<string, string> = {};
try {
const entries = await readdir("dependencies", { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) continue;
const filePath = `dependencies/${entry.name}`;
const content = await readFile(filePath, "utf-8");
// Find matching language
for (const lang of workspaceDependenciesLanguages) {
if (entry.name.endsWith(lang.filename)) {
// Check if out of sync
const contentHash = await generateHash(content + filePath);
const isUpToDate = await checkifMetadataUptodate(filePath, contentHash, undefined);
if (!isUpToDate) {
rawWorkspaceDeps[filePath] = content;
}
@@ -691,6 +690,11 @@ export async function inferSchema(
argSigToJsonSchemaType(arg.typ, currentSchema.properties[arg.name]);
// For T | T[] detection for debouncing arg accumulation
if ((arg as any).otyp && (arg as any).otyp.includes('[') && (arg as any).otyp.includes('|')) {
currentSchema.properties[arg.name].originalType = (arg as any).otyp
}
currentSchema.properties[arg.name].default = arg.default;
if (!arg.has_default && !currentSchema.required.includes(arg.name)) {

View File

@@ -0,0 +1,226 @@
/**
* Datatable settings sync tests
*
* Tests that datatable config is correctly synced via settings.yaml during pull/push operations.
*/
import { expect, test } from "bun:test";
import { writeFile, readFile } from "node:fs/promises";
import { parse, stringify } from "yaml";
import { withTestBackend } from "./test_backend.ts";
import { shouldSkipOnCI } from "./cargo_backend.ts";
import { addWorkspace } from "../workspace.ts";
test.skipIf(shouldSkipOnCI())("Datatable config: included in sync pull settings.yaml", async () => {
await withTestBackend(async (backend, tempDir) => {
const testWorkspace = {
remote: backend.baseUrl,
workspaceId: backend.workspace,
name: "datatable_pull_test",
token: backend.token
};
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
// Configure datatable on backend
const datatableConfig = {
datatables: {
main: {
database: {
resource_path: "u/test/test_db",
resource_type: "postgresql"
}
}
}
};
const configResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/workspaces/edit_datatable_config`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ settings: datatableConfig })
}
);
expect(configResp.ok).toBe(true);
// Create wmill.yaml with includeSettings
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
includeSettings: true`, "utf-8");
// Pull settings
const result = await backend.runCLICommand(['sync', 'pull', '--yes'], tempDir);
expect(result.code).toEqual(0);
// Verify settings.yaml was created and contains datatable
const settingsContent = await readFile(`${tempDir}/settings.yaml`, "utf-8");
expect(settingsContent).toContain("datatable:");
expect(settingsContent).toContain("u/test/test_db");
expect(settingsContent).toContain("postgresql");
});
});
test.skipIf(shouldSkipOnCI())("Datatable config: pushed correctly from settings.yaml", async () => {
await withTestBackend(async (backend, tempDir) => {
const testWorkspace = {
remote: backend.baseUrl,
workspaceId: backend.workspace,
name: "datatable_push_test",
token: backend.token
};
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
// Create wmill.yaml with includeSettings
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
includeSettings: true`, "utf-8");
// First pull to get baseline settings
const pullResult = await backend.runCLICommand(['sync', 'pull', '--yes'], tempDir);
expect(pullResult.code).toEqual(0);
// Read existing settings and add datatable config
const settingsContent = await readFile(`${tempDir}/settings.yaml`, "utf-8");
const existingSettings = parse(settingsContent) as Record<string, unknown>;
existingSettings.datatable = {
datatables: {
analytics: {
database: {
resource_path: "u/admin/analytics_db",
resource_type: "postgresql"
}
}
}
};
await writeFile(`${tempDir}/settings.yaml`, stringify(existingSettings), "utf-8");
// Push the modified settings
const pushResult = await backend.runCLICommand(['sync', 'push', '--yes'], tempDir);
expect(pushResult.code).toEqual(0);
// Verify the backend has the updated datatable config
const settingsResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/workspaces/get_settings`
);
expect(settingsResp.ok).toBe(true);
const settings = await settingsResp.json();
expect(settings.datatable).toBeDefined();
expect(settings.datatable.datatables).toBeDefined();
expect(settings.datatable.datatables.analytics).toBeDefined();
expect(settings.datatable.datatables.analytics.database.resource_path).toEqual("u/admin/analytics_db");
expect(settings.datatable.datatables.analytics.database.resource_type).toEqual("postgresql");
});
});
test.skipIf(shouldSkipOnCI())("Datatable config: empty/undefined doesn't cause errors", async () => {
await withTestBackend(async (backend, tempDir) => {
const testWorkspace = {
remote: backend.baseUrl,
workspaceId: backend.workspace,
name: "datatable_empty_test",
token: backend.token
};
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
// Ensure datatable config is empty/cleared on backend
const clearResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/workspaces/edit_datatable_config`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ settings: { datatables: {} } })
}
);
expect(clearResp.ok).toBe(true);
// Create wmill.yaml with includeSettings
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
includeSettings: true`, "utf-8");
// Pull should succeed even with empty datatable config
const pullResult = await backend.runCLICommand(['sync', 'pull', '--yes'], tempDir);
expect(pullResult.code).toEqual(0);
// Settings.yaml should exist
const settingsContent = await readFile(`${tempDir}/settings.yaml`, "utf-8");
expect(settingsContent.length).toBeGreaterThan(0);
// Push should also succeed
const pushResult = await backend.runCLICommand(['sync', 'push', '--yes'], tempDir);
expect(pushResult.code).toEqual(0);
});
});
test.skipIf(shouldSkipOnCI())("Datatable config: round-trip preserves structure", async () => {
await withTestBackend(async (backend, tempDir) => {
const testWorkspace = {
remote: backend.baseUrl,
workspaceId: backend.workspace,
name: "datatable_roundtrip_test",
token: backend.token
};
await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir });
// Set up a complex datatable config on backend with multiple datatables
const originalConfig = {
datatables: {
users: {
database: {
resource_path: "f/shared/users_db",
resource_type: "postgresql"
}
},
logs: {
database: {
resource_path: "f/shared/logs_db",
resource_type: "instance"
}
}
}
};
const configResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/workspaces/edit_datatable_config`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ settings: originalConfig })
}
);
expect(configResp.ok).toBe(true);
// Create wmill.yaml with includeSettings
await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun
includes:
- "**"
includeSettings: true`, "utf-8");
// Pull
const pullResult = await backend.runCLICommand(['sync', 'pull', '--yes'], tempDir);
expect(pullResult.code).toEqual(0);
// Push back without modification
const pushResult = await backend.runCLICommand(['sync', 'push', '--yes'], tempDir);
expect(pushResult.code).toEqual(0);
// Verify the config is preserved
const settingsResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/workspaces/get_settings`
);
expect(settingsResp.ok).toBe(true);
const settings = await settingsResp.json();
expect(settings.datatable).toBeDefined();
expect(settings.datatable.datatables.users).toBeDefined();
expect(settings.datatable.datatables.logs).toBeDefined();
expect(settings.datatable.datatables.users.database.resource_path).toEqual("f/shared/users_db");
expect(settings.datatable.datatables.logs.database.resource_type).toEqual("instance");
});
});

View File

@@ -107,7 +107,7 @@ async function readFileContent(filePath: string): Promise<string> {
* Create a raw app directory structure on disk
* Uses .raw_app folder suffix with raw_app.yaml metadata
*/
async function createRawAppOnDisk(appDir: string): Promise<void> {
async function createRawAppOnDisk(appDir: string, includeBackend: boolean = false): Promise<void> {
await mkdir(appDir, { recursive: true });
await mkdir(path.join(appDir, "inline_scripts"), { recursive: true });
@@ -131,6 +131,16 @@ async function createRawAppOnDisk(appDir: string): Promise<void> {
INLINE_SCRIPT_A_LOCK,
"utf-8"
);
// Optionally create backend runnable (type: inline)
if (includeBackend) {
await mkdir(path.join(appDir, "backend"), { recursive: true });
await writeFile(path.join(appDir, "backend", "query.yaml"), "type: inline\n", "utf-8");
await writeFile(path.join(appDir, "backend", "query.ts"), `export async function main(x: number): Promise<string> {
return \`Result: \${x}\`;
}
`, "utf-8");
}
}
test("Raw App: full sync workflow - push, pull, modify, push, clear, pull", async () => {
@@ -153,7 +163,7 @@ excludes: []`, "utf-8");
// Create folder structure
const appDir = path.join(tempDir, "f", "test", "my_raw_app.raw_app");
await mkdir(path.join(tempDir, "f", "test"), { recursive: true });
await createRawAppOnDisk(appDir);
await createRawAppOnDisk(appDir, true); // Include backend for metadata test
// =========================================================================
// STEP 1: Initial push - create raw app on backend
@@ -266,6 +276,47 @@ excludes: []`, "utf-8");
const pulledInlineScript = await readFileContent(inlineScriptPath);
expect(pulledInlineScript).toContain("modified:");
// =========================================================================
// STEP 7: Test that script generate-metadata does NOT process backend runnables
// =========================================================================
// Create a standalone script (should be processed by script generate-metadata)
await writeFile(path.join(tempDir, "f", "test", "standalone.ts"), `export async function main(): Promise<string> {
return "hello";
}
`, "utf-8");
// Run script generate-metadata
const metaResult1 = await backend.runCLICommand(
['script', 'generate-metadata', '--yes'],
tempDir, "raw_app_test"
);
expect(metaResult1.code).toEqual(0);
// Run generate-metadata --skip-flows --skip-apps
const metaResult2 = await backend.runCLICommand(
['generate-metadata', '--skip-flows', '--skip-apps', '--yes'],
tempDir, "raw_app_test"
);
expect(metaResult2.code).toEqual(0);
// Backend runnables should NOT have .script.yaml files
const backendDir = path.join(appDir, "backend");
expect(await fileExists(path.join(backendDir, "query.yaml"))).toBeTruthy();
expect(await fileExists(path.join(backendDir, "query.ts"))).toBeTruthy();
expect(await fileExists(path.join(backendDir, "query.script.yaml"))).toBeFalsy();
expect(await fileExists(path.join(backendDir, "query.script.lock"))).toBeFalsy();
// Bug: raw app backend files get misprocessed and create script files at wrong location
// The path f/test/my_raw_app.raw_app/backend/query.ts gets truncated at first "."
// becoming f/test/my_raw_app.script.yaml (stripping .raw_app/backend/query.ts)
expect(await fileExists(path.join(tempDir, "f", "test", "my_raw_app.script.yaml"))).toBeFalsy();
expect(await fileExists(path.join(tempDir, "f", "test", "my_raw_app.script.lock"))).toBeFalsy();
// Standalone script SHOULD have metadata
expect(await fileExists(path.join(tempDir, "f", "test", "standalone.script.yaml"))).toBeTruthy();
expect(await fileExists(path.join(tempDir, "f", "test", "standalone.script.lock"))).toBeTruthy();
});
});

View File

@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.655.0",
"version": "1.657.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.655.0",
"version": "1.657.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@@ -81,11 +81,11 @@
"windmill-parser-wasm-java": "1.510.1",
"windmill-parser-wasm-nu": "1.510.1",
"windmill-parser-wasm-php": "1.647.1",
"windmill-parser-wasm-py": "1.653.0",
"windmill-parser-wasm-py": "1.655.0",
"windmill-parser-wasm-regex": "1.653.0",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.647.1",
"windmill-parser-wasm-ts": "1.653.0",
"windmill-parser-wasm-ts": "1.655.0",
"windmill-parser-wasm-yaml": "1.593.0",
"windmill-sql-datatype-parser-wasm": "1.512.0",
"windmill-utils-internal": "^1.3.4",
@@ -837,6 +837,7 @@
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -848,6 +849,7 @@
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -858,6 +860,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1347,6 +1350,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1503,6 +1507,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1519,6 +1524,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1535,6 +1541,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1551,6 +1558,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1567,6 +1575,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1583,6 +1592,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1599,6 +1609,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1615,6 +1626,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1631,6 +1643,7 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1647,6 +1660,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1663,6 +1677,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1679,6 +1694,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1695,6 +1711,7 @@
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1711,6 +1728,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1727,6 +1745,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2032,6 +2051,7 @@
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -6840,7 +6860,7 @@
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"devOptional": true,
"dev": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@@ -7339,6 +7359,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7359,6 +7380,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7379,6 +7401,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7399,6 +7422,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7419,6 +7443,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7439,6 +7464,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7459,6 +7485,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7479,6 +7506,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7499,6 +7527,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7519,6 +7548,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7539,6 +7569,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -12114,6 +12145,21 @@
}
}
},
"node_modules/svelte-check/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/svelte-eslint-parser": {
"version": "0.43.0",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
@@ -12844,7 +12890,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -13625,9 +13671,9 @@
"integrity": "sha512-u2qaMkupSdhJibxvkLh3r/y36IARvnYNTLXWvOKxcQ0G/BPUB4+yF5o/yf47vv9zUV5WZv4mrdsKDt/pZDYeDg=="
},
"node_modules/windmill-parser-wasm-py": {
"version": "1.653.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.653.0.tgz",
"integrity": "sha512-vMkSL3JpELpag7nmyGA8onhYNiAG3K1mkh2k4vwVHC3W5dUd12fSS9gsBco2FqPGUPnWv1gCaHwmjBrOBVGL1w=="
"version": "1.655.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.655.0.tgz",
"integrity": "sha512-kWpAE2Me/8KwNqq4n+0ZoEqMJZ9aCZf1744j0S6Om+F+XyHFR3e7GYK/slJ4cF7zA9TF6mKUSiW4oHJ+WUlNXQ=="
},
"node_modules/windmill-parser-wasm-regex": {
"version": "1.653.0",
@@ -13645,9 +13691,9 @@
"integrity": "sha512-9yGLYZX2Hn9TdTqGY/5Fp50ftzgUsrfBkSK9vJkKJd5Amyg+yXLBGzd8pz6Org+4uxMenz/16wpsgijvo6uhhQ=="
},
"node_modules/windmill-parser-wasm-ts": {
"version": "1.653.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-ts/-/windmill-parser-wasm-ts-1.653.0.tgz",
"integrity": "sha512-zwBUy7ijo58ooAKcsYflISY/+xllCw3Aq34Kj1PED6uABWVbV6A8MWHqEsjiVzC6iWfihWNtZJpck8zsRr9DCg=="
"version": "1.655.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-ts/-/windmill-parser-wasm-ts-1.655.0.tgz",
"integrity": "sha512-nzvxILV68MccOKuZcGwjZLB+mWzPMMqEP0FTOb9nmkRWzKPFMxs5ADAXrDZINeNLNv64hYQhGw66ouq0P1G6jQ=="
},
"node_modules/windmill-parser-wasm-yaml": {
"version": "1.593.0",

View File

@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.655.0",
"version": "1.657.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -154,11 +154,11 @@
"windmill-parser-wasm-java": "1.510.1",
"windmill-parser-wasm-nu": "1.510.1",
"windmill-parser-wasm-php": "1.647.1",
"windmill-parser-wasm-py": "1.653.0",
"windmill-parser-wasm-py": "1.655.0",
"windmill-parser-wasm-regex": "1.653.0",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.647.1",
"windmill-parser-wasm-ts": "1.653.0",
"windmill-parser-wasm-ts": "1.655.0",
"windmill-parser-wasm-yaml": "1.593.0",
"windmill-sql-datatype-parser-wasm": "1.512.0",
"windmill-utils-internal": "^1.3.4",
@@ -583,4 +583,4 @@
"@rollup/rollup-linux-x64-gnu": "^4.35.0",
"fsevents": "^2.3.3"
}
}
}

View File

@@ -291,7 +291,12 @@
workers.some(([_, pings]) => pings.some((p) => p.native_mode === true)))
)
let nonNativeTags = $derived(
(nconfig?.worker_tags ?? []).filter((t) => !nativeTags.includes(t) && t !== 'flow')
(nconfig?.worker_tags ?? []).filter(
(t) =>
!nativeTags.some((nt) => t === nt || t.startsWith(`${nt}-`)) &&
t !== 'flow' &&
!t.startsWith('flow-')
)
)
let isAutoNativeMode = $derived(name === 'native')
let isNativeModeEnabled = $derived(nconfig?.native_mode === true || isAutoNativeMode)
@@ -569,8 +574,8 @@
<Alert size="xs" type="warning" title="Non-native tags detected">
This worker group has native mode enabled but includes non-native tags: {nonNativeTags.join(
', '
)}. Non-native jobs will be failed. This is fine if those custom tags are only used
for native language jobs.
)}. This is fine if jobs sent to those tags are native only, otherwise they will be
failed.
</Alert>
{/if}
{#if isNativeModeEnabled && nconfig?.worker_tags != undefined && !nconfig.worker_tags.includes(defaultTagPerWorkspace && workspaceTag ? `flow-${workspaceTag}` : 'flow')}

View File

@@ -1,5 +1,16 @@
<script lang="ts" module>
export let openedDrawers: { val: string[] } = $state({ val: [] })
// When a disposable with minZIndex is open, all disposables use that as
// their z-index base so that overlays opened on top (e.g. a Drawer from
// inside a Modal) stack correctly above it.
// We track per-id entries so concurrent modals don't clobber each other
// (closing one must not reset the base while another is still open).
let minZIndexEntries: Record<string, number> = $state({})
let activeMinZIndex = $derived.by(() => {
const values = Object.values(minZIndexEntries)
return values.length > 0 ? Math.max(...values) : 0
})
</script>
<script lang="ts">
@@ -11,6 +22,11 @@
id?: any
preventEscape?: boolean
initialOffset?: number
/** Minimum z-index base for this overlay. While any disposable with a
* minZIndex is open, all disposables use that as their base so that
* subsequent overlays stack above it (e.g. zIndexes.aiChat + 1 for
* modals that need to render above the AI chat panel). */
minZIndex?: number
children?: import('svelte').Snippet<[any]>
onOpen?: () => void
onClose?: () => void
@@ -21,13 +37,17 @@
id = (Math.random() + 1).toString(36).substring(10),
preventEscape = false,
initialOffset = 0,
minZIndex = 0,
children,
onOpen,
onClose
}: Props = $props()
let offset = $state(untrack(() => initialOffset))
let zIndex = $derived(zIndexes.disposables + offset)
// Note: when a Modal with minZIndex is open, all disposables (including
// already-open Drawers) are elevated. This is acceptable — relative
// stacking order is preserved by the per-instance offset.
let zIndex = $derived(Math.max(zIndexes.disposables, activeMinZIndex) + offset)
export function toggleDrawer() {
if (!open) {
@@ -44,6 +64,9 @@
}
openedDrawers.val.push(id)
offset = initialOffset + openedDrawers.val.length
if (minZIndex > 0) {
minZIndexEntries[id] = minZIndex
}
}
export function closeDrawer() {
@@ -51,6 +74,9 @@
offset = initialOffset
if (openedDrawers.val.includes(id)) {
openedDrawers.val = openedDrawers.val.filter((drawer) => drawer !== id)
if (minZIndex > 0) {
delete minZIndexEntries[id]
}
}
}
@@ -89,6 +115,9 @@
if (open) {
openedDrawers.val.push(untrack(() => id))
offset = untrack(() => initialOffset) + openedDrawers.val.length
if (minZIndex > 0) {
minZIndexEntries[untrack(() => id)] = minZIndex
}
}
let wasEverOpen = false

View File

@@ -2,11 +2,14 @@
import { createBubbler, stopPropagation } from 'svelte/legacy'
const bubble = createBubbler()
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { fade } from 'svelte/transition'
import Button from '../button/Button.svelte'
import { twMerge } from 'tailwind-merge'
import CloseButton from '../CloseButton.svelte'
import Disposable from '../drawer/Disposable.svelte'
import { zIndexes } from '$lib/zIndexes'
import { chatState } from '$lib/components/copilot/chat/sharedChatState.svelte'
interface Props {
title: string
@@ -28,12 +31,28 @@
cancelText = undefined,
kind = 'button',
settings,
children,
children: children_render,
actions
}: Props = $props()
const dispatch = createEventDispatcher()
let disposable: Disposable | undefined = $state(undefined)
// Only elevate above the AI chat panel when it's actually open —
// when chat is closed there's nothing at z-index 1200 to stack above.
const minZIndex = $derived(chatState.size > 0 ? zIndexes.aiChat + 1 : 0)
// Both `bind:open` and this $effect are needed: bind:open syncs the
// boolean, while the effect calls openDrawer/closeDrawer to register
// the disposable in the stacking system (same pattern as Drawer.svelte).
$effect(() => {
open
untrack(() => {
open ? disposable?.openDrawer() : disposable?.closeDrawer()
})
})
function onKeyDown(event: KeyboardEvent) {
if (open) {
switch (event.key) {
@@ -58,70 +77,80 @@
<svelte:window onkeydowncapture={onKeyDown} />
{#if open}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
onclick={() => (open = false)}
transition:fadeFast|local
class={'fixed top-0 bottom-0 left-0 right-0 z-[9999]'}
role="dialog"
tabindex="-1"
>
<div
class={twMerge(
'fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity',
open ? 'ease-out duration-300 opacity-100' : 'ease-in duration-200 opacity-0'
)}
></div>
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<Disposable bind:open bind:this={disposable} preventEscape {minZIndex}>
{#snippet children({ zIndex })}
{#if open}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
onclick={() => (open = false)}
transition:fadeFast|local
class="fixed top-0 bottom-0 left-0 right-0"
style="z-index: {zIndex}"
role="dialog"
tabindex="-1"
>
<div
onclick={stopPropagation(bubble('click'))}
class={twMerge(
'relative transform overflow-hidden rounded-md bg-surface px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6',
c,
'fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity',
open
? 'ease-out duration-300 opacity-100 translate-y-0 sm:scale-100'
: 'ease-in duration-200 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
? 'ease-out duration-300 opacity-100'
: 'ease-in duration-200 opacity-0'
)}
{style}
>
{#if kind == 'X'}
<div class="absolute top-4 right-4"><CloseButton on:close={() => (open = false)} /></div
>
{/if}
<div class="flex">
<div class="text-left flex-1">
<div class="flex flex-row items-center justify-between">
<h3 class="text-emphasis text-lg font-semibold">{title}</h3>
{@render settings?.()}
</div>
></div>
<div class="mt-4 text-sm text-primary">
{@render children?.()}
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
onclick={stopPropagation(bubble('click'))}
class={twMerge(
'relative transform overflow-hidden rounded-md bg-surface px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6',
c,
open
? 'ease-out duration-300 opacity-100 translate-y-0 sm:scale-100'
: 'ease-in duration-200 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
)}
{style}
>
{#if kind == 'X'}
<div class="absolute top-4 right-4"
><CloseButton on:close={() => (open = false)} /></div
>
{/if}
<div class="flex">
<div class="text-left flex-1">
<div class="flex flex-row items-center justify-between">
<h3 class="text-emphasis text-lg font-semibold">{title}</h3>
{@render settings?.()}
</div>
<div class="mt-4 text-sm text-primary">
{@render children_render?.()}
</div>
</div>
</div>
{#if kind == 'button'}
<div
class="flex items-center space-x-2 flex-row-reverse space-x-reverse mt-4"
>
{@render actions?.()}
<Button
on:click={() => {
dispatch('canceled')
open = false
}}
color="light"
size="sm"
>
{cancelText ?? 'Cancel'}
</Button>
</div>
{/if}
</div>
</div>
{#if kind == 'button'}
<div class="flex items-center space-x-2 flex-row-reverse space-x-reverse mt-4">
{@render actions?.()}
<Button
on:click={() => {
dispatch('canceled')
open = false
}}
color="light"
size="sm"
>
{cancelText ?? 'Cancel'}
</Button>
</div>
{/if}
</div>
</div>
</div>
</div>
{/if}
{/if}
{/snippet}
</Disposable>

View File

@@ -12,7 +12,7 @@
import { yamlStringifyExceptKeys } from './utils'
import type { ChatCompletionMessageParam } from 'openai/resources/index.mjs'
import { triggerableByAI } from '$lib/actions/triggerableByAI.svelte'
import { validateToolName } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
import { getToolNameError } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
import {
inputBaseClass,
inputBorderClass,
@@ -117,6 +117,7 @@ Generate a tool name for the script below:
elementProps?: Record<string, any>
class?: string
onChange?: (content: string) => void
siblingToolNames?: string[]
}
let {
@@ -130,9 +131,16 @@ Generate a tool name for the script below:
elementType = 'input',
elementProps = {},
class: clazz = '',
onChange = undefined
onChange = undefined,
siblingToolNames = undefined
}: Props = $props()
let toolNameError = $derived(
promptConfigName === 'agentToolFunctionName'
? getToolNameError(content ?? '', undefined, siblingToolNames)
: undefined
)
let el: HTMLElement | undefined = $state()
let generatedContent = $state('')
let active = $state(false)
@@ -347,16 +355,16 @@ Generate a tool name for the script below:
inputBaseClass,
inputSizeClasses.md,
inputBorderClass({
error: promptConfigName === 'agentToolFunctionName' && !validateToolName(content ?? '')
error: !!toolNameError
}),
'w-full'
)}
onfocus={() => (focused = true)}
onblur={() => (focused = false)}
/>
{#if promptConfigName === 'agentToolFunctionName' && !validateToolName(content ?? '')}
{#if toolNameError}
<p class="text-3xs text-red-400 leading-tight mt-0.5">
Invalid tool name, should only contain letters, numbers and underscores
{toolNameError}
</p>
{/if}
{/if}

View File

@@ -30,11 +30,37 @@
fontClass?: string
} = $props()
// Get list of array-type arguments from schema
// Check if an originalType like "string | string[]" is a top-level
// union where at least one member is an array type (ends with "[]").
// Splits on "|" only at the top level (not inside {}, <>, or ()).
function isUnionWithArray(originalType: string | undefined): boolean {
if (!originalType) return false
let depth = 0
const parts: string[] = []
let cur = ''
for (const ch of originalType) {
if (ch === '{' || ch === '<' || ch === '(') depth++
else if (ch === '}' || ch === '>' || ch === ')') depth--
else if (ch === '|' && depth === 0) {
parts.push(cur.trim())
cur = ''
continue
}
cur += ch
}
parts.push(cur.trim())
// Match TS array syntax (T[]) and Python list syntax (list[T] / List[T])
return parts.length > 1 && parts.some((p) => p.endsWith('[]') || /^[Ll]ist\[.+\]$/.test(p))
}
// Get list of arguments eligible for accumulation from schema.
// Includes array-type arguments and union types like T | T[]
// whose scalar values are wrapped into single-element arrays
// at aggregation time.
let arrayArgs = $derived(
schema?.properties
? Object.entries(schema.properties)
.filter(([_, prop]) => prop.type === 'array')
.filter(([_, prop]) => prop.type === 'array' || isUnionWithArray(prop.originalType))
.map(([key, _]) => key)
: []
)
@@ -111,8 +137,8 @@
<Label label="Argument to accumulate (optional)">
{#snippet header()}
<Tooltip>
Select a list-type argument to accumulate across debounced executions. Values from
each debounced execution will be appended together.</Tooltip
Select a list-type argument to accumulate across debounced executions. Values from each
debounced execution will be appended together.</Tooltip
>
{/snippet}
<select disabled={!$enterpriseLicense} bind:value={selectedArg}>
@@ -131,8 +157,8 @@
<Label label="Max total debouncing time (optional)">
{#snippet header()}
<Tooltip>
Maximum total time (in seconds) that a job can be debounced before it must execute.
Once this time is reached, the job will run regardless of ongoing debouncing.</Tooltip
Maximum total time (in seconds) that a job can be debounced before it must execute. Once
this time is reached, the job will run regardless of ongoing debouncing.</Tooltip
>
{/snippet}
<SecondsInput disabled={!$enterpriseLicense} bind:seconds={max_total_debouncing_time} />
@@ -140,11 +166,16 @@
<Label label="Max total debounces amount (optional)">
{#snippet header()}
<Tooltip>
Maximum number of times a job can be debounced before it must execute. Once this
count is reached, the job will run regardless of ongoing debouncing.</Tooltip
Maximum number of times a job can be debounced before it must execute. Once this count
is reached, the job will run regardless of ongoing debouncing.</Tooltip
>
{/snippet}
<input type="number" disabled={!$enterpriseLicense} bind:value={max_total_debounces_amount} min="0" />
<input
type="number"
disabled={!$enterpriseLicense}
bind:value={max_total_debounces_amount}
min="0"
/>
</Label>
</div>
{/if}

View File

@@ -12,6 +12,7 @@
action?: import('svelte').Snippet
children?: import('svelte').Snippet
isAgentTool?: boolean
siblingToolNames?: string[]
}
let {
@@ -23,7 +24,8 @@
header,
action,
children,
isAgentTool = false
isAgentTool = false,
siblingToolNames = undefined
}: Props = $props()
</script>
@@ -38,6 +40,7 @@
{flowModuleValue}
{action}
{isAgentTool}
{siblingToolNames}
>
{@render header?.()}
</FlowCardHeader>

View File

@@ -18,7 +18,7 @@
import { Flag, Lock, RefreshCw, Unlock } from 'lucide-svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { twMerge } from 'tailwind-merge'
import { validateToolName } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
import { getToolNameError } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
import { DEFAULT_HUB_BASE_URL, PRIVATE_HUB_MIN_VERSION } from '$lib/hub'
interface Props {
@@ -28,6 +28,7 @@
children?: import('svelte').Snippet
action?: import('svelte').Snippet
isAgentTool?: boolean
siblingToolNames?: string[]
}
let {
@@ -36,9 +37,14 @@
summary = $bindable(undefined),
children,
action,
isAgentTool = false
isAgentTool = false,
siblingToolNames = undefined
}: Props = $props()
let toolNameError = $derived(
isAgentTool ? getToolNameError(summary ?? '', undefined, siblingToolNames) : undefined
)
let latestHash: string | undefined = $state(undefined)
// Extract version_id from hub path (format: hub/{version_id}/{app}/{summary})
@@ -103,6 +109,7 @@
elementProps={{
placeholder: isAgentTool ? 'Tool name' : 'Summary'
}}
{siblingToolNames}
/>
{:else if flowModuleValue.type === 'script' && 'path' in flowModuleValue && flowModuleValue.path}
<IconedPath path={flowModuleValue.path} hash={flowModuleValue.hash} class="grow" />
@@ -173,14 +180,16 @@
/>
</div>
{/if}
<input
bind:value={summary}
placeholder={isAgentTool ? 'Tool name' : 'Summary'}
class={twMerge(
'w-full grow',
isAgentTool && !validateToolName(summary ?? '') && '!border-red-400'
)}
/>
<div class="flex flex-col w-full grow">
<input
bind:value={summary}
placeholder={isAgentTool ? 'Tool name' : 'Summary'}
class={twMerge('w-full grow', toolNameError && '!border-red-400')}
/>
{#if toolNameError}
<p class="text-3xs text-red-400 leading-tight mt-0.5">{toolNameError}</p>
{/if}
</div>
{:else if flowModuleValue.type === 'flow'}
<Badge color="indigo" capitalize>flow</Badge>
<input bind:value={summary} placeholder="Summary" class="w-full grow" />

View File

@@ -14,6 +14,7 @@
previousModule?: FlowModule | undefined
forceTestTab?: Record<string, boolean>
highlightArg?: Record<string, string | undefined>
siblingToolNames?: string[]
}
let {
@@ -23,7 +24,8 @@
parentModule = undefined,
previousModule = undefined,
forceTestTab,
highlightArg
highlightArg,
siblingToolNames = undefined
}: Props = $props()
</script>
@@ -43,6 +45,7 @@
forceTestTab={forceTestTab?.[tool.id]}
highlightArg={highlightArg?.[tool.id]}
isAgentTool={true}
{siblingToolNames}
/>
{:else if isMcpTool(tool)}
<!-- MCP tool - use McpToolEditor -->

View File

@@ -101,9 +101,7 @@
)
let canMoveSelected = $derived(
resolvedModuleIds.length > 0 &&
areContiguousSiblings(
locateModules(resolvedModuleIds, flowStore.val.value.modules ?? [])
)
areContiguousSiblings(locateModules(resolvedModuleIds, flowStore.val.value.modules ?? []))
)
</script>

View File

@@ -110,6 +110,7 @@
forceTestTab?: boolean
highlightArg?: string
isAgentTool?: boolean
siblingToolNames?: string[]
}
let {
@@ -125,7 +126,8 @@
savedModule = undefined,
forceTestTab = false,
highlightArg = undefined,
isAgentTool = false
isAgentTool = false,
siblingToolNames = undefined
}: Props = $props()
let workspaceScriptTag: string | undefined = $state(undefined)
@@ -237,7 +239,9 @@
}
let forceReload = $state(0)
let editorPanelSize = $state(untrack(() => noEditor) ? 0 : flowModule.value.type == 'script' ? 30 : 50)
let editorPanelSize = $state(
untrack(() => noEditor) ? 0 : flowModule.value.type == 'script' ? 30 : 50
)
let editorSettingsPanelSize = $state(100 - untrack(() => editorPanelSize))
let stepHistoryLoader = getStepHistoryLoaderContext()
@@ -726,6 +730,7 @@
}}
bind:summary={flowModule.summary}
{isAgentTool}
{siblingToolNames}
>
{#snippet header()}
<FlowModuleHeader
@@ -1062,8 +1067,8 @@
{enableAi}
{isAgentTool}
allowedAiTransforms={isAgentTool && flowModule.value.type === 'aiagent'
? ['user_message']
: undefined}
? ['user_message']
: undefined}
helperScript={retrieveDynCodeAndLang(flowModule.value)}
chatInputEnabled={flowStore.val.value?.chat_input_enabled ?? false}
/>

View File

@@ -305,6 +305,7 @@
{enableAi}
{forceTestTab}
{highlightArg}
siblingToolNames={flowModule.value.tools.map((t) => t.summary ?? '')}
/>
{/if}
{/each}

View File

@@ -300,6 +300,7 @@ export type AiToolN = {
data: {
tool: string
type?: string
nameError?: string
eventHandlers: GraphEventHandlers
moduleId: string
insertable: boolean

View File

@@ -1,10 +1,29 @@
<script module lang="ts">
export function validateToolName(name: string, type?: string) {
if (type === 'websearch') return true
import { forbiddenIds } from '$lib/components/flows/idUtils'
export function getToolNameError(
name: string,
type?: string,
siblingNames?: string[]
): string | undefined {
if (type === 'websearch') return undefined
if (type === 'mcp') {
return name.length > 0
return name.length > 0 ? undefined : 'Tool name must not be empty'
}
return /^[a-zA-Z0-9_]+$/.test(name)
if (!/^[a-zA-Z0-9_]+$/.test(name)) {
return 'Tool name must only contain letters, numbers and underscores'
}
if (forbiddenIds.includes(name)) {
return `'${name}' is a reserved name`
}
if (siblingNames && siblingNames.filter((n) => n === name).length > 1) {
return 'Duplicate tool name'
}
return undefined
}
export function validateToolName(name: string, type?: string) {
return getToolNameError(name, type) === undefined
}
export const AI_TOOL_BASE_OFFSET = 5
@@ -147,6 +166,7 @@
}
}
const siblingNames = tools.map((t) => t.name)
const toolNodes: (Node & AiToolN)[] = tools.map((tool, i) => {
let inputToolXGap = 12
let inputToolWidth = (ROW_WIDTH - inputToolXGap) / 2
@@ -160,6 +180,7 @@
data: {
tool: tool.name,
type: tool.type,
nameError: getToolNameError(tool.name, tool.type, siblingNames),
eventHandlers,
moduleId: tool.id,
insertable,
@@ -169,13 +190,13 @@
width: inputToolWidth,
position: {
x:
(tools.length === 1
tools.length === 1
? (ROW_WIDTH - inputToolWidth) / 2
: (i + 1) % 2 === 0
? inputToolWidth + inputToolXGap
: isLastRow && tools.length % 2 === 1
? (ROW_WIDTH - inputToolWidth) / 2
: 0),
: 0,
y:
baseOffset +
rowOffset *
@@ -287,7 +308,7 @@
const flowModuleState = $derived(data.flowModuleStates?.[data.moduleId])
let colorClasses = $derived(
getNodeColorClasses(
!validateToolName(data.tool, data.type) ? 'Failure' : flowModuleState?.type,
data.nameError ? 'Failure' : flowModuleState?.type,
selectionManager?.getSelectedId() === data.moduleId
)
)
@@ -324,12 +345,7 @@
<Wrench size={16} class="ml-1 shrink-0" />
{/if}
<span
class={twMerge(
'text-3xs truncate flex-1',
!validateToolName(data.tool, data.type) && 'text-red-400'
)}
>
<span class={twMerge('text-3xs truncate flex-1', data.nameError && 'text-red-400')}>
{data.tool || 'Missing name'}
</span>
</button>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { run } from 'svelte/legacy'
import { FlowService, ScriptService, UserService, type TruncatedToken } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
@@ -10,23 +10,21 @@
import { capitalize } from '$lib/utils'
interface Props {
isFlow: boolean;
path: string;
labelPrefix: 'email' | 'webhook';
isFlow: boolean
path: string
labelPrefix: 'email' | 'webhook'
}
let { isFlow, path, labelPrefix }: Props = $props();
let { isFlow, path, labelPrefix }: Props = $props()
const { triggersCount } = getContext<TriggerContext>('TriggerContext')
let tokens: TruncatedToken[] | undefined = $state(undefined)
export async function listTokens() {
tokens = (
isFlow
? await FlowService.listTokensOfFlow({ workspace: $workspaceStore!, path })
: await ScriptService.listTokensOfScript({ workspace: $workspaceStore!, path })
).filter((x) => x.label && x.label.startsWith(labelPrefix + '-'))
tokens = isFlow
? await FlowService.listTokensOfFlow({ workspace: $workspaceStore!, path })
: await ScriptService.listTokensOfScript({ workspace: $workspaceStore!, path })
if (labelPrefix == 'email') {
$triggersCount = { ...($triggersCount ?? {}), default_email_count: tokens?.length }
} else {
@@ -43,14 +41,14 @@
run(() => {
$workspaceStore && listTokens()
});
})
</script>
<div class="flex flex-col gap-2">
<Label label="Existing {capitalize(labelPrefix)} Tokens">
{#if tokens}
{#if tokens.length == 0}
<div class="text-xs text-secondary">No {labelPrefix} specific tokens found</div>
<div class="text-xs text-secondary">No tokens with matching scopes found</div>
{:else}
<div class="flex flex-col divide-y pt-2">
<div class="grid grid-cols-6 text-2xs items-center py-2">

View File

@@ -105,17 +105,17 @@ async function initWasmAsset() {
type InferAssetsResult =
| {
status: 'ok'
assets: AssetWithAccessType[]
sql_queries?: InferAssetsSqlQueryDetails[]
columns?: Record<string, AssetUsageAccessType>
}
status: 'ok'
assets: AssetWithAccessType[]
sql_queries?: InferAssetsSqlQueryDetails[]
columns?: Record<string, AssetUsageAccessType>
}
| {
status: 'error'
error: string
assets?: undefined
sql_queries?: undefined
}
status: 'error'
error: string
assets?: undefined
sql_queries?: undefined
}
export type InferAssetsSqlQueryDetails = {
query_string: string // SQL query with $1 placeholders for interpolations
@@ -384,6 +384,11 @@ export async function inferArgs(
argSigToJsonSchemaType(arg.typ, schema.properties[arg.name])
// For T | T[] detection for debouncing arg accumulation
if ((arg as any).otyp && (arg as any).otyp.includes('[') && (arg as any).otyp.includes('|')) {
schema.properties[arg.name].originalType = (arg as any).otyp
}
schema.properties[arg.name].default = arg.default
if (!arg.has_default && !schema.required.includes(arg.name)) {

View File

@@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.655.0"
wmill = ">=1.657.0"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"

View File

@@ -1,7 +1,7 @@
openapi: '3.0.3'
info:
version: 1.655.0
version: 1.657.0
title: OpenFlow Spec
contact:
name: Ruben Fiszel

View File

@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.655.0'
ModuleVersion = '1.657.0'
# Supported PSEditions
# CompatiblePSEditions = @()

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.655.0"
version = "1.657.0"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"

View File

@@ -119,6 +119,23 @@ folder related commands
- `folder add-missing` - create default folder.meta.yaml for all subdirectories of f/ that are missing one
- `-y, --yes` - skip confirmation prompt
### generate-metadata
Generate metadata (locks, schemas) for all scripts, flows, and apps
**Arguments:** `[folder:string]`
**Options:**
- `--yes` - Skip confirmation prompt
- `--dry-run` - Show what would be updated without making changes
- `--lock-only` - Re-generate only the lock files
- `--schema-only` - Re-generate only script schemas (skips flows and apps)
- `--skip-scripts` - Skip processing scripts
- `--skip-flows` - Skip processing flows
- `--skip-apps` - Skip processing apps
- `-i --includes <patterns:file[]>` - Comma separated patterns to specify which files to include
- `-e --excludes <patterns:file[]>` - Comma separated patterns to specify which files to exclude
### gitsync-settings
Manage git-sync settings between local wmill.yaml and Windmill backend

View File

@@ -1493,6 +1493,23 @@ folder related commands
- \`folder add-missing\` - create default folder.meta.yaml for all subdirectories of f/ that are missing one
- \`-y, --yes\` - skip confirmation prompt
### generate-metadata
Generate metadata (locks, schemas) for all scripts, flows, and apps
**Arguments:** \`[folder:string]\`
**Options:**
- \`--yes\` - Skip confirmation prompt
- \`--dry-run\` - Show what would be updated without making changes
- \`--lock-only\` - Re-generate only the lock files
- \`--schema-only\` - Re-generate only script schemas (skips flows and apps)
- \`--skip-scripts\` - Skip processing scripts
- \`--skip-flows\` - Skip processing flows
- \`--skip-apps\` - Skip processing apps
- \`-i --includes <patterns:file[]>\` - Comma separated patterns to specify which files to include
- \`-e --excludes <patterns:file[]>\` - Comma separated patterns to specify which files to exclude
### gitsync-settings
Manage git-sync settings between local wmill.yaml and Windmill backend

View File

@@ -124,6 +124,23 @@ folder related commands
- `folder add-missing` - create default folder.meta.yaml for all subdirectories of f/ that are missing one
- `-y, --yes` - skip confirmation prompt
### generate-metadata
Generate metadata (locks, schemas) for all scripts, flows, and apps
**Arguments:** `[folder:string]`
**Options:**
- `--yes` - Skip confirmation prompt
- `--dry-run` - Show what would be updated without making changes
- `--lock-only` - Re-generate only the lock files
- `--schema-only` - Re-generate only script schemas (skips flows and apps)
- `--skip-scripts` - Skip processing scripts
- `--skip-flows` - Skip processing flows
- `--skip-apps` - Skip processing apps
- `-i --includes <patterns:file[]>` - Comma separated patterns to specify which files to include
- `-e --excludes <patterns:file[]>` - Comma separated patterns to specify which files to exclude
### gitsync-settings
Manage git-sync settings between local wmill.yaml and Windmill backend

View File

@@ -1,6 +1,6 @@
{
"name": "@windmill/windmill",
"version": "1.655.0",
"version": "1.657.0",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]

View File

@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.655.0",
"version": "1.657.0",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"sideEffects": false,

View File

@@ -1 +1 @@
1.655.0
1.657.0