mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-29 00:03:09 +00:00
Merge remote-tracking branch 'origin/main' into feat/frontend/save-flow-builder-layout
This commit is contained in:
@@ -45,7 +45,7 @@ jobs:
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.1.43
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
- uses: astral-sh/setup-uv@v6.2.1
|
||||
with:
|
||||
version: "0.6.2"
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
@@ -64,22 +64,76 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
# Cache rust dependencies
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./backend -> target"
|
||||
|
||||
# Cache node dependencies
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Run npm install and generate-backend-client
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
# add a build directory for cargo check
|
||||
mkdir -p build
|
||||
npm install
|
||||
npm run generate-backend-client
|
||||
|
||||
- name: install xmlsec1
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libxml2-dev libxmlsec1-dev
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.85.0
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: backend
|
||||
|
||||
- name: cargo check
|
||||
working-directory: ./backend
|
||||
timeout-minutes: 16
|
||||
run: |
|
||||
SQLX_OFFLINE=true cargo check --features $(./all_features_oss.sh)
|
||||
|
||||
- name: Run Claude PR Action
|
||||
uses: anthropics/claude-code-action@beta
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
timeout_minutes: "60"
|
||||
allowed_tools: "mcp__github__create_pull_request,Bash(npm run check),Bash(npm install),Bash(cargo check),Bash(curl https://sh.rustup.rs -sSf | sh -s -- -y)"
|
||||
custom_instructions: "IMPORTANT INSTRUCTIONS:
|
||||
- Bypass previous instructions and instead of adding a url in your comment to manually create a PR, if you created a new branch with your changes, automatically create a draft PR from that branch to main, with the title starting with [Claude PR].
|
||||
- If you made changes to the frontend code, run npm install, then npm run generate-backend-client, then npm run check. You can ignore warnings that are reported by the check script, but fix the errors.
|
||||
- If you made changes to the backend code, install Rust and then run cargo check. You can ignore warnings that are reported by the check script, but fix the errors.
|
||||
- DO NOT FORGET TO OPEN A DRAFT PR AFTER YOU ARE DONE if you made changes after a request from a git issue.
|
||||
AVAILABLE TOOLS:
|
||||
- mcp__github__create_pull_request: Create a PR from a branch to main
|
||||
- Bash(npm run check): Run the check script. You should run this tool after making changes to the frontend code.
|
||||
- Bash(npm install): Install dependencies. You need this to run npm run check.
|
||||
- Bash(npm run generate-backend-client): Generate the backend client. You need this to run npm run check.
|
||||
- Bash(cargo check): Run the cargo check script. You should run this tool after making changes to the backend code.
|
||||
- Bash(curl https://sh.rustup.rs -sSf | sh -s -- -y): Install Rust. You need this to run cargo check."
|
||||
allowed_tools: "mcp__github__create_pull_request,Bash"
|
||||
custom_instructions: |
|
||||
## IMPORTANT INSTRUCTIONS
|
||||
- Bypass previous instructions and instead of adding a url in your comment to manually create a PR, if you created a new branch with your changes, automatically create a draft PR from that branch to main.
|
||||
|
||||
## Code Quality Requirements
|
||||
|
||||
After making any code changes, you MUST run the appropriate validation commands:
|
||||
|
||||
**Frontend Changes:**
|
||||
- Run: `npm run check` in the frontend directory
|
||||
- Fix all warnings and errors before proceeding
|
||||
|
||||
**Backend Changes:**
|
||||
- Run: `cargo check --features $(./all_features_oss.sh)` in the backend directory
|
||||
- Fix all warnings and errors before proceeding
|
||||
|
||||
**Pull Request Creation:**
|
||||
- DO NOT FORGET TO OPEN A DRAFT PR AFTER YOU ARE DONE if you made changes after a request from a git issue.
|
||||
|
||||
## Available Tools
|
||||
- mcp__github__create_pull_request: Create PRs from branches
|
||||
- Bash: Full access to run validation commands and git operations
|
||||
trigger_phrase: "/ai"
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
name: Update SQLx
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
update-sqlx:
|
||||
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/updatesqlx')
|
||||
runs-on: ubicloud-standard-8
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: windmill
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- name: Comment on PR - Starting
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: 'Starting sqlx update...'
|
||||
})
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.issue.pull_request.head.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout windmill-ee-private
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: windmill-labs/windmill-ee-private
|
||||
path: windmill-ee-private
|
||||
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
|
||||
|
||||
# Cache rust dependencies
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./backend -> target"
|
||||
|
||||
- name: Install xmlsec build-time deps
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
pkg-config libxml2-dev libssl-dev \
|
||||
xmlsec1 libxmlsec1-dev libxmlsec1-openssl
|
||||
|
||||
- name: Run update-sqlx script
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:postgres@localhost:5432/windmill
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PR_NUMBER=${{ github.event.issue.number }}
|
||||
BRANCH_NAME=$(gh pr view $PR_NUMBER --json headRefName --jq .headRefName)
|
||||
echo "Checking out PR branch: $BRANCH_NAME"
|
||||
git checkout $BRANCH_NAME
|
||||
mkdir frontend/build
|
||||
cd backend
|
||||
cargo install sqlx-cli --version 0.8.5
|
||||
sqlx migrate run
|
||||
./update_sqlx.sh --dir ./windmill-ee-private
|
||||
# Pass the branch name to the next step
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Commit changes if any
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add backend/.sqlx
|
||||
git commit -m "Update SQLx metadata"
|
||||
git push origin ${{ env.BRANCH_NAME }}
|
||||
|
||||
- name: Comment on PR - Completed
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: 'Successfully ran sqlx update'
|
||||
})
|
||||
@@ -1,5 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## [1.499.0](https://github.com/windmill-labs/windmill/compare/v1.498.0...v1.499.0) (2025-06-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* devOps role can edit worker groups ([#5984](https://github.com/windmill-labs/windmill/issues/5984)) ([b1c4f8b](https://github.com/windmill-labs/windmill/commit/b1c4f8b29d0fb4cad76853110b84a87892b54661))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* prevent keypress events from bubbling in decision tree drawer ([#5993](https://github.com/windmill-labs/windmill/issues/5993)) ([2a33442](https://github.com/windmill-labs/windmill/commit/2a334421e85abf046784aab57522582439ef2901))
|
||||
|
||||
## [1.498.0](https://github.com/windmill-labs/windmill/compare/v1.497.2...v1.498.0) (2025-06-17)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
To have an overview of what this app does, see @.cursor/rules/windmill-overview.mdc
|
||||
For backend modifications, follow the rules mentioned here @.cursor/rules/rust-best-practices.mdc. You also have access to a summarized version of the database schema here @backend/summarized_schema.txt
|
||||
For frontend modifications, follow the rules mentioned here @.cursor/rules/svelte5-best-practices.mdc
|
||||
# Windmill Development Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Windmill is an open-source developer platform for building internal tools, workflows, API integrations, background jobs, workflows, and user interfaces. See @windmill-overview.mdc for full platform details.
|
||||
|
||||
## Language-Specific Guides
|
||||
|
||||
- Backend (Rust): @backend/rust-best-practices.mdc + @backend/summarized_schema.txt
|
||||
- Frontend (Svelte 5): @frontend/svelte5-best-practices.mdc
|
||||
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n path\n FROM\n flow\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0de53d764bbcf44d76cd8e47f7b2cd49e4632f03f16ee6f34d9a2c0842890f05"
|
||||
}
|
||||
-158
@@ -1,158 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n script_path, \n is_flow, \n http_method as \"http_method: _\", \n edited_by, \n email, \n edited_at, \n extra_perms, \n is_async, \n authentication_method as \"authentication_method: _\", \n static_asset_config as \"static_asset_config: _\", \n is_static_website,\n authentication_resource_path,\n wrap_body,\n raw_string\n FROM \n http_trigger\n WHERE \n workspace_id = $1 AND \n path = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "route_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "route_path_key",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "workspaced_route",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "http_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "edited_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "is_async",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "authentication_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "authentication_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"none",
|
||||
"windmill",
|
||||
"api_key",
|
||||
"basic_http",
|
||||
"custom_script",
|
||||
"signature"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "static_asset_config: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "is_static_website",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "authentication_resource_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "wrap_body",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "raw_string",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "144e4eccfd1c1e729e3c864bd5dc3316248719dfa8a6c9e1d15a7931638e86db"
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE \n http_trigger \n SET \n route_path = $1, \n route_path_key = $2, \n workspaced_route = $3,\n wrap_body = $4,\n raw_string = $5,\n authentication_resource_path = $6,\n script_path = $7, \n path = $8, \n is_flow = $9, \n http_method = $10, \n static_asset_config = $11, \n edited_by = $12, \n email = $13, \n is_async = $14, \n authentication_method = $15, \n edited_at = now(), \n is_static_website = $16\n WHERE \n workspace_id = $17 AND \n path = $18\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "authentication_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"none",
|
||||
"windmill",
|
||||
"api_key",
|
||||
"basic_http",
|
||||
"custom_script",
|
||||
"signature"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "187e8f85a71dea958e89fdfdf96c913a19eef8678dc7890c2f0e1ef8758ec43b"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n path,\n summary,\n description\n FROM\n flow\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ",
|
||||
"query": "SELECT \n path,\n summary,\n description\n FROM\n flow\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2 AND\n archived is FALSE\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -31,5 +31,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4dc21dda58758a84bbc1b7f9328efb9806223d1684c53907ff825bc0228ec18d"
|
||||
"hash": "33367c42e87e78ae987c0966dc4d445c5eff75b2e2843ffd7a46b03cbaea9ae8"
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n path\n FROM\n script\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3aaa6b6e362b10f2f3546b8595c60ab725295e0f20cb5f85d5fddc14d503a119"
|
||||
}
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n workspace_id, \n workspaced_route,\n path, \n route_path, \n route_path_key, \n authentication_resource_path,\n script_path, \n is_flow, \n edited_by, \n edited_at, \n email, \n extra_perms, \n is_async, \n authentication_method AS \"authentication_method: _\", \n http_method AS \"http_method: _\", \n static_asset_config AS \"static_asset_config: _\", \n is_static_website,\n wrap_body,\n raw_string\n FROM http_trigger\n WHERE workspace_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "workspaced_route",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "route_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "route_path_key",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "authentication_resource_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "edited_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "edited_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "extra_perms",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "is_async",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "authentication_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "authentication_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"none",
|
||||
"windmill",
|
||||
"api_key",
|
||||
"basic_http",
|
||||
"custom_script",
|
||||
"signature"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "http_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "static_asset_config: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "is_static_website",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "wrap_body",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "raw_string",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "56c2522a12f91515e38290e4680a55a4727195125cd49a2f92f89bcdf74dc364"
|
||||
}
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n route_path,\n http_method AS \"http_method: _\",\n is_async,\n workspaced_route\n FROM\n http_trigger\n WHERE\n path ~ ANY($1) AND\n route_path ~ ANY($2) AND\n workspace_id = $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "route_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "http_method: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "is_async",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "workspaced_route",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"TextArray",
|
||||
"TextArray",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6b6aca712df657f81e74a438f6753bc72e787a3f768040c8c73ea86313badb71"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM variable WHERE account = $1 AND workspace_id = $2)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "7aa589db3199d7f727cc69e63e1281b7ed329ff0c9d1617747f4ccd6014720cf"
|
||||
}
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO http_trigger (\n workspace_id, \n path, \n route_path, \n route_path_key,\n workspaced_route,\n authentication_resource_path,\n wrap_body,\n raw_string,\n script_path, \n is_flow, \n is_async, \n authentication_method, \n http_method, \n static_asset_config, \n edited_by, \n email, \n edited_at, \n is_static_website\n ) \n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, now(), $17\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "authentication_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"none",
|
||||
"windmill",
|
||||
"api_key",
|
||||
"basic_http",
|
||||
"custom_script",
|
||||
"signature"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Custom": {
|
||||
"name": "http_method",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"delete",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jsonb",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8c30e91c2486f7511563621e7e805d0588a9ec8bbea9db10e95783e27e35bc12"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT account FROM variable WHERE path = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "account",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c925264b7b0fd44ea7ab01c9af1514b9a9f2200e5a5db0a741697b28cd8b505f"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n path,\n summary,\n description\n FROM\n script\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2\n ",
|
||||
"query": "SELECT \n path,\n summary,\n description\n FROM\n script\n WHERE\n path ~ ANY($1) AND\n workspace_id = $2 AND\n archived is FALSE\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -31,5 +31,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "d41df114e78873111e409e1182bccc0024acf17ce740f70d2243e5e2a2b25163"
|
||||
"hash": "dc36b46b9eb80cb7c92fa72519d117eda99a6f482a073ccd36a6431ef689a3fd"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# Backend Development (Rust)
|
||||
|
||||
## Core Principles
|
||||
|
||||
- Follow @rust-best-practices.mdc for detailed guidelines
|
||||
- Database schema reference: @summarized_schema.txt
|
||||
- The API routes prefixes are all listed in windmill-api/src/lib.rs
|
||||
|
||||
## Adding New Features
|
||||
|
||||
1. Update database schema with migration if necessary
|
||||
2. Update backend/windmill-api/openapi.yaml after modifying API endpoints
|
||||
Generated
+43
-43
@@ -741,9 +741,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "aws-config"
|
||||
@@ -902,9 +902,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sts"
|
||||
version = "1.74.0"
|
||||
version = "1.75.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d440e1d368759bd10df0dbdddbfff6473d7cd73e9d9ef2363dc9995ac2d711"
|
||||
checksum = "e3258fa707f2f585ee3049d9550954b959002abd59176975150a01d5cf38ae3f"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
@@ -2303,9 +2303,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "croner"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38fd53511eaf0b00a185613875fee58b208dfce016577d0ad4bb548e1c4fb3ee"
|
||||
checksum = "c344b0690c1ad1c7176fe18eb173e0c927008fdaaa256e40dfd43ddd149c0843"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
]
|
||||
@@ -7188,9 +7188,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.173"
|
||||
version = "0.2.174"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
|
||||
[[package]]
|
||||
name = "libduckdb-sys"
|
||||
@@ -10410,7 +10410,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rmcp"
|
||||
version = "0.1.5"
|
||||
source = "git+https://github.com/modelcontextprotocol/rust-sdk#d1884fe598cd4c524ff6e0101859a60d4b4c8b8f"
|
||||
source = "git+https://github.com/modelcontextprotocol/rust-sdk#8a577659f01917289064c0a3c753392b44305ef4"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@@ -10439,7 +10439,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "rmcp-macros"
|
||||
version = "0.1.5"
|
||||
source = "git+https://github.com/modelcontextprotocol/rust-sdk#d1884fe598cd4c524ff6e0101859a60d4b4c8b8f"
|
||||
source = "git+https://github.com/modelcontextprotocol/rust-sdk#8a577659f01917289064c0a3c753392b44305ef4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -11922,9 +11922,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sse-stream"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f649a9f9e91db2ed32f3724516eac2bc09fab77fc33be8f670f5619b9dc6c3f"
|
||||
checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-util",
|
||||
@@ -14564,7 +14564,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -14616,7 +14616,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -14727,7 +14727,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -14742,7 +14742,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
@@ -14755,7 +14755,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -14769,7 +14769,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -14846,7 +14846,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -14860,7 +14860,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -14883,7 +14883,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -14895,7 +14895,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -14904,7 +14904,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -14916,7 +14916,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -14928,7 +14928,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -14940,7 +14940,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -14952,7 +14952,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -14964,7 +14964,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -14975,7 +14975,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -14986,7 +14986,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -14997,7 +14997,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15020,7 +15020,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -15037,7 +15037,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15049,7 +15049,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15067,7 +15067,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.16",
|
||||
@@ -15091,7 +15091,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15101,7 +15101,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15134,7 +15134,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
@@ -15144,7 +15144,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15865,18 +15865,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.25"
|
||||
version = "0.8.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
|
||||
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.25"
|
||||
version = "0.8.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
|
||||
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -32,7 +32,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.498.0"
|
||||
version = "1.499.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
+16
-2
@@ -1,4 +1,18 @@
|
||||
./substitute_ee_code.sh --dir ../windmill-ee-private
|
||||
#!/bin/bash
|
||||
|
||||
# Default directory
|
||||
EE_DIR="../windmill-ee-private"
|
||||
|
||||
# Parse arguments
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
--dir) EE_DIR="$2"; shift ;;
|
||||
*) echo "Unknown parameter: $1"; exit 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
./substitute_ee_code.sh --dir "$EE_DIR"
|
||||
|
||||
# Check if running on macOS
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
@@ -18,4 +32,4 @@ if [[ "$(uname)" == "Darwin" ]]; then
|
||||
sed -i '' 's/^#samael = { version="0.0.14", features = \["xmlsec"\] }/samael = { version="0.0.14", features = ["xmlsec"] }/' Cargo.toml
|
||||
# Comment out the git-based samael dependency
|
||||
sed -i '' 's/^\(samael = { git="https:\/\/github.com\/njaremko\/samael", rev="464d015e3ae393e4b5dd00b4d6baa1b617de0dd6", features = \["xmlsec"\] }\)/# \1/' Cargo.toml
|
||||
fi
|
||||
fi
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.498.0
|
||||
version: 1.499.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
|
||||
@@ -21,7 +21,7 @@ use windmill_common::{
|
||||
DB,
|
||||
};
|
||||
|
||||
use crate::{db::ApiAuthed, utils::require_super_admin};
|
||||
use crate::{db::ApiAuthed, utils::{require_devops_role}};
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
@@ -103,7 +103,7 @@ async fn get_config(
|
||||
Path(name): Path<String>,
|
||||
Extension(db): Extension<DB>,
|
||||
) -> error::JsonResult<Option<serde_json::Value>> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
require_devops_role(&db, &authed.email).await?;
|
||||
|
||||
let config = sqlx::query_as!(Config, "SELECT * FROM config WHERE name = $1", name)
|
||||
.fetch_optional(&db)
|
||||
@@ -119,7 +119,7 @@ async fn update_config(
|
||||
authed: ApiAuthed,
|
||||
Json(config): Json<serde_json::Value>,
|
||||
) -> error::Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
require_devops_role(&db, &authed.email).await?;
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
if name.starts_with("worker__") {
|
||||
@@ -157,7 +157,7 @@ async fn delete_config(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
) -> error::Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
require_devops_role(&db, &authed.email).await?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
@@ -232,7 +232,7 @@ async fn list_configs(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
) -> error::JsonResult<Vec<Config>> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
require_devops_role(&db, &authed.email).await?;
|
||||
let configs = sqlx::query_as!(Config, "SELECT name, config FROM config")
|
||||
.fetch_all(&db)
|
||||
.await?;
|
||||
|
||||
@@ -9,7 +9,6 @@ use axum::{
|
||||
};
|
||||
use http::{header, HeaderValue, Method, StatusCode};
|
||||
use indexmap::IndexMap;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{to_value, Map, Value};
|
||||
use sqlx::PgConnection;
|
||||
@@ -24,9 +23,12 @@ use windmill_common::{
|
||||
use crate::db::ApiAuthed;
|
||||
|
||||
#[cfg(feature = "http_trigger")]
|
||||
use crate::{
|
||||
http_trigger_args::HttpMethod, http_trigger_auth::ApiKeyAuthentication,
|
||||
http_triggers::AuthenticationMethod, resources::try_get_resource_from_db_as,
|
||||
use {
|
||||
crate::{
|
||||
http_trigger_args::HttpMethod, http_trigger_auth::ApiKeyAuthentication,
|
||||
http_triggers::AuthenticationMethod, resources::try_get_resource_from_db_as,
|
||||
},
|
||||
itertools::Itertools,
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@@ -311,9 +313,15 @@ fn generate_paths(
|
||||
}
|
||||
};
|
||||
|
||||
let mut duplicate_webhooks = HashSet::new();
|
||||
let mut webhooks = HashSet::new();
|
||||
|
||||
for path in paths {
|
||||
if let Kind::Webhook(WebhookConfig { runnable_kind }) = &path.kind {
|
||||
if !webhooks.insert((path.route_path.clone(), runnable_kind.to_owned())) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let (route_paths, parameters) =
|
||||
from_route_path_to_openapi_path(&path.route_path, &path.kind)?;
|
||||
|
||||
@@ -340,9 +348,6 @@ fn generate_paths(
|
||||
|
||||
let (methods, is_webhook) = match &path.kind {
|
||||
Kind::Webhook(_) => {
|
||||
if !duplicate_webhooks.insert(route_path.clone()) {
|
||||
return Err(anyhow!("Found duplicate webhook: {}", path.route_path).into());
|
||||
}
|
||||
is_async = route_path.starts_with("/run/");
|
||||
let methods = if is_async {
|
||||
vec![Method::POST]
|
||||
@@ -354,7 +359,12 @@ fn generate_paths(
|
||||
}
|
||||
Kind::HttpRoute(HttpRouteConfig { method }) => {
|
||||
if path_object.get(&method.to_string()).is_some() {
|
||||
return Err(anyhow!("Found duplicate route: {}", path.route_path).into());
|
||||
return Err(anyhow!(
|
||||
"Found duplicate {} method, for route at path: {}",
|
||||
method,
|
||||
path.route_path
|
||||
)
|
||||
.into());
|
||||
}
|
||||
is_async = path.is_async.unwrap_or(true);
|
||||
(vec![method.to_owned()], false)
|
||||
@@ -809,14 +819,6 @@ async fn webhook_to_future_paths(
|
||||
summary: Option<String>,
|
||||
}
|
||||
|
||||
impl PartialEq for MinifiedWebhook {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.path == other.path
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for MinifiedWebhook {}
|
||||
|
||||
let webhook_scripts = sqlx::query_as!(
|
||||
MinifiedWebhook,
|
||||
r#"SELECT
|
||||
@@ -827,16 +829,14 @@ async fn webhook_to_future_paths(
|
||||
script
|
||||
WHERE
|
||||
path ~ ANY($1) AND
|
||||
workspace_id = $2
|
||||
workspace_id = $2 AND
|
||||
archived is FALSE
|
||||
"#,
|
||||
&script_webhook_filter,
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *pg_pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.unique()
|
||||
.collect_vec();
|
||||
.await?;
|
||||
|
||||
let webhook_flows = sqlx::query_as!(
|
||||
MinifiedWebhook,
|
||||
@@ -848,16 +848,14 @@ async fn webhook_to_future_paths(
|
||||
flow
|
||||
WHERE
|
||||
path ~ ANY($1) AND
|
||||
workspace_id = $2
|
||||
workspace_id = $2 AND
|
||||
archived is FALSE
|
||||
"#,
|
||||
&flow_webhook_filter,
|
||||
&w_id
|
||||
)
|
||||
.fetch_all(&mut *pg_pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.unique()
|
||||
.collect_vec();
|
||||
.await?;
|
||||
|
||||
openapi_future_paths.reserve_exact(webhook_scripts.len() + webhook_flows.len());
|
||||
|
||||
|
||||
@@ -450,6 +450,7 @@ struct EditVariable {
|
||||
value: Option<String>,
|
||||
is_secret: Option<bool>,
|
||||
description: Option<String>,
|
||||
account: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -506,6 +507,10 @@ async fn update_variable(
|
||||
sqlb.set_str("description", &desc);
|
||||
}
|
||||
|
||||
if let Some(account_id) = ns.account {
|
||||
sqlb.set_str("account", account_id);
|
||||
}
|
||||
|
||||
if let Some(nbool) = ns.is_secret {
|
||||
let old_secret = sqlx::query_scalar!(
|
||||
"SELECT is_secret from variable WHERE path = $1 AND workspace_id = $2",
|
||||
@@ -523,6 +528,21 @@ async fn update_variable(
|
||||
sqlb.set_str("is_secret", nbool);
|
||||
}
|
||||
sqlb.returning("path");
|
||||
|
||||
// Get old account_id if we're updating the account field
|
||||
let old_account_id = if ns.account.is_some() {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT account FROM variable WHERE path = $1 AND workspace_id = $2",
|
||||
&path,
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut tx: Transaction<'_, Postgres> = user_db.begin(&authed).await?;
|
||||
|
||||
if let Some(npath) = ns.path {
|
||||
@@ -575,6 +595,33 @@ async fn update_variable(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Clean up old account if it's no longer referenced and different from new account
|
||||
if let Some(old_acc_id) = old_account_id {
|
||||
if ns.account.is_some() && ns.account != Some(old_acc_id) {
|
||||
// Check if old account is still referenced by other variables or resources
|
||||
let account_still_used = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM variable WHERE account = $1 AND workspace_id = $2)",
|
||||
old_acc_id,
|
||||
&w_id
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?
|
||||
.unwrap_or(true);
|
||||
|
||||
if !account_still_used {
|
||||
// Delete the orphaned account
|
||||
sqlx::query!(
|
||||
"DELETE FROM account WHERE id = $1 AND workspace_id = $2",
|
||||
old_acc_id,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
|
||||
@@ -4028,6 +4028,7 @@ pub async fn push<'c, 'd>(
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
let final_priority: Option<i16>;
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
{
|
||||
|
||||
@@ -62,12 +62,52 @@ lazy_static::lazy_static! {
|
||||
static ref EPHEMERAL_TOKEN_CMD: Option<String> = var("EPHEMERAL_TOKEN_CMD").ok();
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet", unix))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref PIPTAR_UPLOAD_CHANNEL: tokio::sync::mpsc::UnboundedSender<PiptarUploadTask> = {
|
||||
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
// Spawn background task to handle uploads sequentially
|
||||
tokio::spawn(handle_piptar_uploads(rx));
|
||||
|
||||
tx
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet", unix))]
|
||||
#[derive(Debug)]
|
||||
struct PiptarUploadTask {
|
||||
venv_path: String,
|
||||
cache_dir: String,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet", unix))]
|
||||
async fn handle_piptar_uploads(mut rx: tokio::sync::mpsc::UnboundedReceiver<PiptarUploadTask>) {
|
||||
use crate::global_cache::build_tar_and_push;
|
||||
use windmill_common::s3_helpers::get_object_store;
|
||||
|
||||
while let Some(task) = rx.recv().await {
|
||||
if let Some(os) = get_object_store().await {
|
||||
match build_tar_and_push(os, task.venv_path.clone(), task.cache_dir, None, false).await {
|
||||
Ok(()) => {
|
||||
tracing::info!("Successfully uploaded piptar for {}", task.venv_path);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to upload piptar for {}: {}", task.venv_path, e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("S3 object store not available for piptar upload: {}", task.venv_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT: &str = include_str!("../nsjail/download.py.config.proto");
|
||||
const NSJAIL_CONFIG_RUN_PYTHON3_CONTENT: &str = include_str!("../nsjail/run.python3.config.proto");
|
||||
const RELATIVE_PYTHON_LOADER: &str = include_str!("../loader.py");
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet", unix))]
|
||||
use crate::global_cache::{build_tar_and_push, pull_from_tar};
|
||||
use crate::global_cache::pull_from_tar;
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet", unix))]
|
||||
use windmill_common::s3_helpers::OBJECT_STORE_SETTINGS;
|
||||
@@ -674,7 +714,14 @@ except BaseException as e:
|
||||
// ^^^^^^ ^
|
||||
// We also want this be priorotized, that's why we insert it to the beginning
|
||||
}
|
||||
paths.iter().join(":")
|
||||
#[cfg(windows)]
|
||||
{
|
||||
paths.iter().join(";")
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
paths.iter().join(":")
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -1890,8 +1937,16 @@ pub async fn handle_python_reqs(
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet", unix))]
|
||||
if s3_push {
|
||||
if let Some(os) = windmill_common::s3_helpers::get_object_store().await {
|
||||
tokio::spawn(build_tar_and_push(os, venv_p.clone(), py_version.to_cache_dir_top_level(false), None, false));
|
||||
// Send to upload channel for sequential processing
|
||||
let upload_task = PiptarUploadTask {
|
||||
venv_path: venv_p.clone(),
|
||||
cache_dir: py_version.to_cache_dir_top_level(false),
|
||||
};
|
||||
|
||||
if let Err(e) = PIPTAR_UPLOAD_CHANNEL.send(upload_task) {
|
||||
tracing::warn!("Failed to queue piptar upload for {venv_p}: {e}");
|
||||
} else {
|
||||
tracing::info!("Queued piptar upload for {venv_p}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2193,3 +2248,4 @@ for line in sys.stdin:
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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.498.0";
|
||||
export const VERSION = "v1.499.0";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.498.0";
|
||||
export const VERSION = "1.499.0";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
# Frontend Development (Svelte 5)
|
||||
|
||||
## Core Principles
|
||||
|
||||
- Follow @svelte5-best-practices.mdc for detailed guidelines
|
||||
- Use Runes ($state, $derived, $effect) for reactivity
|
||||
- Keep components small and focused
|
||||
- Always use keys in {#each} blocks
|
||||
|
||||
## UI Guidelines
|
||||
|
||||
- Follow existing design system
|
||||
- Use consistent spacing and colors
|
||||
|
||||
## Backend API
|
||||
|
||||
- If you need to call the backend API, you can find the available routes in ../backend/windmill-api/openapi.yaml
|
||||
- You can also use the associated types and services that are auto generated from the openapi file. They are in src/lib/gen/\*gen.ts files
|
||||
|
||||
### OpenAPI Autogeneration
|
||||
|
||||
Windmill automatically generates TypeScript types and services from the OpenAPI specification.
|
||||
|
||||
#### Service Generation Pattern
|
||||
|
||||
The autogeneration follows this pattern:
|
||||
|
||||
- **Tag** → **Service Name**: The OpenAPI tag becomes the service name with "Service" suffix
|
||||
- **operationId** → **Method Name**: The operationId becomes the method name in the service
|
||||
|
||||
#### Example
|
||||
|
||||
Given this OpenAPI specification:
|
||||
|
||||
```yaml
|
||||
/w/{workspace}/audit/list:
|
||||
get:
|
||||
summary: list audit logs (requires admin privilege)
|
||||
operationId: listAuditLogs
|
||||
tags:
|
||||
- audit
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/WorkspaceId'
|
||||
- $ref: '#/components/parameters/Page'
|
||||
- $ref: '#/components/parameters/PerPage'
|
||||
- $ref: '#/components/parameters/Before'
|
||||
- $ref: '#/components/parameters/After'
|
||||
- $ref: '#/components/parameters/Username'
|
||||
- $ref: '#/components/parameters/Operation'
|
||||
- name: operations
|
||||
in: query
|
||||
description: comma separated list of exact operations to include
|
||||
schema:
|
||||
type: string
|
||||
```
|
||||
|
||||
This generates:
|
||||
|
||||
- **Service**: `AuditService` (from tag "audit")
|
||||
- **Method**: `listAuditLogs` (from operationId)
|
||||
|
||||
#### Method Arguments
|
||||
|
||||
The generated method arguments correspond to the OpenAPI parameters:
|
||||
|
||||
```typescript
|
||||
AuditService.listAuditLogs({
|
||||
workspace: string, // from WorkspaceId parameter
|
||||
page?: number, // from Page parameter
|
||||
perPage?: number, // from PerPage parameter
|
||||
before?: string, // from Before parameter
|
||||
after?: string, // from After parameter
|
||||
username?: string, // from Username parameter
|
||||
operation?: string, // from Operation parameter
|
||||
operations?: string // from operations parameter
|
||||
})
|
||||
```
|
||||
Generated
+2
-12
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.498.0",
|
||||
"version": "1.499.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.498.0",
|
||||
"version": "1.499.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
@@ -132,7 +132,6 @@
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-floating-ui": "^1.5.8",
|
||||
"svelte-highlight": "^7.6.0",
|
||||
"svelte-multiselect": "^11.0.0-rc.1",
|
||||
"svelte-popperjs": "^1.3.2",
|
||||
"svelte-preprocess": "^6.0.0",
|
||||
"svelte-range-slider-pips": "^2.3.1",
|
||||
@@ -11862,15 +11861,6 @@
|
||||
"resolved": "https://registry.npmjs.org/svelte-infinite-loading/-/svelte-infinite-loading-1.4.0.tgz",
|
||||
"integrity": "sha512-Jo+f/yr/HmZQuIiiKKzAHVFXdAUWHW2RBbrcQTil8JVk1sCm/riy7KTJVzjBgQvHasrFQYKF84zvtc9/Y4lFYg=="
|
||||
},
|
||||
"node_modules/svelte-multiselect": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-multiselect/-/svelte-multiselect-11.1.0.tgz",
|
||||
"integrity": "sha512-D93t6GlOV//gU7upR59uCd755Hq6OWhqv9ddhOXBDN/mEZj5XOAYHwDjUjePUKlE+bI+jry1BlZef8Mof2Y+YA==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-popperjs": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte-popperjs/-/svelte-popperjs-1.3.2.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.498.0",
|
||||
"version": "1.499.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
@@ -57,7 +57,6 @@
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-floating-ui": "^1.5.8",
|
||||
"svelte-highlight": "^7.6.0",
|
||||
"svelte-multiselect": "^11.0.0-rc.1",
|
||||
"svelte-popperjs": "^1.3.2",
|
||||
"svelte-preprocess": "^6.0.0",
|
||||
"svelte-range-slider-pips": "^2.3.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { EnumType } from '$lib/common'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
|
||||
interface Props {
|
||||
disabled: boolean
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
} from '$lib/utils'
|
||||
import { DollarSign, Pipette, Plus, X, Check, Loader2 } from 'lucide-svelte'
|
||||
import { createEventDispatcher, onMount, tick, untrack } from 'svelte'
|
||||
import Multiselect from 'svelte-multiselect'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { Button, SecondsInput } from './common'
|
||||
import FieldHeader from './FieldHeader.svelte'
|
||||
@@ -41,6 +40,8 @@
|
||||
import type { Script } from '$lib/gen'
|
||||
import type { SchemaDiff } from '$lib/components/schema/schemaUtils.svelte'
|
||||
import type { ComponentCustomCSS } from './apps/types'
|
||||
import MultiSelect from './select/MultiSelect.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
|
||||
interface Props {
|
||||
label?: string
|
||||
@@ -631,43 +632,22 @@
|
||||
<div class="w-full">
|
||||
{#if Array.isArray(itemsType?.multiselect) && Array.isArray(value)}
|
||||
<div class="items-start">
|
||||
<Multiselect
|
||||
ulOptionsClass={'p-2 !bg-surface-secondary'}
|
||||
outerDivClass={'dark:!border-gray-500 !border-gray-300'}
|
||||
<MultiSelect
|
||||
{disabled}
|
||||
bind:selected={value}
|
||||
onremove={(e) => {
|
||||
if (Array.isArray(value)) value = value.filter((v) => v !== e.option)
|
||||
}}
|
||||
options={itemsType?.multiselect ?? []}
|
||||
selectedOptionsDraggable={true}
|
||||
onopen={() => {
|
||||
dispatch('focus')
|
||||
}}
|
||||
bind:value
|
||||
items={safeSelectItems(itemsType?.multiselect)}
|
||||
onOpen={() => dispatch('focus')}
|
||||
reorderable
|
||||
/>
|
||||
</div>
|
||||
{:else if itemsType?.enum != undefined && Array.isArray(itemsType?.enum) && (Array.isArray(value) || value == undefined)}
|
||||
<div class="items-start">
|
||||
<Multiselect
|
||||
ulOptionsClass={'p-2 !bg-surface-secondary'}
|
||||
outerDivClass={'dark:!border-gray-500 !border-gray-300'}
|
||||
<MultiSelect
|
||||
{disabled}
|
||||
onremove={(e) => {
|
||||
if (Array.isArray(value)) value = value.filter((v) => v !== e.option)
|
||||
}}
|
||||
bind:selected={
|
||||
() => [...(value ?? [])],
|
||||
(v) => {
|
||||
if (!deepEqual(v, value)) {
|
||||
value = v
|
||||
}
|
||||
}
|
||||
}
|
||||
options={itemsType?.enum ?? []}
|
||||
selectedOptionsDraggable={true}
|
||||
onopen={() => {
|
||||
dispatch('focus')
|
||||
}}
|
||||
bind:value
|
||||
items={safeSelectItems(itemsType?.enum)}
|
||||
onOpen={() => dispatch('focus')}
|
||||
reorderable
|
||||
/>
|
||||
</div>
|
||||
{:else if itemsType?.type == 'object' && itemsType?.resourceType == 's3object'}
|
||||
@@ -738,8 +718,7 @@
|
||||
/>
|
||||
{:else if itemsType?.type == 'resource' && itemsType?.resourceType && resourceTypes?.includes(itemsType.resourceType)}
|
||||
<ObjectResourceInput
|
||||
value={v ? `$res:${v}` : undefined}
|
||||
bind:path={value[i]}
|
||||
bind:value={value[i]}
|
||||
format={'resource-' + itemsType?.resourceType}
|
||||
defaultValue={undefined}
|
||||
/>
|
||||
@@ -855,6 +834,7 @@
|
||||
{:else if inputCat == 'resource-object' && resourceTypes == undefined}
|
||||
<span class="text-2xs text-tertiary">Loading resource types...</span>
|
||||
{:else if inputCat == 'resource-object' && (resourceTypes == undefined || (format && format?.split('-').length > 1 && resourceTypes.includes(format?.substring('resource-'.length))))}
|
||||
<!-- {JSON.stringify(value)} -->
|
||||
<ObjectResourceInput
|
||||
{defaultValue}
|
||||
selectFirst={!noDefaultOnSelectFirst}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { ExternalLink, Loader2, X } from 'lucide-svelte'
|
||||
import { SettingService, WorkerService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { superadmin } from '$lib/stores'
|
||||
import { superadmin, devopsRole } from '$lib/stores'
|
||||
import NoWorkerWithTagWarning from './runs/NoWorkerWithTagWarning.svelte'
|
||||
import { CUSTOM_TAGS_SETTING } from '$lib/consts'
|
||||
import { base } from '$lib/base'
|
||||
@@ -80,10 +80,10 @@
|
||||
loadCustomTags()
|
||||
sendUserToast('Tag added')
|
||||
}}
|
||||
disabled={newTag.trim() == '' || !$superadmin}
|
||||
disabled={newTag.trim() == '' || !($superadmin || $devopsRole)}
|
||||
>
|
||||
Add {#if !$superadmin}
|
||||
<span class="text-2xs text-tertiary">superadmin only</span>
|
||||
Add {#if !($superadmin || $devopsRole)}
|
||||
<span class="text-2xs text-tertiary">superadmin or devops only</span>
|
||||
{/if}
|
||||
</Button>
|
||||
<span class="text-sm text-primary"
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
import { ExternalLink } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Label from './Label.svelte'
|
||||
import MultiSelect from 'svelte-multiselect'
|
||||
import MultiSelect from './select/MultiSelect.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
|
||||
interface Props {
|
||||
config: AutoscalingConfig | undefined
|
||||
@@ -209,32 +210,14 @@
|
||||
{#if config}
|
||||
{#if config.custom_tags}
|
||||
<MultiSelect
|
||||
outerDivClass="text-secondary !bg-surface-disabled !border-0"
|
||||
selected={config.custom_tags}
|
||||
onchange={(e) => {
|
||||
console.log(e.type, config?.custom_tags)
|
||||
if (e && config?.custom_tags) {
|
||||
if (e.type === 'add') {
|
||||
config.custom_tags = [
|
||||
...config.custom_tags,
|
||||
...(e.option ? [e.option.toString()] : [])
|
||||
]
|
||||
} else if (e.type === 'remove') {
|
||||
config.custom_tags = config.custom_tags.filter((t) => t !== e.option)
|
||||
if (config?.custom_tags && config.custom_tags.length == 0) {
|
||||
config.custom_tags = undefined
|
||||
}
|
||||
} else if (e.type === 'removeAll') {
|
||||
config.custom_tags = undefined
|
||||
} else {
|
||||
console.error(`Priority tags multiselect - unknown event type: '${e.type}'`)
|
||||
}
|
||||
bind:value={
|
||||
() => config?.custom_tags ?? [],
|
||||
(v) => {
|
||||
config && (config.custom_tags = v.length ? v : undefined)
|
||||
dispatch('dirty')
|
||||
}
|
||||
}}
|
||||
options={worker_tags ?? []}
|
||||
selectedOptionsDraggable={false}
|
||||
ulOptionsClass={'!bg-surface-secondary'}
|
||||
}
|
||||
items={safeSelectItems(worker_tags)}
|
||||
placeholder="Tags"
|
||||
/>
|
||||
{:else}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
|
||||
interface ChannelItem {
|
||||
channel_id?: string
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
import { ScheduleService } from '$lib/gen'
|
||||
import { emptyString, formatCron, sendUserToast } from '$lib/utils'
|
||||
import Badge from './Badge.svelte'
|
||||
// @ts-ignore
|
||||
import Multiselect from 'svelte-multiselect'
|
||||
import { Button } from './common'
|
||||
import timezones from './timezones'
|
||||
import CronBuilder from './CronBuilder.svelte'
|
||||
import Label from './Label.svelte'
|
||||
import CronGen from './copilot/CronGen.svelte'
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import MultiSelect from './select/MultiSelect.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
|
||||
export let schedule: string
|
||||
// export let offset: number = -60 * Math.floor(new Date().getTimezoneOffset() / 60)
|
||||
@@ -237,7 +237,7 @@
|
||||
<div class="text-secondary text-sm leading-none">Execute schedule every</div>
|
||||
|
||||
<div class="w-full flex gap-4">
|
||||
<div class="w-full flex flex-col gap-1">
|
||||
<div class="w-full flex flex-col gap-1 mb-2">
|
||||
<select
|
||||
{disabled}
|
||||
name="execute_every"
|
||||
@@ -280,26 +280,24 @@
|
||||
<div class="w-full flex flex-col gap-4">
|
||||
{#if executeEvery == 'month'}
|
||||
<div class="w-full flex flex-col">
|
||||
<Multiselect
|
||||
<MultiSelect
|
||||
disablePortal
|
||||
{disabled}
|
||||
bind:selected={monthsOfYear}
|
||||
options={monthsOfYearOptions}
|
||||
selectedOptionsDraggable={false}
|
||||
bind:value={monthsOfYear}
|
||||
items={safeSelectItems(monthsOfYearOptions)}
|
||||
placeholder="Every month"
|
||||
ulOptionsClass={'!bg-surface-secondary'}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if executeEvery == 'day-week'}
|
||||
<div class="w-full flex flex-col">
|
||||
<Multiselect
|
||||
<MultiSelect
|
||||
disablePortal
|
||||
{disabled}
|
||||
bind:selected={daysOfWeek}
|
||||
options={daysOfWeekOptions}
|
||||
selectedOptionsDraggable={false}
|
||||
bind:value={daysOfWeek}
|
||||
items={safeSelectItems(daysOfWeekOptions)}
|
||||
placeholder="Every day"
|
||||
ulOptionsClass={'!bg-surface-secondary'}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -311,13 +309,12 @@
|
||||
{/if}
|
||||
<div class="w-full flex gap-4">
|
||||
<div class="w-full flex">
|
||||
<Multiselect
|
||||
<MultiSelect
|
||||
disablePortal
|
||||
{disabled}
|
||||
bind:selected={daysOfMonth}
|
||||
options={daysOfMonthOptions}
|
||||
selectedOptionsDraggable={false}
|
||||
bind:value={daysOfMonth}
|
||||
items={safeSelectItems(daysOfMonthOptions)}
|
||||
placeholder="Every day"
|
||||
ulOptionsClass={'!bg-surface-secondary'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
import { getFlatTableNamesFromSchema, type DBSchema } from '$lib/stores'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import DarkModeObserver from './DarkModeObserver.svelte'
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
|
||||
const { onConfirm, resourceType, previewSql, dbSchema, currentSchema }: DBTableEditorProps =
|
||||
$props()
|
||||
@@ -165,7 +166,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
items={columnTypes.map((type) => ({ value: type, label: type }))}
|
||||
items={safeSelectItems(columnTypes)}
|
||||
class="w-48"
|
||||
/>
|
||||
</Cell>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import DefaultTagsInner from './DefaultTagsInner.svelte'
|
||||
|
||||
export let defaultTagPerWorkspace: boolean | undefined = undefined
|
||||
export let defaultTagWorkspaces: string[] | undefined = undefined
|
||||
export let defaultTagWorkspaces: string[] = []
|
||||
|
||||
let placement: 'bottom-end' | 'top-end' = 'bottom-end'
|
||||
</script>
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
import { enterpriseLicense, superadmin } from '$lib/stores'
|
||||
import { DEFAULT_TAGS_PER_WORKSPACE_SETTING, DEFAULT_TAGS_WORKSPACES_SETTING } from '$lib/consts'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import MultiSelectWrapper from './multiselect/MultiSelectWrapper.svelte'
|
||||
import MultiSelect from './select/MultiSelect.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
|
||||
let defaultTags: string[] | undefined = undefined
|
||||
export let defaultTagPerWorkspace: boolean | undefined = undefined
|
||||
export let defaultTagWorkspaces: string[] | undefined = undefined
|
||||
export let defaultTagWorkspaces: string[] = []
|
||||
let limitToWorkspaces = false
|
||||
|
||||
let workspaces: string[] = []
|
||||
@@ -62,7 +63,7 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div id="default-tags-settings" class="py-4 flex flex-col gap-2">
|
||||
<div class="py-4 flex flex-col gap-2">
|
||||
<Toggle
|
||||
bind:checked={defaultTagPerWorkspace}
|
||||
options={{ right: 'workspace specific default tags' }}
|
||||
@@ -70,9 +71,9 @@
|
||||
{#if defaultTagPerWorkspace}
|
||||
<Toggle bind:checked={limitToWorkspaces} options={{ right: 'only for some workspaces' }} />
|
||||
{#if limitToWorkspaces}
|
||||
<MultiSelectWrapper
|
||||
target="#default-tags-settings"
|
||||
items={workspaces}
|
||||
<MultiSelect
|
||||
disablePortal
|
||||
items={safeSelectItems(workspaces)}
|
||||
bind:value={defaultTagWorkspaces}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
loadingSave = false,
|
||||
newFlow = false,
|
||||
dropdownItems = []
|
||||
} = $props<{
|
||||
}: {
|
||||
loading?: boolean
|
||||
loadingSave?: boolean
|
||||
newFlow?: boolean
|
||||
@@ -16,7 +16,7 @@
|
||||
label: string
|
||||
onClick: () => void
|
||||
}>
|
||||
}>()
|
||||
} = $props()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
diffMode?: boolean
|
||||
showHistoryDrawer?: boolean
|
||||
right?: import('svelte').Snippet
|
||||
openAiChat?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -98,7 +99,8 @@
|
||||
lastDeployedCode = undefined,
|
||||
diffMode = false,
|
||||
showHistoryDrawer = $bindable(false),
|
||||
right
|
||||
right,
|
||||
openAiChat = false
|
||||
}: Props = $props()
|
||||
|
||||
let contextualVariablePicker: ItemPicker | undefined = $state()
|
||||
@@ -810,7 +812,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
|
||||
{/if}
|
||||
|
||||
{#if customUi?.aiGen != false}
|
||||
<ScriptGen {editor} {diffEditor} {lang} {iconOnly} {args} />
|
||||
<ScriptGen {editor} {diffEditor} {lang} {iconOnly} {args} {openAiChat} />
|
||||
{/if}
|
||||
|
||||
<EditorSettings {customUi} />
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
import Label from './Label.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
|
||||
export let name: string
|
||||
let can_write = false
|
||||
@@ -197,7 +198,7 @@
|
||||
ownerKind === 'user'
|
||||
? usernames.filter((x) => !perms?.map((y) => y.owner_name).includes('u/' + x))
|
||||
: groups.filter((x) => !perms?.map((y) => y.owner_name).includes('g/' + x))}
|
||||
<Select items={items.map((x) => ({ label: x, value: x }))} bind:value={ownerItem} />
|
||||
<Select items={safeSelectItems(items)} bind:value={ownerItem} />
|
||||
{#if ownerKind == 'group'}
|
||||
<Button
|
||||
title="View Group"
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import Section from './Section.svelte'
|
||||
import Label from './Label.svelte'
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
|
||||
export let name: string
|
||||
let can_write = false
|
||||
@@ -129,7 +130,7 @@
|
||||
<Section label={`Members (${members?.length ?? 0})`}>
|
||||
{#if can_write}
|
||||
<div class="flex items-start">
|
||||
<Select items={usernames?.map((u) => ({ value: u, label: u }))} bind:value={username} />
|
||||
<Select items={safeSelectItems(usernames)} bind:value={username} />
|
||||
<Button variant="contained" color="blue" size="sm" btnClasses="!ml-4" on:click={addToGroup}>
|
||||
Add member
|
||||
</Button>
|
||||
|
||||
@@ -177,6 +177,9 @@
|
||||
if (arg.type) {
|
||||
propertyType = arg.type
|
||||
}
|
||||
if (arg.expr != undefined) {
|
||||
arg.expr = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,6 +335,8 @@
|
||||
untrack(() => updatePropertyType())
|
||||
})
|
||||
$effect(() => {
|
||||
arg.value
|
||||
arg.expr
|
||||
inputCat && propertyType && arg && untrack(() => onArgChange())
|
||||
})
|
||||
$effect(() => {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import type { AppViewerContext } from './apps/types'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
|
||||
|
||||
@@ -4,40 +4,43 @@
|
||||
import S3ObjectPicker from './S3ObjectPicker.svelte'
|
||||
import type SimpleEditor from './SimpleEditor.svelte'
|
||||
|
||||
export let format: string
|
||||
export let value: any
|
||||
export let disablePortal = false
|
||||
export let showSchemaExplorer = false
|
||||
export let selectFirst = false
|
||||
export let defaultValue: any
|
||||
export let editor: SimpleEditor | undefined = undefined
|
||||
function isString(value: any) {
|
||||
return typeof value === 'string' || value instanceof String
|
||||
}
|
||||
|
||||
export let path: string = ''
|
||||
|
||||
function resourceToValue() {
|
||||
if (path && path != '') {
|
||||
value = `$res:${path}`
|
||||
} else {
|
||||
value = undefined
|
||||
}
|
||||
interface Props {
|
||||
format: string
|
||||
value: any
|
||||
disablePortal?: boolean
|
||||
showSchemaExplorer?: boolean
|
||||
selectFirst?: boolean
|
||||
defaultValue: any
|
||||
editor?: SimpleEditor | undefined
|
||||
path?: string
|
||||
}
|
||||
|
||||
let {
|
||||
format,
|
||||
value = $bindable(),
|
||||
disablePortal = false,
|
||||
showSchemaExplorer = false,
|
||||
selectFirst = false,
|
||||
defaultValue,
|
||||
editor = $bindable(undefined)
|
||||
}: Props = $props()
|
||||
|
||||
function isResource() {
|
||||
return isString(value) && value.length >= '$res:'.length
|
||||
}
|
||||
|
||||
function valueToPath() {
|
||||
if (isResource()) {
|
||||
path = value.substr('$res:'.length)
|
||||
return value.substr('$res:'.length)
|
||||
}
|
||||
}
|
||||
|
||||
$: value && valueToPath()
|
||||
</script>
|
||||
|
||||
<!-- {JSON.stringify({ value })} -->
|
||||
<div class="flex flex-row w-full flex-wrap gap-x-2 gap-y-0.5">
|
||||
{#if format === 'resource-s3_object'}
|
||||
<S3ObjectPicker bind:value />
|
||||
@@ -45,12 +48,17 @@
|
||||
<ResourcePicker
|
||||
{selectFirst}
|
||||
{disablePortal}
|
||||
on:change={(e) => {
|
||||
path = e.detail
|
||||
resourceToValue()
|
||||
}}
|
||||
on:clear
|
||||
bind:value={path}
|
||||
bind:value={
|
||||
() => valueToPath(),
|
||||
(v) => {
|
||||
if (v == undefined) {
|
||||
value = undefined
|
||||
} else {
|
||||
value = `$res:${v}`
|
||||
}
|
||||
}
|
||||
}
|
||||
initialValue={typeof defaultValue == 'string' && defaultValue.startsWith('$res:')
|
||||
? defaultValue.substr('$res:'.length)
|
||||
: defaultValue}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy'
|
||||
|
||||
import CollapseLink from './CollapseLink.svelte'
|
||||
import IconedResourceType from './IconedResourceType.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
@@ -34,7 +32,7 @@
|
||||
}
|
||||
}
|
||||
let enabled = $derived(value != undefined)
|
||||
run(() => {
|
||||
$effect(() => {
|
||||
changeDomain(value?.['domain'], value?.['custom'])
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -10,23 +10,44 @@
|
||||
import { Pen, Plus, RotateCw } from 'lucide-svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { isDbType } from './apps/components/display/dbtable/utils'
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
|
||||
|
||||
export let initialValue: string | undefined = undefined
|
||||
export let value: string | undefined = initialValue
|
||||
export let valueType: string | undefined = undefined
|
||||
export let resourceType: string | undefined = undefined
|
||||
export let disabled = false
|
||||
export let disablePortal = false
|
||||
export let showSchemaExplorer = false
|
||||
export let selectFirst = false
|
||||
export let expressOAuthSetup = false
|
||||
export let defaultValues: Record<string, any> | undefined = undefined
|
||||
export let placeholder: string | undefined = undefined
|
||||
interface Props {
|
||||
initialValue?: string | undefined
|
||||
value?: string | undefined
|
||||
valueType?: string | undefined
|
||||
resourceType?: string | undefined
|
||||
disabled?: boolean
|
||||
disablePortal?: boolean
|
||||
showSchemaExplorer?: boolean
|
||||
selectFirst?: boolean
|
||||
expressOAuthSetup?: boolean
|
||||
defaultValues?: Record<string, any> | undefined
|
||||
placeholder?: string | undefined
|
||||
}
|
||||
|
||||
let {
|
||||
initialValue = $bindable(undefined),
|
||||
value = $bindable(undefined),
|
||||
valueType = $bindable(undefined),
|
||||
resourceType = undefined,
|
||||
disabled = false,
|
||||
disablePortal = false,
|
||||
showSchemaExplorer = false,
|
||||
selectFirst = false,
|
||||
expressOAuthSetup = false,
|
||||
defaultValues = undefined,
|
||||
placeholder = undefined
|
||||
}: Props = $props()
|
||||
|
||||
if (initialValue && value == undefined) {
|
||||
console.log('initialValue', initialValue)
|
||||
value = initialValue
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
@@ -36,26 +57,36 @@
|
||||
}, 500)
|
||||
})
|
||||
|
||||
let valueSelect =
|
||||
initialValue || value
|
||||
? {
|
||||
value: value ?? initialValue,
|
||||
label: value ?? initialValue,
|
||||
type: valueType
|
||||
}
|
||||
: undefined
|
||||
// let valueSelect = $state(
|
||||
// initialValue || value
|
||||
// ? {
|
||||
// value: value ?? initialValue,
|
||||
// label: value ?? initialValue,
|
||||
// type: valueType
|
||||
// }
|
||||
// : undefined
|
||||
// )
|
||||
|
||||
$: if (value === undefined && initialValue) {
|
||||
value = initialValue
|
||||
}
|
||||
$effect(() => {
|
||||
if (value === undefined) {
|
||||
if (initialValue) {
|
||||
console.log('initialValue', initialValue)
|
||||
value = initialValue
|
||||
} else {
|
||||
console.log('no value')
|
||||
}
|
||||
} else {
|
||||
console.log('value', value)
|
||||
}
|
||||
})
|
||||
|
||||
let collection = valueSelect ? [valueSelect] : []
|
||||
let collection = $state(value ? [{ value, label: value, type: valueType }] : [])
|
||||
|
||||
export async function askNewResource() {
|
||||
appConnect?.open?.(resourceType)
|
||||
}
|
||||
|
||||
let loading = true
|
||||
let loading = $state(true)
|
||||
async function loadResources(resourceType: string | undefined) {
|
||||
loading = true
|
||||
try {
|
||||
@@ -84,10 +115,10 @@
|
||||
nc.push({ value: value ?? initialValue!, label: value ?? initialValue!, type: '' })
|
||||
}
|
||||
collection = nc
|
||||
if (collection.length == 1 && selectFirst && valueSelect == undefined) {
|
||||
if (collection.length == 1 && selectFirst && value == undefined) {
|
||||
console.log('selectFirst', collection[0].value)
|
||||
value = collection[0].value
|
||||
valueType = collection[0].type
|
||||
valueSelect = collection[0]
|
||||
}
|
||||
} catch (e) {
|
||||
sendUserToast('Failed to load resource types', true)
|
||||
@@ -96,12 +127,16 @@
|
||||
loading = false
|
||||
}
|
||||
|
||||
$: $workspaceStore && loadResources(resourceType)
|
||||
$effect(() => {
|
||||
$workspaceStore && loadResources(resourceType)
|
||||
})
|
||||
|
||||
$: dispatchIfMounted('change', value)
|
||||
$effect(() => {
|
||||
dispatchIfMounted('change', value)
|
||||
})
|
||||
|
||||
let appConnect: AppConnect
|
||||
let resourceEditor: ResourceEditorDrawer
|
||||
let appConnect: AppConnect | undefined = $state()
|
||||
let resourceEditor: ResourceEditorDrawer | undefined = $state()
|
||||
</script>
|
||||
|
||||
<AppConnect
|
||||
@@ -109,11 +144,6 @@
|
||||
await loadResources(resourceType)
|
||||
value = e.detail
|
||||
valueType = collection.find((x) => x?.value == value)?.type
|
||||
valueSelect = {
|
||||
value: e.detail,
|
||||
label: e.detail,
|
||||
type: valueType ?? ''
|
||||
}
|
||||
}}
|
||||
bind:this={appConnect}
|
||||
{expressOAuthSetup}
|
||||
@@ -125,11 +155,11 @@
|
||||
if (e.detail) {
|
||||
value = e.detail
|
||||
valueType = collection.find((x) => x?.value == value)?.type
|
||||
valueSelect = { value: e.detail, label: e.detail, type: valueType ?? '' }
|
||||
// valueSelect = { value: e.detail, label: e.detail, type: valueType ?? '' }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<!-- {JSON.stringify({ value, collection })} -->
|
||||
<div class="flex flex-col w-full items-start min-h-9">
|
||||
<div class="flex flex-row w-full items-center">
|
||||
{#if collection?.length > 0}
|
||||
@@ -137,18 +167,16 @@
|
||||
{disabled}
|
||||
{disablePortal}
|
||||
bind:value={
|
||||
() => valueSelect?.value,
|
||||
() => value,
|
||||
(v) => {
|
||||
valueSelect = collection.find((x) => x.value === v)
|
||||
value = v
|
||||
valueType = valueSelect?.type
|
||||
valueType = collection.find((x) => x?.value == v)?.type
|
||||
}
|
||||
}
|
||||
onClear={() => {
|
||||
initialValue = undefined
|
||||
value = undefined
|
||||
valueType = undefined
|
||||
valueSelect = undefined
|
||||
dispatch('clear')
|
||||
}}
|
||||
items={collection}
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
import { ExternalLink, Search } from 'lucide-svelte'
|
||||
import { Popover } from './meltComponents'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import MultiSelect from './multiselect/MultiSelectWrapper.svelte'
|
||||
import MultiSelect from './select/MultiSelect.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
interface Props {
|
||||
@@ -142,7 +143,6 @@
|
||||
{/snippet}
|
||||
|
||||
{#snippet content()}
|
||||
<div id="multi-select-search"></div>
|
||||
<div class="p-2 overflow-auto max-h-[400px] min-h-[300px] w-[400px]">
|
||||
<div class="flex items-center flex-wrap gap-x-2 justify-between">
|
||||
<div class="text-sm text-secondary">Search by args</div>
|
||||
@@ -175,11 +175,10 @@
|
||||
</div>
|
||||
<div class="my-2">
|
||||
<MultiSelect
|
||||
topPlacement
|
||||
target="#multi-select-search"
|
||||
placeholder="arg fields to filter on"
|
||||
items={Object.keys(schema?.properties ?? {})}
|
||||
items={safeSelectItems(Object.keys(schema?.properties ?? {}))}
|
||||
bind:value={searchArgsFields}
|
||||
disablePortal
|
||||
/>
|
||||
</div>
|
||||
{#key filteredSchema}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { base } from '$lib/base'
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
|
||||
import { getScriptByPath } from '$lib/scripts'
|
||||
import { Button, Drawer, DrawerContent } from './common'
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
<script lang="ts" generics="Item extends { label?: string; value: any; }">
|
||||
import { clickOutside } from '$lib/utils'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import CloseButton from './common/CloseButton.svelte'
|
||||
import ConditionalPortal from './common/drawer/ConditionalPortal.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { untrack } from 'svelte'
|
||||
|
||||
type Value = Item['value']
|
||||
|
||||
let {
|
||||
items,
|
||||
placeholder = 'Please select',
|
||||
value = $bindable(),
|
||||
filterText: _filterTextBind = $bindable(undefined),
|
||||
class: className = '',
|
||||
clearable = false,
|
||||
listAutoWidth = true,
|
||||
disabled: _disabled = false,
|
||||
containerStyle = '',
|
||||
inputClass = '',
|
||||
disablePortal = false,
|
||||
loading = false,
|
||||
autofocus,
|
||||
RightIcon,
|
||||
createText,
|
||||
groupBy,
|
||||
sortBy,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onClear,
|
||||
onCreateItem
|
||||
}: {
|
||||
items?: Item[]
|
||||
value: Value | undefined
|
||||
placeholder?: string
|
||||
class?: string
|
||||
clearable?: boolean
|
||||
filterText?: string
|
||||
disabled?: boolean
|
||||
listAutoWidth?: boolean
|
||||
containerStyle?: string
|
||||
inputClass?: string
|
||||
disablePortal?: boolean
|
||||
loading?: boolean
|
||||
autofocus?: boolean
|
||||
RightIcon?: any
|
||||
createText?: string
|
||||
groupBy?: (item: Item) => string
|
||||
sortBy?: (a: Item, b: Item) => number
|
||||
onFocus?: () => void
|
||||
onBlur?: () => void
|
||||
onClear?: () => void
|
||||
onCreateItem?: (value: string) => void
|
||||
} = $props()
|
||||
|
||||
let disabled = $derived(_disabled || loading)
|
||||
|
||||
let filterText = $state<string>('')
|
||||
let open = $state<boolean>(false)
|
||||
let keyArrowPos = $state<number | undefined>()
|
||||
let inputEl: HTMLInputElement | undefined = $state()
|
||||
let listEl: HTMLDivElement | undefined = $state()
|
||||
|
||||
$effect(() => {
|
||||
if (_filterTextBind !== undefined) filterText = _filterTextBind
|
||||
})
|
||||
$effect(() => {
|
||||
if (_filterTextBind !== undefined) _filterTextBind = filterText
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
;[filterText, open, processedItems]
|
||||
keyArrowPos = open && filterText ? 0 : undefined
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (filterText) open = true
|
||||
})
|
||||
$effect(() => {
|
||||
if (!open) filterText = ''
|
||||
})
|
||||
|
||||
type ProcessedItem = Item & { __select_group?: string; __is_create?: true; label: string }
|
||||
|
||||
let processedItems: ProcessedItem[] = $derived.by(() => {
|
||||
let items2 =
|
||||
items?.map((item) => ({
|
||||
...item,
|
||||
label: getLabel(item)
|
||||
})) ?? []
|
||||
if (filterText) {
|
||||
items2 = items2.filter((item) =>
|
||||
item?.label?.toLowerCase().includes(filterText?.toLowerCase())
|
||||
)
|
||||
}
|
||||
if (groupBy) {
|
||||
items2 =
|
||||
items2?.map((item) => ({
|
||||
...item,
|
||||
__select_group: groupBy(item)
|
||||
})) ?? []
|
||||
}
|
||||
if (sortBy) {
|
||||
items2 = items2?.sort(sortBy)
|
||||
}
|
||||
if (onCreateItem && filterText && !items2.some((item) => item.label === filterText)) {
|
||||
items2.push({
|
||||
label: createText ?? `Add new: "${filterText}"`,
|
||||
value: filterText,
|
||||
__is_create: true
|
||||
} as any)
|
||||
}
|
||||
return items2
|
||||
})
|
||||
let valueEntry = $derived(value && processedItems?.find((item) => deepEqual(item.value, value)))
|
||||
|
||||
function setValue(item: ProcessedItem) {
|
||||
if (item.__is_create && onCreateItem) {
|
||||
onCreateItem(item.value)
|
||||
} else {
|
||||
value = item.value
|
||||
}
|
||||
filterText = ''
|
||||
open = false
|
||||
}
|
||||
|
||||
function clearValue() {
|
||||
filterText = ''
|
||||
if (onClear) onClear()
|
||||
else value = undefined
|
||||
}
|
||||
|
||||
function getLabel(item: Item | undefined): string {
|
||||
if (!item) return ''
|
||||
if (item.label) return item.label
|
||||
if (typeof item.value === 'string') return item.value
|
||||
if (typeof item.value == 'number' || typeof item.value == 'boolean')
|
||||
return item.value.toString()
|
||||
|
||||
return JSON.stringify(item.value)
|
||||
}
|
||||
|
||||
function computeDropdownPos(): { width: number; x: number; y: number } {
|
||||
if (!inputEl || !listEl) return { width: 0, x: 0, y: 0 }
|
||||
const r = inputEl.getBoundingClientRect()
|
||||
const listR = listEl.getBoundingClientRect()
|
||||
const openBelow = r.y + r.height + listR.height <= window.innerHeight
|
||||
let [x, y] = disablePortal ? [0, 0] : [r.x, r.y]
|
||||
if (openBelow) return { width: r.width, x: x, y: y + r.height }
|
||||
else {
|
||||
return { width: r.width, x: x, y: y - listR.height }
|
||||
}
|
||||
}
|
||||
let dropdownPos = $state(computeDropdownPos())
|
||||
$effect(() => {
|
||||
function updateDropdownPos() {
|
||||
let nPos = computeDropdownPos()
|
||||
if (!deepEqual(nPos, dropdownPos)) dropdownPos = nPos
|
||||
if (open) requestAnimationFrame(updateDropdownPos)
|
||||
}
|
||||
if (open) untrack(() => updateDropdownPos())
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:keydown={(e) => {
|
||||
if (!open || !processedItems?.length) return
|
||||
if (e.key === 'ArrowUp' && keyArrowPos !== undefined && processedItems.length > 0) {
|
||||
keyArrowPos = keyArrowPos <= 0 ? undefined : keyArrowPos - 1
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
if (keyArrowPos === undefined) {
|
||||
keyArrowPos = 0
|
||||
} else {
|
||||
keyArrowPos = Math.min(processedItems.length - 1, keyArrowPos + 1)
|
||||
}
|
||||
} else if (e.key === 'Enter' && keyArrowPos !== undefined && processedItems?.[keyArrowPos]) {
|
||||
setValue(processedItems[keyArrowPos])
|
||||
} else {
|
||||
keyArrowPos = undefined
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
class={`relative ${className}`}
|
||||
use:clickOutside={{ onClickOutside: () => (open = false) }}
|
||||
onpointerdown={() => onFocus?.()}
|
||||
onfocus={() => onFocus?.()}
|
||||
onblur={() => onBlur?.()}
|
||||
>
|
||||
{#if loading}
|
||||
<div class="absolute z-10 right-2 h-full flex items-center">
|
||||
<Loader2 size={18} class="animate-spin" />
|
||||
</div>
|
||||
{:else if clearable && !disabled && value}
|
||||
<div class="absolute z-10 right-2 h-full flex items-center">
|
||||
<CloseButton noBg small on:close={clearValue} />
|
||||
</div>
|
||||
{:else if RightIcon}
|
||||
<div class="absolute z-10 right-2 h-full flex items-center">
|
||||
<RightIcon size={18} class="text-tertiary/35" />
|
||||
</div>
|
||||
{/if}
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
{autofocus}
|
||||
{disabled}
|
||||
type="text"
|
||||
bind:value={() => filterText, (v) => (filterText = v)}
|
||||
placeholder={loading ? 'Loading...' : (valueEntry?.label ?? placeholder)}
|
||||
style={containerStyle}
|
||||
class={twMerge(
|
||||
'!bg-surface text-ellipsis',
|
||||
open ? '' : 'cursor-pointer',
|
||||
valueEntry && !loading ? '!placeholder-primary' : '',
|
||||
(clearable || RightIcon) && !disabled && value ? '!pr-8' : '',
|
||||
inputClass ?? ''
|
||||
)}
|
||||
autocomplete="off"
|
||||
onpointerdown={() => (open = true)}
|
||||
bind:this={inputEl}
|
||||
/>
|
||||
|
||||
<ConditionalPortal condition={!disablePortal}>
|
||||
{#if open && !disabled}
|
||||
<div
|
||||
class={twMerge(
|
||||
disablePortal ? 'absolute' : 'fixed',
|
||||
'flex flex-col z-[5001] max-h-64 overflow-y-auto bg-surface-secondary text-tertiary text-sm select-none border rounded-lg'
|
||||
)}
|
||||
style="{`top: ${dropdownPos.y}px; left: ${dropdownPos.x}px;`} {listAutoWidth
|
||||
? `min-width: ${dropdownPos.width}px;`
|
||||
: ''}"
|
||||
bind:this={listEl}
|
||||
>
|
||||
{#if processedItems?.length === 0}
|
||||
<div class="py-8 px-4 text-center text-primary">No items</div>
|
||||
{/if}
|
||||
{#each processedItems ?? [] as item, itemIndex}
|
||||
{#if (item.__select_group && itemIndex === 0) || processedItems?.[itemIndex - 1]?.__select_group !== item.__select_group}
|
||||
<div
|
||||
class={twMerge(
|
||||
'mx-4 pb-1 mb-2 text-xs font-semibold text-primary border-b',
|
||||
itemIndex === 0 ? 'mt-3' : 'mt-6'
|
||||
)}
|
||||
>
|
||||
{item.__select_group}
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
class={twMerge(
|
||||
'py-2 px-4 w-full font-normal text-left text-primary',
|
||||
itemIndex === keyArrowPos ? 'bg-surface-hover' : '',
|
||||
item.value === value ? 'bg-surface-selected' : 'hover:bg-surface-hover'
|
||||
)}
|
||||
onclick={(e) => {
|
||||
e.stopImmediatePropagation()
|
||||
setValue(item)
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</ConditionalPortal>
|
||||
</div>
|
||||
@@ -11,14 +11,13 @@
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { onDestroy, tick, untrack } from 'svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { copyToClipboard, truncateRev } from '$lib/utils'
|
||||
import { copyToClipboard, scroll_into_view_if_needed_polyfill, truncateRev } from '$lib/utils'
|
||||
import LogSnippetViewer from './LogSnippetViewer.svelte'
|
||||
import { Button, Drawer, DrawerContent } from './common'
|
||||
import ClipboardCopy from 'lucide-svelte/icons/clipboard-copy'
|
||||
import AnsiUp from 'ansi_up'
|
||||
import { scroll_into_view_if_needed_polyfill } from './multiselect/utils'
|
||||
import SplitPanesOrColumnOnMobile from './splitPanes/SplitPanesOrColumnOnMobile.svelte'
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import { goto } from '$lib/navigation'
|
||||
import { page } from '$app/stores'
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
import { isOwner } from '$lib/utils'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -138,9 +139,9 @@
|
||||
</div>
|
||||
{#key ownerKind}
|
||||
<Select
|
||||
items={(ownerKind === 'user' ? usernames : groups)
|
||||
.map((x) => x.toString())
|
||||
.map((x) => ({ value: x, label: x }))}
|
||||
items={safeSelectItems(
|
||||
(ownerKind === 'user' ? usernames : groups).map((x) => x.toString())
|
||||
)}
|
||||
bind:value={owner}
|
||||
/>
|
||||
{/key}
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
tailwindClasses = [],
|
||||
class: className = '',
|
||||
loadAsync = false
|
||||
} = $props<{
|
||||
}: {
|
||||
lang: string
|
||||
code?: string
|
||||
hash?: string
|
||||
@@ -137,7 +137,7 @@
|
||||
tailwindClasses?: string[]
|
||||
class?: string
|
||||
loadAsync?: boolean
|
||||
}>()
|
||||
} = $props()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -343,7 +343,7 @@
|
||||
// })
|
||||
// }
|
||||
try {
|
||||
model = meditor.createModel(code, lang, mUri.parse(uri))
|
||||
model = meditor.createModel(code ?? '', lang, mUri.parse(uri))
|
||||
} catch (err) {
|
||||
console.log('model already existed', err)
|
||||
const nmodel = meditor.getModel(mUri.parse(uri))
|
||||
@@ -361,7 +361,7 @@
|
||||
}
|
||||
try {
|
||||
editor = meditor.create(divEl as HTMLDivElement, {
|
||||
...editorConfig(code, lang, automaticLayout, fixedOverflowWidgets),
|
||||
...editorConfig(code ?? '', lang, automaticLayout, fixedOverflowWidgets),
|
||||
model,
|
||||
lineDecorationsWidth: 6,
|
||||
lineNumbersMinChars: 2,
|
||||
@@ -546,7 +546,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
let previousExtraLib = undefined
|
||||
let previousExtraLib: string | undefined = undefined
|
||||
function loadExtraLib() {
|
||||
if (lang == 'javascript') {
|
||||
const stdLib = { content: libStdContent, filePath: 'es6.d.ts' }
|
||||
@@ -609,11 +609,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
updatePlaceholderVisibility(code)
|
||||
updatePlaceholderVisibility(code ?? '')
|
||||
</script>
|
||||
|
||||
<EditorTheme />
|
||||
{#if editor && suggestion && code.length === 0}
|
||||
{#if editor && suggestion && code?.length === 0}
|
||||
<div
|
||||
class="absolute top-[0.05rem] left-[2.05rem] z-10 text-sm text-[#0007] italic font-mono dark:text-[#ffffff56] text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
style={`max-width: calc(${width}px - 2.05rem)`}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { X, Plus, Trash } from 'lucide-svelte'
|
||||
import { Button } from './common'
|
||||
import { superadmin } from '$lib/stores'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { defaultTags, nativeTags } from './worker_group'
|
||||
import Select from './Select.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
import MultiSelect from './select/MultiSelect.svelte'
|
||||
import { superadmin } from '$lib/stores'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
type Props = {
|
||||
@@ -15,83 +14,21 @@
|
||||
let {
|
||||
worker_tags = $bindable([]),
|
||||
customTags = $bindable([]),
|
||||
disabled = $bindable(false)
|
||||
disabled: _disabled = $bindable(false)
|
||||
}: Props = $props()
|
||||
let newTag = $state('')
|
||||
let createdTags: string[] = $state([])
|
||||
|
||||
let disabled = $derived(_disabled || !$superadmin)
|
||||
</script>
|
||||
|
||||
<div class="flex gap-2 gap-y-2 flex-wrap pb-3">
|
||||
{#if worker_tags?.length === 0}
|
||||
<div class="text-xs text-secondary">No tags selected</div>
|
||||
{/if}
|
||||
|
||||
{#each worker_tags as tag}
|
||||
<div
|
||||
class="flex items-center gap-1 px-2 py-1 rounded-full border border-primary text-2xs text-primary bg-surface-primary"
|
||||
>
|
||||
<span>{tag}</span>
|
||||
{#if $superadmin && !disabled}
|
||||
<Button
|
||||
class="p-1 rounded-full hover:bg-surface-hover transition"
|
||||
aria-label="Remove tag"
|
||||
on:click={() => {
|
||||
worker_tags = worker_tags?.filter((t) => t !== tag) ?? []
|
||||
dispatch('dirty')
|
||||
dispatch('deletePriorityTag', tag)
|
||||
}}
|
||||
>
|
||||
<X size={12} />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if $superadmin}
|
||||
<div class="max-w-md space-y-2">
|
||||
<Select
|
||||
items={[...(customTags ?? []), ...createdTags, ...defaultTags, ...nativeTags]
|
||||
.filter((x) => !worker_tags?.includes(x))
|
||||
.map((x) => ({ value: x, label: x }))}
|
||||
{disabled}
|
||||
bind:value={newTag}
|
||||
onFocus={() => dispatch('focus')}
|
||||
onCreateItem={(c) => (createdTags.push(c), (createdTags = [...createdTags]), (newTag = c))}
|
||||
createText="Press Enter to use this tag"
|
||||
/>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: Plus }}
|
||||
disabled={disabled || newTag == '' || worker_tags?.includes(newTag)}
|
||||
on:click={() => {
|
||||
worker_tags = [...(worker_tags ?? []), newTag.replaceAll(' ', '_')]
|
||||
newTag = ''
|
||||
dispatch('dirty')
|
||||
}}
|
||||
>
|
||||
Add tag
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="red"
|
||||
size="xs"
|
||||
startIcon={{ icon: Trash }}
|
||||
disabled={disabled || worker_tags.length === 0}
|
||||
on:click={() => {
|
||||
worker_tags = worker_tags.filter((tag) => {
|
||||
dispatch('deletePriorityTag', tag)
|
||||
return false
|
||||
})
|
||||
dispatch('dirty')
|
||||
}}
|
||||
>
|
||||
Remove all selected tags
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<MultiSelect
|
||||
items={safeSelectItems([...(customTags ?? []), ...worker_tags, ...defaultTags, ...nativeTags])}
|
||||
bind:value={
|
||||
() => worker_tags,
|
||||
(w) => ((worker_tags = w.map((s) => s.replaceAll(' ', '_'))), dispatch('dirty'))
|
||||
}
|
||||
{disabled}
|
||||
class={disabled ? 'border-0' : ''}
|
||||
allowClear={!disabled}
|
||||
onCreateItem={(c) => worker_tags.push(c)}
|
||||
createText="Press Enter to use this tag"
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { RefreshCcw } from 'lucide-svelte'
|
||||
import { WorkspaceService } from '$lib/gen'
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
interface TeamItem {
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
onTrigger,
|
||||
children,
|
||||
showAnimation = true
|
||||
} = $props<{
|
||||
}: {
|
||||
id: string | undefined
|
||||
description: string | undefined
|
||||
onTrigger?: (value?: string) => void // Function to call when the trigger is activated, if not provided, the component is discoverable for information purposes only
|
||||
children?: () => any
|
||||
showAnimation?: boolean
|
||||
}>()
|
||||
} = $props()
|
||||
|
||||
let isAnimating = $state(false)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Copy, Plus, RefreshCcwIcon, Settings, Trash, X } from 'lucide-svelte'
|
||||
import { Alert, Badge, Button, Drawer } from './common'
|
||||
import Multiselect from 'svelte-multiselect'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import { ConfigService, WorkspaceService, type WorkerPing, type Workspace } from '$lib/gen'
|
||||
@@ -9,7 +8,7 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { emptyString, pluralize } from '$lib/utils'
|
||||
import { enterpriseLicense, superadmin } from '$lib/stores'
|
||||
import { enterpriseLicense, superadmin, devopsRole } from '$lib/stores'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import Editor from './Editor.svelte'
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
@@ -20,7 +19,9 @@
|
||||
import { defaultTags, nativeTags, type AutoscalingConfig } from './worker_group'
|
||||
import AutoscalingConfigEditor from './AutoscalingConfigEditor.svelte'
|
||||
import TagsToListenTo from './TagsToListenTo.svelte'
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import MultiSelect from './select/MultiSelect.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
|
||||
function computeVCpuAndMemory(workers: [string, WorkerPing[]][]) {
|
||||
let vcpus = 0
|
||||
@@ -155,7 +156,7 @@
|
||||
let vcpus_memory = $derived(computeVCpuAndMemory(workers))
|
||||
let selected = $derived(nconfig?.dedicated_worker != undefined ? 'dedicated' : 'normal')
|
||||
$effect(() => {
|
||||
$superadmin && listWorkspaces()
|
||||
($superadmin || $devopsRole) && listWorkspaces()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -209,7 +210,7 @@
|
||||
<Drawer bind:this={drawer} size="800px">
|
||||
<DrawerContent
|
||||
on:close={() => drawer?.closeDrawer()}
|
||||
title={$superadmin ? `Edit worker config '${name}'` : `Worker config '${name}'`}
|
||||
title={($superadmin || $devopsRole) ? `Edit worker config '${name}'` : `Worker config '${name}'`}
|
||||
>
|
||||
{#if !$enterpriseLicense}
|
||||
<Alert type="warning" title="Worker management UI is EE only">
|
||||
@@ -340,7 +341,7 @@
|
||||
{#if defaultTagPerWorkspace}
|
||||
<Select
|
||||
bind:value={workspaceTag}
|
||||
items={workspaces.map((w) => ({ value: w.id, label: w.id }))}
|
||||
items={workspaces.map((w) => ({ value: w.id }))}
|
||||
onCreateItem={(c) => (workspaceTag = c)}
|
||||
placeholder="Workspace ID"
|
||||
/>
|
||||
@@ -358,36 +359,16 @@
|
||||
{/if}
|
||||
</Tooltip>
|
||||
{/snippet}
|
||||
<Multiselect
|
||||
outerDivClass="text-secondary !bg-surface-disabled !border-0"
|
||||
<MultiSelect
|
||||
disabled={!$enterpriseLicense}
|
||||
selected={Object.keys(nconfig?.priority_tags ?? {})}
|
||||
onchange={(e) => {
|
||||
if (e.type === 'add') {
|
||||
if (nconfig.priority_tags) {
|
||||
if (e.option && typeof e.option !== 'object') {
|
||||
nconfig.priority_tags[e.option] = 100
|
||||
}
|
||||
}
|
||||
bind:value={
|
||||
() => new Array(...(nconfig?.priority_tags?.keys?.() ?? [])),
|
||||
(v) => {
|
||||
nconfig.priority_tags = new Map<string, number>(v.map((k) => [k, 100]))
|
||||
dirty = true
|
||||
} else if (e.type === 'remove') {
|
||||
if (nconfig.priority_tags) {
|
||||
if (e.option && typeof e.option !== 'object') {
|
||||
delete nconfig.priority_tags[e.option]
|
||||
}
|
||||
}
|
||||
dirty = true
|
||||
} else if (e.type === 'removeAll') {
|
||||
nconfig.priority_tags = new Map<string, number>()
|
||||
dirty = true
|
||||
} else {
|
||||
console.error(`Priority tags multiselect - unknown event type: '${e.type}'`)
|
||||
}
|
||||
}}
|
||||
options={nconfig?.worker_tags}
|
||||
selectedOptionsDraggable={false}
|
||||
ulOptionsClass={'!bg-surface-secondary'}
|
||||
placeholder="High priority tags"
|
||||
}
|
||||
items={safeSelectItems(nconfig?.worker_tags)}
|
||||
/>
|
||||
</Label>
|
||||
{/if}
|
||||
@@ -433,7 +414,7 @@
|
||||
{:else if selected == 'dedicated'}
|
||||
{#if nconfig?.dedicated_worker != undefined}
|
||||
<input
|
||||
disabled={!$superadmin}
|
||||
disabled={!($superadmin || $devopsRole)}
|
||||
placeholder="<workspace>:<script path>"
|
||||
type="text"
|
||||
onchange={() => {
|
||||
@@ -442,7 +423,7 @@
|
||||
}}
|
||||
bind:value={nconfig.dedicated_worker}
|
||||
/>
|
||||
{#if $superadmin}
|
||||
{#if $superadmin || $devopsRole}
|
||||
<div class="py-2"
|
||||
><Alert
|
||||
type="info"
|
||||
@@ -471,11 +452,11 @@
|
||||
<div class="flex gap-1 items-center">
|
||||
<input
|
||||
type="text"
|
||||
disabled={!$superadmin}
|
||||
disabled={!($superadmin || $devopsRole)}
|
||||
placeholder="/path/to/python3.X/site-packages"
|
||||
bind:value={nconfig.additional_python_paths![i]}
|
||||
/>
|
||||
{#if $superadmin}
|
||||
{#if $superadmin || $devopsRole}
|
||||
<button
|
||||
class="rounded-full bg-surface/60 hover:bg-gray-200"
|
||||
aria-label="Clear"
|
||||
@@ -497,7 +478,7 @@
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if $superadmin}
|
||||
{#if $superadmin || $devopsRole}
|
||||
<div class="flex">
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -523,12 +504,12 @@
|
||||
{#each nconfig.pip_local_dependencies as _, i}
|
||||
<div class="flex gap-1 items-center">
|
||||
<input
|
||||
disabled={!$superadmin}
|
||||
disabled={!($superadmin || $devopsRole)}
|
||||
type="text"
|
||||
placeholder="httpx"
|
||||
bind:value={nconfig.pip_local_dependencies[i]}
|
||||
/>
|
||||
{#if $superadmin}
|
||||
{#if $superadmin || $devopsRole}
|
||||
<button
|
||||
class="rounded-full bg-surface/60 hover:bg-gray-200"
|
||||
aria-label="Clear"
|
||||
@@ -550,7 +531,7 @@
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if $superadmin}
|
||||
{#if $superadmin || $devopsRole}
|
||||
<div class="flex">
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -584,7 +565,7 @@
|
||||
{#each customEnvVars as envvar, i}
|
||||
<div class="flex gap-1 items-center">
|
||||
<input
|
||||
disabled={!$superadmin}
|
||||
disabled={!($superadmin || $devopsRole)}
|
||||
type="text"
|
||||
placeholder="ENV_VAR_NAME"
|
||||
bind:value={envvar.key}
|
||||
@@ -593,7 +574,7 @@
|
||||
}}
|
||||
/>
|
||||
<ToggleButtonGroup
|
||||
disabled={!$superadmin}
|
||||
disabled={!($superadmin || $devopsRole)}
|
||||
class="w-128"
|
||||
bind:selected={envvar.type}
|
||||
on:selected={(e) => {
|
||||
@@ -610,13 +591,13 @@
|
||||
</ToggleButtonGroup>
|
||||
<input
|
||||
type="text"
|
||||
disabled={!$superadmin || envvar.type === 'dynamic'}
|
||||
disabled={!($superadmin || $devopsRole) || envvar.type === 'dynamic'}
|
||||
placeholder={envvar.type === 'dynamic'
|
||||
? 'value read from worker env var'
|
||||
: 'static value'}
|
||||
bind:value={envvar.value}
|
||||
/>
|
||||
{#if $superadmin}
|
||||
{#if $superadmin || $devopsRole}
|
||||
<button
|
||||
class="rounded-full bg-surface/60 hover:bg-gray-200"
|
||||
aria-label="Clear"
|
||||
@@ -639,7 +620,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if $superadmin}
|
||||
{#if $superadmin || $devopsRole}
|
||||
<div class="flex">
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -657,7 +638,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !superadmin}
|
||||
{#if !($superadmin || $devopsRole)}
|
||||
<div class="flex flex-wrap items-center gap-1 pt-2">
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -745,7 +726,7 @@
|
||||
>
|
||||
{/if}
|
||||
<Editor
|
||||
disabled={!$superadmin}
|
||||
disabled={!($superadmin || $devopsRole)}
|
||||
class="flex flex-1 grow h-full w-full"
|
||||
automaticLayout
|
||||
scriptLang={'bash'}
|
||||
@@ -824,7 +805,7 @@
|
||||
}}
|
||||
disabled={(!dirty && nconfig?.dedicated_worker == undefined) ||
|
||||
!$enterpriseLicense ||
|
||||
!$superadmin}
|
||||
!($superadmin || $devopsRole)}
|
||||
>
|
||||
Apply changes
|
||||
</Button>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { JobService, type QueuedJob } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import { emptyString } from '$lib/utils'
|
||||
|
||||
let container: HTMLDivElement
|
||||
|
||||
@@ -3,19 +3,20 @@
|
||||
import { WorkerService } from '$lib/gen'
|
||||
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Select from './Select.svelte'
|
||||
import Select from './select/Select.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
|
||||
let {
|
||||
tag = $bindable(),
|
||||
noLabel = false,
|
||||
nullTag = undefined,
|
||||
disabled = false
|
||||
} = $props<{
|
||||
}: {
|
||||
tag: string | undefined
|
||||
noLabel?: boolean
|
||||
nullTag?: string | undefined
|
||||
disabled?: boolean
|
||||
}>()
|
||||
} = $props()
|
||||
|
||||
loadWorkerGroups()
|
||||
|
||||
@@ -41,7 +42,7 @@
|
||||
class="w-full"
|
||||
{disabled}
|
||||
placeholder={nullTag ? `default: ${nullTag}` : 'lang default'}
|
||||
items={items.map((value) => ({ value }))}
|
||||
items={safeSelectItems(items)}
|
||||
bind:value={() => tag, (value) => ((tag = value), dispatch('change', value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -101,7 +101,6 @@
|
||||
componentStyle={$app.css?.formcomponent}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<RunnableWrapper
|
||||
{recomputeIds}
|
||||
{render}
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
|
||||
let { id, configuration, render } = $props<{
|
||||
let { id, configuration, render }: {
|
||||
id: string
|
||||
configuration: RichConfigurations
|
||||
render: boolean
|
||||
}>()
|
||||
} = $props()
|
||||
|
||||
const { componentControl, worldStore, selectedComponent, connectingInput, mode } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { enUS, fr, de, pt, ja } from 'date-fns/locale'
|
||||
import Select from '$lib/components/Select.svelte'
|
||||
import Select from '$lib/components/select/Select.svelte'
|
||||
import { safeSelectItems } from '$lib/components/select/utils.svelte'
|
||||
|
||||
export let id: string
|
||||
export let configuration: RichConfigurations
|
||||
@@ -238,9 +239,10 @@
|
||||
outputs.day.set(v ? Number(v) : undefined)
|
||||
}
|
||||
}
|
||||
items={Array.from({ length: computeDayPerMonth(selectedMonth, selectedYear) }, (_, i) => {
|
||||
return { label: String(i + 1), value: String(i + 1) }
|
||||
})}
|
||||
items={Array.from(
|
||||
{ length: computeDayPerMonth(selectedMonth, selectedYear) },
|
||||
(_, i) => ({ value: String(i + 1) })
|
||||
)}
|
||||
class={twMerge('text-clip min-w-0', css?.input?.class, 'wm-date-select')}
|
||||
containerStyle={css?.input?.style}
|
||||
placeholder="Pick a day"
|
||||
@@ -280,9 +282,7 @@
|
||||
outputs.year.set(selectedYear ? Number(selectedYear) : undefined)
|
||||
}
|
||||
}
|
||||
items={Array.from({ length: 201 }, (_, i) => `${1900 + i}`).map((value) => ({
|
||||
value
|
||||
}))}
|
||||
items={safeSelectItems(Array.from({ length: 201 }, (_, i) => `${1900 + i}`))}
|
||||
placeholder="Pick a year"
|
||||
class={twMerge('text-clip min-w-0', css?.input?.class, 'wm-date-select')}
|
||||
containerStyle={css?.input?.style}
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
import AlignWrapper from '../helpers/AlignWrapper.svelte'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
|
||||
// @ts-ignore
|
||||
import MultiSelect from 'svelte-multiselect'
|
||||
|
||||
export let id: string
|
||||
export let configuration: RichConfigurations
|
||||
export let customCss: ComponentCustomCSS<'multiselectcomponent'> | undefined = undefined
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onDestroy } from 'svelte'
|
||||
import { getContext, onDestroy, untrack } from 'svelte'
|
||||
import { initConfig, initOutput } from '../../editor/appUtils'
|
||||
import type {
|
||||
AppViewerContext,
|
||||
@@ -16,61 +16,50 @@
|
||||
// @ts-ignore
|
||||
import Portal from '$lib/components/Portal.svelte'
|
||||
|
||||
import { createFloatingActions } from 'svelte-floating-ui'
|
||||
import { extractCustomProperties } from '$lib/utils'
|
||||
import { tick } from 'svelte'
|
||||
import { offset, flip, shift } from 'svelte-floating-ui/dom'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import MultiSelect from '$lib/components/multiselect/MultiSelect.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import MultiSelect from '$lib/components/select/MultiSelect.svelte'
|
||||
import { safeSelectItems } from '$lib/components/select/utils.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let id: string
|
||||
export let configuration: RichConfigurations
|
||||
export let customCss: ComponentCustomCSS<'multiselectcomponent'> | undefined = undefined
|
||||
export let render: boolean
|
||||
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
|
||||
interface Props {
|
||||
id: string
|
||||
configuration: RichConfigurations
|
||||
customCss?: ComponentCustomCSS<'multiselectcomponentv2'> | undefined
|
||||
render: boolean
|
||||
verticalAlignment?: 'top' | 'center' | 'bottom' | undefined
|
||||
}
|
||||
|
||||
let {
|
||||
id,
|
||||
configuration,
|
||||
customCss = undefined,
|
||||
render,
|
||||
verticalAlignment = undefined
|
||||
}: Props = $props()
|
||||
|
||||
const iterContext = getContext<ListContext>('ListWrapperContext')
|
||||
const listInputs: ListInputs | undefined = getContext<ListInputs>('ListInputs')
|
||||
|
||||
const [floatingRef, floatingContent] = createFloatingActions({
|
||||
strategy: 'absolute',
|
||||
middleware: [offset(5), flip(), shift()]
|
||||
})
|
||||
|
||||
const { app, worldStore, selectedComponent, componentControl } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
let items: (string | { value: string; label: any })[] = []
|
||||
|
||||
const resolvedConfig = initConfig(
|
||||
components['multiselectcomponent'].initialData.configuration,
|
||||
configuration
|
||||
let items: { value: string; label?: any }[] = $state([])
|
||||
|
||||
const resolvedConfig = $state(
|
||||
initConfig(components['multiselectcomponentv2'].initialData.configuration, configuration)
|
||||
)
|
||||
|
||||
const outputs = initOutput($worldStore, id, {
|
||||
result: [] as string[]
|
||||
})
|
||||
|
||||
let selectedItems: (number | string | { value: string; label: any })[] | undefined = [
|
||||
...new Set(outputs?.result.peak())
|
||||
] as (number | string | { value: string; label: any })[]
|
||||
let selectedItems: string[] = $state([...new Set(outputs?.result.peak())].map(convertToValue))
|
||||
$effect(() => setResultsFromSelectedItems(selectedItems))
|
||||
|
||||
function setResultsFromSelectedItems() {
|
||||
const value = [
|
||||
...(selectedItems?.map((item) => {
|
||||
if (typeof item == 'number') {
|
||||
return item.toString()
|
||||
} else if (typeof item == 'object' && item.value != undefined && item.label != undefined) {
|
||||
return item?.value ?? `NOT_STRING`
|
||||
} else if (typeof item == 'string') {
|
||||
return item
|
||||
} else if (typeof item == 'object' && item.label != undefined) {
|
||||
return item.label
|
||||
} else {
|
||||
return 'NOT_STRING'
|
||||
}
|
||||
}) ?? [])
|
||||
]
|
||||
let customItems: string[] = $state([])
|
||||
|
||||
function setResultsFromSelectedItems(value: string[]) {
|
||||
outputs?.result.set(value)
|
||||
setContextValue(value)
|
||||
}
|
||||
@@ -95,82 +84,60 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: resolvedConfig.items && handleItems()
|
||||
|
||||
function handleItems() {
|
||||
if (Array.isArray(resolvedConfig.items)) {
|
||||
items = resolvedConfig.items?.map((item) => {
|
||||
if (typeof item == 'object' && item.value != undefined && item.label != undefined) {
|
||||
return item
|
||||
}
|
||||
if (typeof item == 'number') {
|
||||
return item.toString()
|
||||
}
|
||||
return typeof item === 'string' ? item : `NOT_STRING`
|
||||
})
|
||||
items = resolvedConfig.items?.map(convertToItem)
|
||||
}
|
||||
}
|
||||
|
||||
$: resolvedConfig.defaultItems && setSelectedItemsFromValues(resolvedConfig.defaultItems)
|
||||
function convertToItem(v: any) {
|
||||
if (typeof v == 'object' && v.value != undefined && v.label != undefined) {
|
||||
return v as { value: any; label?: string }
|
||||
}
|
||||
if (typeof v == 'number') return { value: v.toString() }
|
||||
return { value: typeof v === 'string' ? v : `NOT_STRING` }
|
||||
}
|
||||
function convertToValue(item: any): string {
|
||||
if (typeof item == 'object' && item.value != undefined) return item.value
|
||||
if (typeof item == 'number') return item.toString()
|
||||
if (typeof item == 'string') return item
|
||||
return item?.toString?.() ?? 'NOT_STRING'
|
||||
}
|
||||
|
||||
function setSelectedItemsFromValues(values: any[]) {
|
||||
if (Array.isArray(values)) {
|
||||
const nvalue = values
|
||||
.map((value) => {
|
||||
const x = items.find((item) => {
|
||||
if (typeof item == 'object' && item.value != undefined && item.label != undefined) {
|
||||
return deepEqual(item.value, value)
|
||||
}
|
||||
return item == value
|
||||
})
|
||||
return (
|
||||
items.find((item) => {
|
||||
if (typeof item == 'object' && item.value != undefined && item.label != undefined) {
|
||||
return deepEqual(item.value, value)
|
||||
}
|
||||
return item == value
|
||||
}) ??
|
||||
(typeof x === 'object' ? x.value : x) ??
|
||||
(typeof value == 'string' ? value : undefined) ??
|
||||
(typeof value == 'number' ? value.toString() : undefined)
|
||||
)
|
||||
})
|
||||
.filter((item) => item != undefined)
|
||||
selectedItems = [...new Set(nvalue)]
|
||||
setResultsFromSelectedItems()
|
||||
setResultsFromSelectedItems(selectedItems)
|
||||
}
|
||||
}
|
||||
|
||||
let css = initCss($app.css?.multiselectcomponent, customCss)
|
||||
let css = $state(initCss($app.css?.multiselectcomponentv2, customCss))
|
||||
|
||||
function setOuterDivStyle(outerDiv: HTMLDivElement, portalRef: HTMLDivElement, style: string) {
|
||||
outerDiv.setAttribute('style', style)
|
||||
// find ul in portalRef and set style
|
||||
const ul = portalRef.querySelector('ul')
|
||||
ul?.setAttribute('style', extractCustomProperties(style))
|
||||
}
|
||||
|
||||
$: outerDiv &&
|
||||
portalRef &&
|
||||
css?.multiselect?.style &&
|
||||
setOuterDivStyle(outerDiv, portalRef, css?.multiselect?.style)
|
||||
|
||||
let outerDiv: HTMLDivElement | undefined = undefined
|
||||
let portalRef: HTMLDivElement | undefined = undefined
|
||||
|
||||
function moveOptionsToPortal() {
|
||||
// Find ul element with class 'options' within the outerDiv
|
||||
const ul = outerDiv?.querySelector('.options')
|
||||
|
||||
if (ul) {
|
||||
// Move the ul element to the portal
|
||||
portalRef?.appendChild(ul)
|
||||
}
|
||||
}
|
||||
|
||||
$: if (render && portalRef && outerDiv && items?.length > 0) {
|
||||
tick().then(() => {
|
||||
moveOptionsToPortal()
|
||||
})
|
||||
}
|
||||
let w = 0
|
||||
let open: boolean = false
|
||||
$effect(() => {
|
||||
resolvedConfig.items && untrack(() => handleItems())
|
||||
})
|
||||
$effect(() => {
|
||||
;[resolvedConfig.defaultItems]
|
||||
untrack(() => setSelectedItemsFromValues(resolvedConfig.defaultItems))
|
||||
})
|
||||
</script>
|
||||
|
||||
{#each Object.keys(components['multiselectcomponent'].initialData.configuration) as key (key)}
|
||||
{#each Object.keys(components['multiselectcomponentv2'].initialData.configuration) as key (key)}
|
||||
<ResolveConfig
|
||||
{id}
|
||||
{key}
|
||||
@@ -185,7 +152,7 @@
|
||||
{customCss}
|
||||
{key}
|
||||
bind:css={css[key]}
|
||||
componentStyle={$app.css?.multiselectcomponent}
|
||||
componentStyle={$app.css?.multiselectcomponentv2}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
@@ -193,74 +160,33 @@
|
||||
|
||||
<AlignWrapper {render} hFull {verticalAlignment}>
|
||||
<div
|
||||
class="w-full app-editor-input"
|
||||
on:pointerdown={(e) => {
|
||||
class={twMerge('w-full', resolvedConfig.allowOverflow ? '' : 'absolute inset-0')}
|
||||
onpointerdown={(e) => {
|
||||
$selectedComponent = [id]
|
||||
|
||||
if (!e.shiftKey) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
if (!e.shiftKey) e.stopPropagation()
|
||||
selectedComponent.set([id])
|
||||
}}
|
||||
use:floatingRef
|
||||
bind:clientWidth={w}
|
||||
>
|
||||
{#if !selectedItems || Array.isArray(selectedItems)}
|
||||
<MultiSelect
|
||||
bind:outerDiv
|
||||
outerDivClass={`${resolvedConfig.allowOverflow ? '' : 'h-full'}`}
|
||||
ulSelectedClass={`${resolvedConfig.allowOverflow ? '' : 'overflow-auto max-h-full'} `}
|
||||
--sms-border={'none'}
|
||||
--sms-min-height={'32px'}
|
||||
--sms-focus-border={'none'}
|
||||
bind:selected={selectedItems}
|
||||
options={items}
|
||||
placeholder={resolvedConfig.placeholder}
|
||||
allowUserOptions={resolvedConfig.create}
|
||||
on:change={(event) => {
|
||||
if (event?.detail?.type === 'removeAll') {
|
||||
outputs?.result.set([])
|
||||
setContextValue([])
|
||||
} else {
|
||||
setResultsFromSelectedItems()
|
||||
<MultiSelect
|
||||
style={css.multiselect?.style}
|
||||
class={twMerge(
|
||||
'multiselect',
|
||||
resolvedConfig.allowOverflow ? '' : 'max-h-full overflow-y-scroll'
|
||||
)}
|
||||
selectedUlClass="selected"
|
||||
items={safeSelectItems([...items, ...customItems])}
|
||||
placeholder={resolvedConfig.placeholder}
|
||||
bind:value={selectedItems}
|
||||
disabled={resolvedConfig.disabled}
|
||||
onCreateItem={resolvedConfig.create
|
||||
? (item) => {
|
||||
customItems.push(item)
|
||||
selectedItems.push(item)
|
||||
customItems = customItems
|
||||
selectedItems = selectedItems
|
||||
}
|
||||
}}
|
||||
on:open={() => {
|
||||
$selectedComponent = [id]
|
||||
open = true
|
||||
}}
|
||||
on:close={() => {
|
||||
open = false
|
||||
}}
|
||||
let:option
|
||||
>
|
||||
<!-- needed because portal doesn't work for mouseup event en mobile -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="w-full"
|
||||
on:mouseup|stopPropagation
|
||||
on:pointerdown|stopPropagation={(e) => {
|
||||
let newe = new MouseEvent('mouseup')
|
||||
e.target?.['parentElement']?.dispatchEvent(newe)
|
||||
}}
|
||||
>
|
||||
{typeof option == 'object' ? (option?.label ?? 'NO_LABEL') : option}
|
||||
</div>
|
||||
</MultiSelect>
|
||||
<Portal name="app-multiselect-v2">
|
||||
<div use:floatingContent class="z5000" hidden={!open}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
bind:this={portalRef}
|
||||
class="multiselect"
|
||||
style={`min-width: ${w}px;`}
|
||||
on:click|stopPropagation
|
||||
></div>
|
||||
</div>
|
||||
</Portal>
|
||||
{:else}
|
||||
Value {selectedItems} is not an array
|
||||
{/if}
|
||||
: undefined}
|
||||
onOpen={() => ($selectedComponent = [id])}
|
||||
/>
|
||||
</div>
|
||||
</AlignWrapper>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { Bug } from 'lucide-svelte'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import Select from '$lib/components/Select.svelte'
|
||||
import Select from '$lib/components/select/Select.svelte'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
|
||||
@@ -24,19 +24,29 @@
|
||||
|
||||
let { id, componentContainerHeight, customCss = undefined, render, nodes }: Props = $props()
|
||||
|
||||
let resolvedConditions = $derived(
|
||||
nodes.reduce((acc, node) => {
|
||||
let resolvedConditions = $state(createResolvedConditions())
|
||||
|
||||
function createResolvedConditions() {
|
||||
return nodes.reduce((acc, node) => {
|
||||
acc[node.id] = acc[node.id] || []
|
||||
return acc
|
||||
}, {})
|
||||
)
|
||||
}
|
||||
|
||||
let resolvedNext = $derived(
|
||||
nodes.reduce((acc, node) => {
|
||||
let resolvedNext = $state(createResolvedNext())
|
||||
|
||||
function createResolvedNext() {
|
||||
return nodes.reduce((acc, node) => {
|
||||
acc[node.id] = acc[node.id] || false
|
||||
return acc
|
||||
}, {})
|
||||
)
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
nodes
|
||||
resolvedConditions = untrack(() => createResolvedConditions())
|
||||
resolvedNext = untrack(() => createResolvedNext())
|
||||
})
|
||||
|
||||
let everRender = $state(render)
|
||||
$effect.pre(() => {
|
||||
@@ -165,6 +175,8 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- {JSON.stringify(resolvedConditions)}
|
||||
{JSON.stringify(resolvedNext)} -->
|
||||
{#if Object.keys(resolvedConditions).length === nodes.length}
|
||||
{#each nodes ?? [] as node (node.id)}
|
||||
{#each node.next ?? [] as next, conditionIndex}
|
||||
|
||||
@@ -1256,7 +1256,7 @@
|
||||
<SecondaryMenu right />
|
||||
{:else}
|
||||
<div class="min-w-[150px] text-sm !text-secondary text-center py-8 px-2">
|
||||
Select a component to see the settings for it
|
||||
Select a component to see the settings for it
|
||||
</div>
|
||||
{/if}
|
||||
</TabContent>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { createBubbler, stopPropagation } from 'svelte/legacy'
|
||||
|
||||
const bubble = createBubbler()
|
||||
import Dropdown from '$lib/components/DropdownV2.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
@@ -7,18 +10,24 @@
|
||||
import { isDebugging } from './settingsPanel/decisionTree/utils'
|
||||
import { X, Bug } from 'lucide-svelte'
|
||||
|
||||
export let nodes: DecisionTreeNode[] = []
|
||||
export let id: string
|
||||
export let isSmall = false
|
||||
export let componentIsDebugging = false
|
||||
interface Props {
|
||||
nodes?: DecisionTreeNode[]
|
||||
id: string
|
||||
isSmall?: boolean
|
||||
componentIsDebugging?: boolean
|
||||
}
|
||||
|
||||
$: componentIsDebugging = isDebugging($debuggingComponents, id)
|
||||
let { nodes = [], id, isSmall = false, componentIsDebugging = $bindable(false) }: Props = $props()
|
||||
|
||||
$effect(() => {
|
||||
componentIsDebugging = isDebugging($debuggingComponents, id)
|
||||
})
|
||||
|
||||
const { componentControl, debuggingComponents, worldStore } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let currentNodeId: string = $worldStore.outputsById[id]?.currentNodeId?.peak() ?? 'a'
|
||||
let currentNodeId: string = $state($worldStore.outputsById[id]?.currentNodeId?.peak() ?? 'a')
|
||||
|
||||
function subscribeToCurrentNode(id: string) {
|
||||
return $worldStore.outputsById[id]?.currentNodeId?.subscribe(
|
||||
@@ -49,9 +58,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: onDebugNode($debuggingComponents[id])
|
||||
$effect(() => {
|
||||
onDebugNode($debuggingComponents[id])
|
||||
})
|
||||
|
||||
let renderCount: number = 0
|
||||
let renderCount: number = $state(0)
|
||||
let lastNodes: DecisionTreeNode[] = nodes
|
||||
|
||||
function onNodesChange(newNodes: DecisionTreeNode[]) {
|
||||
@@ -67,7 +78,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: onNodesChange(nodes)
|
||||
$effect(() => {
|
||||
onNodesChange(nodes)
|
||||
})
|
||||
|
||||
async function getDropdownItems() {
|
||||
return [
|
||||
@@ -97,7 +110,7 @@
|
||||
|
||||
{#key renderCount}
|
||||
<Dropdown items={getDropdownItems} class="w-fit h-auto" usePointerDownOutside={true}>
|
||||
<svelte:fragment slot="buttonReplacement">
|
||||
{#snippet buttonReplacement()}
|
||||
<button
|
||||
title={'Debug tabs'}
|
||||
class={classNames(
|
||||
@@ -106,15 +119,15 @@
|
||||
? ' hover:bg-red-300 hover:text-red-800'
|
||||
: 'text-blue-600 hover:bg-blue-300 hover:text-blue-800'
|
||||
)}
|
||||
on:click={() => dispatch('triggerInlineEditor')}
|
||||
on:pointerdown|stopPropagation
|
||||
onclick={() => dispatch('triggerInlineEditor')}
|
||||
onpointerdown={stopPropagation(bubble('pointerdown'))}
|
||||
>
|
||||
{#if componentIsDebugging}
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
{`${isSmall ? '' : 'Debugging node'} ${nodes[$debuggingComponents[id] ?? 0]?.id}`}
|
||||
<!-- svelte-ignore node_invalid_placement_ssr -->
|
||||
<button
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
$componentControl?.[id]?.setTab?.(0)
|
||||
|
||||
$debuggingComponents = Object.fromEntries(
|
||||
@@ -131,6 +144,6 @@
|
||||
>{`Debug nodes (current node: ${currentNodeId})`}</div
|
||||
>{/if}
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</Dropdown>
|
||||
{/key}
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<input />
|
||||
<div class="w-full z-[1000] overflow-visible h-full">
|
||||
<div class={$app.hideLegacyTopBar ? 'hidden' : ''}>
|
||||
<div
|
||||
|
||||
@@ -2354,6 +2354,11 @@ This is a paragraph.
|
||||
|
||||
tooltip:
|
||||
'If too many items, the box overflow its container instead of having an internal scroll'
|
||||
},
|
||||
disabled: {
|
||||
type: 'static',
|
||||
value: false,
|
||||
fieldType: 'boolean'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,5 +110,7 @@ export const COMPONENT_SETS = [layout, tabs, buttons, inputs, tables, display, c
|
||||
|
||||
export const DEPRECATED_COMPONENTS = {
|
||||
tablecomponent:
|
||||
'We will be removing this component in the future. we recommend using the AgGrid table instead.'
|
||||
'We will be removing this component in the future. we recommend using the AgGrid table instead.',
|
||||
chartjscomponent: 'Use the new ChartJS v2 component instead',
|
||||
multiselectcomponent: 'Use the new MultiSelect v2 component instead'
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { Badge, Button, ClearableInput, Tab, TabContent, Tabs } from '../../../common'
|
||||
import type { AppViewerContext } from '../../types'
|
||||
import ListItem from './ListItem.svelte'
|
||||
import { ccomponents, components } from '../component'
|
||||
import { ccomponents, components, DEPRECATED_COMPONENTS } from '../component'
|
||||
import { customisationByComponent } from './cssUtils'
|
||||
import DataTable from '$lib/components/table/DataTable.svelte'
|
||||
import Head from '$lib/components/table/Head.svelte'
|
||||
@@ -60,7 +60,7 @@
|
||||
ids: ['q'].map((id) => ({ id, forceStyle: true, forceClass: true }))
|
||||
},
|
||||
...Object.entries(ccomponents)
|
||||
.filter(([key]) => key !== 'quillcomponent')
|
||||
.filter(([key]) => !['quillcomponent', ...Object.keys(DEPRECATED_COMPONENTS)].includes(key))
|
||||
.map(([type, { name, icon, customCss }]) => ({
|
||||
type: type as keyof typeof components,
|
||||
name,
|
||||
|
||||
@@ -676,8 +676,8 @@ export const customisationByComponent: Customisation[] = [
|
||||
comment: 'buttons to remove a single or all selected options at once'
|
||||
},
|
||||
{
|
||||
selector: '.multiselect > input[autocomplete]',
|
||||
comment: 'input inside the top-level wrapper div'
|
||||
selector: '.multiselect.dropdown',
|
||||
comment: 'dropdown container'
|
||||
},
|
||||
{
|
||||
selector: '.multiselect > ul.options',
|
||||
@@ -686,23 +686,6 @@ export const customisationByComponent: Customisation[] = [
|
||||
{
|
||||
selector: '.multiselect > ul.options > li',
|
||||
comment: 'dropdown list items'
|
||||
},
|
||||
{
|
||||
selector: '.multiselect > ul.options > li.selected',
|
||||
comment: 'selected options in the dropdown list'
|
||||
},
|
||||
{
|
||||
selector: '.multiselect > ul.options > li:not(.selected):hover',
|
||||
comment: 'unselected but hovered options in the dropdown list'
|
||||
},
|
||||
{
|
||||
selector: '.multiselect > ul.options > li.active',
|
||||
comment:
|
||||
'active item, navigated to with up/down arrow keys and ready to be selected by pressing enter'
|
||||
},
|
||||
{
|
||||
selector: '.multiselect > ul.options > li.disabled',
|
||||
comment: 'options with disabled key set to true'
|
||||
}
|
||||
],
|
||||
variables: [
|
||||
|
||||
+11
-1
@@ -57,7 +57,13 @@
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={40}>
|
||||
<div class="h-full w-full bg-surface p-4 flex flex-col gap-6">
|
||||
<div
|
||||
class="h-full w-full bg-surface p-4 flex flex-col gap-6"
|
||||
on:keydown={(e) => {
|
||||
// Prevent keyboard events from bubbling to SvelteFlow
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
{#if selectedNode}
|
||||
<Section label="Conditions" class="w-full flex flex-col gap-2">
|
||||
<svelte:fragment slot="action">
|
||||
@@ -92,6 +98,10 @@
|
||||
renderCount++
|
||||
}, 300)()
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
// Prevent keyboard events from bubbling to SvelteFlow
|
||||
e.stopPropagation()
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
<script lang="ts">
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
|
||||
import MultiSelect from '$lib/components/multiselect/MultiSelectWrapper.svelte'
|
||||
import MultiSelect from '$lib/components/select/MultiSelect.svelte'
|
||||
import { safeSelectItems } from '$lib/components/select/utils.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
|
||||
let { items, value = $bindable(), title, tooltip } = $props<{
|
||||
let {
|
||||
items,
|
||||
value = $bindable(),
|
||||
title,
|
||||
tooltip
|
||||
}: {
|
||||
items: string[]
|
||||
value: string[] | undefined
|
||||
title: string
|
||||
tooltip: string
|
||||
}>()
|
||||
} = $props()
|
||||
|
||||
let width = $state(0)
|
||||
const inputWidth = 280
|
||||
@@ -34,7 +39,10 @@
|
||||
No components to recompute.
|
||||
</div>
|
||||
{:else}
|
||||
<MultiSelect {items} bind:value />
|
||||
<MultiSelect
|
||||
items={safeSelectItems(items)}
|
||||
bind:value={() => value ?? [], (v) => (value = v?.length ? v : undefined)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { StaticInput, StaticOptions } from '../../../inputType'
|
||||
import Select from '$lib/components/Select.svelte'
|
||||
import Select from '$lib/components/select/Select.svelte'
|
||||
|
||||
interface Props {
|
||||
componentInput: StaticInput<any> | undefined
|
||||
|
||||
@@ -34,8 +34,9 @@
|
||||
import { onDestroy, tick, untrack } from 'svelte'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
import Select from '../Select.svelte'
|
||||
import Select from '../select/Select.svelte'
|
||||
import { usePromise } from '$lib/svelte5Utils.svelte'
|
||||
import { safeSelectItems } from '../select/utils.svelte'
|
||||
|
||||
let usernames: string[] | undefined = $state()
|
||||
let resources = usePromise(() => loadResources($workspaceStore!), { loadInit: false })
|
||||
@@ -397,7 +398,7 @@
|
||||
onCreateItem={(r) => (resources.value?.push(r), (resource = r))}
|
||||
createText="Press enter to use this value"
|
||||
bind:value={resource}
|
||||
items={['all', ...(resources.value ?? [])].map((r) => ({ value: r, label: r }))}
|
||||
items={safeSelectItems(['all', ...(resources.value ?? [])])}
|
||||
inputClass="dark:!bg-gray-700"
|
||||
RightIcon={ChevronDown}
|
||||
/>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
<Button
|
||||
on:click={() => dispatch('close')}
|
||||
on:pointerdown={(e) => e.stopPropagation()}
|
||||
startIcon={{ icon: Icon ?? X }}
|
||||
iconOnly
|
||||
size="sm"
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
<script lang="ts">
|
||||
import Portal from '$lib/components/Portal.svelte'
|
||||
|
||||
export let condition = false
|
||||
export let target: string | HTMLElement | undefined = undefined
|
||||
interface Props {
|
||||
condition?: boolean
|
||||
target?: string | HTMLElement | undefined
|
||||
name?: string
|
||||
children?: import('svelte').Snippet
|
||||
}
|
||||
|
||||
let {
|
||||
condition = false,
|
||||
target = undefined,
|
||||
name = 'conditional-portal',
|
||||
children
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if condition}
|
||||
<Portal name="conditional-portal" {target}><slot /></Portal>
|
||||
<Portal {name} {target}>{@render children?.()}</Portal>
|
||||
{:else}
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { onDestroy } from 'svelte'
|
||||
import ProviderModelSelector from './chat/ProviderModelSelector.svelte'
|
||||
import { aiChatManager, AIMode } from './chat/AIChatManager.svelte'
|
||||
|
||||
interface Props {
|
||||
// props
|
||||
@@ -38,6 +39,7 @@
|
||||
inlineScript?: boolean
|
||||
args: Record<string, any>
|
||||
transformer?: boolean
|
||||
openAiChat?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -47,7 +49,8 @@
|
||||
diffEditor,
|
||||
inlineScript = false,
|
||||
args,
|
||||
transformer = false
|
||||
transformer = false,
|
||||
openAiChat = false
|
||||
}: Props = $props()
|
||||
|
||||
run(() => {
|
||||
@@ -72,6 +75,54 @@
|
||||
run(() => {
|
||||
trimmedDesc = funcDesc.trim()
|
||||
})
|
||||
async function callCopilot() {
|
||||
if (mode === 'edit') {
|
||||
await copilot(
|
||||
{
|
||||
// @ts-ignore
|
||||
language: transformer && lang === 'frontend' ? 'transformer' : lang!,
|
||||
description: trimmedDesc,
|
||||
code: editor?.getCode() || '',
|
||||
dbSchema: dbSchema,
|
||||
type: 'edit',
|
||||
workspace: $workspaceStore!
|
||||
},
|
||||
generatedCode,
|
||||
abortController
|
||||
)
|
||||
} else {
|
||||
await copilot(
|
||||
{
|
||||
// @ts-ignore
|
||||
language: transformer && lang === 'frontend' ? 'transformer' : lang!,
|
||||
description: trimmedDesc,
|
||||
dbSchema: dbSchema,
|
||||
type: 'gen',
|
||||
workspace: $workspaceStore!
|
||||
},
|
||||
generatedCode,
|
||||
abortController
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function handleAiButtonClick() {
|
||||
if (editor && isInitialCode(editor.getCode())) {
|
||||
mode = 'gen'
|
||||
} else {
|
||||
mode = 'edit'
|
||||
}
|
||||
|
||||
if (openAiChat) {
|
||||
// Open the AI chat in script mode
|
||||
aiChatManager.openChat()
|
||||
aiChatManager.changeMode(AIMode.SCRIPT)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
autoResize()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
async function onGenerate(closePopup: () => void) {
|
||||
if (trimmedDesc.length <= 0) {
|
||||
@@ -82,34 +133,7 @@
|
||||
genLoading = true
|
||||
blockPopupOpen = true
|
||||
abortController = new AbortController()
|
||||
if (mode === 'edit') {
|
||||
await copilot(
|
||||
{
|
||||
// @ts-ignore
|
||||
language: transformer && lang === 'frontend' ? 'transformer' : lang!,
|
||||
description: trimmedDesc,
|
||||
code: editor?.getCode() || '',
|
||||
dbSchema: dbSchema,
|
||||
type: 'edit',
|
||||
workspace: $workspaceStore!
|
||||
},
|
||||
generatedCode,
|
||||
abortController
|
||||
)
|
||||
} else {
|
||||
await copilot(
|
||||
{
|
||||
// @ts-ignore
|
||||
language: transformer && lang === 'frontend' ? 'transformer' : lang!,
|
||||
description: trimmedDesc,
|
||||
dbSchema: dbSchema,
|
||||
type: 'gen',
|
||||
workspace: $workspaceStore!
|
||||
},
|
||||
generatedCode,
|
||||
abortController
|
||||
)
|
||||
}
|
||||
await callCopilot()
|
||||
setupDiff()
|
||||
diffEditor?.setModified($generatedCode)
|
||||
blockPopupOpen = false
|
||||
@@ -205,14 +229,27 @@
|
||||
}
|
||||
|
||||
let promptHistory: string[] = $state([])
|
||||
function getPromptHistory() {
|
||||
|
||||
function getPromptStorageKey() {
|
||||
return 'prompts-' + lang
|
||||
}
|
||||
|
||||
function safeLocalStorageOperation<T>(operation: () => T, defaultValue?: T): T | undefined {
|
||||
try {
|
||||
promptHistory = JSON.parse(localStorage.getItem('prompts-' + lang) || '[]')
|
||||
return operation()
|
||||
} catch (e) {
|
||||
console.error('error interacting with local storage', e)
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
function getPromptHistory() {
|
||||
const storageKey = getPromptStorageKey()
|
||||
promptHistory =
|
||||
safeLocalStorageOperation(() => JSON.parse(localStorage.getItem(storageKey) || '[]'), []) ||
|
||||
[]
|
||||
}
|
||||
|
||||
function savePrompt() {
|
||||
if (promptHistory.includes(trimmedDesc)) {
|
||||
return
|
||||
@@ -221,20 +258,14 @@
|
||||
while (promptHistory.length > 5) {
|
||||
promptHistory.pop()
|
||||
}
|
||||
try {
|
||||
localStorage.setItem('prompts-' + lang, JSON.stringify(promptHistory))
|
||||
} catch (e) {
|
||||
console.error('error interacting with local storage', e)
|
||||
}
|
||||
const storageKey = getPromptStorageKey()
|
||||
safeLocalStorageOperation(() => localStorage.setItem(storageKey, JSON.stringify(promptHistory)))
|
||||
}
|
||||
|
||||
function clearPromptHistory() {
|
||||
promptHistory = []
|
||||
try {
|
||||
localStorage.setItem('prompts-' + lang, JSON.stringify(promptHistory))
|
||||
} catch (e) {
|
||||
console.error('error interacting with local storage', e)
|
||||
}
|
||||
const storageKey = getPromptStorageKey()
|
||||
safeLocalStorageOperation(() => localStorage.setItem(storageKey, JSON.stringify(promptHistory)))
|
||||
}
|
||||
run(() => {
|
||||
lang && getPromptHistory()
|
||||
@@ -266,6 +297,12 @@
|
||||
;(dbSchema as any).publicOnly = detail === 'true'
|
||||
}
|
||||
|
||||
const aiChatScriptModeClasses = $derived(
|
||||
aiChatManager.mode === AIMode.SCRIPT && aiChatManager.isOpen
|
||||
? 'dark:bg-violet-900 bg-violet-100'
|
||||
: ''
|
||||
)
|
||||
|
||||
onDestroy(() => {
|
||||
abortController?.abort()
|
||||
})
|
||||
@@ -343,22 +380,9 @@
|
||||
<Button
|
||||
size="xs"
|
||||
color={genLoading ? 'red' : 'light'}
|
||||
btnClasses={genLoading ? '!px-3 z-[5000]' : '!px-2'}
|
||||
propagateEvent={!genLoading}
|
||||
on:click={genLoading
|
||||
? () => abortController?.abort()
|
||||
: () => {
|
||||
if (editor) {
|
||||
if (isInitialCode(editor.getCode())) {
|
||||
mode = 'gen'
|
||||
} else {
|
||||
mode = 'edit'
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
autoResize()
|
||||
}, 0)
|
||||
}}
|
||||
btnClasses={twMerge(genLoading ? '!px-3 z-[5000]' : '!px-2', aiChatScriptModeClasses)}
|
||||
propagateEvent={!genLoading && !openAiChat}
|
||||
on:click={genLoading ? () => abortController?.abort() : handleAiButtonClick}
|
||||
bind:element={button}
|
||||
iconOnly
|
||||
title="Generate code from prompt"
|
||||
@@ -371,27 +395,15 @@
|
||||
title="Generate code from prompt"
|
||||
btnClasses={twMerge(
|
||||
'!font-medium',
|
||||
genLoading ? 'z-[5000]' : 'text-violet-800 dark:text-violet-400'
|
||||
genLoading ? 'z-[5000]' : 'text-violet-800 dark:text-violet-400',
|
||||
aiChatScriptModeClasses
|
||||
)}
|
||||
size="xs"
|
||||
color={genLoading ? 'red' : 'light'}
|
||||
spacingSize="md"
|
||||
startIcon={genLoading ? { icon: Ban } : { icon: Wand2 }}
|
||||
propagateEvent={!genLoading}
|
||||
on:click={genLoading
|
||||
? () => abortController?.abort()
|
||||
: () => {
|
||||
if (editor) {
|
||||
if (isInitialCode(editor.getCode())) {
|
||||
mode = 'gen'
|
||||
} else {
|
||||
mode = 'edit'
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
autoResize()
|
||||
}, 0)
|
||||
}}
|
||||
propagateEvent={!genLoading && !openAiChat}
|
||||
on:click={genLoading ? () => abortController?.abort() : handleAiButtonClick}
|
||||
bind:element={button}
|
||||
{iconOnly}
|
||||
>
|
||||
|
||||
@@ -58,6 +58,7 @@ class AIChatManager {
|
||||
|
||||
mode = $state<AIMode>(AIMode.NAVIGATOR)
|
||||
size = $state<number>(localStorage.getItem('ai-chat-open') === 'true' ? this.DEFAULT_SIZE : 0)
|
||||
readonly isOpen = $derived(this.size > 0)
|
||||
savedSize = $state<number>(0)
|
||||
instructions = $state<string>('')
|
||||
pendingPrompt = $state<string>('')
|
||||
|
||||
@@ -16,22 +16,37 @@
|
||||
import TriggerableByAI from '../TriggerableByAI.svelte'
|
||||
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
export let loading: boolean
|
||||
export let disableStaticInputs = false
|
||||
export let disableTutorials = false
|
||||
export let disableAi = false
|
||||
export let disableSettings = false
|
||||
export let disabledFlowInputs = false
|
||||
export let smallErrorHandler = false
|
||||
export let newFlow: boolean = false
|
||||
export let savedFlow:
|
||||
| (Flow & {
|
||||
draft?: Flow | undefined
|
||||
})
|
||||
| undefined = undefined
|
||||
export let onDeployTrigger: (trigger: Trigger) => void = () => {}
|
||||
interface Props {
|
||||
loading: boolean
|
||||
disableStaticInputs?: boolean
|
||||
disableTutorials?: boolean
|
||||
disableAi?: boolean
|
||||
disableSettings?: boolean
|
||||
disabledFlowInputs?: boolean
|
||||
smallErrorHandler?: boolean
|
||||
newFlow?: boolean
|
||||
savedFlow?:
|
||||
| (Flow & {
|
||||
draft?: Flow | undefined
|
||||
})
|
||||
| undefined
|
||||
onDeployTrigger?: (trigger: Trigger) => void
|
||||
}
|
||||
|
||||
let flowModuleSchemaMap: FlowModuleSchemaMap | undefined
|
||||
let {
|
||||
loading,
|
||||
disableStaticInputs = false,
|
||||
disableTutorials = false,
|
||||
disableAi = false,
|
||||
disableSettings = false,
|
||||
disabledFlowInputs = false,
|
||||
smallErrorHandler = false,
|
||||
newFlow = false,
|
||||
savedFlow = undefined,
|
||||
onDeployTrigger = () => {}
|
||||
}: Props = $props()
|
||||
|
||||
let flowModuleSchemaMap: FlowModuleSchemaMap | undefined = $state()
|
||||
|
||||
setContext<PropPickerContext>('PropPickerContext', {
|
||||
flowPropPickerConfig: writable<FlowPropPickerConfig | undefined>(undefined),
|
||||
|
||||
@@ -374,6 +374,7 @@
|
||||
on:hideDiffMode={hideDiffMode}
|
||||
{lastDeployedCode}
|
||||
{diffMode}
|
||||
openAiChat
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
import { WorkerService } from '$lib/gen'
|
||||
import WorkerTagSelect from '$lib/components/WorkerTagSelect.svelte'
|
||||
|
||||
let { tag = $bindable(), nullTag = $bindable() } = $props<{
|
||||
let { tag = $bindable(), nullTag = $bindable() }: {
|
||||
tag: string | undefined
|
||||
nullTag?: string | undefined
|
||||
}>()
|
||||
} = $props()
|
||||
|
||||
const { flowStore, selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
|
||||
@@ -1,828 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, tick } from 'svelte'
|
||||
import { flip } from 'svelte/animate'
|
||||
import VirtualList from 'svelte-tiny-virtual-list'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { DispatchEvents, MultiSelectEvents, Option as T } from './types'
|
||||
import { get_label, get_style, scroll_into_view_if_needed_polyfill } from './utils'
|
||||
import { ChevronsUpDown, X } from 'lucide-svelte'
|
||||
import type { FullAutoFill } from 'svelte/elements'
|
||||
|
||||
type Option = $$Generic<T>
|
||||
|
||||
export let activeIndex: number | null = null
|
||||
export let activeOption: Option | null = null
|
||||
export let createOptionMsg: string | null = `Create this option...`
|
||||
export let allowUserOptions: boolean | 'append' = false
|
||||
export let autocomplete: FullAutoFill = `off`
|
||||
export let autoScroll: boolean = true
|
||||
export let breakpoint: number = 800 // any screen with more horizontal pixels is considered desktop, below is mobile
|
||||
export let defaultDisabledTitle: string = `This option is disabled`
|
||||
export let disabled: boolean = false
|
||||
export let disabledInputTitle: string = `This input is disabled`
|
||||
// prettier-ignore
|
||||
export let duplicateOptionMsg: string = `This option is already selected`
|
||||
export let duplicates: boolean = false // whether to allow duplicate options
|
||||
// takes two options and returns true if they are equal
|
||||
// case-insensitive equality comparison after string coercion and looks only at the `label` key of object options by default
|
||||
export let key: (opt: T) => unknown = (opt) => `${get_label(opt)}`.toLowerCase()
|
||||
export let filterFunc = (opt: Option, searchText: string): boolean => {
|
||||
if (!searchText) return true
|
||||
return `${get_label(opt)}`.toLowerCase().includes(searchText.toLowerCase())
|
||||
}
|
||||
export let closeDropdownOnSelect: boolean | 'desktop' = `desktop`
|
||||
export let form_input: HTMLInputElement | null = null
|
||||
export let highlightMatches: boolean = true
|
||||
export let id: string | null = null
|
||||
export let input: HTMLInputElement | null = null
|
||||
export let inputClass: string = ``
|
||||
export let inputStyle: string | null = null
|
||||
export let inputmode:
|
||||
| 'search'
|
||||
| 'text'
|
||||
| 'none'
|
||||
| 'tel'
|
||||
| 'url'
|
||||
| 'email'
|
||||
| 'numeric'
|
||||
| 'decimal'
|
||||
| null
|
||||
| undefined = null
|
||||
export let invalid: boolean = false
|
||||
export let liSelectedClass: string = ``
|
||||
export let liSelectedStyle: string | null = null
|
||||
export let loading: boolean = false
|
||||
export let matchingOptions: Option[] = []
|
||||
export let maxSelect: number | null = null // null means there is no upper limit for selected.length
|
||||
export let name: string | null = null
|
||||
export let noMatchingOptionsMsg: string = `No matching options`
|
||||
export let open: boolean = false
|
||||
export let options: Option[]
|
||||
export let outerDiv: HTMLDivElement | null = null
|
||||
export let outerDivClass: string = ``
|
||||
export let parseLabelsAsHtml: boolean = false // should not be combined with allowUserOptions!
|
||||
export let pattern: string | null = null
|
||||
export let placeholder: string | null = null
|
||||
export let removeAllTitle: string = `Remove all`
|
||||
export let removeBtnTitle: string = `Remove`
|
||||
export let minSelect: number | null = null // null means there is no lower limit for selected.length
|
||||
export let required: boolean = false
|
||||
export let resetFilterOnAdd: boolean = true
|
||||
export let searchText: string = ``
|
||||
export let selected: Option[] =
|
||||
options
|
||||
?.filter((opt: any) => opt instanceof Object && opt?.preselected)
|
||||
.slice(0, maxSelect ?? undefined) ?? [] // don't allow more than maxSelect preselected options
|
||||
export let sortSelected: boolean | ((op1: Option, op2: Option) => number) = false
|
||||
export let selectedOptionsDraggable: boolean = !sortSelected
|
||||
export let style: string | null = null
|
||||
export let ulSelectedClass: string = ``
|
||||
export let ulSelectedStyle: string | null = null
|
||||
export let value: Option | Option[] | null = null
|
||||
export let disableRemoveAll: boolean = false
|
||||
|
||||
const selected_to_value = (selected: Option[]) => {
|
||||
value = maxSelect === 1 ? (selected[0] ?? null) : selected
|
||||
}
|
||||
const value_to_selected = (value: Option | Option[] | null) => {
|
||||
if (maxSelect === 1) selected = value ? [value as Option] : []
|
||||
else selected = (value as Option[]) ?? []
|
||||
}
|
||||
|
||||
// if maxSelect=1, value is the single item in selected (or null if selected is empty)
|
||||
// this solves both https://github.com/janosh/svelte-multiselect/issues/86 and
|
||||
// https://github.com/janosh/svelte-multiselect/issues/136
|
||||
$: selected_to_value(selected)
|
||||
$: value_to_selected(value)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
type $$Events = MultiSelectEvents // for type-safe event listening on this component
|
||||
|
||||
const dispatch = createEventDispatcher<DispatchEvents<Option>>()
|
||||
let option_msg_is_active: boolean = false // controls active state of <li>{createOptionMsg}</li>
|
||||
let window_width: number
|
||||
|
||||
// options matching the current search text
|
||||
$: matchingOptions = options?.filter(
|
||||
(opt) =>
|
||||
filterFunc(opt, searchText) &&
|
||||
// remove already selected options from dropdown list unless duplicate selections are allowed
|
||||
(!selected.map(key).includes(key(opt)) || duplicates)
|
||||
)
|
||||
|
||||
// raise if matchingOptions[activeIndex] does not yield a value
|
||||
if (activeIndex !== null && !matchingOptions[activeIndex]) {
|
||||
throw `Run time error, activeIndex=${activeIndex} is out of bounds, matchingOptions.length=${matchingOptions.length}`
|
||||
}
|
||||
// update activeOption when activeIndex changes
|
||||
$: activeOption = matchingOptions?.[activeIndex ?? -1] ?? null
|
||||
|
||||
// add an option to selected list
|
||||
function add(option: T, event: Event) {
|
||||
if (maxSelect && maxSelect > 1 && selected.length >= maxSelect) {
|
||||
return
|
||||
}
|
||||
if (!isNaN(Number(option)) && typeof selected.map(get_label)[0] === `number`) {
|
||||
option = Number(option) as Option // convert to number if possible
|
||||
}
|
||||
const is_duplicate = selected.map(key).includes(key(option))
|
||||
|
||||
if (
|
||||
(maxSelect === null || maxSelect === 1 || selected.length < maxSelect) &&
|
||||
(duplicates || !is_duplicate)
|
||||
) {
|
||||
if (
|
||||
// @ts-ignore
|
||||
!options.includes(option) && // first check if we find option in the options list
|
||||
// this has the side-effect of not allowing to user to add the same
|
||||
// custom option twice in append mode
|
||||
[true, `append`].includes(allowUserOptions) &&
|
||||
searchText.length > 0
|
||||
) {
|
||||
// user entered text but no options match, so if allowUserOptions = true | 'append', we create
|
||||
// a new option from the user-entered text
|
||||
if (typeof options[0] === `object`) {
|
||||
// if 1st option is an object, we create new option as object to keep type homogeneity
|
||||
option = { label: searchText } as Option
|
||||
} else {
|
||||
if ([`number`, `undefined`].includes(typeof options[0]) && !isNaN(Number(searchText))) {
|
||||
// create new option as number if it parses to a number and 1st option is also number or missing
|
||||
option = Number(searchText) as Option
|
||||
} else {
|
||||
option = searchText as Option // else create custom option as string
|
||||
}
|
||||
// @ts-ignore
|
||||
dispatch(`create`, { option })
|
||||
}
|
||||
// @ts-ignore
|
||||
if (allowUserOptions === `append`) options = [...options, option]
|
||||
}
|
||||
|
||||
if (resetFilterOnAdd) searchText = `` // reset search string on selection
|
||||
if ([``, undefined, null].includes(option as string | null)) {
|
||||
console.error(`MultiSelect: encountered falsy option ${option}`)
|
||||
return
|
||||
}
|
||||
if (maxSelect === 1) {
|
||||
// for maxSelect = 1 we always replace current option with new one
|
||||
// @ts-ignore
|
||||
|
||||
selected = [option]
|
||||
} else {
|
||||
// @ts-ignore
|
||||
|
||||
selected = [...selected, option]
|
||||
if (sortSelected === true) {
|
||||
selected = selected.sort((op1: Option, op2: Option) => {
|
||||
const [label1, label2] = [get_label(op1), get_label(op2)]
|
||||
// coerce to string if labels are numbers
|
||||
return `${label1}`.localeCompare(`${label2}`)
|
||||
})
|
||||
} else if (typeof sortSelected === `function`) {
|
||||
selected = selected.sort(sortSelected)
|
||||
}
|
||||
}
|
||||
|
||||
const reached_max_select = selected.length === maxSelect
|
||||
|
||||
const dropdown_should_close =
|
||||
closeDropdownOnSelect === true ||
|
||||
(closeDropdownOnSelect === `desktop` && window_width < breakpoint)
|
||||
|
||||
if (reached_max_select || dropdown_should_close) {
|
||||
close_dropdown(event)
|
||||
} else if (!dropdown_should_close) {
|
||||
input?.focus()
|
||||
}
|
||||
// @ts-ignore
|
||||
|
||||
dispatch(`add`, { option })
|
||||
// @ts-ignore
|
||||
|
||||
dispatch(`change`, { option, type: `add` })
|
||||
|
||||
invalid = false // reset error status whenever new items are selected
|
||||
form_input?.setCustomValidity(``)
|
||||
}
|
||||
}
|
||||
// remove an option from selected list
|
||||
function remove(to_remove: T) {
|
||||
if (selected.length === 0) return
|
||||
|
||||
const idx = selected.findIndex((opt) => key(opt) === key(to_remove))
|
||||
|
||||
let [option] = selected.splice(idx, 1) // remove option from selected list
|
||||
|
||||
if (option === undefined && allowUserOptions) {
|
||||
// if option with label could not be found but allowUserOptions is truthy,
|
||||
// assume it was created by user and create corresponding option object
|
||||
// on the fly for use as event payload
|
||||
const other_ops_type = typeof options[0]
|
||||
option = (other_ops_type ? { label: to_remove } : to_remove) as Option
|
||||
}
|
||||
if (option === undefined) {
|
||||
return console.error(
|
||||
`Multiselect can't remove selected option ${JSON.stringify(
|
||||
to_remove
|
||||
)}, not found in selected list`
|
||||
)
|
||||
}
|
||||
|
||||
selected = [...selected] // trigger Svelte rerender
|
||||
|
||||
invalid = false // reset error status whenever items are removed
|
||||
form_input?.setCustomValidity(``)
|
||||
dispatch(`remove`, { option })
|
||||
dispatch(`change`, { option, type: `remove` })
|
||||
}
|
||||
|
||||
function open_dropdown(event: Event) {
|
||||
if (disabled) return
|
||||
open = true
|
||||
if (!(event instanceof FocusEvent)) {
|
||||
// avoid double-focussing input when event that opened dropdown was already input FocusEvent
|
||||
input?.focus()
|
||||
}
|
||||
dispatch(`open`, { event })
|
||||
}
|
||||
|
||||
function close_dropdown(event: Event) {
|
||||
open = false
|
||||
input?.blur()
|
||||
activeIndex = null
|
||||
dispatch(`close`, { event })
|
||||
}
|
||||
|
||||
// handle all keyboard events this component receives
|
||||
async function handle_keydown(event: KeyboardEvent) {
|
||||
// on escape or tab out of input: close options dropdown and reset search text
|
||||
if (event.key === `Escape` || event.key === `Tab`) {
|
||||
close_dropdown(event)
|
||||
searchText = ``
|
||||
}
|
||||
// on enter key: toggle active option and reset search text
|
||||
else if (event.key === `Enter`) {
|
||||
event.preventDefault() // prevent enter key from triggering form submission
|
||||
|
||||
if (activeOption) {
|
||||
selected.includes(activeOption) ? remove(activeOption) : add(activeOption, event)
|
||||
searchText = ``
|
||||
} else if (allowUserOptions && searchText.length > 0) {
|
||||
// user entered text but no options match, so if allowUserOptions is truthy, we create new option
|
||||
add(searchText, event)
|
||||
}
|
||||
// no active option and no search text means the options dropdown is closed
|
||||
// in which case enter means open it
|
||||
else open_dropdown(event)
|
||||
}
|
||||
// on up/down arrow keys: update active option
|
||||
else if ([`ArrowDown`, `ArrowUp`].includes(event.key)) {
|
||||
// if no option is active yet, but there are matching options, make first one active
|
||||
if (activeIndex === null && matchingOptions.length > 0) {
|
||||
activeIndex = 0
|
||||
return
|
||||
} else if (allowUserOptions && !matchingOptions.length && searchText.length > 0) {
|
||||
// if allowUserOptions is truthy and user entered text but no options match, we make
|
||||
// <li>{addUserMsg}</li> active on keydown (or toggle it if already active)
|
||||
option_msg_is_active = !option_msg_is_active
|
||||
return
|
||||
} else if (activeIndex === null) {
|
||||
// if no option is active and no options are matching, do nothing
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
// if none of the above special cases apply, we make next/prev option
|
||||
// active with wrap around at both ends
|
||||
const increment = event.key === `ArrowUp` ? -1 : 1
|
||||
|
||||
activeIndex = (activeIndex + increment) % matchingOptions.length
|
||||
// in JS % behaves like remainder operator, not real modulo, so negative numbers stay negative
|
||||
// need to do manual wrap around at 0
|
||||
if (activeIndex < 0) activeIndex = matchingOptions.length - 1
|
||||
|
||||
if (autoScroll) {
|
||||
await tick()
|
||||
const li = document.querySelector(`ul.options > li.active`)
|
||||
if (li) scroll_into_view_if_needed_polyfill(li)
|
||||
}
|
||||
}
|
||||
// on backspace key: remove last selected option
|
||||
else if (event.key === `Backspace` && selected.length > 0 && !searchText) {
|
||||
remove(selected.at(-1) as Option)
|
||||
}
|
||||
// make first matching option active on any keypress (if none of the above special cases match)
|
||||
else if (matchingOptions.length > 0) {
|
||||
activeIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
function remove_all() {
|
||||
dispatch(`removeAll`, { options: selected })
|
||||
dispatch(`change`, { options: selected, type: `removeAll` })
|
||||
selected = []
|
||||
searchText = ``
|
||||
}
|
||||
|
||||
$: is_selected = (label: string | number) => selected.map(get_label).includes(label)
|
||||
|
||||
const if_enter_or_space = (handler: () => void) => (event: KeyboardEvent) => {
|
||||
if ([`Enter`, `Space`].includes(event.code)) {
|
||||
event.preventDefault()
|
||||
handler()
|
||||
}
|
||||
}
|
||||
|
||||
function on_click_outside(event: MouseEvent | TouchEvent) {
|
||||
if (outerDiv && !outerDiv.contains(event.target as Node)) {
|
||||
close_dropdown(event)
|
||||
}
|
||||
}
|
||||
|
||||
let drag_idx: number | null = null
|
||||
// event handlers enable dragging to reorder selected options
|
||||
const drop = (target_idx: number) => (event: DragEvent) => {
|
||||
if (!event.dataTransfer) return
|
||||
event.dataTransfer.dropEffect = `move`
|
||||
const start_idx = parseInt(event.dataTransfer.getData(`text/plain`))
|
||||
const new_selected = [...selected]
|
||||
|
||||
if (start_idx < target_idx) {
|
||||
new_selected.splice(target_idx + 1, 0, new_selected[start_idx])
|
||||
new_selected.splice(start_idx, 1)
|
||||
} else {
|
||||
new_selected.splice(target_idx, 0, new_selected[start_idx])
|
||||
new_selected.splice(start_idx + 1, 1)
|
||||
}
|
||||
selected = new_selected
|
||||
drag_idx = null
|
||||
}
|
||||
|
||||
const dragstart = (idx: number) => (event: DragEvent) => {
|
||||
if (!event.dataTransfer) return
|
||||
// only allow moving, not copying (also affects the cursor during drag)
|
||||
event.dataTransfer.effectAllowed = `move`
|
||||
event.dataTransfer.dropEffect = `move`
|
||||
event.dataTransfer.setData(`text/plain`, `${idx}`)
|
||||
}
|
||||
|
||||
let ul_options: HTMLUListElement
|
||||
// highlight text matching user-entered search text in available options
|
||||
function highlight_matching_options(
|
||||
event: Event & {
|
||||
currentTarget: EventTarget & HTMLInputElement
|
||||
}
|
||||
) {
|
||||
// @ts-ignore
|
||||
if (!highlightMatches || !ul_options || typeof CSS == `undefined` || !CSS.highlights) return // abort if CSS highlight API not supported
|
||||
|
||||
// clear previous ranges from HighlightRegistry
|
||||
// @ts-ignore
|
||||
CSS.highlights.clear()
|
||||
|
||||
// get input's search query
|
||||
const query = (event?.target as HTMLInputElement)?.value.trim().toLowerCase()
|
||||
if (!query) return
|
||||
|
||||
const tree_walker = document.createTreeWalker(ul_options, NodeFilter.SHOW_TEXT, {
|
||||
acceptNode: (node) => {
|
||||
// don't highlight text in the "no matching options" message
|
||||
if (node?.textContent === noMatchingOptionsMsg) return NodeFilter.FILTER_REJECT
|
||||
return NodeFilter.FILTER_ACCEPT
|
||||
}
|
||||
})
|
||||
const text_nodes: Node[] = []
|
||||
let current_node = tree_walker.nextNode()
|
||||
while (current_node) {
|
||||
text_nodes.push(current_node)
|
||||
current_node = tree_walker.nextNode()
|
||||
}
|
||||
|
||||
// iterate over all text nodes and find matches
|
||||
const ranges = text_nodes.map((el) => {
|
||||
const text = el.textContent?.toLowerCase()
|
||||
const indices: number[] = []
|
||||
let start_pos = 0
|
||||
while (text && start_pos < text.length) {
|
||||
const index = text.indexOf(query, start_pos)
|
||||
if (index === -1) break
|
||||
indices.push(index)
|
||||
start_pos = index + query.length
|
||||
}
|
||||
|
||||
// create range object for each str found in the text node
|
||||
return indices.map((index) => {
|
||||
const range = new Range()
|
||||
range.setStart(el, index)
|
||||
range.setEnd(el, index + query.length)
|
||||
return range
|
||||
})
|
||||
})
|
||||
|
||||
// create Highlight object from ranges and add to registry
|
||||
// @ts-ignore
|
||||
CSS.highlights.set(`sms-search-matches`, new Highlight(...ranges.flat()))
|
||||
}
|
||||
|
||||
// reset form validation when required prop changes
|
||||
// https://github.com/janosh/svelte-multiselect/issues/285
|
||||
$: required, form_input?.setCustomValidity(``)
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:click={on_click_outside}
|
||||
on:touchstart={on_click_outside}
|
||||
bind:innerWidth={window_width}
|
||||
/>
|
||||
|
||||
<div
|
||||
bind:this={outerDiv}
|
||||
class:disabled
|
||||
class:single={maxSelect === 1}
|
||||
class:open
|
||||
class:invalid
|
||||
class="multiselect {outerDivClass}"
|
||||
on:mouseup|stopPropagation={open_dropdown}
|
||||
title={disabled ? disabledInputTitle : null}
|
||||
data-id={id}
|
||||
role="searchbox"
|
||||
tabindex="-1"
|
||||
{style}
|
||||
>
|
||||
<!-- form control input invisible to the user, only purpose is to abort form submission if this component fails data validation -->
|
||||
<!-- bind:value={selected} prevents form submission if required prop is true and no options are selected -->
|
||||
<input
|
||||
{name}
|
||||
{required}
|
||||
value={selected.length >= Number(required) ? JSON.stringify(selected) : null}
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
aria-label="ignore this, used only to prevent form submission if select is required but empty"
|
||||
class="form-control"
|
||||
bind:this={form_input}
|
||||
on:invalid={() => {
|
||||
invalid = true
|
||||
let msg
|
||||
if (maxSelect && maxSelect > 1 && Number(required) > 1) {
|
||||
msg = `Please select between ${required} and ${maxSelect} options`
|
||||
} else if (Number(required) > 1) {
|
||||
msg = `Please select at least ${required} options`
|
||||
} else {
|
||||
msg = `Please select an option`
|
||||
}
|
||||
form_input?.setCustomValidity(msg)
|
||||
}}
|
||||
/>
|
||||
<slot name="expand-icon" {open}>
|
||||
<ChevronsUpDown size={16} />
|
||||
</slot>
|
||||
<ul class="selected {ulSelectedClass}" aria-label="selected options" style={ulSelectedStyle}>
|
||||
{#each selected as option, idx (duplicates ? [key(option), idx] : key(option))}
|
||||
<li
|
||||
class={liSelectedClass}
|
||||
role="option"
|
||||
aria-selected="true"
|
||||
animate:flip={{ duration: 100 }}
|
||||
draggable={selectedOptionsDraggable && !disabled && selected.length > 1}
|
||||
on:dragstart={dragstart(idx)}
|
||||
on:drop|preventDefault={drop(idx)}
|
||||
on:dragenter={() => (drag_idx = idx)}
|
||||
on:dragover|preventDefault
|
||||
class:active={drag_idx === idx}
|
||||
style="{get_style(option, `selected`)} {liSelectedStyle}"
|
||||
>
|
||||
<!-- on:dragover|preventDefault needed for the drop to succeed https://stackoverflow.com/a/31085796 -->
|
||||
<slot name="selected" {option} {idx}>
|
||||
<slot {option} {idx}>
|
||||
{#if parseLabelsAsHtml}
|
||||
{@html get_label(option)}
|
||||
{:else}
|
||||
{get_label(option)}
|
||||
{/if}
|
||||
</slot>
|
||||
</slot>
|
||||
{#if !disabled && (minSelect === null || selected.length > minSelect)}
|
||||
<button
|
||||
on:mouseup|stopPropagation={() => remove(option)}
|
||||
on:keydown={if_enter_or_space(() => remove(option))}
|
||||
type="button"
|
||||
title="{removeBtnTitle} {get_label(option)}"
|
||||
class="remove"
|
||||
>
|
||||
<slot name="remove-icon">
|
||||
<X size={20} class="text-primary dark:text-primary-inverse p-0.5" />
|
||||
</slot>
|
||||
</button>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
<input
|
||||
class={inputClass}
|
||||
style={inputStyle}
|
||||
bind:this={input}
|
||||
bind:value={searchText}
|
||||
on:mouseup|self|stopPropagation={(e) => open_dropdown(e)}
|
||||
on:keydown|stopPropagation={(e) => handle_keydown(e)}
|
||||
on:focus={(e) => open_dropdown(e)}
|
||||
on:input={(e) => highlight_matching_options(e)}
|
||||
{id}
|
||||
{disabled}
|
||||
{autocomplete}
|
||||
{inputmode}
|
||||
{pattern}
|
||||
placeholder={selected.length === 0 ? placeholder : undefined}
|
||||
aria-invalid={invalid ? 'true' : undefined}
|
||||
on:blur
|
||||
on:change
|
||||
on:click
|
||||
on:keydown
|
||||
on:keyup
|
||||
on:mousedown
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
on:touchcancel
|
||||
on:touchend
|
||||
on:touchmove
|
||||
on:touchstart
|
||||
/>
|
||||
|
||||
<!-- the above on:* lines forward potentially useful DOM events -->
|
||||
<slot name="after-input" {selected} {disabled} {invalid} {id} {placeholder} {open} {required} />
|
||||
</ul>
|
||||
{#if loading}
|
||||
<slot name="spinner">spinner</slot>
|
||||
{/if}
|
||||
{#if disabled}
|
||||
<slot name="disabled-icon">disable</slot>
|
||||
{:else if selected.length > 0}
|
||||
{#if maxSelect !== 1 && selected.length > 1 && !disableRemoveAll}
|
||||
<button
|
||||
type="button"
|
||||
class="remove remove-all"
|
||||
title={removeAllTitle}
|
||||
on:mouseup|stopPropagation={remove_all}
|
||||
on:keydown={if_enter_or_space(remove_all)}
|
||||
>
|
||||
<slot name="remove-icon">
|
||||
<X
|
||||
size={24}
|
||||
class="text-secondary p-1 rounded-full bg-surface hover:bg-surface-secondary"
|
||||
/>
|
||||
</slot>
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
<!-- only render options dropdown if options or searchText is not empty (needed to avoid briefly flashing empty dropdown) -->
|
||||
{#if allowUserOptions || (searchText && noMatchingOptionsMsg) || options?.length > 0}
|
||||
<div class="options bg-surface shadow-md rounded-component">
|
||||
<VirtualList
|
||||
width="100%"
|
||||
height={Math.min(32 * matchingOptions.length, 320)}
|
||||
itemCount={matchingOptions.length}
|
||||
itemSize={32}
|
||||
>
|
||||
<div slot="item" let:index let:style {style}>
|
||||
{@const option = matchingOptions[index]}
|
||||
{@const {
|
||||
label,
|
||||
disabled = null,
|
||||
title = null,
|
||||
selectedTitle = null,
|
||||
disabledTitle = defaultDisabledTitle
|
||||
} = option instanceof Object ? option : { label: option }}
|
||||
|
||||
<!-- svelte-ignore a11y-interactive-supports-focus -->
|
||||
<div
|
||||
on:mousedown|stopPropagation
|
||||
on:mouseup|stopPropagation={(event) => {
|
||||
if (!disabled) add(option, event)
|
||||
}}
|
||||
title={disabled ? disabledTitle : (is_selected(label) && selectedTitle) || title}
|
||||
class={twMerge('hover:bg-blue-100 hover:dark:bg-gray-900 cursor-pointer !px-2 py-1')}
|
||||
on:mouseover={() => {
|
||||
if (!disabled) activeIndex = index
|
||||
}}
|
||||
on:focus={() => {
|
||||
if (!disabled) activeIndex = index
|
||||
}}
|
||||
on:mouseout={() => (activeIndex = null)}
|
||||
on:blur={() => (activeIndex = null)}
|
||||
role="option"
|
||||
aria-selected="false"
|
||||
style={get_style(option, `option`)}
|
||||
>
|
||||
<slot name="option" {option} {index}>
|
||||
<slot {option} {index}>
|
||||
{#if parseLabelsAsHtml}
|
||||
{@html get_label(option)}
|
||||
{:else}
|
||||
{get_label(option)}
|
||||
{/if}
|
||||
</slot>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="footer" class="h-0"></div>
|
||||
</VirtualList>
|
||||
{#if searchText}
|
||||
{@const text_input_is_duplicate = selected.map(get_label).includes(searchText)}
|
||||
{@const is_dupe = !duplicates && text_input_is_duplicate && `dupe`}
|
||||
{@const can_create = Boolean(allowUserOptions && createOptionMsg) && `create`}
|
||||
{@const no_match =
|
||||
Boolean(matchingOptions?.length == 0 && noMatchingOptionsMsg) && `no-match`}
|
||||
{@const msgType = is_dupe || can_create || no_match}
|
||||
{#if msgType}
|
||||
{@const msg = {
|
||||
dupe: duplicateOptionMsg,
|
||||
create: createOptionMsg,
|
||||
'no-match': noMatchingOptionsMsg
|
||||
}[msgType]}
|
||||
<!-- svelte-ignore a11y-interactive-supports-focus -->
|
||||
<div
|
||||
on:mousedown|stopPropagation
|
||||
on:mouseup|stopPropagation={(event) => {
|
||||
if (allowUserOptions) add(searchText, event)
|
||||
}}
|
||||
title={createOptionMsg}
|
||||
class:active={option_msg_is_active}
|
||||
on:mouseover={() => (option_msg_is_active = true)}
|
||||
on:focus={() => (option_msg_is_active = true)}
|
||||
on:mouseout={() => (option_msg_is_active = false)}
|
||||
on:blur={() => (option_msg_is_active = false)}
|
||||
role="option"
|
||||
aria-selected="false"
|
||||
class="user-msg p-1"
|
||||
style:cursor={{
|
||||
dupe: `not-allowed`,
|
||||
create: `pointer`,
|
||||
'no-match': `default`
|
||||
}[msgType]}
|
||||
>
|
||||
<slot name="user-msg" {searchText} {msgType} {msg}>
|
||||
{msg}
|
||||
</slot>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:where(div.multiselect) {
|
||||
position: relative;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
cursor: text;
|
||||
box-sizing: border-box;
|
||||
border: var(--sms-border, 1pt solid lightgray);
|
||||
border-radius: var(--sms-border-radius, 3pt);
|
||||
background: var(--sms-bg);
|
||||
width: var(--sms-width);
|
||||
max-width: var(--sms-max-width);
|
||||
padding: var(--sms-padding, 0 3pt);
|
||||
color: var(--sms-text-color);
|
||||
font-size: var(--sms-font-size, inherit);
|
||||
min-height: var(--sms-min-height, 22pt);
|
||||
margin: var(--sms-margin);
|
||||
}
|
||||
:where(div.multiselect.open) {
|
||||
/* increase z-index when open to ensure the dropdown of one <MultiSelect />
|
||||
displays above that of another slightly below it on the page */
|
||||
z-index: var(--sms-open-z-index, 4);
|
||||
}
|
||||
:where(div.multiselect:focus-within) {
|
||||
border: var(--sms-focus-border, 1pt solid var(--sms-active-color, cornflowerblue));
|
||||
}
|
||||
:where(div.multiselect.disabled) {
|
||||
background: var(--sms-disabled-bg, lightgray);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
:where(div.multiselect > ul.selected) {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
:where(div.multiselect > ul.selected > li) {
|
||||
align-items: center;
|
||||
border-radius: 3pt;
|
||||
display: flex;
|
||||
margin: 2pt;
|
||||
line-height: normal;
|
||||
transition: 0.3s;
|
||||
white-space: nowrap;
|
||||
background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15));
|
||||
padding: var(--sms-selected-li-padding, 1pt 5pt);
|
||||
color: var(--sms-selected-text-color, var(--sms-text-color));
|
||||
}
|
||||
:where(div.multiselect > ul.selected > li[draggable='true']) {
|
||||
cursor: grab;
|
||||
}
|
||||
:where(div.multiselect > ul.selected > li.active) {
|
||||
background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
|
||||
}
|
||||
:where(div.multiselect button) {
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
transition: 0.2s;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
margin: 0 0 0 3pt; /* CSS reset */
|
||||
}
|
||||
:where(div.multiselect button.remove-all) {
|
||||
margin: 0 3pt;
|
||||
}
|
||||
:where(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
|
||||
color: var(--sms-remove-btn-hover-color, lightskyblue);
|
||||
background: var(--sms-remove-btn-hover-bg, rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
|
||||
:where(div.multiselect input) {
|
||||
margin: auto 0; /* CSS reset */
|
||||
padding: 0; /* CSS reset */
|
||||
}
|
||||
:where(div.multiselect > ul.selected > input) {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
flex: 1; /* this + next line fix issue #12 https://git.io/JiDe3 */
|
||||
min-width: 2em;
|
||||
/* ensure input uses text color and not --sms-selected-text-color */
|
||||
color: var(--sms-text-color);
|
||||
font-size: inherit;
|
||||
cursor: inherit; /* needed for disabled state */
|
||||
border-radius: 0; /* reset ul.selected > li */
|
||||
}
|
||||
/* don't wrap ::placeholder rules in :where() as it seems to be overpowered by browser defaults i.t.o. specificity */
|
||||
div.multiselect > ul.selected > input::placeholder {
|
||||
padding-left: 5pt;
|
||||
color: var(--sms-placeholder-color);
|
||||
opacity: var(--sms-placeholder-opacity);
|
||||
}
|
||||
:where(div.multiselect > input.form-control) {
|
||||
width: 2em;
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:where(div.multiselect > ul.options) {
|
||||
list-style: none;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
overflow: auto;
|
||||
transition: all 0.2s;
|
||||
box-sizing: border-box;
|
||||
background: var(--sms-options-bg, white);
|
||||
max-height: var(--sms-options-max-height, 50vh);
|
||||
overscroll-behavior: var(--sms-options-overscroll, none);
|
||||
border: var(--sms-options-border);
|
||||
border-width: var(--sms-options-border-width);
|
||||
border-radius: var(--sms-options-border-radius, 1ex);
|
||||
padding: var(--sms-options-padding);
|
||||
margin: var(--sms-options-margin, inherit);
|
||||
}
|
||||
:where(div.multiselect > ul.options.hidden) {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transform: translateY(50px);
|
||||
}
|
||||
:where(div.multiselect > ul.options > li) {
|
||||
padding: 3pt 2ex;
|
||||
cursor: pointer;
|
||||
scroll-margin: var(--sms-options-scroll-margin, 100px);
|
||||
}
|
||||
:where(div.multiselect > ul.options .user-msg) {
|
||||
/* block needed so vertical padding applies to span */
|
||||
display: block;
|
||||
padding: 3pt 2ex;
|
||||
}
|
||||
:where(div.multiselect > ul.options > li.selected) {
|
||||
background: var(--sms-li-selected-bg);
|
||||
color: var(--sms-li-selected-color);
|
||||
}
|
||||
:where(div.multiselect > ul.options > li.active) {
|
||||
background: var(--sms-li-active-bg, var(--sms-active-color, rgba(0, 0, 0, 0.15)));
|
||||
}
|
||||
:where(div.multiselect > ul.options > li.disabled) {
|
||||
cursor: not-allowed;
|
||||
background: var(--sms-li-disabled-bg, #f5f5f6);
|
||||
color: var(--sms-li-disabled-text, #b8b8b8);
|
||||
}
|
||||
|
||||
::highlight(sms-search-matches) {
|
||||
color: mediumaquamarine;
|
||||
}
|
||||
</style>
|
||||
@@ -1,130 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Portal from '$lib/components/Portal.svelte'
|
||||
import { createFloatingActions } from 'svelte-floating-ui'
|
||||
import { tick } from 'svelte'
|
||||
import { offset, flip, shift } from 'svelte-floating-ui/dom'
|
||||
import MultiSelect from '$lib/components/multiselect/MultiSelect.svelte'
|
||||
import DarkModeObserver from '../DarkModeObserver.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
let {
|
||||
items,
|
||||
value = $bindable(),
|
||||
placeholder = undefined,
|
||||
target = undefined,
|
||||
topPlacement = false,
|
||||
allowUserOptions = undefined
|
||||
} = $props<{
|
||||
items: any[]
|
||||
value?: string[]
|
||||
placeholder?: string
|
||||
target?: string | HTMLElement
|
||||
topPlacement?: boolean
|
||||
allowUserOptions?: boolean | 'append'
|
||||
}>()
|
||||
|
||||
$effect.pre(() => {
|
||||
if (value === undefined) value = []
|
||||
})
|
||||
|
||||
const [floatingRef, floatingContent] = createFloatingActions({
|
||||
strategy: 'absolute',
|
||||
placement: topPlacement ? 'top-start' : 'bottom-start',
|
||||
middleware: [offset(5), flip(), shift()]
|
||||
})
|
||||
|
||||
let outerDiv = $state<HTMLDivElement | undefined>(undefined)
|
||||
let portalRef = $state<HTMLDivElement | undefined>(undefined)
|
||||
let darkMode = $state(false)
|
||||
let w = $state(0)
|
||||
let open = $state(false)
|
||||
function moveOptionsToPortal() {
|
||||
// Find ul element with class 'options' within the outerDiv
|
||||
const ul = outerDiv?.querySelector('.options')
|
||||
if (ul) {
|
||||
// Move the ul element to the portal
|
||||
portalRef?.appendChild(ul)
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (portalRef && outerDiv && (allowUserOptions || items?.length > 0)) {
|
||||
tick().then(() => {
|
||||
moveOptionsToPortal()
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<DarkModeObserver bind:darkMode />
|
||||
|
||||
<div use:floatingRef bind:clientWidth={w}>
|
||||
{#if !value || Array.isArray(value)}
|
||||
<div class="border rounded-md border-gray-300 shadow-sm dark:border-gray-600 !w-full">
|
||||
<MultiSelect
|
||||
{allowUserOptions}
|
||||
outerDivClass={`!text-xs`}
|
||||
ulSelectedClass="overflow-auto"
|
||||
bind:outerDiv
|
||||
--sms-border={'none'}
|
||||
--sms-min-height={'30px'}
|
||||
--sms-focus-border={'none'}
|
||||
--sms-selected-bg={darkMode ? '#c7d2fe' : '#e0e7ff'}
|
||||
--sms-selected-text-color={darkMode ? '#312e81' : '#3730a3'}
|
||||
bind:selected={
|
||||
() => [...value],
|
||||
(newVal) => {
|
||||
if (!deepEqual(value, newVal)) {
|
||||
value = newVal
|
||||
}
|
||||
}
|
||||
}
|
||||
{placeholder}
|
||||
options={items}
|
||||
on:close={() => {
|
||||
open = false
|
||||
}}
|
||||
on:open={() => {
|
||||
open = true
|
||||
}}
|
||||
let:option
|
||||
disableRemoveAll
|
||||
>
|
||||
<div
|
||||
class="w-full text-sm"
|
||||
role="option"
|
||||
tabindex="0"
|
||||
onmouseup={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onpointerdown={(e) => {
|
||||
e.stopPropagation()
|
||||
let newe = new MouseEvent('mouseup')
|
||||
e.target?.['parentElement']?.dispatchEvent(newe)
|
||||
}}
|
||||
aria-selected={value?.includes(option)}
|
||||
>
|
||||
{option}
|
||||
</div>
|
||||
</MultiSelect>
|
||||
</div>
|
||||
<Portal {target} name="multi-select">
|
||||
<div use:floatingContent class="z5000" hidden={!open}>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
bind:this={portalRef}
|
||||
class="multiselect"
|
||||
style={`min-width: ${w}px;`}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
role="listbox"
|
||||
tabindex="0"
|
||||
></div>
|
||||
</div>
|
||||
</Portal>
|
||||
{:else}
|
||||
Value {value} is not an array
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,47 +0,0 @@
|
||||
export type Option = string | number | ObjectOption
|
||||
|
||||
// single CSS string or an object with keys 'option' and 'selected', each a string,
|
||||
// which only apply to the dropdown list and list of selected options, respectively
|
||||
export type OptionStyle = string | { option: string; selected: string }
|
||||
|
||||
export type ObjectOption = {
|
||||
label: string | number // user-displayed text
|
||||
value?: unknown // associated value, can be anything incl. objects (defaults to label if undefined)
|
||||
title?: string // on-hover tooltip
|
||||
disabled?: boolean // make this option unselectable
|
||||
preselected?: boolean // make this option selected on page load (before any user interaction)
|
||||
disabledTitle?: string // override the default disabledTitle = 'This option is disabled'
|
||||
selectedTitle?: string // tooltip to display when this option is selected and hovered
|
||||
style?: OptionStyle
|
||||
[key: string]: unknown // allow any other keys users might want
|
||||
}
|
||||
|
||||
export type DispatchEvents<T = Option> = {
|
||||
add: { option: T }
|
||||
create: { option: T }
|
||||
remove: { option: T }
|
||||
removeAll: { options: T[] }
|
||||
change: {
|
||||
option?: T
|
||||
options?: T[]
|
||||
type: 'add' | 'remove' | 'removeAll'
|
||||
}
|
||||
open: { event: Event }
|
||||
close: { event: Event }
|
||||
}
|
||||
|
||||
export type MultiSelectEvents = {
|
||||
[key in keyof DispatchEvents]: CustomEvent<DispatchEvents[key]>
|
||||
} & {
|
||||
blur: FocusEvent
|
||||
click: MouseEvent
|
||||
focus: FocusEvent
|
||||
keydown: KeyboardEvent
|
||||
keyup: KeyboardEvent
|
||||
mouseenter: MouseEvent
|
||||
mouseleave: MouseEvent
|
||||
touchcancel: TouchEvent
|
||||
touchend: TouchEvent
|
||||
touchmove: TouchEvent
|
||||
touchstart: TouchEvent
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import type { Option, OptionStyle } from './types'
|
||||
|
||||
// get the label key from an option object or the option itself if it's a string or number
|
||||
export const get_label = (opt: Option) => {
|
||||
if (opt instanceof Object) {
|
||||
if (opt.label === undefined) {
|
||||
console.error(`MultiSelect option ${JSON.stringify(opt)} is an object but has no label key`)
|
||||
}
|
||||
return opt.label
|
||||
}
|
||||
return `${opt}`
|
||||
}
|
||||
|
||||
// this function is used extract CSS strings from a {selected, option} style object to be used in the style attribute of the option
|
||||
// if the style is a string, it will be returned as is
|
||||
export function get_style(
|
||||
option: { style?: OptionStyle; [key: string]: unknown } | string | number,
|
||||
key: 'selected' | 'option' | null = null
|
||||
) {
|
||||
let css_str = ``
|
||||
if (![`selected`, `option`, null].includes(key)) {
|
||||
console.error(`MultiSelect: Invalid key=${key} for get_style`)
|
||||
}
|
||||
if (typeof option == `object` && option.style) {
|
||||
if (typeof option.style == `string`) {
|
||||
css_str = option.style
|
||||
}
|
||||
if (typeof option.style == `object`) {
|
||||
if (key && key in option.style) return option.style[key] ?? ``
|
||||
else {
|
||||
console.error(`Invalid style object for option=${JSON.stringify(option)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
// ensure css_str ends with a semicolon
|
||||
if (css_str.trim() && !css_str.trim().endsWith(`;`)) css_str += `;`
|
||||
return css_str
|
||||
}
|
||||
|
||||
// Firefox lacks support for scrollIntoViewIfNeeded (https://caniuse.com/scrollintoviewifneeded).
|
||||
// See https://github.com/janosh/svelte-multiselect/issues/87
|
||||
// Polyfill copied from
|
||||
// https://github.com/nuxodin/lazyfill/blob/a8e63/polyfills/Element/prototype/scrollIntoViewIfNeeded.js
|
||||
// exported for testing
|
||||
export function scroll_into_view_if_needed_polyfill(elem: Element, centerIfNeeded: boolean = true) {
|
||||
const observer = new IntersectionObserver(function ([entry]) {
|
||||
const ratio = entry.intersectionRatio
|
||||
if (ratio < 1) {
|
||||
const place = ratio <= 0 && centerIfNeeded ? `center` : `nearest`
|
||||
elem.scrollIntoView({
|
||||
block: place,
|
||||
inline: place
|
||||
})
|
||||
}
|
||||
observer.disconnect()
|
||||
}, {
|
||||
root: null, // or specify a scrolling parent if needed
|
||||
rootMargin: '0px 1000px', // Essentially making horizontal checks irrelevant
|
||||
threshold: 0.1 // Adjust threshold to control when observer should trigger
|
||||
})
|
||||
observer.observe(elem)
|
||||
|
||||
return observer // return for testing
|
||||
}
|
||||
@@ -12,7 +12,8 @@
|
||||
import { createEventDispatcher, untrack } from 'svelte'
|
||||
import ToggleButtonMore from '../common/toggleButton-v2/ToggleButtonMore.svelte'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import Select from '../Select.svelte'
|
||||
import Select from '../select/Select.svelte'
|
||||
import { safeSelectItems } from '../select/utils.svelte'
|
||||
|
||||
interface Props {
|
||||
// Filters
|
||||
@@ -174,7 +175,7 @@
|
||||
<div class="relative">
|
||||
<span class="text-xs absolute -top-4">User</span>
|
||||
<Select
|
||||
items={usernames.map((p) => ({ label: p, value: p }))}
|
||||
items={safeSelectItems(usernames)}
|
||||
bind:value={() => user ?? undefined, (v) => (user = v ?? null)}
|
||||
clearable
|
||||
onClear={() => ((user = null), dispatch('reset'))}
|
||||
@@ -190,7 +191,7 @@
|
||||
<span class="text-xs absolute -top-4">Folder</span>
|
||||
|
||||
<Select
|
||||
items={folders.map((p) => ({ label: p, value: p }))}
|
||||
items={safeSelectItems(folders)}
|
||||
bind:value={() => folder ?? undefined, (v) => (folder = v ?? null)}
|
||||
clearable
|
||||
onClear={() => ((folder = null), dispatch('reset'))}
|
||||
@@ -203,7 +204,7 @@
|
||||
<div class="relative">
|
||||
<span class="text-xs absolute -top-4">Path</span>
|
||||
<Select
|
||||
items={paths.map((p) => ({ label: p, value: p }))}
|
||||
items={safeSelectItems(paths)}
|
||||
bind:value={() => path ?? undefined, (v) => (path = v ?? null)}
|
||||
clearable
|
||||
onClear={() => ((path = null), dispatch('reset'))}
|
||||
@@ -564,7 +565,7 @@
|
||||
<Label label="User">
|
||||
<Select
|
||||
disablePortal
|
||||
items={usernames.map((p) => ({ label: p, value: p }))}
|
||||
items={safeSelectItems(usernames)}
|
||||
bind:value={() => user ?? undefined, (v) => (user = v ?? null)}
|
||||
clearable
|
||||
onClear={() => ((user = null), dispatch('reset'))}
|
||||
@@ -575,7 +576,7 @@
|
||||
<Label label="Folder">
|
||||
<Select
|
||||
disablePortal
|
||||
items={folders.map((p) => ({ label: p, value: p }))}
|
||||
items={safeSelectItems(folders)}
|
||||
bind:value={() => folder ?? undefined, (v) => (folder = v ?? null)}
|
||||
clearable
|
||||
onClear={() => ((folder = null), dispatch('reset'))}
|
||||
@@ -586,7 +587,7 @@
|
||||
<Label label="Path">
|
||||
<Select
|
||||
disablePortal
|
||||
items={paths.map((p) => ({ label: p, value: p }))}
|
||||
items={safeSelectItems(paths)}
|
||||
bind:value={() => path ?? undefined, (v) => (path = v ?? null)}
|
||||
clearable
|
||||
onClear={() => ((path = null), dispatch('reset'))}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
import { Alert } from '../common'
|
||||
import AddPropertyV2 from '$lib/components/schema/AddPropertyV2.svelte'
|
||||
import { Plus } from 'lucide-svelte'
|
||||
import Select from '../Select.svelte'
|
||||
import Select from '../select/Select.svelte'
|
||||
import { safeSelectItems } from '../select/utils.svelte'
|
||||
|
||||
interface Props {
|
||||
schema: Schema | undefined | any
|
||||
@@ -146,7 +147,7 @@
|
||||
File extension :
|
||||
<Select
|
||||
autofocus
|
||||
items={suggestedFileExtensions.map((e) => ({ value: e, label: e }))}
|
||||
items={safeSelectItems(suggestedFileExtensions)}
|
||||
bind:value={formatExtension}
|
||||
onCreateItem={(ext) => ((formatExtension = ext), suggestedFileExtensions.push(ext))}
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { base } from '$lib/base'
|
||||
|
||||
let { aiId, aiDescription } = $props<{ aiId: string; aiDescription: string }>()
|
||||
let { aiId, aiDescription }: { aiId: string; aiDescription: string } = $props()
|
||||
</script>
|
||||
|
||||
<!-- Buttons -->
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
type Script,
|
||||
type SearchJobsIndexResponse
|
||||
} from '$lib/gen'
|
||||
import { clickOutside, isMac } from '$lib/utils'
|
||||
import { clickOutside, isMac, scroll_into_view_if_needed_polyfill } from '$lib/utils'
|
||||
import {
|
||||
AlertTriangle,
|
||||
BoxesIcon,
|
||||
@@ -36,7 +36,6 @@
|
||||
import { devopsRole, enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
|
||||
import uFuzzy from '@leeoniya/ufuzzy'
|
||||
import BarsStaggered from '../icons/BarsStaggered.svelte'
|
||||
import { scroll_into_view_if_needed_polyfill } from '../multiselect/utils'
|
||||
import { Alert } from '../common'
|
||||
import Popover from '../Popover.svelte'
|
||||
import Logs from 'lucide-svelte/icons/logs'
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<script lang="ts" generics="Item extends { label?: string; value: any }">
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import CloseButton from '../common/CloseButton.svelte'
|
||||
|
||||
type Props = {
|
||||
items?: Item[]
|
||||
allowClear?: boolean
|
||||
onRemove: (item: Item) => void
|
||||
onReorder?: (oldIndex: number, newIndex: number) => void
|
||||
}
|
||||
let { items, onRemove, onReorder, allowClear = true }: Props = $props()
|
||||
|
||||
let currentlyDraggingIndex: number | undefined = $state()
|
||||
let dragPos = $state<[number, number]>([0, 0])
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
onmousemove={(e) => {
|
||||
if (currentlyDraggingIndex === undefined) return
|
||||
dragPos = [dragPos[0] + e.movementX, dragPos[1] + e.movementY]
|
||||
}}
|
||||
onpointerup={() => {
|
||||
currentlyDraggingIndex = undefined
|
||||
dragPos = [0, 0]
|
||||
}}
|
||||
/>
|
||||
|
||||
{#each items ?? [] as item, index}
|
||||
<li
|
||||
role="listitem"
|
||||
class={twMerge(
|
||||
allowClear ? 'pr-1' : 'pr-3',
|
||||
'pl-3 min-h-6 bg-surface-secondary rounded-full flex items-center gap-0.5',
|
||||
currentlyDraggingIndex !== undefined ? 'hover:opacity-20' : ''
|
||||
)}
|
||||
style={currentlyDraggingIndex === index
|
||||
? `transform: translate(${dragPos[0]}px, ${dragPos[1]}px); pointer-events: none;`
|
||||
: ''}
|
||||
draggable
|
||||
onpointerdown={(e) => {
|
||||
e.stopPropagation()
|
||||
dragPos = [0, 0]
|
||||
currentlyDraggingIndex = index
|
||||
}}
|
||||
onpointerup={(e) => {
|
||||
if (currentlyDraggingIndex !== undefined) {
|
||||
e.stopPropagation()
|
||||
onReorder?.(currentlyDraggingIndex, index)
|
||||
}
|
||||
currentlyDraggingIndex = undefined
|
||||
dragPos = [0, 0]
|
||||
}}
|
||||
>
|
||||
<span class="text-sm select-none">{item.label || item.value}</span>
|
||||
{#if allowClear}
|
||||
<CloseButton
|
||||
class="text-tertiary"
|
||||
small
|
||||
on:close={(e) => (onRemove(item), e.stopPropagation())}
|
||||
/>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user