Compare commits

...

29 Commits

Author SHA1 Message Date
Ádám Kovács
1b29703669 fix(frontend): Enable supabase connect CSP 2023-03-16 10:47:21 +01:00
Ruben Fiszel
52157faf72 chore(main): release 1.78.0 (#1292)
* chore(main): release 1.78.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-16 03:11:38 +01:00
Ruben Fiszel
a9e8aa0f1c update cli error message for folders 2023-03-16 03:10:45 +01:00
Ruben Fiszel
50c1c614ef context panel transitions 2023-03-16 03:00:01 +01:00
Ruben Fiszel
11567d6280 feat(frontend): app textcomponent editable + tooltip 2023-03-16 02:34:39 +01:00
Ruben Fiszel
a6e1510405 fix minor flow UX 2023-03-16 01:18:11 +01:00
Ruben Fiszel
b7d8fd1a4d apps minor fixes 2023-03-16 00:55:45 +01:00
Ruben Fiszel
e27de7fb5d lightarginput for apps forms 2023-03-16 00:13:09 +01:00
Ruben Fiszel
99ec12e10c various minor fixes 2023-03-15 23:31:35 +01:00
Ruben Fiszel
9bfd471439 context panel refactor 2023-03-15 22:24:13 +01:00
Ruben Fiszel
dbdfd62638 fix(frontend): remove staticOutputs from apps 2023-03-15 17:22:46 +01:00
Ruben Fiszel
6f890f2120 fix(frontend): improve rendering performance after component moving 2023-03-15 12:35:11 +01:00
Ruben Fiszel
183a4591df fix(backend): whitelist for include_header was ignored in some cases 2023-03-14 23:47:57 +01:00
Ruben Fiszel
646c0f23da chore(main): release 1.77.0 (#1286)
* chore(main): release 1.77.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-14 22:27:08 +01:00
Ádám Kovács
dea12e8870 fix(frontend): Update setting accordion (#1285)
* fix(frontend): Update setting accordion

* remove empty tooltip

---------

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
2023-03-14 22:24:23 +01:00
Ryan Rich
944795f6ee feat(deno): add support for custom npm repo (#1291)
* feat(deno): add support for custom npm repo

* feat(deno): add support for custom npm repo

* feat(deno): fix comment being on wrong line

* feat(deno): review feedback
2023-03-14 22:23:44 +01:00
Ruben Fiszel
65d4bc519c add env build configuration 2023-03-14 22:20:45 +01:00
Ruben Fiszel
4d3507aec2 add .env.production to .gitignore 2023-03-14 22:19:08 +01:00
Ruben Fiszel
1d395ccc17 add env build configuration 2023-03-14 22:16:22 +01:00
hcourdent
3a7129de4b Added tooltips to App editor (#1289) 2023-03-14 22:05:34 +01:00
Ruben Fiszel
2f0acb9ffa feat(apps): state can be used as input in apps 2023-03-14 20:44:14 +01:00
Ruben Fiszel
81f989837b improve reactivity of apps 2023-03-14 18:46:31 +01:00
Faton Ramadani
c71a577fea fix(frontend): fix app tabs (#1288) 2023-03-14 16:11:07 +01:00
Faton Ramadani
bc870bd03e fix(frontend): fix container deletion (#1287)
* fix(frontend): fix container deletion

* fix(frontend): fix container deletion
2023-03-14 14:57:44 +01:00
Faton Ramadani
751edcf9b8 feat(frontend): app components output panel (#1283)
* feat(frontend): hierarchical output panel WIP

* feat(frontend): wip

* feat(frontend): working animations

* feat(frontend): working animations

* feat(frontend): wip

* feat(frontend): wip

* feat(frontend): improving connection

* feat(frontend): fix search

* feat(frontend): output panel v2

* feat(frontend): support table actions

* feat(frontend): support table actions

* feat(frontend): support background script

* feat(frontend): fix background scripts

* feat(frontend): simplify code

* feat(frontend): fix animation

* feat(frontend): fix wording

* feat(frontend): fix bg script click

* feat(frontend): fix bg script click

* feat(frontend): fix bg script click
2023-03-14 14:42:40 +01:00
Ruben Fiszel
c2a97c53cf feat(frontend): add setTab to frontend scripts 2023-03-14 14:42:26 +01:00
Ruben Fiszel
eb73f2a687 fix(backend): do not cache reference to workspace scripts 2023-03-14 14:16:07 +01:00
Ruben Fiszel
cd645d0935 feat(apps): tabs can be made pages or invisible + better frontend scripts reactivity 2023-03-14 12:34:08 +01:00
Ruben Fiszel
ac9bd7ef8c fix flow viewer select event 2023-03-14 00:11:37 +01:00
120 changed files with 2245 additions and 1271 deletions

View File

@@ -1,6 +1,39 @@
# Changelog
## [1.78.0](https://github.com/windmill-labs/windmill/compare/v1.77.0...v1.78.0) (2023-03-16)
### Features
* **frontend:** app textcomponent editable + tooltip ([11567d6](https://github.com/windmill-labs/windmill/commit/11567d6280ea60f1a8c3c6607c724179775cbbe3))
### Bug Fixes
* **backend:** whitelist for include_header was ignored in some cases ([183a459](https://github.com/windmill-labs/windmill/commit/183a4591df700ab4720de6e92a83631256940089))
* **frontend:** improve rendering performance after component moving ([6f890f2](https://github.com/windmill-labs/windmill/commit/6f890f2120885f90d986fbd655096b45bf9de539))
* **frontend:** remove staticOutputs from apps ([dbdfd62](https://github.com/windmill-labs/windmill/commit/dbdfd626386398180ecba7976714f86365eeccd8))
## [1.77.0](https://github.com/windmill-labs/windmill/compare/v1.76.0...v1.77.0) (2023-03-14)
### Features
* **apps:** state can be used as input in apps ([2f0acb9](https://github.com/windmill-labs/windmill/commit/2f0acb9ffa8dace4a886527dcee49809d019b271))
* **apps:** tabs can be made pages or invisible + better frontend scripts reactivity ([cd645d0](https://github.com/windmill-labs/windmill/commit/cd645d0935f2d06e0ff71f14d2cf63accd378ff3))
* **deno:** add support for custom npm repo ([#1291](https://github.com/windmill-labs/windmill/issues/1291)) ([944795f](https://github.com/windmill-labs/windmill/commit/944795f6eeaa7d01ab1a35a80570a55c363723e6))
* **frontend:** add setTab to frontend scripts ([c2a97c5](https://github.com/windmill-labs/windmill/commit/c2a97c53cfff0fdb35dd8bc249490566eebdc1a9))
* **frontend:** app components output panel ([#1283](https://github.com/windmill-labs/windmill/issues/1283)) ([751edcf](https://github.com/windmill-labs/windmill/commit/751edcf9b8e0976a1d073603c9eff5dc6e714490))
### Bug Fixes
* **backend:** do not cache reference to workspace scripts ([eb73f2a](https://github.com/windmill-labs/windmill/commit/eb73f2a687f6faad301b9038ab8585450bec7481))
* **frontend:** fix app tabs ([#1288](https://github.com/windmill-labs/windmill/issues/1288)) ([c71a577](https://github.com/windmill-labs/windmill/commit/c71a577fead90c9cd01a736b54d859ec4f0b7807))
* **frontend:** fix container deletion ([#1287](https://github.com/windmill-labs/windmill/issues/1287)) ([bc870bd](https://github.com/windmill-labs/windmill/commit/bc870bd03eb76cb8bc0e0c861f6cd8a9c661186b))
* **frontend:** Update setting accordion ([#1285](https://github.com/windmill-labs/windmill/issues/1285)) ([dea12e8](https://github.com/windmill-labs/windmill/commit/dea12e8870ece998bb6607723cbaab9b9a958f22))
## [1.76.0](https://github.com/windmill-labs/windmill/compare/v1.75.0...v1.76.0) (2023-03-13)

View File

@@ -316,6 +316,7 @@ you to have it being synced automatically everyday.
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
| DENO_FLAGS | None | Override the flags passed to deno (default --allow-all) to tighten permissions. Minimum permissions needed are "--allow-read=args.json --allow-write=result.json" | Worker |
| NPM_CONFIG_REGISTRY | None | Registry to use for NPM dependencies, set if you have a private repository you need to use instead of the default public NPM registry | Worker |
| PIP_LOCAL_DEPENDENCIES | None | Specify dependencies that are installed locally and do not need to be solved nor installed again | |
| ADDITIONAL_PYTHON_PATHS | None | Specify python paths (separated by a :) to be appended to the PYTHONPATH of the python jobs. To be used with PIP_LOCAL_DEPENDENCIES to use python codebases within Windmill | Worker |
| INCLUDE_HEADERS | None | Whitelist of headers that are passed to jobs as args (separated by a comma) | Server |

54
backend/Cargo.lock generated
View File

@@ -2171,9 +2171,9 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.45"
version = "0.10.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1"
checksum = "fd2523381e46256e40930512c7fd25562b9eae4812cb52078f155e87217c9d1e"
dependencies = [
"bitflags",
"cfg-if",
@@ -2203,9 +2203,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.80"
version = "0.9.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7"
checksum = "176be2629957c157240f68f61f2d0053ad3a4ecfdd9ebf1e6521d18d9635cf67"
dependencies = [
"autocfg",
"cc",
@@ -2973,7 +2973,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#8ff947e83a65b74c920edc63aed8a0a5854b8612"
source = "git+https://github.com/RustPython/RustPython#2efc566b14e64c4a5d8120fcc167a49d2a9e5615"
dependencies = [
"num-bigint",
"rustpython-compiler-core",
@@ -2982,7 +2982,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#8ff947e83a65b74c920edc63aed8a0a5854b8612"
source = "git+https://github.com/RustPython/RustPython#2efc566b14e64c4a5d8120fcc167a49d2a9e5615"
dependencies = [
"bitflags",
"bstr",
@@ -2995,7 +2995,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython#8ff947e83a65b74c920edc63aed8a0a5854b8612"
source = "git+https://github.com/RustPython/RustPython#2efc566b14e64c4a5d8120fcc167a49d2a9e5615"
dependencies = [
"ahash 0.7.6",
"anyhow",
@@ -3167,9 +3167,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.155"
version = "1.0.156"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71f2b4817415c6d4210bfe1c7bfcf4801b2d904cb4d0e1a8fdb651013c9e86b8"
checksum = "314b5b092c0ade17c00142951e50ced110ec27cea304b1037c6969246c2469a4"
dependencies = [
"serde_derive",
]
@@ -3196,9 +3196,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.155"
version = "1.0.156"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d071a94a3fac4aff69d023a7f411e33f40f3483f8c5190b1953822b6b76d7630"
checksum = "d7e29c4601e36bcec74a223228dce795f4cd3616341a4af93520ca1a837c087d"
dependencies = [
"proc-macro2",
"quote",
@@ -4072,9 +4072,9 @@ checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
[[package]]
name = "toml_edit"
version = "0.19.6"
version = "0.19.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08de71aa0d6e348f070457f85af8bd566e2bc452156a423ddf22861b3a953fae"
checksum = "dc18466501acd8ac6a3f615dd29a3438f8ca6bb3b19537138b3106e575621274"
dependencies = [
"indexmap",
"toml_datetime",
@@ -4743,7 +4743,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.76.0"
version = "1.78.0"
dependencies = [
"anyhow",
"axum",
@@ -4770,7 +4770,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.76.0"
version = "1.78.0"
dependencies = [
"anyhow",
"argon2",
@@ -4825,7 +4825,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.76.0"
version = "1.78.0"
dependencies = [
"base64 0.21.0",
"chrono",
@@ -4840,7 +4840,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.76.0"
version = "1.78.0"
dependencies = [
"chrono",
"serde",
@@ -4853,7 +4853,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.76.0"
version = "1.78.0"
dependencies = [
"anyhow",
"axum",
@@ -4878,7 +4878,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.76.0"
version = "1.78.0"
dependencies = [
"serde",
"serde_json",
@@ -4886,7 +4886,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.76.0"
version = "1.78.0"
dependencies = [
"anyhow",
"itertools",
@@ -4901,7 +4901,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.76.0"
version = "1.78.0"
dependencies = [
"anyhow",
"itertools",
@@ -4913,7 +4913,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.76.0"
version = "1.78.0"
dependencies = [
"anyhow",
"itertools",
@@ -4928,7 +4928,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.76.0"
version = "1.78.0"
dependencies = [
"anyhow",
"deno_core",
@@ -4942,7 +4942,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.76.0"
version = "1.78.0"
dependencies = [
"anyhow",
"chrono",
@@ -4965,7 +4965,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.76.0"
version = "1.78.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -5081,9 +5081,9 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "winnow"
version = "0.3.5"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee7b2c67f962bf5042bfd8b6a916178df33a26eec343ae064cb8e069f638fa6f"
checksum = "23d020b441f92996c80d94ae9166e8501e59c7bb56121189dc9eab3bd8216966"
dependencies = [
"memchr",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.76.0"
version = "1.78.0"
authors.workspace = true
edition.workspace = true
@@ -19,7 +19,7 @@ members = [
]
[workspace.package]
version = "1.76.0"
version = "1.78.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"

View File

@@ -112,6 +112,7 @@ Windmill Community Edition {GIT_VERSION}
"QUEUE_LIMIT_WAIT_RESULT",
"DENO_AUTH_TOKENS",
"DENO_FLAGS",
"NPM_CONFIG_REGISTRY",
"PIP_LOCAL_DEPENDENCIES",
"ADDITIONAL_PYTHON_PATHS",
"INCLUDE_HEADERS",

View File

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

View File

@@ -300,9 +300,6 @@ pub fn add_include_headers(
headers: HeaderMap,
mut args: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
if include_header.is_none() {
return args;
}
let whitelist = include_header
.as_ref()
.map(|s| s.split(",").map(|s| s.to_string()).collect::<Vec<_>>())
@@ -321,6 +318,7 @@ pub fn add_include_headers(
});
args
}
#[derive(Deserialize)]
pub struct ListQueueQuery {
pub script_path_start: Option<String>,

View File

@@ -384,8 +384,9 @@ async fn update_variable(
path,
w_id
)
.fetch_one(&mut tx)
.await?;
.fetch_optional(&mut tx)
.await?
.flatten();
if let Some(old_v) = v {
v = Some(replace_path(

View File

@@ -236,8 +236,7 @@ pub async fn create_token_for_owner<'c>(
) -> error::Result<(Transaction<'c, Postgres>, String)> {
// TODO: Bad implementation. We should not have access to this DB here.
let token: String = rd_string(30);
let is_super_admin = owner.contains('@')
&& sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", owner)
let is_super_admin = sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email)
.fetch_optional(&mut tx)
.await?
.unwrap_or(false);
@@ -330,7 +329,7 @@ lazy_static::lazy_static! {
.ok()
.map(|x| format!(";{x}"))
.unwrap_or_else(|| String::new());
static ref NPM_CONFIG_REGISTRY: Option<String> = std::env::var("NPM_CONFIG_REGISTRY").ok();
@@ -1481,6 +1480,26 @@ fn capitalize(s: &str) -> String {
}
}
fn get_common_deno_proc_envs(token: &str, base_internal_url: &str) -> HashMap<String, String> {
let hostname_base = BASE_URL.split("://").last().unwrap_or("localhost");
let hostname_internal = base_internal_url.split("://").last().unwrap_or("localhost");
let deno_auth_tokens_base = DENO_AUTH_TOKENS.as_str();
let deno_auth_tokens =
format!("{token}@{hostname_base};{token}@{hostname_internal}{deno_auth_tokens_base}",);
let mut deno_envs: HashMap<String, String> = HashMap::from([
(String::from("PATH"), PATH_ENV.clone()),
(String::from("DENO_AUTH_TOKENS"), deno_auth_tokens),
(String::from("BASE_INTERNAL_URL"), base_internal_url.to_string()),
(String::from("NO_COLOR"), String::from("true")),
]);
if let Some(ref s) = *NPM_CONFIG_REGISTRY {
deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), s.clone());
}
return deno_envs;
}
#[tracing::instrument(level = "trace", skip_all)]
async fn handle_deno_job(
logs: &mut String,
@@ -1546,11 +1565,9 @@ run().catch(async (e) => {{
let mut reserved_variables = get_reserved_variables(job, &token, db).await?;
reserved_variables.insert("RUST_LOG".to_string(), "info".to_string());
let hostname_base = BASE_URL.split("://").last().unwrap_or("localhost");
let hostname_internal = base_internal_url.split("://").last().unwrap_or("localhost");
let deno_auth_tokens_base = DENO_AUTH_TOKENS.as_str();
let deno_auth_tokens =
format!("{token}@{hostname_base};{token}@{hostname_internal}{deno_auth_tokens_base}",);
let common_deno_proc_envs = get_common_deno_proc_envs(&token, base_internal_url);
//do not cache local dependencies
let reload = format!("--reload={base_internal_url}");
let child = async {
Ok(if !*DISABLE_NSJAIL {
let _ = write_file(
@@ -1574,6 +1591,7 @@ run().catch(async (e) => {{
}
args.push("--import-map");
args.push("/tmp/import_map.json");
args.push(&reload);
args.push("--unstable");
if let Some(deno_flags) = DENO_FLAGS.as_ref() {
for flag in deno_flags {
@@ -1588,9 +1606,7 @@ run().catch(async (e) => {{
.current_dir(job_dir)
.env_clear()
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.env("DENO_AUTH_TOKENS", deno_auth_tokens)
.env("BASE_INTERNAL_URL", base_internal_url)
.envs(common_deno_proc_envs)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
@@ -1602,6 +1618,7 @@ run().catch(async (e) => {{
args.push("run");
args.push("--import-map");
args.push(&import_map_path);
args.push(&reload);
args.push("--unstable");
if let Some(deno_flags) = DENO_FLAGS.as_ref() {
for flag in deno_flags {
@@ -1615,11 +1632,8 @@ run().catch(async (e) => {{
.current_dir(job_dir)
.env_clear()
.envs(reserved_variables)
.env("PATH", PATH_ENV.as_str())
.envs(common_deno_proc_envs)
.env("DENO_DIR", DENO_CACHE_DIR)
.env("DENO_AUTH_TOKENS", deno_auth_tokens)
.env("BASE_INTERNAL_URL", base_internal_url)
.env("NO_COLOR", "true")
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())

View File

@@ -20,11 +20,17 @@ import {
export class FolderFile implements Resource, PushDiffs {
@property(array(() => String))
owners: Array<string> | undefined;
@property(map(() => String, () => Boolean, { shape: MapShape.Object }))
@property(
map(
() => String,
() => Boolean,
{ shape: MapShape.Object }
)
)
extra_perms: Map<string, boolean> | undefined;
@property(() => String)
display_name: string| undefined;
display_name: string | undefined;
async push(workspace: string, remotePath: string): Promise<void> {
if (remotePath.startsWith("/")) {
remotePath = remotePath.substring(1);
@@ -36,14 +42,14 @@ export class FolderFile implements Resource, PushDiffs {
await this.pushDiffs(
workspace,
remotePath,
microdiff({}, this, { cyclesFix: false }),
microdiff({}, this, { cyclesFix: false })
);
}
async pushDiffs(
workspace: string,
remotePath: string,
diffs: Difference[],
diffs: Difference[]
): Promise<void> {
if (remotePath.startsWith("/")) {
remotePath = remotePath.substring(1);
@@ -55,15 +61,18 @@ export class FolderFile implements Resource, PushDiffs {
// TODO: Support this in backend
let exists: boolean;
try {
exists = !!await FolderService.getFolder({ workspace, name: remotePath });
exists = !!(await FolderService.getFolder({
workspace,
name: remotePath,
}));
} catch {
exists = false;
}
if (exists) {
console.log(
colors.bold.yellow(
`Applying ${diffs.length} diffs to existing folder... ${remotePath}`,
),
`Applying ${diffs.length} diffs to existing folder... ${remotePath}`
)
);
const changeset: {
@@ -74,12 +83,12 @@ export class FolderFile implements Resource, PushDiffs {
for (const diff of diffs) {
if (
diff.type !== "REMOVE" &&
(
diff.path.length !== 1 ||
!["owners", "extra_perms", "display_name"].includes(diff.path[0] as string)
)
(diff.path.length !== 1 ||
!["owners", "extra_perms", "display_name"].includes(
diff.path[0] as string
))
) {
console.log(diff.path)
console.log(diff.path);
throw new Error("Invalid folder diff with path " + diff.path);
}
if (diff.type === "CREATE" || diff.type === "CHANGE") {
@@ -89,17 +98,27 @@ export class FolderFile implements Resource, PushDiffs {
}
}
const hasChanges = Object.values(changeset).some((v) =>
v !== null && typeof v !== "undefined"
const hasChanges = Object.values(changeset).some(
(v) => v !== null && typeof v !== "undefined"
);
if (!hasChanges) {
return;
}
await FolderService.updateFolder({
workspace: workspace,
name: remotePath,
requestBody: {...changeset, extra_perms: changeset.extra_perms ? Object.fromEntries(this.extra_perms?.entries() ?? []) : undefined}
});
try {
await FolderService.updateFolder({
workspace: workspace,
name: remotePath,
requestBody: {
...changeset,
extra_perms: changeset.extra_perms
? Object.fromEntries(this.extra_perms?.entries() ?? [])
: undefined,
},
});
} catch (e) {
console.error(colors.red.bold(e.body));
throw e;
}
} else {
console.log(colors.bold.yellow("Creating new folder: " + remotePath));
await FolderService.createFolder({
@@ -118,7 +137,7 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
const workspace = await resolveWorkspace(opts);
await requireLogin(opts);
if (!await validatePath(opts, remotePath)) {
if (!(await validatePath(opts, remotePath))) {
return;
}
@@ -136,11 +155,11 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
export async function pushFolder(
workspace: string,
filePath: string,
remotePath: string,
remotePath: string
) {
const data = decoverto.type(FolderFile).rawToInstance(
await Deno.readTextFile(filePath),
);
const data = decoverto
.type(FolderFile)
.rawToInstance(await Deno.readTextFile(filePath));
data.push(workspace, remotePath);
}
@@ -148,7 +167,7 @@ const command = new Command()
.description("resource related commands")
.command(
"push",
"push a local folder spec. This overrides any remote versions.",
"push a local folder spec. This overrides any remote versions."
)
.arguments("<file_path:string> <remote_path:string>")
.action(push as any);

View File

@@ -1,4 +1,9 @@
import { Command, CompletionsCommand, DenoLandProvider, UpgradeCommand } from "./deps.ts";
import {
Command,
CompletionsCommand,
DenoLandProvider,
UpgradeCommand,
} from "./deps.ts";
import flow from "./flow.ts";
import script from "./script.ts";
import workspace from "./workspace.ts";
@@ -13,19 +18,18 @@ import sync from "./sync.ts";
import { tryResolveVersion } from "./context.ts";
import { GlobalOptions } from "./types.ts";
const VERSION = "v1.76.0";
export const VERSION = "v1.77.0";
let command: any = new Command()
.name("wmill")
.description("A simple CLI tool for windmill.")
.action(() => command.showHelp())
.globalOption(
"--workspace <workspace:string>",
"Specify the target workspace. This overrides the default workspace.",
"Specify the target workspace. This overrides the default workspace."
)
.globalOption(
"--token <token:string>",
"Specify an API token. This will override any stored token.",
"Specify an API token. This will override any stored token."
)
.version(VERSION)
.command("flow", flow)
@@ -59,14 +63,12 @@ let command: any = new Command()
"--unstable",
],
provider: new DenoLandProvider({ name: "wmill" }),
}),
})
)
.command("completions", new CompletionsCommand());
if (Number.parseInt(VERSION.replace("v", "").replace(".", "")) > 1700) {
command = command
.command("push", push)
.command("pull", pull);
command = command.command("push", push).command("pull", pull);
}
try {

1
frontend/.env Normal file
View File

@@ -0,0 +1 @@
VITE_DEFAULT_WEBHOOK_TYPE=async

1
frontend/.gitignore vendored
View File

@@ -7,3 +7,4 @@ node_modules
CaddyfileRemoteRuben
tests-out/
storageState.json
.env.production

View File

@@ -17,7 +17,7 @@ In the root folder:
```bash
docker build . -t windmill
docker-compose up db windmill_server windmill_worker
docker compose up db windmill_server windmill_worker
```
### 2. Backend is run by cargo
@@ -145,3 +145,25 @@ Recommended config for VS Code:
```
- turn _format on save_ on
## Building
The project is built with [SvelteKit](https://kit.svelte.dev/) and uses as output static files.
There are others adapters for sveltekit, but we use the static adapter.
To build the frontend as static assets, use:
```
npm run build
```
The output is in the `build` folder.
The default build assume you serve every non static files as the 200.html file which is catchall. If you prefer a normal layout, you can use:
```
NOTCATCHALL=true npm run build
```
which will generate an index.html and allow you to serve the frontend with any static server.
Env variables used for build are set in .env file. See [https://vitejs.dev/guide/env-and-mode.html#env-files](https://vitejs.dev/guide/env-and-mode.html#env-files) for more details.

View File

@@ -1,12 +1,12 @@
{
"name": "windmill",
"version": "1.76.0",
"version": "1.78.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "windmill",
"version": "1.76.0",
"version": "1.78.0",
"dependencies": {
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",

View File

@@ -1,6 +1,6 @@
{
"name": "windmill",
"version": "1.76.0",
"version": "1.78.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",

View File

@@ -7,7 +7,7 @@
faPlus
} from '@fortawesome/free-solid-svg-icons'
import { setInputCat as computeInputCat, type InputCat } from '$lib/utils'
import { setInputCat as computeInputCat } from '$lib/utils'
import { Badge, Button } from './common'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
@@ -152,6 +152,8 @@
value = enum_?.[0]
} else if (inputCat == 'boolean') {
value = false
} else if (inputCat == 'list') {
value = []
}
}
}
@@ -288,7 +290,7 @@
on:click={() => {
value = value.filter((el) => el != v)
if (value.length == 0) {
value = undefined
value = []
}
}}
>
@@ -450,19 +452,6 @@
{/if}
</div>
{/if}
{#if !required && inputCat != 'resource-object'}
<!-- <Tooltip placement="bottom" content="Reset to default value">
<Button
on:click={() => (value = undefined)}
{disabled}
color="alternative"
size="sm"
class="h-8"
>
<Icon data={faArrowRotateLeft} />
</Button>
</Tooltip> -->
{/if}
<slot name="actions" />
</div>
{#if !compact || (error && error != '')}

View File

@@ -248,6 +248,6 @@
<Highlight language={json} code={JSON.stringify(result, null, 4).replace(/\\n/g, '\n')} />
{/if}
{:else}
<div class="text-gray-500 text-sm">No result</div>
<div class="text-gray-500 text-sm">No result: {JSON.stringify(result)}</div>
{/if}
</div>

View File

@@ -88,7 +88,7 @@
minHeight={400}
modules={flow?.value?.modules}
failureModule={flow?.value?.failure_module}
on:click={(e) => (stepDetail = e.detail)}
on:select={(e) => (stepDetail = e.detail)}
/>
</div>
{#if !noSide}

View File

@@ -0,0 +1,331 @@
<script lang="ts">
import { faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'
import { setInputCat as computeInputCat } from '$lib/utils'
import { Badge, Button } from './common'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
import FieldHeader from './FieldHeader.svelte'
import ObjectResourceInput from './ObjectResourceInput.svelte'
import ResourcePicker from './ResourcePicker.svelte'
import type { SchemaProperty } from '$lib/common'
import autosize from 'svelte-autosize'
import Toggle from './Toggle.svelte'
import Range from './Range.svelte'
import LightweightSchemaForm from './LightweightSchemaForm.svelte'
export let label: string = ''
export let value: any
export let defaultValue: any = undefined
export let description: string = ''
export let format: string = ''
export let contentEncoding: 'base64' | 'binary' | undefined = undefined
export let type: string | undefined = undefined
export let required = false
export let pattern: undefined | string = undefined
export let valid = required ? false : true
export let maxRows = 10
export let enum_: string[] | undefined = undefined
export let itemsType:
| { type?: 'string' | 'number' | 'bytes'; contentEncoding?: 'base64' }
| undefined = undefined
export let displayHeader = true
export let properties: { [name: string]: SchemaProperty } | undefined = undefined
export let extra: Record<string, any> = {}
const dispatch = createEventDispatcher()
$: maxHeight = maxRows ? `${1 + maxRows * 1.2}em` : `auto`
$: validateInput(pattern, value)
let error: string = ''
let el: HTMLTextAreaElement | undefined = undefined
let rawValue: string | undefined = undefined
$: {
if (rawValue) {
try {
value = JSON.parse(rawValue)
error = ''
} catch (err) {
error = err.toString()
}
}
}
$: {
error = ''
if (inputCat === 'object') {
evalValueToRaw()
validateInput(pattern, value)
}
}
export function evalValueToRaw() {
if (value) {
rawValue = JSON.stringify(value, null, 4)
}
}
function fileChanged(e: any, cb: (v: string | undefined) => void) {
let t = e.target
if (t && 'files' in t && t.files.length > 0) {
let reader = new FileReader()
reader.onload = (e: any) => {
cb(e.target.result.split('base64,')[1])
}
reader.readAsDataURL(t.files[0])
} else {
cb(undefined)
}
}
export function focus() {
el?.focus()
if (el) {
el.style.height = '5px'
el.style.height = el.scrollHeight + 50 + 'px'
}
}
function validateInput(pattern: string | undefined, v: any): void {
if (required && (v == undefined || v == null || v === '')) {
error = 'This field is required'
valid = false
} else {
if (pattern && !testRegex(pattern, v)) {
error = `Should match ${pattern}`
valid = false
} else {
error = ''
valid = true
}
}
}
function testRegex(pattern: string, value: any): boolean {
try {
const regex = new RegExp(pattern)
return regex.test(value)
} catch (err) {
return false
}
}
$: {
if (value == undefined || value == null) {
value = defaultValue
if (defaultValue === undefined || defaultValue === null) {
if (inputCat === 'string') {
value = ''
} else if (inputCat == 'enum') {
value = enum_?.[0]
} else if (inputCat == 'boolean') {
value = false
}
}
}
}
$: inputCat = computeInputCat(type, format, itemsType?.type, enum_, contentEncoding)
</script>
<div class="flex flex-col w-full min-w-[250px]">
<div>
{#if displayHeader}
<FieldHeader {label} {required} {type} {contentEncoding} {format} {itemsType} />
{/if}
{#if description}
<div class="text-sm italic pb-1">
{description}
</div>
{/if}
<div class="flex space-x-1">
{#if inputCat == 'number'}
{#if extra['min'] != undefined && extra['max'] != undefined}
<div class="flex w-full gap-1">
<span>{extra['min']}</span>
<div class="grow">
<Range bind:value min={extra['min']} max={extra['max']} />
</div>
<span>{extra['max']}</span>
<span class="mx-2"><Badge large color="blue">{value}</Badge></span>
</div>
{:else}
<input
on:focus={(e) => {
window.dispatchEvent(new Event('pointerup'))
dispatch('focus')
}}
type="number"
class={valid
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}
placeholder={defaultValue ?? ''}
bind:value
min={extra['min']}
max={extra['max']}
on:input={() => dispatch('input', { value, isRaw: true })}
/>
{/if}
{:else if inputCat == 'boolean'}
<Toggle
on:pointerdown={(e) => {
e?.stopPropagation()
window.dispatchEvent(new Event('pointerup'))
}}
class={valid
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}
bind:checked={value}
/>
{#if type == 'boolean' && value == undefined}
<span>&nbsp; Not set</span>
{/if}
{:else if inputCat == 'list'}
<div>
<div>
{#each value ?? [] as v, i}
<div class="flex flex-row max-w-md mt-1">
{#if itemsType?.type == 'number'}
<input type="number" bind:value={v} />
{:else if itemsType?.type == 'string' && itemsType?.contentEncoding == 'base64'}
<input
type="file"
class="my-6"
on:change={(x) => fileChanged(x, (val) => (value[i] = val))}
multiple={false}
/>
{:else}
<input type="text" bind:value={v} />
{/if}
<Button
variant="border"
color="red"
size="sm"
btnClasses="mx-6"
on:click={() => {
value = value.filter((el) => el != v)
if (value.length == 0) {
value = undefined
}
}}
>
<Icon data={faMinus} />
</Button>
</div>
{/each}
</div>
<Button
variant="border"
color="blue"
size="sm"
btnClasses="mt-1"
on:click={() => {
if (value == undefined || !Array.isArray(value)) {
value = []
}
value = value.concat('')
}}
>
<Icon data={faPlus} class="mr-2" />
Add item
</Button>
<span class="ml-2">
{(value ?? []).length} item{(value ?? []).length > 1 ? 's' : ''}
</span>
</div>
{:else if inputCat == 'resource-object'}
<ObjectResourceInput {format} bind:value />
{:else if inputCat == 'object'}
{#if properties && Object.keys(properties).length > 0}
<div class="p-4 pl-8 border rounded w-full">
<LightweightSchemaForm
schema={{ properties, $schema: '', required: [], type: 'object' }}
bind:args={value}
/>
</div>
{:else}
<textarea
bind:this={el}
on:focus={(e) => {
window.dispatchEvent(new Event('pointerup'))
dispatch('focus')
}}
use:autosize
style="max-height: {maxHeight}"
on:input={() => {
dispatch('input', { rawValue: value, isRaw: false })
}}
class="col-span-10 {valid
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}"
placeholder={defaultValue ? JSON.stringify(defaultValue, null, 4) : ''}
bind:value={rawValue}
/>
{/if}
{:else if inputCat == 'enum'}
<select
on:focus={(e) => {
window.dispatchEvent(new Event('pointerup'))
dispatch('focus')
}}
class="px-6"
bind:value
>
{#each enum_ ?? [] as e}
<option>{e}</option>
{/each}
</select>
{:else if inputCat == 'date'}
<input class="inline-block" type="datetime-local" bind:value />
{:else if inputCat == 'base64'}
<input
type="file"
class="my-6"
on:change={(x) => fileChanged(x, (val) => (value = val))}
multiple={false}
/>
{:else if inputCat == 'resource-string'}
<ResourcePicker
bind:value
resourceType={format.split('-').length > 1
? format.substring('resource-'.length)
: undefined}
/>
{:else if inputCat == 'string'}
<div class="flex flex-col w-full">
<div class="flex flex-row w-full items- justify-between">
<textarea
rows="1"
bind:this={el}
on:focus={(e) => {
window.dispatchEvent(new Event('pointerup'))
dispatch('focus')
}}
on:blur={() => dispatch('blur')}
use:autosize
type="text"
class="col-span-10 {valid
? ''
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}"
placeholder={defaultValue ?? ''}
bind:value
on:input={() => {
dispatch('input', { rawValue: value, isRaw: false })
}}
/>
</div>
</div>
{/if}
<slot name="actions" />
</div>
</div>
</div>

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import LightweightArgInput from './LightweightArgInput.svelte'
export let schema: Schema
export let args: Record<string, any> | undefined = undefined
$: if (args === undefined) {
args = {}
}
</script>
<div class="w-full">
{#each Object.keys(schema.properties ?? {}) as argName, i (argName)}
<div>
{#if typeof args == 'object' && schema?.properties[argName] && args}
<LightweightArgInput
label={argName}
description={schema.properties[argName].description}
bind:value={args[argName]}
type={schema.properties[argName].type}
required={schema.required.includes(argName)}
pattern={schema.properties[argName].pattern}
defaultValue={schema.properties[argName].default}
enum_={schema.properties[argName].enum}
format={schema.properties[argName].format}
contentEncoding={schema.properties[argName].contentEncoding}
properties={schema.properties[argName].properties}
itemsType={schema.properties[argName].items}
extra={schema.properties[argName]}
/>
{/if}
</div>
{/each}
</div>

View File

@@ -60,6 +60,7 @@
if (testJob && !testJob.canceled && testJob.type == 'CompletedJob' && `result` in testJob) {
if ($flowStateStore[mod.id]?.previewResult) {
$flowStateStore[mod.id].previewResult = testJob.result
$flowStateStore = $flowStateStore
}
}
}

View File

@@ -200,6 +200,10 @@
}
function initPath() {
if (path != undefined && path != '') {
meta = pathToMeta(path)
return
}
if (initialPath == undefined || initialPath == '') {
reset()
} else {

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { VariableService, type InputTransform } from '$lib/gen'
import { VariableService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { allTrue } from '$lib/utils'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
@@ -10,7 +10,7 @@
import VariableEditor from './VariableEditor.svelte'
export let schema: Schema
export let args: Record<string, InputTransform | any> = {}
export let args: Record<string, any> = {}
export let disabledArgs: string[] = []
export let disabled = false

View File

@@ -230,77 +230,79 @@
<!-- metadata -->
{#if step === 1}
<CenteredPage>
<h2 class="border-b pb-1 mt-8 mb-2">Path</h2>
<Path
bind:this={pathC}
bind:error={pathError}
bind:path={script.path}
{initialPath}
on:enter={() => changeStep(2)}
namePlaceholder="script"
kind="script"
/>
<h2 class="border-b pb-1 mt-12 mb-4">Summary</h2>
<input
type="text"
bind:this={summaryC}
bind:value={script.summary}
placeholder="Short summary to be displayed when listed"
/>
<h2 class="border-b pb-1 mt-12 mb-6">Language</h2>
{#if lockedLanguage}
<div class="text-sm text-gray-600 italic mb-2">
As a forked script, the language '{script.language}' cannot be modified.
</div>
{/if}
<div class="flex flex-row gap-2 flex-wrap">
{#each langs as [label, lang]}
{@const isPicked = script.language == lang && template == 'script'}
<SettingSection title="Path" element="h2">
<Path
bind:this={pathC}
bind:error={pathError}
bind:path={script.path}
{initialPath}
on:enter={() => changeStep(2)}
namePlaceholder="script"
kind="script"
/>
</SettingSection>
<SettingSection title="Summary" element="h2">
<input
type="text"
bind:this={summaryC}
bind:value={script.summary}
placeholder="Short summary to be displayed when listed"
/>
</SettingSection>
<SettingSection title="Language" element="h2">
{#if lockedLanguage}
<div class="text-sm text-gray-600 italic mb-2">
As a forked script, the language '{script.language}' cannot be modified.
</div>
{/if}
<div class="flex flex-row gap-2 flex-wrap">
{#each langs as [label, lang]}
{@const isPicked = script.language == lang && template == 'script'}
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'dark'}
btnClasses={isPicked ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = lang
template = 'script'
initContent(lang, script.kind, template)
}}
disabled={lockedLanguage}
>
<LanguageIcon {lang} />
<span class="ml-2 py-2">{label}</span>
</Button>
{/each}
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'dark'}
btnClasses={isPicked ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = lang
template = 'script'
initContent(lang, script.kind, template)
}}
color={template == 'pgsql' ? 'blue' : 'dark'}
btnClasses={template == 'pgsql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
disabled={lockedLanguage}
on:click={() => {
script.language = Script.language.DENO
template = 'pgsql'
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon {lang} />
<span class="ml-2 py-2">{label}</span>
<LanguageIcon lang="pgsql" /><span class="ml-2 py-2">PostgreSQL</span>
</Button>
{/each}
<Button
size="sm"
variant="border"
color={template == 'pgsql' ? 'blue' : 'dark'}
btnClasses={template == 'pgsql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
disabled={lockedLanguage}
on:click={() => {
script.language = Script.language.DENO
template = 'pgsql'
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon lang="pgsql" /><span class="ml-2 py-2">PostgreSQL</span>
</Button>
<!-- <Button
size="sm"
variant="border"
color={template == 'mysql' ? 'blue' : 'dark'}
btnClasses={template == 'mysql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = Script.language.DENO
template = 'mysql'
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon lang="mysql" /><span class="ml-2 py-2">MySQL</span>
</Button> -->
</div>
<!-- <Button
size="sm"
variant="border"
color={template == 'mysql' ? 'blue' : 'dark'}
btnClasses={template == 'mysql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = Script.language.DENO
template = 'mysql'
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon lang="mysql" /><span class="ml-2 py-2">MySQL</span>
</Button> -->
</div>
</SettingSection>
<SettingSection
title="Script kind"
element="h3"

View File

@@ -5,7 +5,7 @@
import Tooltip from './Tooltip.svelte'
export let title: string
export let tooltip: string
export let tooltip: string = ''
export let element: `h${IntRange<1, 6>}` = 'h2'
export let accordion = false
let showContent = !accordion
@@ -14,25 +14,29 @@
<div class="border-b [&:has(button:hover)]:border-gray-400 duration-200 pb-1 mt-8 mb-2">
{#if accordion}
<button
class="flex w-full justify-between items-center"
class="flex w-full justify-start items-center"
on:click={() => (showContent = !showContent)}
>
<span class="rounded-full hover:bg-gray-100 focus:bg-gray-100 p-1 mr-1">
<ChevronDown size={22} class="rotate-0 duration-300 {showContent ? '!-rotate-180' : ''}" />
</span>
<svelte:element this={element}>
{title}
<Tooltip scale={0.9} class="mb-0.5">
{tooltip}
</Tooltip>
{#if tooltip}
<Tooltip scale={0.9} class="mb-0.5">
{tooltip}
</Tooltip>
{/if}
</svelte:element>
<span class="rounded-full hover:bg-gray-100 focus:bg-gray-100 p-1">
<ChevronDown size={22} class="rotate-0 duration-300 {showContent ? '!rotate-180' : ''}" />
</span>
</button>
{:else}
<svelte:element this={element}>
{title}
<Tooltip scale={0.9} class="mb-0.5">
{tooltip}
</Tooltip>
{#if tooltip}
<Tooltip scale={0.9} class="mb-0.5">
{tooltip}
</Tooltip>
{/if}
</svelte:element>
{/if}
</div>

View File

@@ -99,7 +99,7 @@
}
}
function format() {
export function format() {
if (editor) {
code = getCode()
editor.getAction('editor.action.formatDocument').run()

View File

@@ -577,6 +577,10 @@
})
}
export function focus() {
editor?.focus()
}
onMount(() => {
if (browser) {
loadMonaco()

View File

@@ -57,7 +57,8 @@
decryptSecret: false
})
can_write =
getV.workspace_id == $workspaceStore && canWrite(path, getV.extra_perms ?? {}, $userStore)
getV.workspace_id == $workspaceStore &&
canWrite(edit_path, getV.extra_perms ?? {}, $userStore)
variable = {
value: getV.value ?? '',

View File

@@ -11,6 +11,7 @@
import { loadIcon } from '../icon'
import { twMerge } from 'tailwind-merge'
import { goto } from '$app/navigation'
import { initOutput } from '../../editor/appUtils'
export let id: string
export let componentInput: AppInput | undefined
@@ -23,8 +24,7 @@
export let preclickAction: (() => Promise<void>) | undefined = undefined
export let customCss: ComponentCustomCSS<'button'> | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['loading', 'result']
export let initializing: boolean | undefined = true
const { worldStore, app } = getContext<AppViewerContext>('AppViewerContext')
@@ -62,10 +62,10 @@
}
}
$: outputs = $worldStore?.outputsById[id] as {
result: Output<Array<any>>
loading: Output<boolean>
}
let outputs = initOutput($worldStore, id, {
result: undefined,
loading: false
})
$: triggerOnAppLoad && runnableComponent?.runComponent()
@@ -74,6 +74,7 @@
}
$: outputs?.loading.subscribe({
id: 'loading-' + id,
next: (value) => {
isLoading = value
if (ownClick && !value) {
@@ -112,7 +113,12 @@
}
</script>
<InputValue {id} input={configuration.label} bind:value={labelValue} />
<InputValue
on:done={() => initializing && (initializing = false)}
{id}
input={configuration.label}
bind:value={labelValue}
/>
<InputValue {id} input={configuration.goto} bind:value={gotoUrl} />
<InputValue {id} input={configuration.color} bind:value={color} />
<InputValue {id} input={configuration.size} bind:value={size} />
@@ -131,7 +137,6 @@
<InputValue {id} input={configuration.gotoNewTab} bind:value={gotoNewTab} />
<RunnableWrapper
flexWrap
{recomputeIds}
bind:runnableComponent
{componentInput}

View File

@@ -47,6 +47,7 @@
}
$: outputs?.loading.subscribe({
id: 'loading-' + id,
next: (value) => {
isLoading = value
}

View File

@@ -26,7 +26,7 @@
export const staticOutputs: string[] = ['loading', 'result']
const { app, runnableComponents, worldStore } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
let labelValue: string = 'Default label'
let color: ButtonType.Color
@@ -57,6 +57,7 @@
}
$: outputs?.loading.subscribe({
id: 'loading-' + id,
next: (value) => {
isLoading = value
if (ownClick && !value) {

View File

@@ -18,6 +18,7 @@
import { concatCustomCss } from '../../utils'
import { getContext } from 'svelte'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import { initOutput } from '../../editor/appUtils'
export let id: string
export let componentInput: AppInput | undefined
@@ -26,8 +27,12 @@
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['loading', 'result']
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
initOutput($worldStore, id, {
result: undefined,
loading: false
})
ChartJS.register(
Title,
@@ -90,7 +95,7 @@
<InputValue {id} input={configuration.theme} bind:value={theme} />
<InputValue {id} input={configuration.line} bind:value={lineChart} />
<RunnableWrapper {render} flexWrap autoRefresh {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
<div class="w-full h-full {css?.container?.class ?? ''}" style={css?.container?.style ?? ''}>
{#if result}
{#if lineChart}

View File

@@ -2,6 +2,7 @@
import DisplayResult from '$lib/components/DisplayResult.svelte'
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import { initOutput } from '../../editor/appUtils'
import type { AppInput } from '../../inputType'
import {
IS_APP_PUBLIC_CONTEXT_KEY,
@@ -17,13 +18,16 @@
export let render: boolean
const requireHtmlApproval = getContext<boolean | undefined>(IS_APP_PUBLIC_CONTEXT_KEY)
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
let result: any = undefined
export const staticOutputs: string[] = ['result', 'loading']
initOutput($worldStore, id, {
result: undefined,
loading: false
})
</script>
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} {componentInput} {id} bind:initializing bind:result>
<div
class={twMerge(
'w-full border-b px-2 text-xs p-1 font-semibold bg-gray-500 text-white rounded-t-sm',

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { getContext } from 'svelte'
import { initOutput } from '../../editor/appUtils'
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
@@ -11,8 +12,12 @@
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['result', 'loading']
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
initOutput($worldStore, id, {
result: undefined,
loading: false
})
let result: string | undefined = undefined
let h: number | undefined = undefined
@@ -29,15 +34,7 @@
bind:clientHeight={h}
bind:clientWidth={w}
>
<RunnableWrapper
{render}
autoRefresh
flexWrap
{componentInput}
{id}
bind:initializing
bind:result
>
<RunnableWrapper {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
{#key result}
<iframe
frameborder="0"

View File

@@ -10,7 +10,6 @@
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = 'left'
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export let configuration: Record<string, AppInput>
export const staticOutputs: string[] = []
export let customCss: ComponentCustomCSS<'container' | 'icon'> | undefined = undefined
export let render: boolean

View File

@@ -11,7 +11,6 @@
export let id: string
export let configuration: Record<string, AppInput>
export const staticOutputs: string[] = ['loading']
export let customCss: ComponentCustomCSS<'image'> | undefined = undefined
export let render: boolean

View File

@@ -11,8 +11,7 @@
import { Vector as VectorLayer, Tile as TileLayer } from 'ol/layer'
import { Point } from 'ol/geom'
import { defaults as defaultControls } from 'ol/control'
import { findGridItem } from '../../editor/appUtils'
import type { Output } from '../../rx'
import { findGridItem, initOutput } from '../../editor/appUtils'
interface Marker {
lon: number
@@ -30,19 +29,18 @@
export let id: string
export let configuration: Record<string, AppInput>
export const staticOutputs: string[] = ['mapRegion']
export let customCss: ComponentCustomCSS<'map'> | undefined = undefined
export let render: boolean
const { app, worldStore, selectedComponent, connectingInput, focusedGrid, mode } =
getContext<AppViewerContext>('AppViewerContext')
$: outputs = $worldStore?.outputsById[id] as {
mapRegion: Output<{
topLeft: { lat: number; lon: number }
bottomRight: { lat: number; lon: number }
}>
}
let outputs = initOutput($worldStore, id, {
mapRegion: {
topLeft: { lat: 0, lon: 0 },
bottomRight: { lat: 0, lon: 0 }
}
})
let map: Map
let mapElement: HTMLDivElement
@@ -173,6 +171,7 @@
updateRegionOutput()
if (z) {
//@ts-ignore
gridItem.data.configuration.zoom.value = z
}
@@ -182,7 +181,9 @@
}
if (gridItem) {
//@ts-ignore
gridItem.data.configuration.longitude.value = center[0]
//@ts-ignore
gridItem.data.configuration.latitude.value = center[1]
}
}

View File

@@ -11,15 +11,20 @@
import { Button } from '../../../common'
import { Download, Loader2, MoveHorizontal, ZoomIn, ZoomOut } from 'lucide-svelte'
import { fade } from 'svelte/transition'
import { findGridItem } from '../../editor/appUtils'
import { findGridItem, initOutput } from '../../editor/appUtils'
export let id: string
export let configuration: Record<string, AppInput>
export const staticOutputs: string[] = ['loading']
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
export let render: boolean
const { app, mode, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
const { app, mode, worldStore, selectedComponent } =
getContext<AppViewerContext>('AppViewerContext')
initOutput($worldStore, id, {
result: undefined,
loading: false
})
let source: string | ArrayBuffer | undefined = undefined
let wrapper: HTMLDivElement | undefined = undefined
@@ -150,7 +155,9 @@
function syncZoomValue() {
const gridItem = findGridItem($app, id)
//@ts-ignore
if (gridItem && gridItem.data.configuration.zoom.value !== zoom) {
//@ts-ignore
gridItem.data.configuration.zoom.value = zoom
}

View File

@@ -17,6 +17,7 @@
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import { getContext } from 'svelte'
import { initOutput } from '../../editor/appUtils'
export let id: string
export let componentInput: AppInput | undefined
@@ -25,8 +26,12 @@
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['loading', 'result']
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
initOutput($worldStore, id, {
result: undefined,
loading: false
})
ChartJS.register(
Title,
@@ -73,7 +78,7 @@
<InputValue {id} input={configuration.theme} bind:value={theme} />
<InputValue {id} input={configuration.doughnutStyle} bind:value={doughnut} />
<RunnableWrapper {render} flexWrap autoRefresh {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
<div class="w-full h-full {css?.container?.class ?? ''}" style={css?.container?.style ?? ''}>
{#if result}
{#if doughnut}

View File

@@ -20,6 +20,7 @@
import { concatCustomCss } from '../../utils'
import { getContext } from 'svelte'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import { initOutput } from '../../editor/appUtils'
export let id: string
export let componentInput: AppInput | undefined
@@ -31,8 +32,12 @@
let zoomable = false
let pannable = false
export const staticOutputs: string[] = ['loading', 'result']
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
initOutput($worldStore, id, {
result: undefined,
loading: false
})
ChartJS.register(
Title,
@@ -79,7 +84,7 @@
<InputValue {id} input={configuration.zoomable} bind:value={zoomable} />
<InputValue {id} input={configuration.pannable} bind:value={pannable} />
<RunnableWrapper {render} flexWrap autoRefresh {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
<div class="w-full h-full {css?.container?.class ?? ''}" style={css?.container?.style ?? ''}>
{#if result}
<Scatter {data} {options} />

View File

@@ -8,9 +8,10 @@
import InputValue from '../helpers/InputValue.svelte'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import { twMerge } from 'tailwind-merge'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppEditorContext, AppViewerContext, ComponentCustomCSS } from '../../types'
import { getContext } from 'svelte'
import ResizeWrapper from '../helpers/ResizeWrapper.svelte'
import { initOutput } from '../../editor/appUtils'
import Tooltip from '$lib/components/Tooltip.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -21,14 +22,19 @@
export let customCss: ComponentCustomCSS<'text'> | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['result', 'loading']
const { app, worldStore, mode } = getContext<AppViewerContext>('AppViewerContext')
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { ontextfocus } = getContext<AppEditorContext>('AppEditorContext')
initOutput($worldStore, id, {
result: undefined,
loading: false
})
let result: string | undefined = undefined
let style: 'Title' | 'Subtitle' | 'Body' | 'Caption' | 'Label' | undefined = undefined
let copyButton: boolean
let fitContent = false
let tooltip: string = ''
function getComponent() {
switch (style) {
@@ -66,17 +72,27 @@
<InputValue {id} input={configuration.style} bind:value={style} />
<InputValue {id} input={configuration.copyButton} bind:value={copyButton} />
<InputValue {id} input={configuration.fitContent} bind:value={fitContent} />
<InputValue {id} input={configuration.tooltip} bind:value={tooltip} />
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<ResizeWrapper {id} shouldWrap={fitContent}>
<RunnableWrapper {render} {componentInput} {id} bind:initializing bind:result>
<div class="h-full w-full overflow-hidden">
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
{#if !result || result === ''}
<div class="text-gray-400 bg-gray-100 flex justify-center items-center h-full w-full">
No text
</div>
{:else}
<div class="flex flex-wrap gap-2 pb-0.5 overflow-x-auto">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="flex flex-wrap gap-2 pb-0.5 {$mode === 'dnd' && componentInput?.type == 'template'
? 'cursor-text'
: ''}"
on:click={() => {
if ($mode === 'dnd' && componentInput?.type == 'template') {
$ontextfocus?.()
}
}}
>
<svelte:element
this={component}
class={twMerge(
@@ -88,17 +104,26 @@
style={[$app.css?.['textcomponent']?.['text']?.style, customCss?.text?.style].join(';')}
>
{String(result)}
{#if tooltip != ''}
<Tooltip>{tooltip}</Tooltip>
{/if}
{#if copyButton && result}
<Popover notClickable>
<Button
variant="border"
size="xs"
color="dark"
btnClasses="!p-1"
on:click={() => copyToClipboard(result)}
>
<Clipboard size={14} strokeWidth={2} />
</Button>
<svelte:fragment slot="text">Copy to clipboard</svelte:fragment>
</Popover>
{/if}
</svelte:element>
{#if copyButton && result}
<Popover notClickable>
<Button size="xs" btnClasses="!px-2" on:click={() => copyToClipboard(result)}>
<Clipboard size={14} strokeWidth={2} />
</Button>
<svelte:fragment slot="text">Copy to clipboard</svelte:fragment>
</Popover>
{/if}
</div>
{/if}
</AlignWrapper>
</ResizeWrapper>
</div>
</RunnableWrapper>

View File

@@ -23,6 +23,7 @@
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import { getContext } from 'svelte'
import { concatCustomCss } from '../../utils'
import { initOutput } from '../../editor/appUtils'
export let id: string
export let componentInput: AppInput | undefined
@@ -31,8 +32,12 @@
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['loading', 'result']
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
initOutput($worldStore, id, {
result: undefined,
loading: false
})
let logarithmicScale = false
let zoomable = false
@@ -94,7 +99,7 @@
<InputValue {id} input={configuration.zoomable} bind:value={zoomable} />
<InputValue {id} input={configuration.pannable} bind:value={pannable} />
<RunnableWrapper {render} flexWrap autoRefresh {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
<div class="w-full h-full {css?.container?.class ?? ''}" style={css?.container?.style ?? ''}>
{#if result}
<Scatter {data} {options} />

View File

@@ -1,6 +1,8 @@
<script lang="ts">
import { onMount } from 'svelte'
import { getContext, onMount } from 'svelte'
import { initOutput } from '../../editor/appUtils'
import type { AppInput } from '../../inputType'
import type { AppViewerContext } from '../../types'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
export let id: string
@@ -9,7 +11,12 @@
export let initializing: boolean | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['result', 'loading']
const { worldStore } = getContext<AppViewerContext>('AppViewerContext')
initOutput($worldStore, id, {
result: undefined,
loading: false
})
let result: object | undefined = undefined
let divEl: HTMLDivElement | null = null
@@ -34,13 +41,13 @@
Plotly.newPlot(
divEl,
[result],
{ width: w, height: h, margin: { l: 40, r: 40, b: 40, t: 40, pad: 4 } },
{ width: w, height: h, margin: { l: 50, r: 40, b: 40, t: 40, pad: 4 } },
{ responsive: true, displayModeBar: false }
)
</script>
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} {componentInput} {id} bind:initializing bind:result>
<div on:pointerdown bind:this={divEl} />
</RunnableWrapper>
</div>

View File

@@ -1,6 +1,8 @@
<script lang="ts">
import { onMount } from 'svelte'
import { getContext, onMount } from 'svelte'
import { initOutput } from '../../editor/appUtils'
import type { AppInput } from '../../inputType'
import type { AppViewerContext } from '../../types'
import InputValue from '../helpers/InputValue.svelte'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
@@ -10,11 +12,16 @@
export let initializing: boolean | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['result', 'loading']
let result: object | undefined = undefined
let divEl: HTMLDivElement | null = null
const { worldStore } = getContext<AppViewerContext>('AppViewerContext')
initOutput($worldStore, id, {
result: undefined,
loading: false
})
let vegaEmbed
onMount(async () => {
if (divEl) {
@@ -53,7 +60,7 @@
<InputValue {id} input={configuration.canvas} bind:value={canvas} />
<div class="w-full h-full" bind:clientHeight={h} bind:clientWidth={w}>
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} {componentInput} {id} bind:initializing bind:result>
<div on:pointerdown bind:this={divEl} />
</RunnableWrapper>
</div>

View File

@@ -12,6 +12,7 @@
import { isObject } from '$lib/utils'
import Alert from '$lib/components/common/alert/Alert.svelte'
import { initOutput } from '$lib/components/apps/editor/appUtils'
export let id: string
export let componentInput: AppInput | undefined
@@ -19,12 +20,17 @@
export let initializing: boolean | undefined = undefined
export let render: boolean
export const staticOutputs: string[] = ['selectedRow', 'loading', 'result', 'selectedRowIndex']
let result: Record<string, any>[] | undefined = undefined
let result: Record<number, any>[] | undefined = undefined
const { worldStore, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
let outputs = initOutput($worldStore, id, {
selectedRowIndex: 0,
selectedRow: {},
result: [] as Record<number, any>[],
loading: false
})
let selectedRowIndex = -1
function toggleRow(row: Record<string, any>, rowIndex: number) {
@@ -48,13 +54,7 @@
outputs &&
toggleRow({ original: result[0] }, 0)
$: outputs = $worldStore?.outputsById[id] as {
selectedRowIndex: Output<number>
selectedRow: Output<any>
result: Output<any>
}
$: outputs?.result?.set(result)
$: outputs?.result?.set(result ?? [])
let clientHeight
let clientWidth
@@ -81,7 +81,7 @@
<InputValue {id} input={configuration.pagination} bind:value={pagination} />
<InputValue {id} input={configuration.pageSize} bind:value={pageSize} />
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} {componentInput} {id} bind:initializing bind:result>
{#if Array.isArray(result) && result.every(isObject)}
<div
class="border border-gray-300 shadow-sm divide-y divide-gray-300 flex flex-col h-full"
@@ -109,7 +109,10 @@
</div>
{:else if result != undefined}
<Alert title="Parsing issues" type="error" size="xs">
The result should be an array of objects
The result should be an array of objects, received:
<pre class="overflow-auto">
{JSON.stringify(result)}
</pre>
</Alert>
{/if}
</RunnableWrapper>

View File

@@ -16,6 +16,7 @@
import type { ButtonComponent } from '../../../editor/component'
import { concatCustomCss } from '../../../utils'
import { twMerge } from 'tailwind-merge'
import { initOutput } from '$lib/components/apps/editor/appUtils'
export let id: string
export let componentInput: AppInput | undefined
@@ -27,14 +28,6 @@
| undefined = undefined
export let render: boolean
export const staticOutputs: string[] = [
'selectedRow',
'loading',
'result',
'search',
'selectedRowIndex'
]
type T = Record<string, any>
let result: Record<string, any>[] | undefined = undefined
@@ -57,11 +50,7 @@
let table = createSvelteTable(options)
const {
app,
worldStore,
staticOutputs: staticOutputsStore
} = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
let selectedRowIndex = -1
@@ -131,11 +120,13 @@
$: filteredResult && setOptions(filteredResult)
$: search === 'By Component' && (filteredResult = searchInResult(result ?? [], searchValue))
$: (search === 'By Runnable' || search === 'Disabled') && (filteredResult = result ?? [])
$: outputs = $worldStore?.outputsById[id] as {
selectedRowIndex: Output<number>
selectedRow: Output<any>
search: Output<string>
}
let outputs = initOutput($worldStore, id, {
selectedRowIndex: 0,
selectedRow: undefined,
loading: false,
result: [],
search: ''
})
function rerender() {
table = createSvelteTable(options)
@@ -151,7 +142,7 @@
<InputValue {id} input={configuration.search} bind:value={search} />
<RunnableWrapper {render} flexWrap {componentInput} {id} bind:initializing bind:result>
<RunnableWrapper {render} {componentInput} {id} bind:initializing bind:result>
{#if Array.isArray(result) && result.every(isObject)}
<div
class={twMerge(
@@ -243,7 +234,11 @@
{/each}
{#if actionButtons.length > 0}
<td class="p-2 " on:click={() => toggleRow(row, rowIndex)}>
<td
class="p-2"
on:keypress={() => toggleRow(row, rowIndex)}
on:click={() => toggleRow(row, rowIndex)}
>
<div class="center-center h-full w-full flex-wrap gap-1">
{#each actionButtons as actionButton, actionIndex (actionIndex)}
{#if rowIndex == 0}
@@ -256,7 +251,6 @@
}}
extraQueryParams={{ row: row.original }}
bind:componentInput={actionButton.componentInput}
bind:staticOutputs={$staticOutputsStore[actionButton.id]}
/>
{:else}
<AppButton
@@ -290,7 +284,10 @@
</div>
{:else if result != undefined}
<Alert title="Parsing issues" type="error" size="xs">
The result should be an array of objects
The result should be an array of objects. Received:
<pre class="overflow-auto">
{JSON.stringify(result)}
</pre>
</Alert>
{/if}
</RunnableWrapper>

View File

@@ -1,11 +1,13 @@
<script lang="ts">
import { getContext } from 'svelte'
import { initOutput } from '../../editor/appUtils'
import type {
ConnectedAppInput,
RowAppInput,
StaticAppInput,
UserAppInput
} from '../../inputType'
import type { InlineScript } from '../../types'
import type { AppViewerContext, InlineScript } from '../../types'
import RunnableComponent from './RunnableComponent.svelte'
export let id: string
@@ -16,7 +18,12 @@
let result: any = undefined
export const staticOutputs: string[] = ['result', 'loading']
const { worldStore } = getContext<AppViewerContext>('AppViewerContext')
initOutput($worldStore, id, {
result: undefined,
loading: false
})
</script>
<RunnableComponent

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { isCodeInjection } from '$lib/components/flows/utils'
import { deepEqual } from 'fast-equals'
import { getContext } from 'svelte'
import { createEventDispatcher, getContext } from 'svelte'
import type { AppInput, EvalAppInput, UploadAppInput } from '../../inputType'
import type { AppViewerContext } from '../../types'
import { accessPropertyByPath } from '../../utils'
@@ -9,12 +9,20 @@
type T = string | number | boolean | Record<string | number, any> | undefined
export let input: AppInput
export let input: AppInput | undefined
export let value: T
export let id: string | undefined = undefined
export let error: string = ''
export let extraContext: Record<string, any> = {}
const { componentControl } = getContext<AppViewerContext>('AppViewerContext')
const dispatch = createEventDispatcher()
if (input == undefined) {
dispatch('done')
}
let lastInput = input ? JSON.parse(JSON.stringify(input)) : undefined
$: if (input && !deepEqual(input, lastInput)) {
@@ -27,7 +35,7 @@
const { worldStore, state, mode } = getContext<AppViewerContext>('AppViewerContext')
$: stateId = $worldStore?.state
$: stateId = $worldStore?.stateId
let timeout: NodeJS.Timeout | undefined = undefined
const debounce_ms = 50
@@ -43,16 +51,19 @@
lastInput.type == 'template' &&
$stateId &&
$state &&
debounce(async () => (value = await getValue(lastInput)))
debounce(async () => {
value = await getValue(lastInput)
dispatch('done')
})
$: lastInput &&
lastInput.type == 'eval' &&
$stateId &&
state &&
$state &&
debounce(async () => (value = await evalExpr(lastInput)))
async function handleConnection() {
if (lastInput.type === 'connected') {
$worldStore?.connect<any>(lastInput, onValueChange)
$worldStore?.connect<any>(lastInput, onValueChange, id)
} else if (lastInput.type === 'static' || lastInput.type == 'template') {
value = await getValue(lastInput)
} else if (lastInput.type == 'eval') {
@@ -62,6 +73,7 @@
} else {
value = undefined
}
dispatch('done')
}
async function evalExpr(input: EvalAppInput) {
@@ -71,7 +83,9 @@
computeGlobalContext($worldStore, id, extraContext),
true,
$state,
$mode == 'dnd'
$mode == 'dnd',
$componentControl,
$worldStore
)
error = ''
return r
@@ -89,7 +103,9 @@
computeGlobalContext($worldStore, id, extraContext),
true,
$state,
$mode == 'dnd'
$mode == 'dnd',
$componentControl,
$worldStore
)
error = ''
return r

View File

@@ -15,7 +15,7 @@
// Sync the result to the output
const { worldStore } = getContext<AppViewerContext>('AppViewerContext')
$: outputs = $worldStore?.outputsById[id] as {
$: outputs = $worldStore?.outputsById?.[id] as {
loading: Output<boolean>
result: Output<any>
}
@@ -25,7 +25,7 @@
}
function setOutput(v: any) {
outputs.result?.set(v, true)
outputs?.result?.set(v, true)
}
$: result && outputs && setOutput(result)

View File

@@ -14,6 +14,7 @@
}
let loading = false
$: $worldStore?.outputsById[componentId]?.['loading']?.subscribe({
id: 'refresh-' + componentId,
next: (value) => {
loading = value
}

View File

@@ -5,7 +5,7 @@
export let id: string
export let shouldWrap: boolean = false
const { app, breakpoint } = getContext<AppViewerContext>('AppViewerContext')
const { app, breakpoint, mode } = getContext<AppViewerContext>('AppViewerContext')
$: gridItem = findGridItem($app, id)
@@ -16,13 +16,12 @@
const wrapperHeight = wrapper.getBoundingClientRect().height
const width = $breakpoint === 'sm' ? 3 : 12
gridItem[width].h = Math.ceil(wrapperHeight / 36)
gridItem = gridItem
}
}
</script>
{#if shouldWrap}
<div bind:this={wrapper}>
{#if shouldWrap && $mode !== 'preview'}
<div class="h-full w-full" bind:this={wrapper}>
<slot />
</div>
{:else}

View File

@@ -2,6 +2,7 @@
import { goto } from '$app/navigation'
import type { Schema } from '$lib/common'
import Alert from '$lib/components/common/alert/Alert.svelte'
import LightweightSchemaForm from '$lib/components/LightweightSchemaForm.svelte'
import Popover from '$lib/components/Popover.svelte'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
@@ -24,7 +25,6 @@
export let autoRefresh: boolean = true
export let result: any = undefined
export let forceSchemaDisplay: boolean = false
export let flexWrap = false
export let wrapperClass = ''
export let wrapperStyle = ''
export let initializing: boolean | undefined = undefined
@@ -45,7 +45,8 @@
errorByComponent,
mode,
stateId,
state
state,
componentControl
} = getContext<AppViewerContext>('AppViewerContext')
$: autoRefresh && handleAutorefresh()
@@ -95,20 +96,24 @@
refreshIfAutoRefresh()
}
$: fields && (lazyStaticValues = computeStaticValues())
$: (runnableInputValues || extraQueryParams || args) && testJobLoader && refreshIfAutoRefresh()
$: runnable?.type == 'runnableByName' &&
runnable.inlineScript?.language == 'frontend' &&
($stateId || $state) &&
refreshIfAutoRefresh()
function refreshIfAutoRefresh() {
if (autoRefresh) {
setDebouncedExecute()
}
}
$: fields && (lazyStaticValues = computeStaticValues())
$: (runnableInputValues || extraQueryParams || args) && testJobLoader && refreshIfAutoRefresh()
// Test job internal state
let testJob: CompletedJob | undefined = undefined
let testJobLoader: TestJobLoader | undefined = undefined
$: outputs = initOutput($worldStore, id, { result: undefined, loading: false })
let outputs = initOutput($worldStore, id, { result: undefined, loading: false })
$: outputs?.loading?.set(testIsLoading)
$: schemaStripped = stripSchema(fields, $stateId)
@@ -138,16 +143,6 @@
return schemaStripped as Schema
}
$: disabledArgs = Object.keys(fields ?? {}).reduce(
(disabledArgsAccumulator: string[], inputName: string) => {
if (fields[inputName].type === 'static') {
disabledArgsAccumulator = [...disabledArgsAccumulator, inputName]
}
return disabledArgsAccumulator
},
[]
)
async function executeComponent(noToast = false) {
if (runnable?.type === 'runnableByName' && runnable.inlineScript?.language === 'frontend') {
outputs?.loading?.set(true)
@@ -157,7 +152,9 @@
computeGlobalContext($worldStore, id, {}),
false,
$state,
$mode == 'dnd'
$mode == 'dnd',
$componentControl,
$worldStore
)
setResult(r)
$state = $state
@@ -293,14 +290,7 @@
<div class="h-full flex relative flex-row flex-wrap {wrapperClass}" style={wrapperStyle}>
{#if schemaStripped && Object.keys(schemaStripped?.properties ?? {}).length > 0 && (autoRefresh || forceSchemaDisplay)}
<div class="px-2 h-fit min-h-0">
<SchemaForm
{flexWrap}
schema={schemaStripped}
bind:args
{disabledArgs}
shouldHideNoInputs
noVariablePicker
/>
<LightweightSchemaForm schema={schemaStripped} bind:args />
</div>
{/if}
@@ -344,6 +334,4 @@
</div>
{/if}
</div>
{:else}
<div class="w-full h-full" />
{/if}

View File

@@ -15,7 +15,6 @@
export let autoRefresh: boolean = true
export let runnableComponent: RunnableComponent | undefined = undefined
export let forceSchemaDisplay: boolean = false
export let flexWrap = false
export let runnableClass = ''
export let runnableStyle = ''
export let goto: string | undefined = undefined
@@ -48,7 +47,6 @@
{recomputeIds}
gotoUrl={goto}
{gotoNewTab}
{flexWrap}
bind:this={runnableComponent}
fields={componentInput.fields}
bind:result

View File

@@ -1,4 +1,3 @@
import { goto } from '$app/navigation'
import { sendUserToast } from '$lib/utils'
import type { World } from '../../rx'
@@ -10,7 +9,7 @@ export function computeGlobalContext(
return {
...Object.fromEntries(
Object.entries(world?.outputsById ?? {})
.filter(([k, _]) => k != id)
.filter(([k, _]) => k != id && k != 'state')
.map(([key, value]) => {
return [
key,
@@ -24,7 +23,7 @@ export function computeGlobalContext(
function create_context_function_template(eval_string, context, noReturn: boolean) {
return `
return async function (context, state, goto) {
return async function (context, state, goto, setTab) {
"use strict";
${
Object.keys(context).length > 0
@@ -40,30 +39,50 @@ function make_context_evaluator(
eval_string,
context,
noReturn: boolean
): (context, state, goto) => Promise<any> {
): (context, state, goto, setTab) => Promise<any> {
let template = create_context_function_template(eval_string, context, noReturn)
let functor = Function(template)
return functor()
}
export async function eval_like(
text,
text: string,
context = {},
noReturn: boolean,
state: any,
editor: boolean
state: Record<string, any>,
editor: boolean,
controlComponents: Record<string, { setTab?: (index: number) => void }>,
worldStore: World | undefined
) {
let evaluator = make_context_evaluator(text, context, noReturn)
return await evaluator(context, state, async (x, newTab) => {
if (newTab || editor) {
if (!newTab) {
sendUserToast(
'In editor mode, `goto` opens a new tab to prevent losing your work. To test the redirection , use the preview mode.'
)
const proxiedState = new Proxy(state, {
set(target, key, value) {
if (typeof key !== 'string') {
throw new Error('Invalid key')
}
window.open(x, '_blank')
} else {
await goto(x)
let o = worldStore?.newOutput('state', key, value)
o?.set(value)
target[key] = value
return true
}
})
let evaluator = make_context_evaluator(text, context, noReturn)
return await evaluator(
context,
proxiedState,
async (x, newTab) => {
if (newTab || editor) {
if (!newTab) {
sendUserToast(
'In editor mode, `goto` opens a new tab to prevent losing your work. To test the redirection , use the preview mode.'
)
}
window.open(x, '_blank')
} else {
await newTab(x)
}
},
(id, index) => {
controlComponents[id]?.setTab?.(index)
}
)
}

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import Toggle from '$lib/components/Toggle.svelte'
import { getContext } from 'svelte'
import { initOutput } from '../../editor/appUtils'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
@@ -17,17 +18,15 @@
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
export const staticOutputs: string[] = ['result']
let defaultValue: boolean | undefined = undefined
let labelValue: string = 'Default label'
// As the checkbox is a special case and has no input
// we need to manually set the output
$: outputs = $worldStore?.outputsById[id] as {
result: Output<boolean>
}
let outputs = initOutput($worldStore, id, {
result: false
})
$: defaultValue != undefined && outputs?.result.set(defaultValue)

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import { initOutput } from '../../editor/appUtils'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
@@ -12,7 +13,6 @@
export let configuration: Record<string, AppInput>
export let inputType: 'date'
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export const staticOutputs: string[] = ['result']
export let customCss: ComponentCustomCSS<'input'> | undefined = undefined
export let render: boolean
@@ -24,9 +24,9 @@
let value: string | undefined = undefined
$: outputs = $worldStore?.outputsById[id] as {
result: Output<string | undefined>
}
let outputs = initOutput($worldStore, id, {
result: undefined as string | undefined
})
$: handleDefault(defaultValue)

View File

@@ -2,6 +2,7 @@
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import { FileInput } from '../../../common'
import { initOutput } from '../../editor/appUtils'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
@@ -10,7 +11,6 @@
export let id: string
export let configuration: Record<string, AppInput>
export const staticOutputs: string[] = ['result']
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
export let render: boolean
@@ -20,9 +20,9 @@
let allowMultiple: boolean | undefined = undefined
let text: string | undefined = undefined
$: outputs = $worldStore?.outputsById[id] as {
result: Output<string[] | undefined>
}
let outputs = initOutput($worldStore, id, {
result: [] as string[] | undefined
})
// Receives Base64 encoded strings from the input component
async function handleChange(files: string[] | undefined) {

View File

@@ -8,8 +8,8 @@
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
import { SELECT_INPUT_DEFAULT_STYLE } from '../../../../defaults'
import { initOutput } from '../../editor/appUtils'
export const staticOutputs: string[] = ['result']
export let id: string
export let configuration: Record<string, AppInput>
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
@@ -22,9 +22,9 @@
let items: { label: string; value: string }[]
let placeholder: string = 'Select an item'
$: outputs = $worldStore?.outputsById[id] as {
result: Output<string[] | undefined>
}
let outputs = initOutput($worldStore, id, {
result: [] as string[]
})
// $: outputs && handleOutputs()

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import { initOutput } from '../../editor/appUtils'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
@@ -11,7 +12,6 @@
export let id: string
export let configuration: Record<string, AppInput>
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export const staticOutputs: string[] = ['result']
export let customCss: ComponentCustomCSS<'input'> | undefined = undefined
export let render: boolean
@@ -25,9 +25,9 @@
let max: number | undefined = undefined
let step = 1
$: outputs = $worldStore?.outputsById[id] as {
result: Output<number | undefined>
}
let outputs = initOutput($worldStore, id, {
result: undefined as number | undefined
})
$: handleDefault(defaultValue)

View File

@@ -8,11 +8,11 @@
import InputValue from '../helpers/InputValue.svelte'
import { concatCustomCss } from '../../utils'
import { twMerge } from 'tailwind-merge'
import { initOutput } from '../../editor/appUtils'
export let id: string
export let configuration: Record<string, AppInput>
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export const staticOutputs: string[] = ['result']
export let customCss: ComponentCustomCSS<'handles' | 'bar' | 'limits' | 'values'> | undefined =
undefined
export let render: boolean
@@ -25,9 +25,11 @@
let values: [number, number] = [0, 0]
$: outputs = $worldStore?.outputsById[id] as {
result: Output<[number, number] | null>
}
let outputs = initOutput($worldStore, id, {
result: null as [number, number] | null
})
console.log('reset')
$: outputs?.result.set(values)

View File

@@ -8,8 +8,8 @@
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
import { SELECT_INPUT_DEFAULT_STYLE } from '../../../../defaults'
import { initOutput } from '../../editor/appUtils'
export const staticOutputs: string[] = ['result']
export let id: string
export let configuration: Record<string, AppInput>
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
@@ -22,9 +22,9 @@
let items: { label: string; value: any; created?: boolean }[]
let placeholder: string = 'Select an item'
$: outputs = $worldStore?.outputsById[id] as {
result: Output<any | undefined>
}
let outputs = initOutput($worldStore, id, {
result: undefined
})
let value: string | undefined = outputs?.result.peak()

View File

@@ -8,11 +8,11 @@
import InputValue from '../helpers/InputValue.svelte'
import { twMerge } from 'tailwind-merge'
import { concatCustomCss } from '../../utils'
import { initOutput } from '../../editor/appUtils'
export let id: string
export let configuration: Record<string, AppInput>
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export const staticOutputs: string[] = ['result']
export let customCss: ComponentCustomCSS<'handle' | 'limits' | 'value' | 'bar'> | undefined =
undefined
export let render: boolean
@@ -23,9 +23,9 @@
let step = 1
let slider: HTMLElement
$: outputs = $worldStore?.outputsById[id] as {
result: Output<number | null>
}
let outputs = initOutput($worldStore, id, {
result: (0 as number) || null
})
let values: [number] = [outputs?.result.peak() ?? 0]

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import { initOutput } from '../../editor/appUtils'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
@@ -12,7 +13,6 @@
export let configuration: Record<string, AppInput>
export let inputType = 'text'
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export const staticOutputs: string[] = ['result']
export let customCss: ComponentCustomCSS<'input'> | undefined = undefined
export let appCssKey: 'textinputcomponent' | 'passwordinputcomponent' | 'emailinputcomponent' =
'textinputcomponent'
@@ -24,13 +24,13 @@
let defaultValue: string | undefined = undefined
let value: string | undefined = undefined
$: outputs = $worldStore?.outputsById[id] as {
result: Output<string | undefined>
}
let outputs = initOutput($worldStore, id, {
result: ''
})
$: handleDefault(defaultValue)
$: outputs?.result.set(value)
$: outputs?.result.set(value ?? '')
function handleDefault(defaultValue: string | undefined) {
value = defaultValue

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { initOutput } from '$lib/components/apps/editor/appUtils'
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import type { AppInput } from '../../../inputType'
@@ -12,12 +13,15 @@
export let id: string
export let configuration: Record<string, AppInput>
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export const staticOutputs: string[] = ['result']
export let customCss: ComponentCustomCSS<'input'> | undefined = undefined
export let render: boolean
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
initOutput($worldStore, id, {
result: undefined
})
let defaultValue: number | undefined = undefined
let isNegativeAllowed: boolean | undefined = undefined

View File

@@ -4,7 +4,6 @@
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS, GridItem } from '../../types'
import { concatCustomCss } from '../../utils'
import InputValue from '../helpers/InputValue.svelte'
export let id: string
export let configuration: Record<string, AppInput>
@@ -12,9 +11,6 @@
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
export let render: boolean
let noPadding: boolean | undefined = undefined
export const staticOutputs: string[] = []
const { app, focusedGrid, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
function onFocus() {
@@ -29,13 +25,10 @@
$: css = concatCustomCss($app.css?.containercomponent, customCss)
</script>
<InputValue {id} input={configuration.noPadding} bind:value={noPadding} />
<div>
<div class="w-full h-full">
{#if $app.subgrids?.[`${id}-0`]}
<SubGridEditor
visible={render}
{noPadding}
{id}
class={css?.container.class}
style={css?.container.style}

View File

@@ -21,7 +21,6 @@
const { app, focusedGrid, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
let gridContent: string[] | undefined = undefined
let noPadding: boolean | undefined = undefined
let drawerTitle: string | undefined = undefined
let appDrawer: Drawer
@@ -62,7 +61,6 @@
</div>
<InputValue {id} input={configuration.gridContent} bind:value={gridContent} />
<InputValue {id} input={configuration.noPadding} bind:value={noPadding} />
<InputValue {id} input={configuration.drawerTitle} bind:value={drawerTitle} />
<InputValue {id} input={configuration.label} bind:value={labelValue} />
<InputValue {id} input={configuration.color} bind:value={color} />
@@ -81,7 +79,6 @@
{#if $app.subgrids?.[`${id}-0`]}
<SubGridEditor
visible={open && render}
{noPadding}
{id}
class={css?.container.class}
style={css?.container.style}

View File

@@ -16,11 +16,8 @@
export let panes: number[]
export let render: boolean
const { app, focusedGrid, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
const { componentControl } = getContext<AppEditorContext>('AppEditorContext')
let noPadding: boolean | undefined = undefined
const { app, focusedGrid, selectedComponent, componentControl } =
getContext<AppViewerContext>('AppViewerContext')
function onFocus() {
$focusedGrid = {
@@ -66,8 +63,6 @@
}
</script>
<InputValue {id} input={configuration.noPadding} bind:value={noPadding} />
<div class="h-full w-full border" on:pointerdown|stopPropagation>
<Splitpanes {horizontal}>
{#each sumedup as paneSize, index (index)}
@@ -76,7 +71,6 @@
<SubGridEditor
visible={render}
noYPadding
{noPadding}
{id}
shouldHighlight={$focusedGrid?.subGridIndex === index}
class={css?.container.class}

View File

@@ -4,8 +4,7 @@
import { initOutput } from '../../editor/appUtils'
import SubGridEditor from '../../editor/SubGridEditor.svelte'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppEditorContext, AppViewerContext, ComponentCustomCSS } from '../../types'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import InputValue from '../helpers/InputValue.svelte'
@@ -17,22 +16,18 @@
| ComponentCustomCSS<'tabRow' | 'tabs' | 'selectedTab' | 'container'>
| undefined = undefined
export let render: boolean
export let initializing: boolean | undefined = configuration.tabsKind != undefined
export const staticOutputs: string[] = ['selectedTabIndex']
const { app, worldStore, focusedGrid, selectedComponent } =
const { app, worldStore, focusedGrid, selectedComponent, mode, componentControl } =
getContext<AppViewerContext>('AppViewerContext')
const { componentControl } = getContext<AppEditorContext>('AppEditorContext')
let selected: string = tabs[0]
let noPadding: boolean | undefined = undefined
let kind: 'tabs' | 'sidebar' | 'invisibleOnView' | undefined = undefined
let tabHeight: number = 0
$: outputs = $worldStore
? initOutput($worldStore, id, {
selectedTabIndex: 0
})
: undefined
let outputs = initOutput($worldStore, id, {
selectedTabIndex: 0
})
function handleTabSelection() {
const selectedIndex = tabs?.indexOf(selected)
@@ -78,6 +73,9 @@
return true
}
return false
},
setTab: (tab: number) => {
selected = tabs[tab]
}
}
@@ -86,40 +84,64 @@
$: selectedIndex = tabs?.indexOf(selected) ?? -1
$: css = concatCustomCss($app.css?.tabscomponent, customCss)
let subgridWidth: number | undefined = undefined
</script>
<InputValue {id} input={configuration.noPadding} bind:value={noPadding} />
<InputValue
on:done={() => initializing && (initializing = false)}
{id}
input={configuration.tabsKind}
bind:value={kind}
/>
<div>
<div bind:clientHeight={tabHeight}>
<Tabs bind:selected class={css?.tabRow?.class} style={css?.tabRow?.style}>
<div class={kind == 'sidebar' ? 'flex gap-4 w-full' : 'w-full'}>
{#if !kind || kind == 'tabs' || (kind == 'invisibleOnView' && $mode == 'dnd')}
<div bind:clientHeight={tabHeight}>
<Tabs bind:selected class={css?.tabRow?.class} style={css?.tabRow?.style}>
{#each tabs ?? [] as res}
<Tab
value={res}
class={css?.tabs?.class}
style={css?.tabs?.style}
selectedClass={css?.selectedTab?.class}
selectedStyle={css?.selectedTab?.style}
>
<span class="font-semibold">{res}</span>
</Tab>
{/each}
</Tabs>
</div>
{:else if kind == 'sidebar'}
<div
class="flex gap-y-2 flex-col w-1/6 max-w-[160px] bg-white text-[#2e3440] opacity-80 px-4 pt-4 border-r border-gray-400"
>
{#each tabs ?? [] as res}
<Tab
value={res}
class={css?.tabs?.class}
style={css?.tabs?.style}
selectedClass={css?.selectedTab?.class}
selectedStyle={css?.selectedTab?.style}
<button
class="rounded-sm !truncate text-sm hover:bg-gray-100 hover:border hover:text-black px-1 py-2 {selected ==
res
? 'outline outline-gray-500 outline-1 bg-white text-black'
: ''}"
on:pointerdown|stopPropagation
on:click={() => (selected = res)}>{res}</button
>
<span class="font-semibold">{res}</span>
</Tab>
{/each}
</Tabs>
</div>
{#if $app.subgrids}
{#each tabs ?? [] as res, i}
<SubGridEditor
{id}
visible={render && i === selectedIndex}
bind:subGrid={$app.subgrids[`${id}-${i}`]}
{noPadding}
class={css?.container?.class}
style={css?.container?.style}
containerHeight={componentContainerHeight - tabHeight}
on:focus={() => {
$selectedComponent = id
}}
/>
{/each}
</div>
{/if}
<div bind:clientWidth={subgridWidth} class="w-full">
{#if $app.subgrids}
{#each tabs ?? [] as res, i}<SubGridEditor
{id}
visible={render && i === selectedIndex}
bind:subGrid={$app.subgrids[`${id}-${i}`]}
class={css?.container?.class}
style={css?.container?.style}
containerHeight={componentContainerHeight - tabHeight}
on:focus={() => {
$selectedComponent = id
}}
/>{/each}
{/if}
</div>
</div>

View File

@@ -27,7 +27,8 @@
id={component.id}
shouldCapitalize={false}
bind:inputSpecs={component.componentInput.fields}
userInputEnabled={component.type !== 'buttoncomponent'}
userInputEnabled={component.type === 'formcomponent' ||
component.type === 'formbuttoncomponent'}
{resourceOnly}
/>
{/if}

View File

@@ -1,18 +0,0 @@
<script lang="ts">
import type { ConnectedAppInput, RowAppInput, StaticAppInput, UserAppInput } from '../inputType'
import type { AppComponent } from './component'
import InputsSpecsEditor from './settingsPanel/InputsSpecsEditor.svelte'
export let fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
export let component: AppComponent
export let resourceOnly: boolean = false
$: fields = resourceOnly ? fields : fields
</script>
<InputsSpecsEditor
id={component.id}
shouldCapitalize={false}
bind:inputSpecs={fields}
userInputEnabled={component.type !== 'buttoncomponent'}
/>

View File

@@ -49,8 +49,6 @@
export let fromHub: boolean = false
const appStore = writable<App>(app)
const worldStore = writable<World | undefined>(undefined)
const staticOutputs = writable<Record<string, string[]>>({})
const selectedComponent = writable<string | undefined>(undefined)
const mode = writable<EditorMode>(initialMode)
const breakpoint = writable<EditorBreakpoint>('lg')
@@ -66,10 +64,15 @@
const errorByComponent = writable<Record<string, { error: string; componentId: string }>>({})
const focusedGrid = writable<FocusedGrid | undefined>(undefined)
const pickVariableCallback: Writable<((path: string) => void) | undefined> = writable(undefined)
let context = {
email: $userStore?.email,
username: $userStore?.username,
query: Object.fromEntries($page.url.searchParams.entries()),
hash: $page.url.hash
}
setContext<AppViewerContext>('AppViewerContext', {
worldStore,
staticOutputs,
worldStore: buildWorld(context),
app: appStore,
summary: summaryStore,
selectedComponent,
@@ -89,13 +92,14 @@
focusedGrid,
stateId: writable(0),
parentWidth: writable(0),
state: writable({})
state: writable({}),
componentControl: writable({})
})
setContext<AppEditorContext>('AppEditorContext', {
history,
componentControl: writable({}),
pickVariableCallback
pickVariableCallback,
ontextfocus: writable(undefined)
})
let timeout: NodeJS.Timeout | undefined = undefined
@@ -119,19 +123,11 @@
mounted = true
})
let context = {
email: $userStore?.email,
username: $userStore?.username,
query: Object.fromEntries($page.url.searchParams.entries()),
hash: $page.url.hash
}
function hashchange(e: HashChangeEvent) {
context.hash = e.newURL.split('#')[1]
context = context
}
$: mounted && ($worldStore = buildWorld($staticOutputs, $worldStore, context))
$: previewing = $mode === 'preview'
$: width = $breakpoint === 'sm' ? 'min-w-[400px] max-w-[656px]' : 'min-w-[710px] w-full'
@@ -186,13 +182,13 @@
{:else}
<SplitPanesWrapper>
<Splitpanes class="max-w-full overflow-hidden">
<Pane size={$connectingInput?.opened ? 40 : 15} minSize={5} maxSize={33}>
<Pane size={15} minSize={5} maxSize={33}>
<ContextPanel />
</Pane>
<Pane size={64}>
<SplitPanesWrapper>
<Splitpanes horizontal>
<Pane size={$connectingInput?.opened ? 100 : 70}>
<Pane size={70}>
<div
on:pointerdown={(e) => {
$selectedComponent = undefined

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount, setContext } from 'svelte'
import { setContext } from 'svelte'
import { writable, type Writable } from 'svelte/store'
import { buildWorld, type World } from '../rx'
import { buildWorld } from '../rx'
import type {
App,
AppEditorContext,
@@ -10,11 +10,16 @@
EditorBreakpoint,
EditorMode
} from '../types'
import GridEditor from './GridEditor.svelte'
import { classNames } from '$lib/utils'
import type { Policy } from '$lib/gen'
import Button from '../../common/button/Button.svelte'
import { Unlock } from 'lucide-svelte'
import RecomputeAllComponents from './RecomputeAllComponents.svelte'
import GridViewer from './GridViewer.svelte'
import { Component } from './component'
import { twMerge } from 'tailwind-merge'
import { columnConfiguration } from '../gridUtils'
import { HiddenComponent } from '../components'
export let app: App
export let appPath: string
@@ -28,8 +33,6 @@
export let isLocked = false
const appStore = writable<App>(app)
const worldStore = writable<World | undefined>(undefined)
const staticOutputs = writable<Record<string, string[]>>({})
const selectedComponent = writable<string | undefined>(undefined)
const mode = writable<EditorMode>('preview')
@@ -41,9 +44,9 @@
const runnableComponents = writable<Record<string, () => Promise<void>>>({})
const parentWidth = writable(0)
setContext<AppViewerContext>('AppViewerContext', {
worldStore,
staticOutputs,
worldStore: buildWorld(context),
app: appStore,
summary: writable(summary),
selectedComponent,
@@ -62,20 +65,15 @@
openDebugRun: writable(undefined),
focusedGrid: writable(undefined),
stateId: writable(0),
parentWidth: writable(0),
state: writable({})
parentWidth,
state: writable({}),
componentControl: writable({})
})
setContext<AppEditorContext>('AppEditorContext', {
history: undefined,
componentControl: writable({}),
pickVariableCallback: writable(undefined)
})
let mounted = false
onMount(() => {
mounted = true
pickVariableCallback: writable(undefined),
ontextfocus: writable(undefined)
})
let ncontext = context
@@ -85,7 +83,6 @@
ncontext = ncontext
}
$: mounted && ($worldStore = buildWorld($staticOutputs, $worldStore, ncontext))
$: width = $breakpoint === 'sm' ? 'max-w-[640px]' : 'w-full '
$: lockedClasses = isLocked ? '!max-h-[400px] overflow-hidden pointer-events-none' : ''
</script>
@@ -101,9 +98,48 @@
>
{#if $appStore.grid}
<div class={classNames('mx-auto', width)}>
<GridEditor {policy} />
<div
class="w-full sticky top-0 flex justify-between border-b bg-gray-50 px-4 py-1 items-center gap-4"
style="z-index: 1000;"
>
<h2 class="truncate">{summary}</h2>
<RecomputeAllComponents />
<div class="text-2xs text-gray-600">
{policy.on_behalf_of ? `on behalf of ${policy.on_behalf_of_email}` : ''}
</div>
</div>
</div>
{/if}
<div
style={app.css?.['app']?.['grid']?.style}
class={twMerge('px-4 pt-4 pb-2 overflow-visible', app.css?.['app']?.['grid']?.class ?? '')}
bind:clientWidth={$parentWidth}
>
<div>
<GridViewer
onTopId={$selectedComponent}
items={app.grid}
let:dataItem
rowHeight={36}
cols={columnConfiguration}
gap={[4, 2]}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class={'h-full w-full center-center'}
on:pointerdown={() => ($selectedComponent = dataItem.data.id)}
>
<Component
render={true}
pointerdown={false}
component={dataItem.data}
selected={false}
locked={true}
/>
</div>
</GridViewer>
</div>
</div>
</div>
{#if isLocked}
@@ -119,3 +155,17 @@
</div>
{/if}
</div>
{#if app.hiddenInlineScripts}
{#each app.hiddenInlineScripts as script, index}
{#if script}
<HiddenComponent
id={`bg_${index}`}
inlineScript={script.inlineScript}
name={script.name}
fields={script.fields}
autoRefresh={script.autoRefresh ?? false}
/>
{/if}
{/each}
{/if}

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import { getContext, afterUpdate } from 'svelte'
import type { App, AppEditorContext, AppViewerContext, GridItem } from '../types'
import type { App, AppEditorContext, AppViewerContext } from '../types'
import { classNames } from '$lib/utils'
import { columnConfiguration, disableDrag, enableDrag, isFixed, toggleFixed } from '../gridUtils'
import { columnConfiguration, isFixed, toggleFixed } from '../gridUtils'
import { twMerge } from 'tailwind-merge'
import RecomputeAllComponents from './RecomputeAllComponents.svelte'
@@ -21,7 +21,6 @@
app,
mode,
connectingInput,
staticOutputs,
runnableComponents,
summary,
focusedGrid,
@@ -31,31 +30,6 @@
const { history } = getContext<AppEditorContext>('AppEditorContext')
// The drag is disabled when the user is connecting an input
// or when the user is previewing the app
// or when the focused grid is a subgrid
$: setAllDrags($mode === 'preview' || $connectingInput.opened)
function setAllDrags(enable: boolean) {
const fct = enable ? disableDrag : enableDrag
$app.grid.map((gridItem) => {
const disabledGridItem = fct(gridItem)
if (disabledGridItem?.data?.subGrids) {
disabledGridItem.data.subGrids = disabledGridItem.data.subGrids.map(
(subgrid: GridItem[]) => subgrid?.map((subgridItem: GridItem) => fct(subgridItem)) ?? []
)
}
return disabledGridItem
})
Object.values($app.subgrids ?? {}).map(
(subgrid: GridItem[]) => subgrid?.map((subgridItem: GridItem) => fct(subgridItem)) ?? []
)
}
function removeGridElement(component) {
if (component) {
$app.grid = $app.grid.filter((gridComponent) => {
@@ -83,10 +57,6 @@
return gridComponent.data.id !== component?.id
})
// Delete static inputs
delete $staticOutputs[component.id]
$staticOutputs = $staticOutputs
delete $runnableComponents[component.id]
$runnableComponents = $runnableComponents
@@ -95,12 +65,11 @@
}
function selectComponent(id: string) {
// Component selection is handled manually in the Map component (pointerdown
// event propagation is stopped to enable paning).
// Update the 'selectComponent()' function as well when this is updated.
if (!$connectingInput.opened) {
$selectedComponent = id
if ($selectedComponent !== id) {
$selectedComponent = id
}
$focusedGrid = undefined
}
}
@@ -169,64 +138,62 @@
<div class={!$focusedGrid && $mode !== 'preview' ? 'border-gray-400 border border-dashed' : ''}>
<Grid
onTopId={$selectedComponent}
fillSpace={false}
bind:items={$app.grid}
items={$app.grid}
on:redraw={(e) => {
push(history, $app)
$app.grid = e.detail
}}
let:dataItem
rowHeight={36}
cols={columnConfiguration}
fastStart={true}
gap={[4, 2]}
>
{#each $app.grid as gridComponent (gridComponent.id)}
{#if gridComponent?.data?.id && gridComponent?.data?.id === dataItem?.data?.id}
<!-- svelte-ignore a11y-click-events-have-key-events -->
{#if $connectingInput.opened}
<div
on:pointerenter={() => ($connectingInput.hoveredComponent = gridComponent.data.id)}
on:pointerleave={() => ($connectingInput.hoveredComponent = undefined)}
class="absolute w-full h-full bg-black border-2 bg-opacity-25 z-20 flex justify-center items-center"
/>
<div
style="transform: translate(-50%, -50%);"
class="absolute w-fit justify-center bg-indigo-500/90 left-[50%] top-[50%] z-50 px-6 rounded border text-white py-2 text-5xl center-center"
>
{dataItem.data.id}
</div>
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
on:click|stopPropagation
on:pointerdown={() => selectComponent(dataItem.data.id)}
class={classNames(
'h-full w-full center-center',
$selectedComponent === dataItem.data.id ? 'active-grid-item' : '',
gridComponent.data.card ? 'border border-gray-100' : ''
)}
>
<Component
render={true}
{pointerdown}
component={gridComponent.data}
selected={$selectedComponent === dataItem.data.id}
locked={isFixed(gridComponent)}
on:delete={() => removeGridElement(gridComponent.data)}
on:lock={() => {
const gridItem = findGridItem($app, gridComponent.data.id)
if (gridItem) {
toggleFixed(gridItem)
}
$app = $app
}}
on:expand={() => {
push(history, $app)
$selectedComponent = gridComponent.data.id
expandGriditem($app.grid, gridComponent, $breakpoint)
$app = $app
}}
/>
</div>
{/if}
{/each}
<!-- svelte-ignore a11y-click-events-have-key-events -->
{#if $connectingInput.opened}
<div
on:pointerenter={() => ($connectingInput.hoveredComponent = dataItem.id)}
on:pointerleave={() => ($connectingInput.hoveredComponent = undefined)}
class="absolute w-full h-full bg-black border-2 bg-opacity-25 z-20 flex justify-center items-center"
/>
<div
style="transform: translate(-50%, -50%);"
class="absolute w-fit justify-center bg-indigo-500/90 left-[50%] top-[50%] z-50 px-6 rounded border text-white py-2 text-5xl center-center"
>
{dataItem.data.id}
</div>
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
on:click|stopPropagation
on:pointerdown={() => selectComponent(dataItem.data.id)}
class={classNames(
'h-full w-full center-center',
$selectedComponent === dataItem.data.id ? 'active-grid-item' : ''
)}
>
<Component
render={true}
{pointerdown}
component={dataItem.data}
selected={$selectedComponent === dataItem.data.id}
locked={isFixed(dataItem)}
on:delete={() => removeGridElement(dataItem.data)}
on:lock={() => {
const gridItem = findGridItem($app, dataItem.data.id)
if (gridItem) {
toggleFixed(gridItem)
}
$app = $app
}}
on:expand={() => {
push(history, $app)
$selectedComponent = dataItem.data.id
expandGriditem($app.grid, dataItem, $breakpoint)
$app = $app
}}
/>
</div>
</Grid>
</div>
</div>
@@ -241,7 +208,6 @@
name={script.name}
fields={script.fields}
autoRefresh={script.autoRefresh ?? false}
bind:staticOutputs={$staticOutputs[`bg_${index}`]}
/>
{/if}
{/each}

View File

@@ -6,9 +6,11 @@
const { selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
export let gridItems: GridItem[]
export let parent: string | undefined
//$: sortedItems = gridItems.sort((a, b) => a.id.localeCompare - b.id)
</script>
{#each gridItems as gridItem, i (gridItem?.data?.id ?? i)}
{#each gridItems as gridItem (gridItem.id)}
{#if gridItem?.data?.id === $selectedComponent}
<ComponentPanel {parent} bind:component={gridItem.data} />
{:else if gridItem?.data?.type === 'tablecomponent'}
@@ -21,6 +23,7 @@
bind:component={actionButton}
duplicateMoveAllowed={false}
onDelete={() => {
//@ts-ignore
gridItem.data.actionButtons = gridItem.data.actionButtons.filter(
(c) => c.id !== actionButton.id
)

View File

@@ -0,0 +1,114 @@
<script lang="ts">
import { onMount, createEventDispatcher } from 'svelte'
import type { FilledItem } from '../svelte-grid/types'
import { getColumn, throttle } from '../svelte-grid/utils/other'
import { getContainerHeight } from '../svelte-grid/utils/container'
import { specifyUndefinedColumns } from '../svelte-grid/utils/item'
const dispatch = createEventDispatcher()
type T = $$Generic
export let items: FilledItem<T>[]
export let rowHeight: number
export let cols: [number, number][]
export let gap = [10, 10]
export let throttleUpdate = 100
export let onTopId: string | undefined = undefined
export let containerWidth: number | undefined = undefined
export let parentWidth: number | undefined = undefined
let getComputedCols
let container
$: [gapX, gapY] = gap
let xPerPx = 0
let yPerPx = rowHeight
$: containerHeight = getContainerHeight(items, yPerPx, getComputedCols)
const onResize = throttle(() => {
items = specifyUndefinedColumns(items, getComputedCols, cols)
dispatch('resize', {
cols: getComputedCols,
xPerPx,
yPerPx,
width: containerWidth
})
}, throttleUpdate)
onMount(() => {
const sizeObserver = new ResizeObserver((entries) => {
requestAnimationFrame(() => {
let width = entries[0].contentRect.width
if (width === containerWidth) return
getComputedCols = getColumn(parentWidth ?? width, cols)
xPerPx = width / getComputedCols
if (!containerWidth) {
items = specifyUndefinedColumns(items, getComputedCols, cols)
dispatch('mount', {
cols: getComputedCols,
xPerPx,
yPerPx // same as rowHeight
})
} else {
onResize()
}
containerWidth = width
})
})
sizeObserver.observe(container)
return () => sizeObserver.disconnect()
})
</script>
<div class="svlt-grid-container" style="height: {containerHeight}px" bind:this={container}>
{#if xPerPx}
{#each items as item (item.id)}
{@const onTop = item.id == onTopId}
{@const width =
Math.min(getComputedCols, item[getComputedCols] && item[getComputedCols].w) * xPerPx -
gapX * 2}
{@const height = (item[getComputedCols] && item[getComputedCols].h) * yPerPx - gapY * 2}
{@const top = (item[getComputedCols] && item[getComputedCols].y) * yPerPx + gapY}
{@const left = (item[getComputedCols] && item[getComputedCols].x) * xPerPx + gapX}
<div
class="svlt-grid-item"
style="width: {width}px; height:{height}px; {onTop
? 'z-index: 1000;'
: ''} top: {top}px; left: {left}px;"
>
{#if item[getComputedCols]}
<slot dataItem={item} item={item[getComputedCols]} />
{/if}
</div>
{/each}
{/if}
</div>
<style>
.svlt-grid-container {
position: relative;
width: 100%;
}
.svlt-grid-item {
touch-action: none;
position: absolute;
will-change: auto;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
</style>

View File

@@ -5,11 +5,13 @@
import { columnConfiguration, isFixed, toggleFixed } from '../gridUtils'
import type { AppEditorContext, AppViewerContext, GridItem } from '../types'
import Component from './component/Component.svelte'
import { expandGriditem, findGridItem } from './appUtils'
import { expandGriditem, findGridItem, sortGridItemsPosition } from './appUtils'
import { push } from '$lib/history'
import Grid from '../svelte-grid/Grid.svelte'
import GridViewer from './GridViewer.svelte'
export let containerHeight: number
export let containerWidth: number | undefined = undefined
let classes = ''
export { classes as class }
export let style = ''
@@ -48,18 +50,14 @@
function selectComponent(id: string) {
onComponent = id
if (!$connectingInput.opened) {
$selectedComponent = id
/*
$focusedGrid = {
parentComponentId: parentId,
subGridIndex: index
if (id !== $selectedComponent) {
$selectedComponent = id
}
*/
}
}
function lock(gridComponent: GridItem) {
let fComponent = findGridItem($app, gridComponent.data.id)
function lock(dataItem: GridItem) {
let fComponent = findGridItem($app, dataItem.data.id)
if (fComponent) {
fComponent = toggleFixed(fComponent)
}
@@ -71,7 +69,9 @@
</script>
<div
class="relative w-full subgrid {visible ? 'visible' : 'invisible h-0 overflow-hidden'} "
class="translate-x-0 translate-y-0 relative w-full subgrid {visible
? 'visible'
: 'invisible h-0 overflow-hidden'} "
bind:this={container}
>
<div
@@ -86,68 +86,94 @@
on:pointerup={onpointerup}
style="height: {containerHeight}px; {style ?? ''}"
>
<div class={highlight && $mode !== 'preview' ? 'border-gray-400 border border-dashed' : ''}>
<Grid
bind:items={subGrid}
{#if $mode !== 'preview'}
<div class={highlight ? 'border-gray-400 border border-dashed' : ''}>
<Grid
items={subGrid}
on:redraw={(e) => {
push(history, $app)
subGrid = e.detail
}}
let:dataItem
rowHeight={36}
cols={columnConfiguration}
fastStart={true}
gap={[4, 2]}
scroller={container}
parentWidth={$parentWidth - 17}
{containerWidth}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
{#if $connectingInput.opened}
<div
on:pointerenter={() => ($connectingInput.hoveredComponent = dataItem.data.id)}
on:pointerleave={() => ($connectingInput.hoveredComponent = undefined)}
class="absolute w-full h-full bg-black border-2 bg-opacity-25 z-20 flex justify-center items-center"
/>
<div
style="transform: translate(-50%, -50%);"
class="absolute w-fit justify-center bg-indigo-500/90 left-[50%] top-[50%] z-50 px-6 rounded border text-white py-2 text-5xl center-center"
>
{dataItem.data.id}
</div>
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
on:pointerdown={() => selectComponent(dataItem.data.id)}
class={classNames(
'h-full w-full center-center',
$selectedComponent === dataItem.data.id ? 'active-grid-item' : '',
'top-0'
)}
>
<Component
render={visible}
{pointerdown}
component={dataItem.data}
selected={$selectedComponent === dataItem.data.id}
locked={isFixed(dataItem)}
on:lock={() => lock(dataItem)}
on:expand={() => {
const parentGridItem = findGridItem($app, id)
if (!parentGridItem) {
return
}
$selectedComponent = dataItem.data.id
push(history, $app)
expandGriditem(subGrid, dataItem, $breakpoint, parentGridItem)
$app = $app
}}
/>
</div>
</Grid>
</div>
{:else}
<GridViewer
items={subGrid}
let:dataItem
rowHeight={36}
cols={columnConfiguration}
fastStart={true}
gap={[4, 2]}
scroller={container}
parentWidth={$parentWidth - 17}
{containerWidth}
>
{#each subGrid as gridComponent (gridComponent.id)}
{#if gridComponent?.data?.id && gridComponent?.data?.id === dataItem?.data?.id}
<!-- svelte-ignore a11y-click-events-have-key-events -->
{#if $connectingInput.opened}
<div
on:pointerenter={() => ($connectingInput.hoveredComponent = gridComponent.data.id)}
on:pointerleave={() => ($connectingInput.hoveredComponent = undefined)}
class="absolute w-full h-full bg-black border-2 bg-opacity-25 z-20 flex justify-center items-center"
/>
<div
style="transform: translate(-50%, -50%);"
class="absolute w-fit justify-center bg-indigo-500/90 left-[50%] top-[50%] z-50 px-6 rounded border text-white py-2 text-5xl center-center"
>
{dataItem.data.id}
</div>
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
on:pointerdown={() => selectComponent(dataItem.data.id)}
class={classNames(
'h-full w-full center-center',
$selectedComponent === dataItem.data.id ? 'active-grid-item' : '',
gridComponent.data.card ? 'border border-gray-100' : '',
'top-0'
)}
>
<Component
render={visible}
{pointerdown}
component={gridComponent.data}
selected={$selectedComponent === dataItem.data.id}
locked={isFixed(gridComponent)}
on:lock={() => lock(gridComponent)}
on:expand={() => {
const parentGridItem = findGridItem($app, id)
if (!parentGridItem) {
return
}
$selectedComponent = gridComponent.data.id
push(history, $app)
expandGriditem(subGrid, gridComponent, $breakpoint, parentGridItem)
$app = $app
}}
/>
</div>
{/if}
{/each}
</Grid>
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
on:pointerdown={() => selectComponent(dataItem.data.id)}
class={classNames('h-full w-full center-center', 'top-0')}
>
<Component
render={visible}
{pointerdown}
component={dataItem.data}
selected={$selectedComponent === dataItem.data.id}
locked={isFixed(dataItem)}
/>
</div>
</GridViewer>
{/if}
</div>
</div>

View File

@@ -1,10 +1,11 @@
import { getNextId } from '$lib/components/flows/flowStateUtils'
import type { App, EditorBreakpoint, FocusedGrid, GridItem } from '../types'
import type { App, ConnectingInput, EditorBreakpoint, FocusedGrid, GridItem } from '../types'
import { getRecommendedDimensionsByComponent, type AppComponent } from './component'
import { gridColumns } from '../gridUtils'
import { allItems } from '../utils'
import type { Output, World } from '../rx'
import gridHelp from '../svelte-grid/utils/helper'
import type { FilledItem } from '../svelte-grid/types'
function findGridItemById(
root: GridItem[],
@@ -34,6 +35,14 @@ export function findGridItemParentGrid(app: App, id: string): string | undefined
}
}
export function allsubIds(app: App, parentId: string): string[] {
let item = findGridItem(app, parentId)
if (!item?.data.numberOfSubgrids) {
return [parentId]
}
return getAllSubgridsAndComponentIds(app, item?.data)[1]
}
export function findGridItem(app: App, id: string): GridItem | undefined {
return findGridItemById(app.grid, app.subgrids, id)
}
@@ -294,15 +303,9 @@ export function initOutput<I extends Record<string, any>>(
if (!world) {
return {} as any
}
const output = world.outputsById[id] as Outputtable<I>
if (init) {
for (const key in init) {
if (output && output[key] && output[key].peak() == undefined) {
output[key].set(init[key] as any)
}
}
}
return output
return Object.fromEntries(
Object.entries(init).map(([key, value]) => [key, world.newOutput(id, key, value)])
) as Outputtable<I>
}
export function expandGriditem(
grid: GridItem[],
@@ -325,3 +328,80 @@ export function expandGriditem(
item.w = item.w + left + right
item.h = item.h + top + bottom
}
export function sortGridItemsPosition<T>(
gridItems: FilledItem<T>[],
width: number
): FilledItem<T>[] {
return gridItems.sort((a: FilledItem<T>, b: FilledItem<T>) => {
const aX = a[width].x
const aY = a[width].y
const bX = b[width].x
const bY = b[width].y
if (aY < bY) {
return -1
} else if (aY > bY) {
return 1
} else {
if (aX < bX) {
return -1
} else if (aX > bX) {
return 1
} else {
return 0
}
}
})
}
export function connectInput(
connectingInput: ConnectingInput,
componentId: string,
path: string
): ConnectingInput {
if (connectingInput) {
connectingInput = {
opened: false,
input: {
connection: {
componentId,
path
},
type: 'connected'
},
hoveredComponent: undefined
}
}
return connectingInput
}
export function recursivelyFilterKeyInJSON(
json: object,
search: string,
extraSearch?: string | undefined
): object {
if (!search || search == '') {
return json
}
let filteredJSON = {}
Object.keys(json).forEach((key) => {
if (
key.toLowerCase().includes(search.toLowerCase()) ||
extraSearch?.toLowerCase().includes(search.toLowerCase()) ||
(typeof json[key] === 'string' && json[key].toLowerCase().includes(search.toLowerCase())) ||
(typeof json[key] === 'number' &&
json[key].toString().toLowerCase().includes(search.toLowerCase()))
) {
filteredJSON[key] = json[key]
} else if (typeof json[key] === 'object') {
const res = recursivelyFilterKeyInJSON(json[key], search, extraSearch)
if (Object.keys(res).length !== 0) {
filteredJSON[key] = res
}
}
})
return filteredJSON
}

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import { getContext } from 'svelte'
import { fade } from 'svelte/transition'
import { Loader2 } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import type { AppViewerContext } from '../../types'
@@ -48,17 +47,12 @@
export let pointerdown: boolean = false
export let render: boolean
const { staticOutputs, mode, connectingInput, app, errorByComponent } =
const { mode, connectingInput, app, errorByComponent } =
getContext<AppViewerContext>('AppViewerContext')
let hover = false
let initializing: boolean | undefined = undefined
let componentContainerHeight: number = 0
// let component = JSON.parse(JSON.stringify({ conf }))
// $: if (!deepEqual(component, pComponent)) {
// component = JSON.parse(JSON.stringify(pComponent))
// }
$: componentWithErrors = Object.values($errorByComponent).map((e) => e.componentId)
$: hasError = componentWithErrors.includes(component.id)
</script>
@@ -66,7 +60,7 @@
<div
on:pointerenter={() => (hover = true)}
on:pointerleave={() => (hover = false)}
class="h-full flex flex-col w-full component"
class="h-full flex flex-col w-full component {initializing ? 'overflow-hidden h-0' : ''}"
>
{#if $mode !== 'preview'}
<ComponentHeader
@@ -82,18 +76,10 @@
{/if}
<div
on:pointerdown={(e) => {
// Removed in https://github.com/windmill-labs/windmill/pull/1171
// In case of a bug, try stopping propagation on the native event
// and dispatch a custom event: `e?.stopPropagation(); dispatch('select');`
// if ($mode === 'preview') {
// e?.stopPropagation()
// }
}}
class={twMerge(
'h-full bg-white/40',
selected && $mode !== 'preview' ? 'border border-blue-500' : '',
!selected && $mode !== 'preview' && !component.card ? 'border-gray-100' : '',
!selected && $mode !== 'preview' ? 'border-gray-100' : '',
$mode !== 'preview' && !$connectingInput.opened ? 'hover:border-blue-500' : '',
component.softWrap || hasError ? '' : 'overflow-auto',
$mode != 'preview' ? 'cursor-pointer' : '',
@@ -109,7 +95,6 @@
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'barchartcomponent'}
@@ -119,7 +104,6 @@
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'timeseriescomponent'}
@@ -129,7 +113,6 @@
configuration={component.configuration}
bind:initializing
componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'htmlcomponent'}
@@ -138,7 +121,6 @@
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'vegalitecomponent'}
@@ -147,7 +129,6 @@
id={component.id}
bind:initializing
componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'plotlycomponent'}
@@ -155,7 +136,6 @@
id={component.id}
bind:initializing
componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'scatterchartcomponent'}
@@ -165,7 +145,6 @@
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'piechartcomponent'}
@@ -174,7 +153,6 @@
id={component.id}
customCss={component.customCss}
bind:initializing
bind:staticOutputs={$staticOutputs[component.id]}
componentInput={component.componentInput}
{render}
/>
@@ -184,7 +162,6 @@
id={component.id}
customCss={component.customCss}
bind:initializing
bind:staticOutputs={$staticOutputs[component.id]}
componentInput={component.componentInput}
bind:actionButtons={component.actionButtons}
{render}
@@ -194,7 +171,6 @@
id={component.id}
configuration={component.configuration}
bind:initializing
bind:staticOutputs={$staticOutputs[component.id]}
componentInput={component.componentInput}
{render}
/>
@@ -207,7 +183,6 @@
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'buttoncomponent'}
@@ -218,8 +193,8 @@
configuration={component.configuration}
customCss={component.customCss}
componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
recomputeIds={component.recomputeIds}
bind:initializing
{render}
/>
{:else if component.type === 'selectcomponent' || component.type === 'resourceselectcomponent'}
@@ -229,7 +204,6 @@
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'multiselectcomponent'}
@@ -239,7 +213,6 @@
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'formcomponent'}
@@ -249,7 +222,6 @@
configuration={component.configuration}
customCss={component.customCss}
componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
recomputeIds={component.recomputeIds}
{render}
/>
@@ -261,7 +233,6 @@
configuration={component.configuration}
customCss={component.customCss}
componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
recomputeIds={component.recomputeIds}
{render}
/>
@@ -272,7 +243,6 @@
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'textinputcomponent'}
@@ -281,7 +251,6 @@
verticalAlignment={component.verticalAlignment}
configuration={component.configuration}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'emailinputcomponent'}
@@ -292,7 +261,6 @@
appCssKey="emailinputcomponent"
id={component.id}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'passwordinputcomponent'}
@@ -303,7 +271,6 @@
appCssKey="passwordinputcomponent"
id={component.id}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'dateinputcomponent'}
@@ -313,7 +280,6 @@
inputType="date"
id={component.id}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'numberinputcomponent'}
@@ -322,7 +288,6 @@
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'currencycomponent'}
@@ -331,7 +296,6 @@
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'slidercomponent'}
@@ -340,7 +304,6 @@
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'horizontaldividercomponent'}
@@ -369,7 +332,6 @@
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'tabscomponent'}
@@ -378,16 +340,15 @@
id={component.id}
tabs={component.tabs}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{componentContainerHeight}
{render}
bind:initializing
/>
{:else if component.type === 'containercomponent'}
<AppContainer
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{componentContainerHeight}
{render}
/>
@@ -417,7 +378,6 @@
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'fileinputcomponent'}
@@ -425,7 +385,6 @@
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'imagecomponent'}
@@ -433,7 +392,6 @@
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'drawercomponent'}
@@ -450,7 +408,6 @@
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{:else if component.type === 'pdfcomponent'}
@@ -458,17 +415,13 @@
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
{render}
/>
{/if}
</div>
</div>
{#if initializing}
<div
out:fade|local={{ duration: 200 }}
class="absolute inset-0 center-center flex-col bg-white text-gray-600 border"
>
<div class="absolute inset-0 center-center flex-col bg-white text-gray-600 border">
<Loader2 class="animate-spin" size={16} />
<span class="text-xs mt-1">Loading</span>
</div>

View File

@@ -1,60 +1,54 @@
<script lang="ts">
import { getContext } from 'svelte'
import {
deleteGridItem,
findGridItem,
findGridItemParentGrid,
getAllSubgridsAndComponentIds,
getGridItems,
insertNewGridItem
} from '../appUtils'
import type { AppEditorContext, AppViewerContext, EditorBreakpoint, GridItem } from '../../types'
import type { AppEditorContext, AppViewerContext, GridItem } from '../../types'
import { push } from '$lib/history'
import { sendUserToast } from '$lib/utils'
const { app, selectedComponent, breakpoint, focusedGrid } =
const { app, selectedComponent, worldStore, focusedGrid, componentControl } =
getContext<AppViewerContext>('AppViewerContext')
const { componentControl, history } = getContext<AppEditorContext>('AppEditorContext')
const { history } = getContext<AppEditorContext>('AppEditorContext')
let tempGridItem: GridItem | undefined = undefined
let copiedGridItem: GridItem | undefined = undefined
function getSortedGridItemsOfChildren(): GridItem[] {
if (!$focusedGrid) {
return sortGridItemsPosition($app.grid, $breakpoint)
return $app.grid
}
if (!$app.subgrids) {
return []
}
return sortGridItemsPosition(
$app.subgrids[`${$focusedGrid.parentComponentId}-${$focusedGrid.subGridIndex}`],
$breakpoint
)
return $app.subgrids[`${$focusedGrid.parentComponentId}-${$focusedGrid.subGridIndex}`] ?? []
}
function getSortedGridItemsOfGrid(): GridItem[] {
function getGridItems(): GridItem[] {
if ($app.grid.find((item) => item.id === $selectedComponent)) {
return sortGridItemsPosition($app.grid, $breakpoint)
return $app.grid
}
if (!$app.subgrids) {
return []
}
return sortGridItemsPosition(
return (
Object.values($app.subgrids ?? {}).find((grid) =>
grid.find((item) => item.id === $selectedComponent)
) ?? [],
$breakpoint
) ?? []
)
}
function left(event: KeyboardEvent) {
if (!$componentControl[$selectedComponent!]?.left?.()) {
const sortedGridItems = getSortedGridItemsOfGrid()
const sortedGridItems = getGridItems()
const currentIndex = sortedGridItems.findIndex((item) => item.id === $selectedComponent)
if (currentIndex !== -1 && currentIndex > 0) {
@@ -68,7 +62,7 @@
function right(event: KeyboardEvent) {
let r = $componentControl[$selectedComponent!]?.right?.()
if (!r) {
const sortedGridItems = getSortedGridItemsOfGrid()
const sortedGridItems = getGridItems()
const currentIndex = sortedGridItems.findIndex((item) => item.id === $selectedComponent)
if (currentIndex !== -1 && currentIndex < sortedGridItems.length - 1) {
@@ -91,10 +85,8 @@
return
}
const sortedGridItems = sortGridItemsPosition(subgrid, $breakpoint)
if (sortedGridItems) {
$selectedComponent = sortedGridItems[0].id
if (subgrid) {
$selectedComponent = subgrid[0].id
}
event.preventDefault()
}
@@ -109,6 +101,7 @@
function handleArrowUp(event: KeyboardEvent) {
if (!$selectedComponent) return
let parentId = findGridItemParentGrid($app, $selectedComponent)?.split('-')[0]
if (parentId) {
$selectedComponent = parentId
} else {
@@ -171,10 +164,16 @@
insertNewGridItem($app, copiedGridItem.data, $focusedGrid, false)
}
$worldStore = $worldStore
$app = $app
}
function keydown(event: KeyboardEvent) {
// Ignore keydown events if the user is typing in monaco
let classes = event.target?.['className']
if (typeof classes === 'string' && classes.includes('inputarea')) {
return
}
switch (event.key) {
case 'Escape':
handleEscape(event)
@@ -182,6 +181,7 @@
case 'ArrowUp': {
handleArrowUp(event)
break
}
case 'ArrowDown': {
@@ -221,31 +221,6 @@
break
}
}
function sortGridItemsPosition(gridItems: GridItem[], breakpoint: EditorBreakpoint): GridItem[] {
return gridItems.sort((a: GridItem, b: GridItem) => {
const width = breakpoint === 'lg' ? 12 : 3
const aX = a[width].x
const aY = a[width].y
const bX = b[width].x
const bY = b[width].y
if (aY < bY) {
return -1
} else if (aY > bY) {
return 1
} else {
if (aX < bX) {
return -1
} else if (aX > bX) {
return 1
} else {
return 0
}
}
})
}
</script>
<svelte:window on:keydown={keydown} />

View File

@@ -34,7 +34,7 @@ import {
FileText,
AtSignIcon
} from 'lucide-svelte'
import type { BaseAppComponent } from '../../types'
import type { BaseAppComponent, GridItem } from '../../types'
import type { Size } from '../../svelte-grid/types'
type BaseComponent<T extends string> = {
@@ -68,7 +68,7 @@ export type BarChartComponent = BaseComponent<'barchartcomponent'>
export type PieChartComponent = BaseComponent<'piechartcomponent'>
export type ScatterChartComponent = BaseComponent<'scatterchartcomponent'>
export type TableComponent = BaseComponent<'tablecomponent'> & {
actionButtons: (BaseAppComponent & ButtonComponent)[]
actionButtons: (BaseAppComponent & ButtonComponent & GridItem)[]
}
export type AggridComponent = BaseComponent<'aggridcomponent'>
export type DisplayComponent = BaseComponent<'displaycomponent'>
@@ -180,8 +180,7 @@ export const components: Record<AppComponent['type'], AppComponentConfig> = {
customCss: {
header: { class: '', style: '' },
container: { class: '', style: '' }
} as const,
card: false
} as const
}
},
containercomponent: {
@@ -192,19 +191,12 @@ export const components: Record<AppComponent['type'], AppComponentConfig> = {
softWrap: true,
id: '',
type: 'containercomponent',
configuration: {
noPadding: {
type: 'static',
fieldType: 'boolean',
value: false,
onlyStatic: true
}
},
configuration: {},
customCss: {
container: { class: '', style: '' }
} as const,
componentInput: undefined,
card: false,
numberOfSubgrids: 1
}
},
@@ -213,7 +205,7 @@ export const components: Record<AppComponent['type'], AppComponentConfig> = {
icon: Type,
dims: '1:1-3:1',
data: {
softWrap: false,
softWrap: true,
horizontalAlignment: 'left',
verticalAlignment: 'top',
id: '',
@@ -237,17 +229,17 @@ export const components: Record<AppComponent['type'], AppComponentConfig> = {
fieldType: 'boolean',
onlyStatic: true
},
fitContent: {
tooltip: {
type: 'static',
value: false,
fieldType: 'boolean',
onlyStatic: true
value: '',
fieldType: 'text',
onlyStatic: true,
tooltip: 'Tooltip text if not empty'
}
},
customCss: {
text: { class: '', style: '' }
} as const,
card: false
} as const
}
},
buttoncomponent: {
@@ -304,7 +296,7 @@ export const components: Record<AppComponent['type'], AppComponentConfig> = {
value: ''
},
gotoNewTab: {
tooltip: 'Go to create a new tab',
tooltip: 'Go to an url on a new tab',
fieldType: 'boolean',
type: 'static',
value: true
@@ -330,8 +322,7 @@ export const components: Record<AppComponent['type'], AppComponentConfig> = {
},
customCss: {
button: { style: '', class: '' }
} as const,
card: false
} as const
}
},
formcomponent: {
@@ -379,8 +370,7 @@ export const components: Record<AppComponent['type'], AppComponentConfig> = {
customCss: {
container: { class: '', style: '' },
button: { class: '', style: '' }
} as const,
card: true
} as const
}
},
formbuttoncomponent: {
@@ -423,8 +413,7 @@ export const components: Record<AppComponent['type'], AppComponentConfig> = {
customCss: {
button: { class: '', style: '' },
popup: { class: '', style: '' }
} as const,
card: true
} as const
}
},
piechartcomponent: {
@@ -456,8 +445,7 @@ export const components: Record<AppComponent['type'], AppComponentConfig> = {
},
customCss: {
container: { class: '', style: '' }
} as const,
card: true
} as const
}
},
barchartcomponent: {
@@ -489,8 +477,7 @@ export const components: Record<AppComponent['type'], AppComponentConfig> = {
},
customCss: {
container: { class: '', style: '' }
} as const,
card: true
} as const
}
},
htmlcomponent: {
@@ -514,8 +501,7 @@ Hello \${ctx.username}
configuration: {},
customCss: {
container: { class: '', style: '' }
} as const,
card: false
} as const
}
},
vegalitecomponent: {
@@ -551,11 +537,10 @@ Hello \${ctx.username}
onlyStatic: true,
fieldType: 'boolean',
value: false,
tooltip: 'use the canvas renderer instead of the svg one for more interactive plots'
tooltip: 'Use the canvas renderer instead of the svg one for more interactive plots'
}
},
customCss: {},
card: false
customCss: {}
}
},
plotlycomponent: {
@@ -582,8 +567,7 @@ Hello \${ctx.username}
}
},
configuration: {},
customCss: {},
card: false
customCss: {}
}
},
timeseriescomponent: {
@@ -658,8 +642,7 @@ Hello \${ctx.username}
},
customCss: {
container: { class: '', style: '' }
} as const,
card: true
} as const
}
},
scatterchartcomponent: {
@@ -710,8 +693,7 @@ Hello \${ctx.username}
},
customCss: {
container: { class: '', style: '' }
} as const,
card: true
} as const
}
},
tablecomponent: {
@@ -753,7 +735,7 @@ Hello \${ctx.username}
tableBody: { class: '', style: '' },
tableFooter: { class: '', style: '' }
} as const,
card: true,
actionButtons: []
}
},
@@ -775,7 +757,8 @@ Hello \${ctx.username}
type: 'static',
fieldType: 'boolean',
value: false,
onlyStatic: true
onlyStatic: true,
tooltip: 'Configure all columns as Editable by users'
},
pagination: {
type: 'static',
@@ -787,7 +770,8 @@ Hello \${ctx.username}
type: 'static',
fieldType: 'number',
value: 10,
onlyStatic: true
onlyStatic: true,
tooltip: 'Number of rows per page'
}
},
componentInput: {
@@ -807,8 +791,7 @@ Hello \${ctx.username}
}
]
},
customCss: {},
card: true
customCss: {}
}
},
checkboxcomponent: {
@@ -835,8 +818,7 @@ Hello \${ctx.username}
},
customCss: {
text: { class: '', style: '' }
} as const,
card: false
} as const
}
},
textinputcomponent: {
@@ -864,8 +846,7 @@ Hello \${ctx.username}
},
customCss: {
input: { class: '', style: '' }
} as const,
card: false
} as const
}
},
selectcomponent: {
@@ -891,7 +872,8 @@ Hello \${ctx.username}
type: 'static',
fieldType: 'boolean',
value: false,
onlyStatic: true
onlyStatic: true,
tooltip: 'Allows user to manually add new value'
},
placeholder: {
type: 'static',
@@ -908,7 +890,7 @@ Hello \${ctx.username}
customCss: {
input: { style: '' }
} as const,
card: false,
softWrap: true
}
},
@@ -938,7 +920,7 @@ Hello \${ctx.username}
customCss: {
input: { style: '' }
} as const,
card: false,
softWrap: true
}
},
@@ -968,7 +950,7 @@ Hello \${ctx.username}
customCss: {
input: { style: '' }
} as const,
card: false,
softWrap: true
}
},
@@ -1007,13 +989,13 @@ Hello \${ctx.username}
step: {
type: 'static',
value: 1,
fieldType: 'number'
fieldType: 'number',
tooltip: 'Spread between each number suggestion'
}
},
customCss: {
input: { class: '', style: '' }
} as const,
card: false
} as const
}
},
currencycomponent: {
@@ -1050,13 +1032,13 @@ Hello \${ctx.username}
value: 'en-US',
fieldType: 'select',
onlyStatic: true,
optionValuesKey: 'localeOptions'
optionValuesKey: 'localeOptions',
tooltip: 'Currency format'
}
},
customCss: {
input: { class: '', style: '' }
} as const,
card: false
} as const
}
},
slidercomponent: {
@@ -1089,7 +1071,8 @@ Hello \${ctx.username}
step: {
type: 'static',
value: 1,
fieldType: 'number'
fieldType: 'number',
tooltip: 'Spread between each number suggestion'
}
},
customCss: {
@@ -1097,8 +1080,7 @@ Hello \${ctx.username}
handle: { style: '' },
limits: { class: '', style: '' },
value: { class: '', style: '' }
} as const,
card: false
} as const
}
},
rangecomponent: {
@@ -1135,7 +1117,8 @@ Hello \${ctx.username}
step: {
type: 'static',
value: 1,
fieldType: 'number'
fieldType: 'number',
tooltip: 'Spread between each number suggestion'
}
},
customCss: {
@@ -1143,8 +1126,7 @@ Hello \${ctx.username}
bar: { style: '' },
limits: { class: '', style: '' },
values: { class: '', style: '' }
} as const,
card: false
} as const
}
},
passwordinputcomponent: {
@@ -1167,8 +1149,7 @@ Hello \${ctx.username}
},
customCss: {
input: { class: '', style: '' }
} as const,
card: false
} as const
}
},
emailinputcomponent: {
@@ -1196,8 +1177,7 @@ Hello \${ctx.username}
},
customCss: {
input: { class: '', style: '' }
} as const,
card: false
} as const
}
},
dateinputcomponent: {
@@ -1229,8 +1209,7 @@ Hello \${ctx.username}
},
customCss: {
input: { class: '', style: '' }
} as const,
card: false
} as const
}
},
tabscomponent: {
@@ -1242,11 +1221,12 @@ Hello \${ctx.username}
id: '',
type: 'tabscomponent',
configuration: {
noPadding: {
tabsKind: {
fieldType: 'select',
type: 'static',
fieldType: 'boolean',
value: false,
onlyStatic: true
onlyStatic: true,
optionValuesKey: 'tabsKindOptions',
value: 'tabs'
}
},
customCss: {
@@ -1256,7 +1236,7 @@ Hello \${ctx.username}
container: { class: '', style: '' }
} as const,
componentInput: undefined,
card: false,
numberOfSubgrids: 2,
tabs: ['First tab', 'Second tab']
}
@@ -1299,8 +1279,7 @@ Hello \${ctx.username}
customCss: {
container: { class: '', style: '' },
icon: { class: '', style: '' }
} as const,
card: false
} as const
}
},
horizontaldividercomponent: {
@@ -1329,8 +1308,7 @@ Hello \${ctx.username}
customCss: {
container: { class: '', style: '' },
divider: { class: '', style: '' }
} as const,
card: false
} as const
}
},
verticaldividercomponent: {
@@ -1359,8 +1337,7 @@ Hello \${ctx.username}
customCss: {
container: { class: '', style: '' },
divider: { class: '', style: '' }
} as const,
card: false
} as const
}
},
fileinputcomponent: {
@@ -1394,8 +1371,7 @@ Hello \${ctx.username}
},
customCss: {
container: { class: '', style: '' }
} as const,
card: false
} as const
}
},
imagecomponent: {
@@ -1433,8 +1409,7 @@ Hello \${ctx.username}
},
customCss: {
image: { class: '', style: '' }
} as const,
card: false
} as const
}
},
drawercomponent: {
@@ -1448,12 +1423,6 @@ Hello \${ctx.username}
horizontalAlignment: 'center',
verticalAlignment: 'center',
configuration: {
noPadding: {
type: 'static',
fieldType: 'boolean',
value: false,
onlyStatic: true
},
drawerTitle: {
type: 'static',
fieldType: 'text',
@@ -1492,7 +1461,7 @@ Hello \${ctx.username}
}
},
componentInput: undefined,
card: false,
numberOfSubgrids: 1
}
},
@@ -1548,8 +1517,7 @@ Hello \${ctx.username}
},
customCss: {
map: { class: '', style: '' }
} as const,
card: false
} as const
}
},
verticalsplitpanescomponent: {
@@ -1560,19 +1528,12 @@ Hello \${ctx.username}
softWrap: true,
id: '',
type: 'verticalsplitpanescomponent',
configuration: {
noPadding: {
type: 'static',
fieldType: 'boolean',
value: false,
onlyStatic: true
}
},
configuration: {},
customCss: {
container: { class: '', style: '' }
} as const,
componentInput: undefined,
card: false,
panes: [50, 50],
numberOfSubgrids: 2
}
@@ -1585,19 +1546,12 @@ Hello \${ctx.username}
softWrap: true,
id: '',
type: 'horizontalsplitpanescomponent',
configuration: {
noPadding: {
type: 'static',
fieldType: 'boolean',
value: false,
onlyStatic: true
}
},
configuration: {},
customCss: {
container: { class: '', style: '' }
} as const,
componentInput: undefined,
card: false,
panes: [50, 50],
numberOfSubgrids: 2
}
@@ -1628,8 +1582,7 @@ Hello \${ctx.username}
},
customCss: {
container: { class: '', style: '' }
} as const,
card: false
} as const
}
}
}

View File

@@ -10,7 +10,8 @@
import { push } from '$lib/history'
import { flip } from 'svelte/animate'
const { app, selectedComponent, focusedGrid } = getContext<AppViewerContext>('AppViewerContext')
const { app, selectedComponent, focusedGrid, worldStore } =
getContext<AppViewerContext>('AppViewerContext')
const { history } = getContext<AppEditorContext>('AppEditorContext')
@@ -23,6 +24,7 @@
const id = insertNewGridItem($app, data, $focusedGrid)
$selectedComponent = id
$worldStore = $worldStore
}
let search = ''
@@ -37,7 +39,7 @@
}))
</script>
<section class="p-2 sticky bg-white w-full z-20 top-0">
<section class="p-2 sticky bg-white w-full z-10 top-0">
<div class="relative">
<input
bind:value={search}
@@ -68,12 +70,12 @@
<div in:fade|local={{ duration: 50, delay: 50 }} out:fade|local={{ duration: 50 }}>
{#each componentsFiltered as { title, components }, index (index)}
{#if components.length}
<div transition:slide|local={{ duration: 300 }}>
<div transition:slide|local={{ duration: 100 }}>
<ListItem title={`${title} (${components.length})`}>
<div class="flex flex-wrap gap-2 py-2">
{#each components as item (item)}
<button
animate:flip={{ duration: 300 }}
animate:flip={{ duration: 100 }}
on:click={() => addComponent(item)}
title={componentsRecord[item].name}
class="border w-24 shadow-sm h-16 p-2 flex flex-col gap-2 items-center

View File

@@ -4,6 +4,7 @@ const buttonColorOptions = [...BUTTON_COLORS]
export const staticValues = {
buttonColorOptions,
tabsKindOptions: ['tabs', 'sidebar', 'invisibleOnView'],
buttonSizeOptions: ['xs', 'sm', 'md', 'lg', 'xl'],
tableSearchOptions: ['By Component', 'By Runnable', 'Disabled'],
chartThemeOptions: ['theme1', 'theme2', 'theme3'],

View File

@@ -0,0 +1,71 @@
<script lang="ts">
import { components } from '../component'
import { getContext } from 'svelte'
import type { AppViewerContext, GridItem } from '../../types'
import ComponentOutputViewer from './ComponentOutputViewer.svelte'
import { connectInput } from '../appUtils'
import SubGridOutput from './SubGridOutput.svelte'
import OutputHeader from './components/OutputHeader.svelte'
import TableActionsOutput from './components/TableActionsOutput.svelte'
export let gridItem: GridItem
export let first: boolean = false
export let nested: boolean = false
export let parentId: string | undefined = undefined
export let expanded: boolean = false
const { selectedComponent, connectingInput } = getContext<AppViewerContext>('AppViewerContext')
const name = getComponentNameById(gridItem.id)
function getComponentNameById(componentId: string) {
if (gridItem?.data?.type) {
return components[gridItem?.data.type].name
} else if (componentId == 'ctx') {
return 'Context'
} else if (componentId.startsWith('bg_')) {
return 'Background'
} else {
return 'Table action'
}
}
$: subGrids = Array.from({ length: gridItem.data.numberOfSubgrids ?? 0 }).map(
(_, i) => `${gridItem.id}-${i}`
)
function onHeaderClick(manuallyOpen: boolean) {
if (manuallyOpen) {
if (parentId) {
$selectedComponent = parentId
} else {
$selectedComponent = undefined
}
} else {
$selectedComponent = gridItem.id
}
}
</script>
<OutputHeader
on:handleClick={(e) => {
if (!$connectingInput.opened) {
onHeaderClick(e.detail.manuallyOpen)
}
}}
id={gridItem.id}
name={getComponentNameById(gridItem.id)}
{first}
{nested}
>
<ComponentOutputViewer
componentId={gridItem.id}
on:select={({ detail }) => {
if ($connectingInput.opened) {
$connectingInput = connectInput($connectingInput, gridItem.id, detail)
}
}}
/>
<SubGridOutput {name} {expanded} {subGrids} parentId={gridItem.id} />
<TableActionsOutput {gridItem} />
</OutputHeader>

View File

@@ -1,35 +1,43 @@
<script lang="ts">
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
import { getContext } from 'svelte'
import type { Writable } from 'svelte/store'
import type { Output } from '../../rx'
import type { AppViewerContext } from '../../types'
import type { AppViewerContext, ContextPanelContext } from '../../types'
import { recursivelyFilterKeyInJSON as recursivelyFilterInJSON } from '../appUtils'
export let outputs: string[] = []
export let componentId: string
const { worldStore } = getContext<AppViewerContext>('AppViewerContext')
const { search, hasResult } = getContext<ContextPanelContext>('ContextPanel')
let object = {}
function subscribeToAllOutputs(observableOutputs: Record<string, Output<any>>) {
function subscribeToAllOutputs(observableOutputs: Record<string, Output<any>> | undefined) {
if (observableOutputs) {
outputs.forEach((output: string) => {
object[output] = undefined
observableOutputs[output]?.subscribe({
Object.entries(observableOutputs).forEach(([k, output]) => {
object[k] = undefined
output?.subscribe({
id: 'alloutputs' + componentId + '-' + k,
next: (value) => {
object[output] = value
object[k] = value
}
})
})
}
}
$: $worldStore?.outputsById[componentId] &&
subscribeToAllOutputs($worldStore.outputsById[componentId])
$: subscribeToAllOutputs($worldStore?.outputsById?.[componentId])
$: filtered = recursivelyFilterInJSON(object, $search, componentId)
$: $hasResult[componentId] = Object.keys(filtered).length > 0
</script>
{#if Object.keys(object).length > 0}
<ObjectViewer json={object} on:select topBrackets={false} />
{#if $hasResult[componentId] || $search == ''}
<ObjectViewer json={filtered} on:select topBrackets={false} />
{:else if $search.length > 0}
<div class="text-xs pl-2 text-gray-600">No results</div>
{:else}
<div class="text-xs text-gray-500 px-4">No outputs</div>
<div class="text-xs pl-2 text-gray-600">No outputs</div>
{/if}

View File

@@ -1,167 +1,92 @@
<script lang="ts">
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
import { classNames } from '$lib/utils'
import { X } from 'lucide-svelte'
import { getContext } from 'svelte'
import { flip } from 'svelte/animate'
import { fade } from 'svelte/transition'
import type { AppViewerContext } from '../../types'
import { findGridItem } from '../appUtils'
import { components } from '../component'
import { getContext, setContext } from 'svelte'
import { writable } from 'svelte/store'
import type { AppViewerContext, ContextPanelContext } from '../../types'
import { connectInput } from '../appUtils'
import PanelSection from '../settingsPanel/common/PanelSection.svelte'
import ComponentOutput from './ComponentOutput.svelte'
import ComponentOutputViewer from './ComponentOutputViewer.svelte'
import BackgroundScriptsOutput from './components/BackgroundScriptsOutput.svelte'
import MinMaxButton from './components/MinMaxButton.svelte'
import OutputHeader from './components/OutputHeader.svelte'
const { connectingInput, staticOutputs, worldStore, selectedComponent, app, state } =
getContext<AppViewerContext>('AppViewerContext')
const { connectingInput, app } = getContext<AppViewerContext>('AppViewerContext')
function connectInput(componentId: string, path: string) {
if ($connectingInput) {
$connectingInput = {
opened: false,
input: {
connection: {
componentId,
path
},
type: 'connected'
},
hoveredComponent: undefined
}
}
}
let search = writable<string>('')
let expanded = writable(false)
function getComponentNameById(componentId: string) {
const component = findGridItem($app, componentId)
if (component?.data?.type) {
return components[component?.data.type].name
} else if (componentId == 'ctx') {
return 'Context'
} else if (componentId.startsWith('bg_')) {
return 'Background'
} else {
return 'Table action'
}
}
$: panels = [['ctx', ['email', 'username', 'query', 'hash']] as [string, string[]]].concat(
Object.entries($staticOutputs)
)
let search = ''
// filter out outputs that don't match the search by name (computed by getComponentNameById) and id
// The output should be [string, string[]][]
$: filteredPanels = panels.filter(([componentId, outputs]) => {
const name = getComponentNameById(componentId)
return (
name.toLowerCase().includes(search.toLowerCase()) ||
componentId.toLowerCase().includes(search.toLowerCase())
)
setContext<ContextPanelContext>('ContextPanel', {
search,
manuallyOpened: writable<Record<string, boolean>>({}),
hasResult: writable<Record<string, boolean>>({}),
expanded
})
</script>
<PanelSection noPadding titlePadding="px-4 pt-2 pb-0.5" title="Outputs">
<div class="overflow-auto h-full min-w-[150px] w-full relative flex flex-col">
<div class="sticky z-50 top-0 left-0 w-full bg-white px-2 pb-2">
<div class="relative">
<input
bind:value={search}
class="px-2 py-1 border border-gray-300 rounded-sm {search ? 'pr-8' : ''}"
placeholder="Search outputs..."
/>
{#if search}
<button
class="absolute right-2 top-1/2 transform -translate-y-1/2 hover:bg-gray-200 rounded-full p-0.5"
on:click|stopPropagation|preventDefault={() => (search = '')}
>
<X size="14" />
</button>
{/if}
</div>
</div>
<div class="relative p-2">
{#each filteredPanels as [componentId, outputs] (componentId)}
<div
animate:flip={{ duration: 300 }}
in:fade|local={{ duration: 100, delay: 50 }}
out:fade|local={{ duration: 100 }}
class="pb-2"
>
{#if outputs.length > 0 && $worldStore?.outputsById[componentId]}
{@const name = getComponentNameById(componentId)}
<div>
<div
class="flex {$connectingInput?.opened
? 'bg-white z-50'
: ''} flex-row justify-between w-full"
>
<button
on:click|stopPropagation|preventDefault={$connectingInput.opened
? undefined
: () => ($selectedComponent = componentId)}
class={classNames(
'px-2 text-2xs py-0.5 border-t border-x font-bold rounded-t-sm w-fit',
$selectedComponent === componentId
? ' bg-indigo-500/90 text-white border-indigo-500/90'
: 'bg-gray-100 text-gray-500 border-gray-200'
)}
>
{componentId}
</button>
<span
class={classNames(
'px-1 text-2xs py-0.5 font-semibold rounded-t-sm w-fit',
'bg-gray-700 text-white'
)}
>
{name}
</span>
</div>
<div
class={classNames(
$connectingInput?.opened ? 'bg-white z-50' : '',
`w-full py-2 grow border relative break-all `,
$selectedComponent === componentId ? 'border border-indigo-500/90 ' : '',
$connectingInput.hoveredComponent === componentId
? 'outline outline-indigo-500/90'
: ''
)}
>
{#key $selectedComponent}
{#key $connectingInput?.opened}
<ComponentOutputViewer
outputs={$connectingInput?.opened && $selectedComponent === componentId
? name == 'Table'
? ['search']
: []
: outputs}
{componentId}
on:select={({ detail }) => {
connectInput(componentId, detail)
}}
/>
{/key}
{/key}
</div>
</div>
<div class="bg-white w-full h-full z-30">
<div class="min-w-[150px]">
<div class="sticky top-0 left-0 w-full bg-white p-2">
<div class="relative">
<input
bind:value={$search}
class="px-2 py-1 border border-gray-300 rounded-sm {search ? 'pr-8' : ''}"
placeholder="Search outputs..."
/>
{#if search}
<button
class="absolute right-2 top-1/2 transform -translate-y-1/2 hover:bg-gray-200 rounded-full p-0.5"
on:click|stopPropagation|preventDefault={() => ($search = '')}
>
<X size="14" />
</button>
{/if}
</div>
{:else}
<div
in:fade|local={{ duration: 50, delay: 100 }}
out:fade|local={{ duration: 50 }}
class="absolute left-0 top-0 w-full text-sm text-gray-500 text-center py-4 px-2"
>
No outputs found
</div>
{/each}
</div>
<PanelSection noPadding titlePadding="px-4 pt-2 pb-0.5" title="State">
<div class="mx-2 px-1 border w-full mb-8">
{#key $state}
<ObjectViewer json={$state} />
{/key}
</div>
</PanelSection>
<div class="p-1 ">
<MinMaxButton bind:expanded={$expanded} />
</div>
<div class="flex flex-col gap-4">
<div>
<span class="text-sm font-bold p-2">State & Context</span>
<OutputHeader id={'ctx'} name={'App Context'} first color="blue">
<ComponentOutputViewer
componentId={'ctx'}
on:select={({ detail }) => {
$connectingInput = connectInput($connectingInput, 'ctx', detail)
}}
/>
</OutputHeader>
<OutputHeader id={'state'} name={'State'} color="blue">
<ComponentOutputViewer
componentId={'state'}
on:select={({ detail }) => {
$connectingInput = connectInput($connectingInput, 'state', detail)
}}
/>
</OutputHeader>
</div>
<div>
<span class="text-sm font-bold p-2">Components</span>
{#each $app.grid as gridItem, index (gridItem.id)}
<ComponentOutput {gridItem} first={index === 0} />
{/each}
</div>
<div>
<span class="text-sm font-bold p-2">Background scripts</span>
<div class="border-t">
<BackgroundScriptsOutput />
</div>
</div>
</div>
</div>
</div>
</PanelSection>

View File

@@ -0,0 +1,77 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import { getContext } from 'svelte'
import { slide } from 'svelte/transition'
import type { Output } from '../../rx'
import type { AppViewerContext } from '../../types'
import { connectInput } from '../appUtils'
import ComponentOutput from './ComponentOutput.svelte'
export let name: string | undefined = undefined
export let parentId: string
export let expanded: boolean = false
export let subGrids: string[]
const { app, connectingInput, worldStore } = getContext<AppViewerContext>('AppViewerContext')
let selected = 0
$: outputs = $worldStore?.outputsById[parentId] as {
selectedTabIndex: Output<number>
}
$: subgridItems = subGrids.map((k) => ({
k,
items: $app.subgrids?.[k] ?? []
}))
$: if (outputs?.selectedTabIndex) {
outputs.selectedTabIndex.subscribe({
id: 'subgridoutput-' + parentId,
next: (value) => {
selected = value
}
})
}
</script>
{#each subgridItems as { k, items }, index (k)}
<div class="ml-2 my-2">
{#if subGrids.length > 1}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class={classNames(
'px-1 py-0.5 flex justify-between items-center font-semibold text-xs border-l border-y w-full cursor-pointer',
selected === index ? 'bg-gray-200' : 'bg-gray-50'
)}
on:click={() => {
selected = index
}}
>
<div class="text-xs">
{name ? name : 'Should implement name'}
{index + 1}
</div>
</div>
{/if}
{#if selected === index || name !== 'Tabs'}
<div transition:slide|local class="border-l">
{#if items.length > 0}
{#each items as subGridItem, index (subGridItem.id)}
<ComponentOutput
gridItem={subGridItem}
first={index === 0}
{expanded}
on:select={({ detail }) => {
$connectingInput = connectInput($connectingInput, subGridItem.id, detail)
}}
/>
{/each}
{:else}
<div class="text-xs text-gray-500 border-y border-l p-1">No components</div>
{/if}
</div>
{/if}
</div>
{/each}

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import type { AppViewerContext } from '$lib/components/apps/types'
import { getContext } from 'svelte'
import { connectInput } from '../../appUtils'
import ComponentOutputViewer from '../ComponentOutputViewer.svelte'
import OutputHeader from './OutputHeader.svelte'
const { selectedComponent, connectingInput } = getContext<AppViewerContext>('AppViewerContext')
export let id: string
export let name: string
export let first: boolean = false
function onHeaderClick(manuallyOpen: boolean) {
if (manuallyOpen) {
if (id) {
$selectedComponent = id
} else {
$selectedComponent = undefined
}
} else {
$selectedComponent = id
}
}
</script>
<OutputHeader
{id}
{name}
color="blue"
{first}
on:handleClick={(e) => {
if (!$connectingInput.opened) {
onHeaderClick(e.detail.manuallyOpen)
}
}}
>
<ComponentOutputViewer
componentId={id}
on:select={({ detail }) => {
$connectingInput = connectInput($connectingInput, id, detail)
}}
/>
</OutputHeader>

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import type { AppViewerContext } from '$lib/components/apps/types'
import { getContext } from 'svelte'
import BackgroundScriptOutput from './BackgroundScriptOutput.svelte'
const { app } = getContext<AppViewerContext>('AppViewerContext')
export let expanded: boolean = false
</script>
{#each $app.hiddenInlineScripts as action, index}
<BackgroundScriptOutput id={`bg_${index}`} name={action.name} />
{/each}

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import Button from '$lib/components/common/button/Button.svelte'
import { Maximize, Minimize } from 'lucide-svelte'
export let expanded: boolean = false
</script>
<Button
on:click={() => {
expanded = !expanded
}}
color="light"
size="xs"
variant="border"
>
{#if !expanded}
<div class="flex flex-row gap-2">
<Maximize size="14" />
Expand all
</div>
{:else}
<div class="flex flex-row gap-2">
<Minimize size="14" />
Collapse all
</div>
{/if}
</Button>

View File

@@ -0,0 +1,112 @@
<script lang="ts">
import type { AppViewerContext, ContextPanelContext } from '$lib/components/apps/types'
import { classNames } from '$lib/utils'
import { ChevronDown, ChevronUp } from 'lucide-svelte'
import { createEventDispatcher, getContext } from 'svelte'
import { slide } from 'svelte/transition'
import { allsubIds } from '../../appUtils'
export let id: string
export let name: string
export let first: boolean = false
export let nested: boolean = false
export let color: 'blue' | 'indigo' = 'indigo'
const { expanded, manuallyOpened, search, hasResult } =
getContext<ContextPanelContext>('ContextPanel')
const { selectedComponent, app } = getContext<AppViewerContext>('AppViewerContext')
$: subids = allsubIds($app, id)
$: inSearch =
$search != '' &&
($hasResult[id] ||
Object.entries($hasResult).some(([key, value]) => value && subids.includes(key)))
$: open =
$expanded || subids.includes($selectedComponent ?? '') || $manuallyOpened[id] || inSearch
const dispatch = createEventDispatcher()
const hoverColor = {
blue: 'hover:bg-blue-300 hover:text-blue-600',
indigo: 'hover:bg-indigo-300 hover:text-indigo-600'
}
const openBackground = {
blue: 'bg-blue-50',
indigo: 'bg-indigo-50'
}
const manuallyOpenColor = {
blue: 'text-blue-600',
indigo: 'text-indigo-600'
}
const idClass = {
blue: 'bg-blue-500 text-white',
indigo: 'bg-indigo-500 text-white'
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class={$search == '' || inSearch ? '' : 'invisible h-0 overflow-hidden'}>
<div
class={classNames(
'flex items-center justify-between p-1 cursor-pointer border-b gap-1 truncate',
hoverColor[color],
$selectedComponent == id ? openBackground[color] : 'bg-white',
first ? 'border-t' : '',
nested ? 'border-l' : ''
)}
on:click={() => {
dispatch('handleClick', { manuallyOpen: $manuallyOpened[id] })
$manuallyOpened[id] = $manuallyOpened[id] != undefined ? !$manuallyOpened[id] : true
}}
>
<div
class={classNames(
'text-2xs ml-0.5 font-bold px-2 py-0.5 rounded-sm',
$selectedComponent == id ? idClass[color] : ' bg-gray-100'
)}
>
{id}
</div>
<div
on:click|stopPropagation={() => {
$manuallyOpened[id] = $manuallyOpened[id] != undefined ? !$manuallyOpened[id] : true
}}
class="text-2xs font-bold flex flex-row gap-2 items-center truncate"
>
{name}
{#if !open}
<ChevronDown size={14} />
{:else if $manuallyOpened[id]}
<ChevronUp size={14} class={manuallyOpenColor[color]} strokeWidth={4} />
{:else}
<ChevronUp size={14} />
{/if}
</div>
</div>
<div class="scale border-b overflow-hidden {open ? 'py-1 scale-y' : 'scale-0 max-h-0'} ">
<div class={classNames(nested ? 'border-l ml-2' : '')}>
<slot />
</div>
</div>
</div>
<style>
.scale {
transform-origin: top;
transition: transform 0.26s ease;
}
.scale-y {
transform: scaleY(1);
max-height: 100%;
}
.scale-0 {
transform: scaleY(0);
overflow: hidden;
max-height: 0;
}
</style>

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import type { AppViewerContext } from '$lib/components/apps/types'
import { getContext } from 'svelte'
import { connectInput } from '../../appUtils'
import ComponentOutputViewer from '../ComponentOutputViewer.svelte'
import OutputHeader from './OutputHeader.svelte'
const { connectingInput } = getContext<AppViewerContext>('AppViewerContext')
export let id: string
export let first: boolean = false
</script>
<OutputHeader {id} name={'Table action'} {first}>
<ComponentOutputViewer
componentId={id}
on:select={({ detail }) => {
$connectingInput = connectInput($connectingInput, id, detail)
}}
/>
</OutputHeader>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { GridItem } from '$lib/components/apps/types'
import TableActionOutput from './TableActionOutput.svelte'
export let gridItem: GridItem
</script>
<div class="my-1">
{#if gridItem.data.type === 'tablecomponent' && gridItem.data.actionButtons.length > 0}
<div class="ml-2 border-l">
{#each gridItem.data.actionButtons as action, index}
<TableActionOutput id={action.id} first={index === 0} />
{/each}
</div>
{/if}
</div>

View File

@@ -49,14 +49,14 @@
const componentType = $app.grid.find((c) => c.data.id === id)?.data?.type
const content =
defaultCode(componentType, language) ??
defaultCode(componentType ?? '', language) ??
initialCode(language, Script.kind.SCRIPT, subkind ?? 'flow')
return newInlineScript(content, language, path)
}
async function newInlineScript(content: string, language: Preview.language, path: string) {
const fullPath = `${appPath}/inline-script/${path}`
const fullPath = `${appPath}/${path}`
let schema: Schema = emptySchema()

View File

@@ -27,10 +27,11 @@
export let fields: Record<string, AppInput> = {}
export let syncFields: boolean = false
const { runnableComponents, stateId, worldStore, state } =
const { runnableComponents, stateId, worldStore, state, appPath } =
getContext<AppViewerContext>('AppViewerContext')
let editor: Editor
let simpleEditor: SimpleEditor
let validCode = true
async function inferInlineScriptSchema(
@@ -49,6 +50,8 @@
return schema
}
$: inlineScript.path = `${appPath}/${name}`
onMount(async () => {
if (inlineScript && !inlineScript.schema) {
if (inlineScript.language != 'frontend') {
@@ -144,7 +147,8 @@
color="light"
btnClasses="!px-2 !py-1"
on:click={async () => {
editor.format()
editor?.format()
simpleEditor?.format()
}}
>
<div class="flex flex-row gap-1 items-center">
@@ -215,6 +219,7 @@
/>
{:else}
<SimpleEditor
bind:this={simpleEditor}
cmdEnterAction={async () => {
runLoading = true
await $runnableComponents[id]?.()

View File

@@ -8,8 +8,7 @@
import EmptyInlineScript from './EmptyInlineScript.svelte'
import InlineScriptEditorPanel from './InlineScriptEditorPanel.svelte'
const { app, staticOutputs, runnableComponents } =
getContext<AppViewerContext>('AppViewerContext')
const { app, runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
let selectedScriptComponentId: string | undefined = undefined
@@ -18,9 +17,7 @@
$app.hiddenInlineScripts.splice(index, 1)
$app.hiddenInlineScripts = [...$app.hiddenInlineScripts]
delete $staticOutputs[`bg_${index}`]
delete $runnableComponents[`bg_${index}`]
$staticOutputs = $staticOutputs
}
</script>
@@ -36,18 +33,17 @@
</div>
{/if}
{#each $app.grid as gridItem, index (gridItem?.data?.id ?? index)}
{#each $app.grid as gridItem (gridItem?.data?.id)}
{#if gridItem?.data?.id && gridItem.data.id === selectedScriptComponentId}
<InlineScriptEditorPanel
defaultUserInput={gridItem?.data?.type == 'formcomponent' ||
gridItem?.data?.type == 'buttonformcomponent'}
defaultUserInput={gridItem?.data?.type == 'formcomponent'}
id={gridItem.data.id}
bind:componentInput={gridItem.data.componentInput}
/>
{/if}
{#if gridItem?.data?.type === 'tablecomponent'}
{#each gridItem.data.actionButtons as actionButton, index (index)}
{#each gridItem.data.actionButtons as actionButton (actionButton.id)}
{#if actionButton.id === selectedScriptComponentId}
<InlineScriptEditorPanel
id={actionButton.id}
@@ -63,8 +59,7 @@
{#each $app.subgrids[key] as gridItem (gridItem?.data?.id)}
{#if gridItem?.data?.id && gridItem.data.id === selectedScriptComponentId}
<InlineScriptEditorPanel
defaultUserInput={gridItem.data?.type == 'formcomponent' ||
gridItem.data?.type == 'buttonformcomponent'}
defaultUserInput={gridItem.data?.type == 'formcomponent'}
id={gridItem.data.id}
bind:componentInput={gridItem.data.componentInput}
/>

View File

@@ -33,18 +33,14 @@
export let noGrid = false
export let duplicateMoveAllowed = true
const {
app,
staticOutputs,
runnableComponents,
selectedComponent,
worldStore,
focusedGrid,
stateId,
state
} = getContext<AppViewerContext>('AppViewerContext')
let editor: TemplateEditor | undefined = undefined
const { history } = getContext<AppEditorContext>('AppEditorContext')
const { app, runnableComponents, selectedComponent, worldStore, focusedGrid, stateId, state } =
getContext<AppViewerContext>('AppViewerContext')
const { history, ontextfocus } = getContext<AppEditorContext>('AppEditorContext')
$: editor && ($ontextfocus = () => editor?.focus())
function removeGridElement() {
push(history, $app)
@@ -53,15 +49,12 @@
if (component && !noGrid) {
let ids = deleteGridItem($app, component, parent, false)
for (const key of ids) {
delete $staticOutputs[key]
delete $runnableComponents[key]
}
}
delete $staticOutputs[component.id]
delete $runnableComponents[component.id]
$app = $app
$staticOutputs = $staticOutputs
$runnableComponents = $runnableComponents
onDelete?.()
@@ -120,7 +113,12 @@
<StaticInputEditor bind:componentInput={component.componentInput} />
{:else if component.componentInput.type === 'template' && component.componentInput !== undefined}
<div class="py-1 min-h-[28px] rounded border border-1 border-gray-500">
<TemplateEditor fontSize={12} bind:code={component.componentInput.eval} {extraLib} />
<TemplateEditor
bind:this={editor}
fontSize={12}
bind:code={component.componentInput.eval}
{extraLib}
/>
</div>
{:else if component.componentInput.type === 'connected' && component.componentInput !== undefined}
<ConnectedInputEditor bind:componentInput={component.componentInput} />
@@ -153,7 +151,8 @@
id={component.id}
shouldCapitalize={false}
bind:inputSpecs={component.componentInput.fields}
userInputEnabled={component.type !== 'buttoncomponent'}
userInputEnabled={component.type === 'formcomponent' ||
component.type === 'formbuttoncomponent'}
{rowColumns}
/>
</PanelSection>

View File

@@ -10,8 +10,7 @@
export let panes: number[]
export let component: AppComponent
const { app, staticOutputs, runnableComponents } =
getContext<AppViewerContext>('AppViewerContext')
const { app, runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
function addTab() {
const numberOfPanes = panes.length
@@ -33,11 +32,9 @@
const components = deleteGridItem($app, item.data, subgrid, false)
console.log(components)
for (const key in components) {
delete $staticOutputs[key]
delete $runnableComponents[key]
}
}
$staticOutputs = $staticOutputs
$runnableComponents = $runnableComponents
for (let i = index; i < panes.length - 1; i++) {
$app!.subgrids![`${component.id}-${i}`] = $app!.subgrids![`${component.id}-${i + 1}`]

View File

@@ -12,8 +12,7 @@
export let tabs: string[]
export let component: AppComponent
const { app, staticOutputs, runnableComponents, focusedGrid } =
getContext<AppViewerContext>('AppViewerContext')
const { app, runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
function addTab() {
const numberOfTabs = tabs.length
@@ -33,11 +32,9 @@
const components = deleteGridItem($app, item.data, subgrid, false)
console.log(components)
for (const key in components) {
delete $staticOutputs[key]
delete $runnableComponents[key]
}
}
$staticOutputs = $staticOutputs
$runnableComponents = $runnableComponents
for (let i = index; i < tabs.length - 1; i++) {
$app!.subgrids![`${component.id}-${i}`] = $app!.subgrids![`${component.id}-${i + 1}`]

View File

@@ -11,7 +11,7 @@
export let id: string
export let inputSpecs: BaseAppComponent['configuration']
export let userInputEnabled: boolean = true
export let userInputEnabled: boolean = false
export let shouldCapitalize: boolean = true
export let rowColumns = false
export let resourceOnly = false
@@ -67,21 +67,6 @@
/>
<svelte:fragment slot="text">Static</svelte:fragment>
</Popover>
<!-- {#if rowColumns}
<Popover placement="bottom" notClickable disapperTimoout={0}>
<ToggleButton
position="center"
value="row"
startIcon={{ icon: faTableCells }}
size="xs"
>
<Tooltip scale={0.6} placement="top-end" wrapperClass="center-center">
Use the column name to have the value of the cell be passed to the action
</Tooltip>
</ToggleButton>
<svelte:fragment slot="text">Column</svelte:fragment>
</Popover>
{/if} -->
{#if userInputEnabled && !input.format?.startsWith('resource-')}
<Popover placement="bottom" notClickable disapperTimoout={0}>
<ToggleButton

Some files were not shown because too many files have changed in this diff Show More