Compare commits

..

37 Commits

Author SHA1 Message Date
Faton Ramadani
4140117b6a Merge branch 'main' into app-preview-scroll 2023-03-07 16:50:59 +01:00
Faton Ramadani
f20ea37eb0 feat(frontend): Fix app preview error display 2023-03-07 16:48:23 +01:00
Ruben Fiszel
67d8009dcf fix multiple app nits 2023-03-07 15:51:02 +01:00
Faton Ramadani
87d31ee63b Merge branch 'main' into app-preview-scroll 2023-03-07 15:29:24 +01:00
Faton Ramadani
fb2d52f99b feat(frontend): Fix recomputeIds + app table 2023-03-07 15:28:58 +01:00
Ruben Fiszel
95ccc9edf8 fix z-index for flowbuilder 2023-03-07 14:57:51 +01:00
Ruben Fiszel
9e4d90ad37 feat(frontend): add history to flows and apps 2023-03-07 14:47:17 +01:00
Faton Ramadani
2e3271257f Merge branch 'main' into app-preview-scroll 2023-03-07 11:42:10 +01:00
Ádám Kovács
c638897fdc fix(frontend): Side menu z-index issue (#1265) 2023-03-07 09:53:19 +01:00
Ruben Fiszel
71305e5154 show archived + fix graphs 2023-03-07 01:59:35 +01:00
Ruben Fiszel
9e9f8efb8e feat(frontend): add fork/save buttons + consistent styling for slider/range 2023-03-06 22:35:25 +01:00
Ádám Kovács
3e5d09ef0b feat(frontend): Add app PDF viewer (#1254)
* feat(frontend): Add app PDF viewer (wip)

* fix(frontend): Handle file upload

* fix(frontend): Handle multi page pdf

* feat(frontend): Add pdf page numbering

* feat(frontend): Add more pdf viewer controls

* save

* fix(frontend): Pdf loading

* fix(frontend): Resize PDF in small window

* fix(frontend): Minor fixes

* feat(frontend): Add pdf zoom configuration

* fix wip

* save

* bg color

* save progress

* pdf scaling

* feat(frontend): fix zoom synchro

* fix(frontend): Pdf scroll tracking

* fix(frontend): Double scrollbar

* nits

* fixes

---------

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-03-06 20:17:36 +01:00
Faton Ramadani
b013996585 Merge branch 'main' into app-preview-scroll 2023-03-06 18:49:55 +01:00
Ruben Fiszel
614fb5022a feat(frontend): add ability to move nodes 2023-03-06 18:41:20 +01:00
Ruben Fiszel
0beadfd1ac fix z-index of inputransformform 2023-03-06 16:04:32 +01:00
Faton Ramadani
0a76e3533c feat(frontend): remove useless softWrap 2023-03-06 15:46:00 +01:00
Faton Ramadani
7648b382ff feat(frontend): remvove useless softWrap 2023-03-06 15:14:23 +01:00
Ruben Fiszel
25580c1272 add trigger button 2023-03-06 14:11:17 +01:00
Faton Ramadani
e2747e824f feat(frontend): merge main 2023-03-06 13:59:08 +01:00
Faton Ramadani
2060445d20 feat(frontend): fix text resize 2023-03-06 13:40:07 +01:00
Faton Ramadani
2557e136bd fix(frontend): fix app map reactivity (#1260) 2023-03-06 11:26:00 +01:00
Ruben Fiszel
200cb69d82 make default branch non removable for branchone 2023-03-06 11:19:56 +01:00
Ruben Fiszel
9ee261fe1a Update docker-compose.yml 2023-03-06 10:39:00 +01:00
Ruben Fiszel
8e563a42f5 Update docker-compose.yml with oauth example 2023-03-06 10:38:01 +01:00
Faton Ramadani
a999eb2112 fix(frontend): fix branch deletion (#1261)
* fix(frontend): fix branch deletion

* fix(frontend): fix branch deletion

* fix(frontend): fix branch deletion
2023-03-06 09:01:19 +01:00
Ruben Fiszel
e5dbe7076c handle larger sized graphs 2023-03-06 08:33:53 +01:00
Faton Ramadani
94e30052bf Merge branch 'main' into app-preview-scroll 2023-03-06 08:23:05 +01:00
Faton Ramadani
68add7932e feat(frontend): add a way to automatically resize (wip) + add automatic resizable component 2023-03-06 08:21:54 +01:00
Ruben Fiszel
2ac51b0af0 feat(frontend): refactor entire flow builder UX 2023-03-05 23:00:43 +01:00
Ruben Fiszel
f3232062c3 make tailwind inputs class configurable 2023-03-03 22:32:35 +01:00
Ruben Fiszel
b11a5a2df6 only bind the staticoutputs of the first row 2023-03-03 18:00:09 +01:00
Ruben Fiszel
e2c4545240 fix(frontend): arginput + apppreview fixes 2023-03-03 17:34:08 +01:00
Faton Ramadani
70dd6f759c App small fixes (#1258)
* fix(frontend): Fix runnable editor

* fix(frontend): remove isopenstore

* fix(frontend): add output searchbar

* fix(frontend): fix build

* fix(frontend): add missing clear button
2023-03-03 15:15:29 +01:00
Ruben Fiszel
dcfb29fb80 fix sqlx offline 2023-03-03 13:04:24 +01:00
Faton Ramadani
94f1aadef2 feat(frontend): Fix object viewer style (#1255)
Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2023-03-03 12:47:55 +01:00
Ruben Fiszel
58300eb6ac introduce root_job and leaf_jobs for efficient result_by_id 2023-03-03 12:44:44 +01:00
Ashutosh Narang
304dea4b74 update build instructions (#1256) 2023-03-03 11:19:12 +01:00
190 changed files with 8201 additions and 1810 deletions

View File

@@ -0,0 +1 @@
-- Add down migration script here

View File

@@ -0,0 +1,3 @@
-- Add up migration script here
ALTER TABLE queue ADD COLUMN root_job uuid;
ALTER TABLE queue ADD COLUMN leaf_jobs jsonb;

View File

@@ -645,27 +645,6 @@
},
"query": "SELECT * FROM workspace_settings WHERE workspace_id = $1"
},
"176afff6dfceac701d499663f15fb7f569fd9d449406df4c5ecc811414429d42": {
"describe": {
"columns": [
{
"name": "flow_status",
"ordinal": 0,
"type_info": "Jsonb"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
}
},
"query": "SELECT flow_status FROM completed_job WHERE id = $1 AND workspace_id = $2 \n UNION ALL SELECT flow_status FROM queue WHERE id = $1 AND workspace_id = $2 "
},
"187a27d18f78f068319a0ec684f71a76c49dd09c5c74c2c777e35233c4f5a281": {
"describe": {
"columns": [
@@ -826,75 +805,6 @@
},
"query": "\n SELECT id, flow_status, suspend, script_path\n FROM queue\n WHERE id = $1\n "
},
"1e35c39bc786d638252e5483ca4efae9a041f7e845341f8bfd715ddd9e899499": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Uuid"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Varchar",
"Uuid",
"Bool",
"Uuid",
"Varchar",
"Varchar",
"Timestamptz",
"Int8",
"Varchar",
"Text",
"Text",
"Jsonb",
{
"Custom": {
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies"
]
},
"name": "job_kind"
}
},
"Varchar",
"Jsonb",
"Jsonb",
"Bool",
{
"Custom": {
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash"
]
},
"name": "script_lang"
}
},
"Bool",
"Text",
"Varchar",
"Bool"
]
}
},
"query": "INSERT INTO queue\n (workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22) RETURNING id"
},
"1eaf8d677d520c7f2f303a731de6b6d939918e41ad0d1c748d80db3fd33cb9d3": {
"describe": {
"columns": [],
@@ -1176,6 +1086,20 @@
},
"query": "SELECT set_config('session.folders_read', $1, true)"
},
"2a3ebe1b0eae5b2164894321e138cc4dc0293788aeb98d05d95d18dfc708d6a6": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Jsonb",
"Uuid"
]
}
},
"query": "\n UPDATE queue\n SET leaf_jobs = JSONB_SET(coalesce(leaf_jobs, '{}'::jsonb), ARRAY[$1::TEXT], $2)\n WHERE COALESCE((SELECT root_job FROM queue WHERE id = $3), $3) = id\n "
},
"2a4be8334db7d39f3d954193a8b0169cc4a4a07e081d2fa61d8764879d6a8ff5": {
"describe": {
"columns": [],
@@ -3657,33 +3581,6 @@
},
"query": "SELECT is_admin FROM usr where username = $1 AND workspace_id = $2 AND disabled = false"
},
"92ce38eaaef9e793a49788bf3b7fc88340e83da57b785364a477f08c776f66f3": {
"describe": {
"columns": [
{
"name": "flow_status",
"ordinal": 0,
"type_info": "Jsonb"
},
{
"name": "parent_job",
"ordinal": 1,
"type_info": "Uuid"
}
],
"nullable": [
null,
null
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
}
},
"query": "SELECT flow_status, parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2 \n UNION ALL SELECT flow_status, parent_job FROM queue WHERE id = $1 AND workspace_id = $2 "
},
"930ad84a4db26fa6d2c8447d447099e944a0ea7b1266b4d02cee620fe3d761a5": {
"describe": {
"columns": [],
@@ -4009,6 +3906,76 @@
},
"query": "SELECT content FROM script WHERE path = $1 AND workspace_id = $2 AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND archived = false AND workspace_id = $2)"
},
"a1c41bbeb2d64fa1e7dfd2ed053191a1de5d786ae8c22e225e450865ecac94e9": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Uuid"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Varchar",
"Uuid",
"Bool",
"Uuid",
"Varchar",
"Varchar",
"Timestamptz",
"Int8",
"Varchar",
"Text",
"Text",
"Jsonb",
{
"Custom": {
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies"
]
},
"name": "job_kind"
}
},
"Varchar",
"Jsonb",
"Jsonb",
"Bool",
{
"Custom": {
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash"
]
},
"name": "script_lang"
}
},
"Bool",
"Text",
"Varchar",
"Bool",
"Uuid"
]
}
},
"query": "INSERT INTO queue\n (workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner, root_job)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22, $23) RETURNING id"
},
"a227548b6604c56bfc15eb780bd8ee72a89dc6701a50f5048e928bd87baa7b9a": {
"describe": {
"columns": [
@@ -4119,6 +4086,28 @@
},
"query": "UPDATE flow SET dependency_job = $1 WHERE path = $2 AND workspace_id = $3"
},
"a5f9fb82791103e2bbaf9cb6d87e8c50495d12d87f8ed83382068203a8dd7a67": {
"describe": {
"columns": [
{
"name": "?column?",
"ordinal": 0,
"type_info": "Jsonb"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text"
]
}
},
"query": "SELECT leaf_jobs->$1::text FROM queue WHERE COALESCE((SELECT root_job FROM queue WHERE id = $2), $2) = id AND workspace_id = $3"
},
"a6145b0482c9e5da245059a80b1563cad20318fd2dd8aef33f9ca97de1826b8b": {
"describe": {
"columns": [],
@@ -4875,27 +4864,6 @@
},
"query": "SELECT result FROM completed_job WHERE id = $1"
},
"c2d0e44faab6981a21ca28dfd6f4eef9dfcafb471852e701c0bbc8ae11344325": {
"describe": {
"columns": [
{
"name": "parent_job",
"ordinal": 0,
"type_info": "Uuid"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
}
},
"query": "SELECT parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT parent_job FROM queue WHERE id = $1 AND workspace_id = $2"
},
"c2d6cb56c1dea4498e2aab9ea9301dbbaa127602a38f57f5add4108fdc209b1a": {
"describe": {
"columns": [

View File

@@ -836,6 +836,7 @@ impl RunJob {
/* scheduled_for_o */ None,
/* schedule_path */ None,
/* parent_job */ None,
/* root job */ None,
/* is_flow_step */ false,
/* running */ false,
None,

View File

@@ -2538,11 +2538,6 @@ paths:
required: true
schema:
type: string
- name: skip_direct
description: Skip checking that the node is part of the given flow.
in: query
schema:
type: boolean
responses:
"200":
description: job result

View File

@@ -670,6 +670,7 @@ async fn execute_component(
None,
None,
None,
None,
false,
false,
None,

View File

@@ -237,6 +237,7 @@ async fn create_flow(
None,
None,
None,
None,
false,
false,
None,
@@ -393,6 +394,7 @@ async fn update_flow(
None,
None,
None,
None,
false,
false,
None,

View File

@@ -102,10 +102,9 @@ pub fn global_service() -> Router {
async fn get_result_by_id(
Extension(db): Extension<DB>,
Query(ResultByIdQuery { skip_direct }): Query<ResultByIdQuery>,
Path((w_id, flow_id, node_id)): Path<(String, String, String)>,
Path((w_id, flow_id, node_id)): Path<(String, Uuid, String)>,
) -> windmill_common::error::JsonResult<serde_json::Value> {
let res = windmill_queue::get_result_by_id(db, skip_direct, w_id, flow_id, node_id).await?;
let res = windmill_queue::get_result_by_id(db, w_id, flow_id, node_id).await?;
Ok(Json(res))
}
@@ -177,11 +176,6 @@ async fn get_job(
Ok(Json(job))
}
#[derive(Deserialize)]
pub struct ResultByIdQuery {
pub skip_direct: bool,
}
pub async fn get_job_by_id<'c>(
mut tx: Transaction<'c, Postgres>,
w_id: &str,
@@ -1078,6 +1072,8 @@ impl From<UnifiedJob> for Job {
visible_to_owner: uj.visible_to_owner,
suspend: uj.suspend,
mem_peak: uj.mem_peak,
root_job: None,
leaf_jobs: None,
}),
t => panic!("job type {} not valid", t),
}
@@ -1173,6 +1169,7 @@ pub async fn run_flow_by_path(
scheduled_for,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,
@@ -1208,6 +1205,7 @@ pub async fn run_job_by_path(
scheduled_for,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,
@@ -1360,6 +1358,7 @@ pub async fn run_wait_result_job_by_path(
scheduled_for,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,
@@ -1406,6 +1405,7 @@ pub async fn run_wait_result_job_by_hash(
scheduled_for,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,
@@ -1451,6 +1451,7 @@ pub async fn run_wait_result_flow_by_path(
scheduled_for,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,
@@ -1513,6 +1514,7 @@ async fn run_preview_job(
scheduled_for,
None,
None,
None,
false,
false,
None,
@@ -1546,6 +1548,7 @@ async fn run_preview_flow_job(
scheduled_for,
None,
None,
None,
false,
false,
None,
@@ -1581,6 +1584,7 @@ pub async fn run_job_by_hash(
scheduled_for,
None,
run_query.parent_job,
run_query.parent_job,
false,
false,
None,

View File

@@ -734,14 +734,10 @@ async fn slack_command(
.map_err(|_| error::Error::BadRequest("invalid payload".to_string()))?;
let body = String::from_utf8_lossy(&body);
if SLACK_SIGNING_SECRET
.as_ref()
.as_ref()
.map(|sv| sv.verify(&ts, &body, &sig).ok())
.flatten()
.is_none()
{
return Err(error::Error::BadRequest("verification failed".to_owned()));
if let Some(sv) = SLACK_SIGNING_SECRET.as_ref() {
if sv.verify(&ts, &body, &sig).ok().is_none() {
return Err(error::Error::BadRequest("verification failed".to_owned()));
}
}
let mut tx = db.begin().await?;
@@ -785,6 +781,7 @@ async fn slack_command(
None,
None,
None,
None,
false,
false,
None,

View File

@@ -123,6 +123,7 @@ async fn list_scripts(
AND workspace_id = ?)"
.bind(&w_id),
);
sqlb.and_where_eq("archived", true);
} else {
sqlb.and_where_eq("archived", false);
}
@@ -377,6 +378,7 @@ async fn create_script(
None,
None,
None,
None,
false,
false,
None,

View File

@@ -125,7 +125,7 @@ pub enum FlowStatusModule {
},
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum JobResult {
SingleJob(Uuid),
ListJob(Vec<Uuid>),

View File

@@ -6,10 +6,7 @@
* LICENSE-AGPL for a copy of the license.
*/
use std::{
collections::{HashMap, VecDeque},
str::FromStr,
};
use std::collections::HashMap;
use anyhow::Context;
use reqwest::Client;
@@ -20,7 +17,7 @@ use ulid::Ulid;
use uuid::Uuid;
use windmill_audit::{audit_log, ActionKind};
use windmill_common::{
error::{self, to_anyhow, Error},
error::{self, Error},
flow_status::{FlowStatus, JobResult, MAX_RETRY_ATTEMPTS, MAX_RETRY_INTERVAL},
flows::{FlowModule, FlowModuleValue, FlowValue},
scripts::{get_full_hub_script_by_path, HubScript, ScriptHash, ScriptLang},
@@ -153,102 +150,26 @@ pub async fn pull(
Ok(job)
}
pub async fn find_recursively_downward(
db: &Pool<Postgres>,
w_id: &str,
flow_id: Uuid,
node_id: &str,
) -> windmill_common::error::Result<Option<JobResult>> {
let mut bfs_stack = VecDeque::new();
bfs_stack.push_back(flow_id);
while bfs_stack.len() > 0 {
let parent_id = bfs_stack.pop_front().unwrap();
let job = sqlx::query_scalar!(
"SELECT flow_status FROM completed_job WHERE id = $1 AND workspace_id = $2
UNION ALL SELECT flow_status FROM queue WHERE id = $1 AND workspace_id = $2 ",
parent_id,
w_id
)
.fetch_optional(db)
.await?
.flatten();
if let Some(r) = job {
let status = serde_json::from_value::<FlowStatus>(r).map_err(to_anyhow)?;
for m in status.modules.iter() {
let id = m.id();
if id == node_id {
return Ok(m.job_result());
}
if let Some(job_id) = m.job() {
bfs_stack.push_back(job_id);
}
}
}
}
Ok(None)
}
pub async fn get_result_by_id(
db: Pool<Postgres>,
mut skip_direct: bool,
w_id: String,
flow_id: String,
flow_id: Uuid,
node_id: String,
) -> error::Result<serde_json::Value> {
let mut result_id: Option<JobResult> = None;
let mut parent_id = Uuid::from_str(&flow_id).ok();
let mut lparent_id = parent_id.clone();
while result_id.is_none() && parent_id.is_some() {
if !skip_direct {
let r = sqlx::query!(
"SELECT flow_status, parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2
UNION ALL SELECT flow_status, parent_job FROM queue WHERE id = $1 AND workspace_id = $2 ",
parent_id.unwrap(),
w_id,
)
.fetch_optional(&db)
.await?;
if let Some(r) = r {
let value = r
.flow_status
.as_ref()
.ok_or_else(|| Error::InternalErr(format!("requiring a flow status value")))?
.to_owned();
lparent_id = parent_id;
parent_id = r.parent_job;
let status_o = serde_json::from_value::<FlowStatus>(value).ok();
result_id = status_o.and_then(|status| {
status
.modules
.iter()
.find(|m| m.id() == node_id)
.and_then(|m| m.job_result())
});
} else {
parent_id = None;
}
} else {
let q_parent = sqlx::query_scalar!(
"SELECT parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT parent_job FROM queue WHERE id = $1 AND workspace_id = $2",
parent_id.unwrap(),
w_id,
)
.fetch_optional(&db)
.await?
.flatten();
lparent_id = parent_id;
parent_id = q_parent;
skip_direct = false
}
}
// we could not find the node going upward from the flow by looking at all the jobs (in progress or completed)
// we now look downward from the flow root to the all the children completed job for a job that might hide itself
// in a deep non-direct parent job such as in nested branches
if result_id.is_none() && lparent_id.is_some() {
result_id = find_recursively_downward(&db, &w_id, lparent_id.unwrap(), &node_id).await?;
}
let job_result: Option<JobResult> = sqlx::query_scalar!(
"SELECT leaf_jobs->$1::text FROM queue WHERE COALESCE((SELECT root_job FROM queue WHERE id = $2), $2) = id AND workspace_id = $3",
node_id,
flow_id,
w_id,
)
.fetch_optional(&db)
.await?
.flatten()
.map(|x| serde_json::from_value(x).ok())
.flatten();
let result_id = windmill_common::utils::not_found_if_none(
result_id,
job_result,
"Flow result by id",
format!("{}, {}", flow_id, node_id),
)?;
@@ -330,6 +251,7 @@ pub async fn push<'c>(
scheduled_for_o: Option<chrono::DateTime<chrono::Utc>>,
schedule_path: Option<String>,
parent_job: Option<Uuid>,
root_job: Option<Uuid>,
is_flow_step: bool,
mut same_worker: bool,
pre_run_error: Option<&windmill_common::error::Error>,
@@ -582,12 +504,13 @@ pub async fn push<'c>(
.unwrap_or_else(|| (None, None));
let flow_status = raw_flow.as_ref().map(FlowStatus::new);
let uuid = sqlx::query_scalar!(
"INSERT INTO queue
(workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for,
script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, \
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22) \
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner, root_job)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22, $23) \
RETURNING id",
workspace_id,
job_id,
@@ -610,7 +533,8 @@ pub async fn push<'c>(
same_worker,
pre_run_error.map(|e| e.to_string()),
email,
visible_to_owner
visible_to_owner,
root_job
)
.fetch_one(&mut tx)
.await
@@ -723,6 +647,10 @@ pub struct QueuedJob {
pub suspend: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mem_peak: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub root_job: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub leaf_jobs: Option<serde_json::Value>,
}
impl QueuedJob {

View File

@@ -85,6 +85,7 @@ pub async fn push_scheduled_job<'c>(
Some(next),
Some(schedule.path.clone()),
None,
None,
false,
false,
None,

View File

@@ -40,7 +40,7 @@ pub async fn eval_timeout(
let (sender, mut receiver) = oneshot::channel::<IsolateHandle>();
let base_internal_url: String = base_internal_url.to_string();
timeout(
std::time::Duration::from_millis(2000),
std::time::Duration::from_millis(3000),
tokio::task::spawn_blocking(move || {
let mut ops = vec![];
@@ -104,7 +104,7 @@ pub async fn eval_timeout(
isolate.terminate_execution();
};
Error::ExecutionErr(format!(
"The expression of evaluation `{expr2}` took too long to execute (>2000ms)"
"The expression of evaluation `{expr2}` took too long to execute (>3000ms)"
))
})??
}
@@ -315,7 +315,7 @@ async fn op_get_id(args: Vec<String>) -> Result<Option<serde_json::Value>, anyho
let client = windmill_api_client::create_client(base_url, token.clone());
let result = client
.result_by_id(workspace, flow_job_id, node_id, Some(true))
.result_by_id(workspace, flow_job_id, node_id)
.await
.map_or(None, |e| Some(e.into_inner()));

View File

@@ -326,6 +326,21 @@ pub async fn update_flow_status_after_job_completion(
)
.execute(&mut tx)
.await?;
if let Some(job_result) = new_status.job_result() {
sqlx::query!(
"
UPDATE queue
SET leaf_jobs = JSONB_SET(coalesce(leaf_jobs, '{}'::jsonb), ARRAY[$1::TEXT], $2)
WHERE COALESCE((SELECT root_job FROM queue WHERE id = $3), $3) = id
",
new_status.id(),
json!(job_result),
flow
)
.execute(&mut tx)
.await?;
}
}
}
@@ -1237,6 +1252,7 @@ async fn push_next_flow_job(
Ok(v) => (Some(v), None),
Err(e) => (None, Some(e)),
};
let root_job = flow_job.root_job.or_else(|| Some(flow_job.id));
let (uuid, inner_tx) = push(
tx,
&flow_job.workspace_id,
@@ -1248,6 +1264,7 @@ async fn push_next_flow_job(
scheduled_for_o,
flow_job.schedule_path.clone(),
Some(flow_job.id),
root_job,
true,
continue_on_same_worker,
err,

View File

@@ -38,7 +38,9 @@ services:
depends_on:
db:
condition: service_healthy
# volumes:
# - ./oauth.json/:/usr/src/app/oauth.json
windmill_worker:
image: ghcr.io/windmill-labs/windmill:main
deploy:
@@ -61,7 +63,6 @@ services:
# to mount the worker folder to debug,, KEEP_JOB_DIR=true and mount /tmp/windmill
volumes:
- worker_dependency_cache:/tmp/windmill/cache
# - ./oauth.json/:/usr/src/app/oauth.json
lsp:
image: ghcr.io/windmill-labs/windmill-lsp:latest

View File

@@ -1,6 +1,6 @@
# Developing
## Starting the Development Server
## Starting The Development Server
Once you've created a project and installed dependencies with `npm install` (or
`pnpm install` or `yarn`), start a development server:
@@ -17,7 +17,7 @@ In the root folder:
```bash
docker build . -t windmill
docker-compose up db server
docker-compose up db windmill_server windmill_worker
```
### 2. Backend is run by cargo
@@ -25,9 +25,9 @@ docker-compose up db server
**Prerequisites**
- Install Rust [as explained on the website](https://www.rust-lang.org/tools/install).
- Install llvm
- Install llvm
**on OSX:**
**On OSX:**
```bash
brew install llvm caddy gsed
@@ -36,7 +36,20 @@ docker-compose up db server
# now, restart your shell. You should now have the `lld` binary on your PATH.
```
- To test that you have Rust and Cargo installed run `cargo --version`
- In your terminal, go to the backend directory and run `cargo build`
- Run `cargo run`
**Known issue on M1 Mac while running `cargo build`**
- You may encounter `linking with cc failed` build time error.
- To solve this run:
```bash
echo 'export RUSTFLAGS="-L/opt/homebrew/opt/libomp/lib"' >> ~/.zshrc
source ~/.zshrc
```
**Do a Frontend Build**
In order to run the backend, you need to have a frontend build inside `frontend/build/`.
@@ -56,6 +69,14 @@ npm run build
# now, you'll have a `frontend/build` folder.
```
**Known issue while running `npm run build`**
- You may encounter `FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory` error.
- To solve this run:
```bash
export NODE_OPTIONS=--max_old_space_size=8096
```
- run `npm run build` again
In the root folder:
```bash
@@ -82,7 +103,7 @@ sudo caddy run --config ./Caddyfile
and then go to <http://localhost>
### Backend is run by remote!
### 3. Backend is run by remote!
```bash
sudo caddy run --config ./CaddyfileRemote
@@ -119,8 +140,8 @@ Recommended config for VS Code:
```json
"[svelte]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
```
- turn _format on save_ on

File diff suppressed because it is too large Load Diff

View File

@@ -21,12 +21,13 @@
"@sveltejs/package": "^1.0.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@types/d3": "^7.4.0",
"@types/d3-zoom": "^3.0.2",
"@types/node": "^18.11.18",
"@types/vscode": "~1.74.0",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.48.0",
"@windmill-labs/svelte-grid": "^5.1.6",
"@windmill-labs/svelvet": "^4.0.20",
"@zerodevx/svelte-toast": "^0.8.1",
"autoprefixer": "^10.4.13",
"cssnano": "^5.1.14",
@@ -37,6 +38,7 @@
"ol": "^7.2.2",
"openapi-typescript-codegen": "^0.23.0",
"path-browserify": "^1.0.1",
"pdfjs-dist": "^3.4.120",
"postcss": "^8.4.18",
"postcss-load-config": "^4.0.1",
"prettier": "^2.8.3",
@@ -72,6 +74,7 @@
"chart.js": "^3.9.1",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-plugin-zoom": "^2.0.0",
"d3-zoom": "^3.0.0",
"date-fns": "^2.29.3",
"fast-equals": "^4.0.3",
"highlight.js": "^11.7.0",

View File

@@ -50,12 +50,23 @@
.Template-editor span.mtk20 {
color: black !important;
}
::-webkit-scrollbar {
width: 9px;
height: 9px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: rgba(155, 155, 155, 0.5);
border: transparent;
}
}
@layer components {
/* Flow graph viewer -> Svelvet library internal class overwrite */
.Node {
display: flex !important;
cursor: pointer !important;
}
}

View File

@@ -110,7 +110,7 @@
{#if result != undefined}
{#if resultKind && resultKind != 'json'}
<div class="mb-2 text-gray-500 text-sm bg-gray-50/20">
as JSON&nbsp;<input type="checkbox" bind:checked={forceJson} /></div
as JSON&nbsp;<input class="windmillapp" type="checkbox" bind:checked={forceJson} /></div
>{/if}{#if typeof result == 'object' && Object.keys(result).length > 0}<div
class="mb-2 min-w-[400px] text-sm text-gray-700 relative"
>The result keys are: <b>{truncate(Object.keys(result).join(', '), 50)}</b>

View File

@@ -11,17 +11,21 @@
| undefined = undefined
</script>
<div class="inline-flex flex-row items-center">
<div class="inline-flex flex-row items-center truncated">
<span class="font-semibold">
{label}
</span>
<Required {required} class="!ml-0" />
<span class="text-sm italic ml-1 text-indigo-800">
({type ?? 'any'}{contentEncoding && contentEncoding != ''
? `, encoding: ${contentEncoding}`
: ''}{format && format != '' ? `, format: ${format}` : ''}{itemsType?.type
? ` of ${itemsType?.type}s`
: ''})</span
>
{#if format && format != ''}
<span class="text-sm italic ml-1 text-indigo-800">
({format})
</span>
{:else}
<span class="text-sm italic ml-1 text-indigo-800">
({type ?? 'any'}{contentEncoding && contentEncoding != ''
? `, encoding: ${contentEncoding}`
: ''})</span
>
{/if}
</div>

View File

@@ -1,22 +1,21 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { FlowService, ScheduleService, type Flow } from '$lib/gen'
import { FlowService, ScheduleService, type Flow, type FlowModule } from '$lib/gen'
import { initHistory, redo, undo } from '$lib/history'
import { userStore, workspaceStore } from '$lib/stores'
import { encodeState, formatCron, loadHubScripts, sendUserToast } from '$lib/utils'
import { faCalendarAlt, faEye, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
import { Redo, Undo } from 'lucide-svelte'
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
import { writable, type Writable } from 'svelte/store'
import CenteredPage from './CenteredPage.svelte'
import { Button, Drawer, DrawerContent } from './common'
import { Button, ButtonPopup, ButtonPopupItem } from './common'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
import { OFFSET } from './CronInput.svelte'
import FlowGraphViewer from './FlowGraphViewer.svelte'
import ScriptEditorDrawer from './flows/content/ScriptEditorDrawer.svelte'
import FlowEditor from './flows/FlowEditor.svelte'
import { flowStateStore } from './flows/flowState'
import { flowStore } from './flows/flowStore'
import type { FlowState } from './flows/flowState'
import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte'
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
import { loadFlowSchedule, type Schedule } from './flows/scheduleUtils'
@@ -27,8 +26,8 @@
export let selectedId: string | undefined
export let initialArgs: Record<string, any> = {}
export let loading = false
let pathError = ''
export let flowStore: Writable<Flow>
export let flowStateStore: Writable<FlowState>
async function createSchedule(path: string) {
const { cron, args, enabled } = $scheduleStore
@@ -51,7 +50,10 @@
}
}
async function saveFlow(): Promise<void> {
let loadingSave = false
async function saveFlow(leave: boolean): Promise<void> {
loadingSave = true
const flow = cleanInputs($flowStore)
const { cron, args, enabled } = $scheduleStore
$dirtyStore = false
@@ -113,7 +115,12 @@
await createSchedule(flow.path)
}
}
goto(`/flows/get/${$flowStore.path}?workspace_id=${$workspaceStore}`)
loadingSave = false
if (leave) {
goto(`/flows/get/${$flowStore.path}?workspace_id=${$workspaceStore}`)
} else if (initialPath !== $flowStore.path) {
goto(`/flows/edit/${$flowStore.path}?workspace_id=${$workspaceStore}`)
}
}
let timeout: NodeJS.Timeout | undefined = undefined
@@ -141,11 +148,15 @@
}, 500)
}
const selectedIdStore = writable<string>(selectedId)
const selectedIdStore = writable<string>(selectedId ?? 'settings-metadata')
const scheduleStore = writable<Schedule>({ args: {}, cron: '', enabled: false })
const previewArgsStore = writable<Record<string, any>>(initialArgs)
const scriptEditorDrawer = writable<ScriptEditorDrawer | undefined>(undefined)
const moving = writable<{ module: FlowModule; modules: FlowModule[] } | undefined>(undefined)
const history = initHistory($flowStore)
const testStepStore = writable<Record<string, any>>({})
function select(selectedId: string) {
selectedIdStore.set(selectedId)
@@ -154,9 +165,13 @@
setContext<FlowEditorContext>('FlowEditorContext', {
selectedId: selectedIdStore,
schedule: scheduleStore,
select,
previewArgs: previewArgsStore,
scriptEditorDrawer
scriptEditorDrawer,
moving,
history,
flowStateStore,
flowStore,
testStepStore
})
async function loadSchedule() {
@@ -178,40 +193,47 @@
$: initialPath && $workspaceStore && loadSchedule()
loadHubScripts()
let flowViewer: Drawer
</script>
{#if !$userStore?.operator}
<ScriptEditorDrawer bind:this={$scriptEditorDrawer} />
<UnsavedConfirmationModal />
<Drawer bind:this={flowViewer} size="75%">
<DrawerContent title="View Graph" on:close={flowViewer.closeDrawer} noPadding>
<div class="overflow-hidden h-full w-full">
<FlowGraphViewer flow={$flowStore} />
</div>
</DrawerContent>
</Drawer>
<div class="flex flex-col flex-1 h-screen">
<!-- Nav between steps-->
<div
class="justify-between flex flex-row w-full items-center pl-2.5 pr-6 space-x-4 overflow-x-auto scrollbar-hidden max-h-12 h-full"
>
<div class="flex flex-row">
<div class="flex flex-row gap-4 items-center">
<FlowImportExportMenu />
<Button
btnClasses="inline-flex"
startIcon={{ icon: faEye }}
variant="border"
color="light"
size="sm"
on:click={flowViewer.openDrawer}
>
Graph
</Button>
<div class="flex gap-1">
<Button
title="Undo"
disabled={$history.index == 0}
variant="border"
color="dark"
size="xs"
on:click={async () => {
$flowStore = undo(history, $flowStore)
}}
>
<Undo size={14} />
</Button>
<Button
title="Redo"
disabled={$history.index == $history.history.length - 1}
variant="border"
color="dark"
size="xs"
on:click={async () => {
$flowStore = redo(history)
}}
>
<Redo size={14} />
</Button>
</div>
</div>
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
{#if $scheduleStore.enabled}
<Button
@@ -258,12 +280,22 @@
<div class="flex flex-row space-x-2">
<FlowPreviewButtons />
<div class="center-center">
<Button
disabled={pathError != ''}
startIcon={{ icon: faSave }}
<ButtonPopup
loading={loadingSave}
size="sm"
on:click={saveFlow}>Save</Button
startIcon={{ icon: faSave }}
on:click={() => saveFlow(false)}
>
<svelte:fragment slot="main">Save</svelte:fragment>
<ButtonPopupItem on:click={() => saveFlow(true)}>Save and exit</ButtonPopupItem>
{#if initialPath != ''}
<ButtonPopupItem
on:click={() => {
window.open(`/flows/add?template=${initialPath}`)
}}>Fork</ButtonPopupItem
>
{/if}
</ButtonPopup>
</div>
</div>
</div>

View File

@@ -79,14 +79,13 @@
</Drawer>
<div class="grid grid-cols-3 w-full">
<div
bind:clientHeight={topHeight}
class="{noSide
? 'col-span-3'
: 'sm:col-span-2 col-span-3'} w-full border border-gray-400 h-screen"
: 'sm:col-span-2 col-span-3'} w-full border border-gray-400 max-h-screen"
class:overflow-auto={overflowAuto}
>
<FlowGraph
minHeight={topHeight}
minHeight={400}
modules={flow?.value?.modules}
failureModule={flow?.value?.failure_module}
on:click={(e) => (stepDetail = e.detail)}

View File

@@ -5,13 +5,12 @@
import { Button, Kbd } from './common'
import { createEventDispatcher, getContext } from 'svelte'
import Icon from 'svelte-awesome'
import { dfs, flowStore } from './flows/flowStore'
import { dfs } from './flows/flowStore'
import type { FlowEditorContext } from './flows/types'
import { runFlowPreview } from './flows/utils'
import SchemaForm from './SchemaForm.svelte'
import FlowStatusViewer from '../components/FlowStatusViewer.svelte'
import FlowProgressBar from './flows/FlowProgressBar.svelte'
import { flowStateStore } from './flows/flowState'
import CapturePayload from './flows/content/CapturePayload.svelte'
import { Loader2 } from 'lucide-svelte'
@@ -24,7 +23,8 @@
let isRunning: boolean = false
let jobProgressReset: () => void
const { selectedId, previewArgs } = getContext<FlowEditorContext>('FlowEditorContext')
const { selectedId, previewArgs, flowStateStore, flowStore } =
getContext<FlowEditorContext>('FlowEditorContext')
const dispatch = createEventDispatcher()
function sliceModules(modules: FlowModule[], upTo: number, idOrders: string[]): FlowModule[] {

View File

@@ -452,13 +452,17 @@
<FlowGraph
success={isSuccess(job?.['success'])}
flowModuleStates={localFlowModuleStates}
on:click={(e) => {
if (e.detail.id) {
on:select={(e) => {
if (typeof e.detail == 'string') {
if (e.detail == 'Input') {
selectedNode = 'start'
} else if (e.detail == 'Result') {
selectedNode = 'end'
} else {
selectedNode = e.detail
}
} else {
selectedNode = e.detail.id
} else if (e.detail == 'Result') {
selectedNode = 'end'
} else if (e.detail == 'Input') {
selectedNode = 'start'
}
}}
modules={job.raw_flow?.modules ?? []}

View File

@@ -148,7 +148,7 @@
{/if}
</div>
{#if !noDynamicToggle}
<div class="flex flex-row gap-x-4 gap-y-1 flex-wrap">
<div class="flex flex-row gap-x-4 gap-y-1 flex-wrap z-10">
<ToggleButtonGroup
bind:selected={propertyType}
on:selected={(e) => {
@@ -190,10 +190,10 @@
>
{#if isStaticTemplate(inputCat)}
<ToggleButton light position="left" value="static" size="xs">
{'${} '}Template &nbsp; <Tooltip
>Write javascript expressions between "{openBracket}" and "{closeBracket}". You may
refer to contextual objects like 'flow_input', or 'result' or functions like
'resource' and 'variable'
{'${} '}&nbsp;
<Tooltip
>Write text or surround javascript with "{openBracket}" and "{closeBracket}". Use
`result` to connect to another node's output.
</Tooltip></ToggleButton
>
{:else}
@@ -206,9 +206,11 @@
value="javascript"
startIcon={{ icon: faCode }}
size="xs"
>&nbsp;<Tooltip
>Write javascript expressions directly, using 'flow_input' or 'result'. You can use
multiline javascript.
</Tooltip></ToggleButton
>
Dynamic (JS)
</ToggleButton>
</ToggleButtonGroup>
<Button
variant="contained"
@@ -299,6 +301,4 @@
Not recognized input type {argName}
{/if}
</div>
{:else}
<p class="text-sm text-gray-700">Argument at {argName} is undefined</p>
{/if}

View File

@@ -0,0 +1,114 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { VariableService, type InputTransform } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { allTrue } from '$lib/utils'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { Button } from './common'
import InputTransformForm from './InputTransformForm.svelte'
import ItemPicker from './ItemPicker.svelte'
import VariableEditor from './VariableEditor.svelte'
export let schema: Schema
export let args: Record<string, InputTransform | any> = {}
export let isValid: boolean = true
export let extraLib: string = 'missing extraLib'
export let previousModuleId: string | undefined = undefined
export let filter: string[] | undefined = undefined
export let noDynamicToggle = false
let clazz: string = ''
export { clazz as class }
let inputCheck: { [id: string]: boolean } = {}
$: isValid = allTrue(inputCheck) ?? false
$: if (args == undefined || typeof args !== 'object') {
args = {}
}
function removeExtraKey() {
const nargs = {}
Object.keys(args ?? {}).forEach((key) => {
if (keys.includes(key)) {
nargs[key] = args[key]
}
})
args = nargs
}
let pickForField: string | undefined
let itemPicker: ItemPicker | undefined = undefined
let variableEditor: VariableEditor | undefined = undefined
let keys: string[] = []
$: {
let lkeys = Object.keys(schema?.properties ?? {})
if (schema?.properties && JSON.stringify(lkeys) != JSON.stringify(keys)) {
keys = lkeys
removeExtraKey()
}
}
</script>
<div class="w-full {clazz}">
{#if keys.length > 0}
{#each keys as argName, i (argName)}
{#if (!filter || filter.includes(argName)) && Object.keys(schema.properties ?? {}).includes(argName)}
<div class="z-10">
<InputTransformForm
{previousModuleId}
bind:arg={args[argName]}
bind:schema
bind:argName
bind:inputCheck={inputCheck[argName]}
bind:extraLib
{variableEditor}
{itemPicker}
bind:pickForField
{noDynamicToggle}
/>
</div>
{/if}
{/each}
{:else}
<div class="text-gray-500 text-sm">No inputs</div>
{/if}
</div>
<ItemPicker
bind:this={itemPicker}
pickCallback={(path, _) => {
if (pickForField) {
args[pickForField].value = '$var:' + path
}
}}
itemName="Variable"
extraField="path"
loadItems={async () =>
(await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({
name: x.path,
...x
}))}
>
<div
slot="submission"
class="flex flex-row-reverse w-full bg-white border-t border-gray-200 rounded-bl-lg rounded-br-lg"
>
<Button
variant="border"
color="blue"
size="sm"
startIcon={{ icon: faPlus }}
on:click={() => {
variableEditor?.initNew?.()
}}
>
New variable
</Button>
</div>
</ItemPicker>
<VariableEditor bind:this={variableEditor} />

View File

@@ -42,7 +42,7 @@
<button on:click={logViewer.openDrawer}>Expand</button>
<div class="py-2 pr-2 text-xs flex gap-2 items-center">
Auto scroll
<input type="checkbox" bind:checked={scroll} />
<input class="windmillapp" type="checkbox" bind:checked={scroll} />
</div>
</div>
</div>

View File

@@ -8,14 +8,17 @@
import LogViewer from './LogViewer.svelte'
import DisplayResult from './DisplayResult.svelte'
import Button from './common/button/Button.svelte'
import { flowStateStore, testStepStore } from './flows/flowState'
import { flowStore } from './flows/flowStore'
import { workspaceStore } from '$lib/stores'
import { Loader2 } from 'lucide-svelte'
import { getContext } from 'svelte'
import type { FlowEditorContext } from './flows/types'
export let mod: FlowModule
export let schema: Schema
const { flowStore, flowStateStore, testStepStore } =
getContext<FlowEditorContext>('FlowEditorContext')
// Test
let testJobLoader: TestJobLoader
let testIsLoading = false

View File

@@ -167,7 +167,7 @@
{#if showOptions}
<ul
class="options"
transition:fly={{ duration: 200, y: 5 }}
transition:fly|local={{ duration: 200, y: 5 }}
on:mousedown|preventDefault={handleOptionMousedown}
>
{#each filtered as option}

View File

@@ -49,22 +49,11 @@
</script>
{#if notClickable}
<span
use:popperRef
on:mouseenter={open}
on:mouseleave={close}
class={$$props.class}
>
<span use:popperRef on:mouseenter={open} on:mouseleave={close} class={$$props.class}>
<slot />
</span>
{:else}
<button
use:popperRef
on:mouseenter={open}
on:mouseleave={close}
on:click
class={$$props.class}
>
<button use:popperRef on:mouseenter={open} on:mouseleave={close} on:click class={$$props.class}>
<slot />
</button>
{/if}

View File

@@ -6,11 +6,9 @@
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import ArgInput from './ArgInput.svelte'
import { Button } from './common'
import InputTransformForm from './InputTransformForm.svelte'
import ItemPicker from './ItemPicker.svelte'
import VariableEditor from './VariableEditor.svelte'
export let inputTransform = false
export let schema: Schema
export let args: Record<string, InputTransform | any> = {}
export let disabledArgs: string[] = []
@@ -18,16 +16,12 @@
export let editableSchema = false
export let isValid: boolean = true
export let extraLib: string = 'missing extraLib'
export let autofocus = false
export let previousModuleId: string | undefined = undefined
export let shouldHideNoInputs: boolean = false
export let compact = false
export let password: string | undefined = undefined
export let noVariablePicker = false
export let filter: string[] | undefined = undefined
export let noDynamicToggle = false
export let flexWrap = false
export let noDelete = false
@@ -42,17 +36,19 @@
}
function removeExtraKey() {
const nargs = {}
Object.keys(args ?? {}).forEach((key) => {
if (!keys.includes(key)) {
delete args[key]
delete inputCheck[key]
if (keys.includes(key)) {
nargs[key] = args[key]
}
})
args = nargs
}
let pickForField: string | undefined
let itemPicker: ItemPicker | undefined = undefined
let variableEditor: VariableEditor | undefined = undefined
let keys: string[] = []
$: {
let lkeys = Object.keys(schema?.properties ?? {})
@@ -68,48 +64,60 @@
<div class="w-full {clazz} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 gap-y-2' : ''}">
{#if keys.length > 0}
{#each keys as argName, i (argName)}
{#if (!filter || filter.includes(argName)) && Object.keys(schema.properties ?? {}).includes(argName)}
{#if Object.keys(schema.properties ?? {}).includes(argName)}
<div>
{#if inputTransform}
<InputTransformForm
{previousModuleId}
bind:arg={args[argName]}
bind:schema
bind:argName
bind:inputCheck={inputCheck[argName]}
bind:extraLib
{variableEditor}
{itemPicker}
bind:pickForField
{noDynamicToggle}
/>
{:else if typeof args == 'object' && schema?.properties[argName]}
<ArgInput
autofocus={i == 0 && autofocus}
label={argName}
bind:description={schema.properties[argName].description}
bind:value={args[argName]}
type={schema.properties[argName].type}
required={schema.required.includes(argName)}
bind:pattern={schema.properties[argName].pattern}
bind:valid={inputCheck[argName]}
defaultValue={schema.properties[argName].default}
bind:enum_={schema.properties[argName].enum}
bind:format={schema.properties[argName].format}
contentEncoding={schema.properties[argName].contentEncoding}
properties={schema.properties[argName].properties}
bind:itemsType={schema.properties[argName].items}
disabled={disabledArgs.includes(argName) || disabled}
{editableSchema}
{compact}
password={argName == password}
{variableEditor}
{itemPicker}
bind:pickForField
bind:extra={schema.properties[argName]}
/>
{:else}
Expected argument to be an object, got {JSON.stringify(args)} instead
{#if typeof args == 'object' && schema?.properties[argName]}
{#if editableSchema}
<ArgInput
autofocus={i == 0 && autofocus}
label={argName}
bind:description={schema.properties[argName].description}
bind:value={args[argName]}
type={schema.properties[argName].type}
required={schema.required.includes(argName)}
bind:pattern={schema.properties[argName].pattern}
bind:valid={inputCheck[argName]}
defaultValue={schema.properties[argName].default}
bind:enum_={schema.properties[argName].enum}
bind:format={schema.properties[argName].format}
contentEncoding={schema.properties[argName].contentEncoding}
properties={schema.properties[argName].properties}
bind:itemsType={schema.properties[argName].items}
disabled={disabledArgs.includes(argName) || disabled}
{editableSchema}
{compact}
password={argName == password}
{variableEditor}
{itemPicker}
bind:pickForField
bind:extra={schema.properties[argName]}
/>
{:else}
<ArgInput
autofocus={i == 0 && autofocus}
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}
bind:valid={inputCheck[argName]}
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}
disabled={disabledArgs.includes(argName) || disabled}
{editableSchema}
{compact}
password={argName == password}
{variableEditor}
{itemPicker}
bind:pickForField
extra={schema.properties[argName]}
/>
{/if}
{/if}
</div>
{/if}
@@ -124,11 +132,7 @@
bind:this={itemPicker}
pickCallback={(path, _) => {
if (pickForField) {
if (inputTransform) {
args[pickForField].value = '$var:' + path
} else {
args[pickForField] = '$var:' + path
}
args[pickForField] = '$var:' + path
}
}}
itemName="Variable"

View File

@@ -14,7 +14,7 @@
import CenteredPage from './CenteredPage.svelte'
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
import { Button, Kbd } from './common'
import { Button, ButtonPopup, ButtonPopupItem, Kbd } from './common'
import { faChevronDown, faChevronUp, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
import Breadcrumb from './common/breadcrumb/Breadcrumb.svelte'
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
@@ -54,7 +54,9 @@
script.content = initialCode(language, kind, template)
}
async function editScript(): Promise<void> {
let loadingSave = false
async function editScript(leave: boolean): Promise<void> {
loadingSave = true
try {
$dirtyStore = false
localStorage.removeItem(script.path)
@@ -83,11 +85,17 @@
kind: script.kind
}
})
history.replaceState(history.state, '', `/scripts/edit/${newHash}?step=2`)
goto(`/scripts/get/${newHash}?workspace_id=${$workspaceStore}`)
if (leave) {
history.replaceState(history.state, '', `/scripts/edit/${newHash}?step=2`)
goto(`/scripts/get/${newHash}?workspace_id=${$workspaceStore}`)
} else {
await goto(`/scripts/edit/${newHash}?step=2`)
script.hash = newHash
}
} catch (error) {
sendUserToast(`Impossible to save the script: ${error.body}`, true)
sendUserToast(`Impossible to save the script: ${error.body || error.message}`, true)
}
loadingSave = false
}
async function changeStep(step: number) {
@@ -182,14 +190,24 @@
>
Next {#if step == 1}<Kbd>Enter</Kbd>{/if}
</Button>
<Button
<ButtonPopup
loading={loadingSave}
size="sm"
variant={step == 1 ? 'border' : 'contained'}
disabled={step === 1 && pathError !== ''}
btnClasses={step == 1 && initialPath == '' ? 'invisible' : ''}
startIcon={{ icon: faSave }}
on:click={editScript}>Save</Button
on:click={() => editScript(false)}
>
<svelte:fragment slot="main">Save</svelte:fragment>
<ButtonPopupItem on:click={() => editScript(true)}>Save and exit</ButtonPopupItem>
{#if initialPath != ''}
<ButtonPopupItem
on:click={() => {
window.open(`/scripts/add?template=${initialPath}`)
}}>Fork</ButtonPopupItem
>
{/if}
</ButtonPopup>
</div>
</div>
</div>

View File

@@ -209,7 +209,7 @@
})
</script>
<div bind:this={divEl} class="{$$props.class} editor" bind:clientWidth={width} />
<div bind:this={divEl} class="{$$props.class ?? ''} editor" bind:clientWidth={width} />
<style>
.editor {

View File

@@ -12,5 +12,5 @@
{#if !view}<ChevronDown />{:else}<ChevronUp />{/if}</Button
>
{#if view}
<div class="my-4 px-2" transition:slide><slot /></div>
<div class="my-4 px-2" transition:slide|local><slot /></div>
{/if}

View File

@@ -596,7 +596,7 @@
<div
bind:this={divEl}
style="height: 18px;"
class="{$$props.class} template rounded-lg min-h-4 mx-0.5"
class="{$$props.class ?? ''} template rounded-lg min-h-4 mx-0.5 overflow-clip"
bind:clientWidth={width}
/>

View File

@@ -13,7 +13,7 @@
<Popover notClickable {placement} class={wrapperClass}>
<Icon
class="{light
? 'text-gray-300'
? 'text-gray-400'
: ' text-gray-500'} font-thin inline-block align-middle w-4 {$$props.class}"
data={faInfoCircle}
{scale}

View File

@@ -86,6 +86,33 @@
$: errorsMessage = Object.values(errors)
.filter((x) => x != '')
.join('\n')
async function handleClick(event: CustomEvent) {
event?.stopPropagation()
event?.preventDefault()
if (preclickAction) {
await preclickAction()
}
ownClick = true
if (!runnableComponent) {
if (gotoUrl) {
if (gotoNewTab) {
window.open(gotoUrl, '_blank')
} else {
goto(gotoUrl)
}
}
} else {
await runnableComponent?.runComponent()
}
if (recomputeIds) {
await Promise.all(recomputeIds.map((id) => $runnableComponents?.[id]?.()))
}
}
</script>
<InputValue {id} input={configuration.label} bind:value={labelValue} />
@@ -131,32 +158,7 @@
e?.stopPropagation()
window.dispatchEvent(new Event('pointerup'))
}}
on:click={async (e) => {
if (preclickAction) {
await preclickAction()
}
e?.stopPropagation()
e?.preventDefault()
ownClick = true
if (!runnableComponent) {
if (gotoUrl) {
if (gotoNewTab) {
window.open(gotoUrl, '_blank')
} else {
goto(gotoUrl)
}
}
} else {
await runnableComponent?.runComponent()
}
if (recomputeIds) {
recomputeIds.forEach((id) => {
$runnableComponents[id]?.()
})
}
}}
on:click={handleClick}
{size}
{color}
{loading}

View File

@@ -147,7 +147,6 @@
})
$: css = concatCustomCss($app.css?.mapcomponent, customCss)
$: gridItem = findGridItem($app, id)
function updateRegionOutput() {
if (map) {
@@ -164,6 +163,7 @@
}
function handleSyncRegion() {
const gridItem = findGridItem($app, id)
if (!map || !gridItem) {
return
}

View File

@@ -0,0 +1,318 @@
<script lang="ts">
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import { getDocument, type PDFDocumentProxy, type PDFPageProxy } from 'pdfjs-dist'
import 'pdfjs-dist/build/pdf.worker.entry'
import type { AppInput } from '../../inputType'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import InputValue from '../helpers/InputValue.svelte'
import { throttle } from '../../../../utils'
import { Button } from '../../../common'
import { Download, Loader2, MoveHorizontal, ZoomIn, ZoomOut } from 'lucide-svelte'
import { fade } from 'svelte/transition'
import { findGridItem } 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
const { app, mode, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
let source: string | ArrayBuffer | undefined = undefined
let wrapper: HTMLDivElement | undefined = undefined
let error: string | undefined = undefined
let doc: PDFDocumentProxy | undefined = undefined
let pages: PDFPageProxy[] = []
let zoom: number | undefined = undefined
let controlsWidth: number | undefined = undefined
let controlsHeight: number | undefined = undefined
let pageNumber = 1
$: if (source == '') {
resetDoc()
error = 'Set the "Source" attribute of the PDF component'
}
$: zoom && handleZoom()
$: wrapper && loadDocument(source)
$: wideView = controlsWidth && controlsWidth > 450
async function resetDoc() {
await doc?.destroy()
doc = undefined
}
function handleZoom() {
if (zoom && wrapper) {
try {
renderPdf(false)
} catch (err) {
error = err?.message ?? (typeof err === 'string' ? err : 'Error loading PDF')
}
}
}
async function loadDocument(src: string | ArrayBuffer | undefined) {
if (!src) {
return
}
try {
await resetDoc()
doc = await getDocument(src).promise
pageNumber = 1
await renderPdf(false, false)
error = undefined
} catch (err) {
await resetDoc()
error = err?.message ?? (typeof err === 'string' ? err : 'Error loading PDF')
console.log(err)
}
}
async function renderPdf(scaleToViewport = true, resizing = false) {
if (!(doc && wrapper && zoom)) {
return
}
const scrollPosition = wrapper.scrollTop / wrapper.scrollHeight
if (!resizing) {
pages = []
}
const nextPages: typeof pages = []
const nextChildren: HTMLCanvasElement[] = []
const { width } = wrapper.getBoundingClientRect()
let scale = zoom / 100
if (scaleToViewport) {
const firstViewport = (await doc.getPage(1)).getViewport({ scale: 1 })
// Rounded to the first integer that is a multiple of 10 and is less than the viewport width
zoom = Math.floor((width / firstViewport.width) * 10) * 10
scale = zoom / 100
}
for (let i = 0; i < doc.numPages; i++) {
const canvas = document.createElement('canvas')
const canvasContext = canvas.getContext('2d')
if (!canvasContext) {
console.warn('Could not get canvas context for PDF page ' + i)
continue
}
const page = await doc.getPage(i + 1)
nextPages.push(page)
const viewport = page.getViewport({ scale })
canvas.height = viewport.height
canvas.width = viewport.width
canvas.classList.add('mx-auto', 'my-4', 'shadow-sm')
await page.render({ canvasContext, viewport }).promise
nextChildren.push(canvas)
}
while (wrapper.firstChild) {
wrapper.removeChild(wrapper.firstChild)
}
pages = [...nextPages]
wrapper.append(...nextChildren)
wrapper.scrollTo({
top: scrollPosition * wrapper.scrollHeight
})
}
function scrollToPage(page: number) {
page = pageNumber = minMax(page, 1, pages.length)
const offset = (wrapper?.children.item(page - 1) as HTMLCanvasElement | null)?.offsetTop
if (!offset) {
return
}
// controlsHeight + 2px border + half of the top margin
const padding = (controlsHeight ? controlsHeight + 2 : 0) + 8
wrapper?.scrollTo({
top: offset - padding
})
}
const throttledScroll = throttle(onScroll, 400)
function onScroll() {
if (!wrapper) {
return
}
const THRESHOLD = 50
let scrollPosition = wrapper.scrollTop + THRESHOLD + (controlsHeight ?? 0)
let page = 1
for (let i = 0; i < pages.length; i++) {
const canvas = wrapper.children.item(i) as HTMLCanvasElement | null
if (scrollPosition < (canvas?.offsetTop ?? wrapper.scrollHeight)) {
break
}
page = i + 1
}
pageNumber = page
}
function syncZoomValue() {
const gridItem = findGridItem($app, id)
if (gridItem && gridItem.data.configuration.zoom.value !== zoom) {
gridItem.data.configuration.zoom.value = zoom
}
$app = $app
}
async function downloadPdf() {
if (!doc) {
return
}
const data = await doc.saveDocument()
const url = URL.createObjectURL(new Blob([data.buffer]))
const link = document.createElement('a')
link.href = url
link.download = 'document.pdf'
link.click()
URL.revokeObjectURL(url)
}
function minMax(value: number, min: number, max: number) {
if (value < min) {
return min
} else if (value > max) {
return max
}
return value
}
$: css = concatCustomCss($app.css?.pdfcomponent, customCss)
</script>
<InputValue {id} input={configuration.source} bind:value={source} />
<InputValue {id} input={configuration.zoom} bind:value={zoom} />
<div class="relative w-full h-full bg-gray-100">
{#if source && zoom}
{#if pages?.length}
<div
bind:clientWidth={controlsWidth}
bind:clientHeight={controlsHeight}
class="fixed flex {$mode !== 'preview'
? 'w-[calc(100%-2px)] top-[1px]'
: 'w-full top-0'} {wideView
? 'justify-center gap-14'
: '!justify-between'} overflow-x-auto bg-white border mx-auto py-1"
>
<div class="flex justify-start items-center px-2 text-gray-600 text-sm">
<Button
on:click={() => zoom && (zoom -= 10)}
disabled={!doc}
size="xs"
color="light"
variant="border"
title="Zoom out"
aria-label="Zoom out"
btnClasses="!rounded-r-none !px-2"
>
<ZoomOut size={16} />
</Button>
{#if wideView}
<Button
on:click={() => (zoom = 100)}
disabled={!doc}
size="xs"
color="light"
variant="border"
title="Reset zoom"
aria-label="Reset zoom"
btnClasses="!w-[50px] !font-medium !rounded-none !border-l-0 !px-1"
>
{zoom.toFixed(0)}%
</Button>
{/if}
<Button
on:click={() => renderPdf(true, true)}
disabled={!doc}
size="xs"
color="light"
variant="border"
title="Scale to viewport"
aria-label="Scale to viewport"
btnClasses="!rounded-none !border-l-0 !px-2"
>
<MoveHorizontal size={16} />
</Button>
<Button
on:click={() => zoom && (zoom += 10)}
disabled={!doc}
size="xs"
color="light"
variant="border"
title="Zoom in"
aria-label="Zoom in"
btnClasses="!rounded-l-none !px-2 !border-l-0"
>
<ZoomIn size={16} />
</Button>
</div>
<div class="center-center px-2 text-gray-600 text-sm">
<input
on:input={({ currentTarget }) => {
scrollToPage(currentTarget.valueAsNumber)
}}
min="1"
max={pages.length}
value={pageNumber}
disabled={!doc}
type="number"
class="!w-[45px] !px-1 !py-0"
/>
<span class="whitespace-nowrap pl-1">
/ {pages.length}
</span>
</div>
<div class="flex justify-end items-center px-2 text-gray-600 text-sm">
<Button
on:click={downloadPdf}
disabled={!doc}
size="xs"
color="light"
variant="border"
title="Download PDF"
aria-label="Download PDF"
btnClasses="!font-medium !px-2"
>
{#if wideView}
<span class="mr-1"> Download </span>
{/if}
<Download size={16} />
</Button>
</div>
</div>
{:else}
<div
out:fade={{ duration: 200 }}
class="absolute inset-0 center-center flex-col text-center text-sm bg-white text-gray-600"
>
<Loader2 class="animate-spin mb-2" />
Loading PDF
</div>
{/if}
<div
bind:this={wrapper}
on:scroll={throttledScroll}
class={twMerge('w-full h-full overflow-auto', css?.container?.class ?? '', 'bg-gray-100')}
style="padding-top: {controlsHeight ?? 0}px; {css?.container?.style ?? ''}"
/>
{/if}
{#if $mode !== 'preview' && $selectedComponent === id}
<button
class="fixed z-10 bottom-0 left-0 px-2 py-0.5 bg-indigo-500/90
hover:bg-indigo-500 focus:bg-indigo-500 duration-200 text-white text-2xs"
on:click={() => syncZoomValue()}
>
Sync zoom value
</button>
{/if}
{#if error}
<div
class="absolute inset-0 z-20 center-center
bg-gray-100 text-center text-gray-600 text-sm"
>
{error}
</div>
{/if}
</div>

View File

@@ -10,6 +10,7 @@
import { twMerge } from 'tailwind-merge'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
import { getContext } from 'svelte'
import ResizeWrapper from '../helpers/ResizeWrapper.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -26,6 +27,7 @@
let result: string | undefined = undefined
let style: 'Title' | 'Subtitle' | 'Body' | 'Caption' | 'Label' | undefined = undefined
let copyButton: boolean
let fitContent = false
function getComponent() {
switch (style) {
@@ -63,36 +65,39 @@
<InputValue {id} input={configuration.style} bind:value={style} />
<InputValue {id} input={configuration.copyButton} bind:value={copyButton} />
<InputValue {id} input={configuration.fitContent} bind:value={fitContent} />
<RunnableWrapper flexWrap {componentInput} {id} bind:initializing bind:result>
<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:element
this={component}
class={twMerge(
'whitespace-pre-wrap',
$app.css?.['textcomponent']?.['text']?.class,
customCss?.text?.class,
classes
)}
style={[$app.css?.['textcomponent']?.['text']?.style, customCss?.text?.style].join(';')}
>
{String(result)}
</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 {id} shouldWrap={fitContent}>
<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:element
this={component}
class={twMerge(
'whitespace-pre-wrap',
$app.css?.['textcomponent']?.['text']?.class,
customCss?.text?.class,
classes
)}
style={[$app.css?.['textcomponent']?.['text']?.style, customCss?.text?.style].join(';')}
>
{String(result)}
</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>
</RunnableWrapper>

View File

@@ -6,6 +6,7 @@ export { default as AppHtml } from './AppHtml.svelte'
export { default as AppIcon } from './AppIcon.svelte'
export { default as AppImage } from './AppImage.svelte'
export { default as AppMap } from './AppMap.svelte'
export { default as AppPdf } from './AppPdf.svelte'
export { default as AppPieChart } from './AppPieChart.svelte'
export { default as AppScatterChart } from './AppScatterChart.svelte'
export { default as AppText } from './AppText.svelte'

View File

@@ -64,11 +64,11 @@
let selectedRowIndex = -1
function toggleRow(row: Record<string, any>, rowIndex: number) {
if (selectedRowIndex !== rowIndex) {
function toggleRow(row: Record<string, any>, rowIndex: number, force: boolean = false) {
if (selectedRowIndex !== rowIndex || force) {
selectedRowIndex = rowIndex
outputs?.selectedRow.set(row.original)
outputs?.selectedRowIndex.set(rowIndex)
outputs?.selectedRow.set(row.original, force)
outputs?.selectedRowIndex.set(rowIndex, force)
}
}
@@ -138,6 +138,9 @@
function rerender() {
table = createSvelteTable(options)
if (result) {
toggleRow({ original: result[0] }, 0, true)
}
}
$: result && rerender()
@@ -242,16 +245,28 @@
<td class="p-2 " on:click={() => toggleRow(row, rowIndex)}>
<div class="center-center h-full w-full flex-wrap gap-1">
{#each actionButtons as actionButton, actionIndex (actionIndex)}
<AppButton
noWFull
{...actionButton}
preclickAction={async () => {
toggleRow(row, rowIndex)
}}
extraQueryParams={{ row: row.original }}
bind:componentInput={actionButton.componentInput}
bind:staticOutputs={$staticOutputsStore[actionButton.id]}
/>
{#if rowIndex == 0}
<AppButton
noWFull
{...actionButton}
preclickAction={async () => {
toggleRow(row, rowIndex)
}}
extraQueryParams={{ row: row.original }}
bind:componentInput={actionButton.componentInput}
bind:staticOutputs={$staticOutputsStore[actionButton.id]}
/>
{:else}
<AppButton
noWFull
{...actionButton}
preclickAction={async () => {
toggleRow(row, rowIndex)
}}
extraQueryParams={{ row: row.original }}
bind:componentInput={actionButton.componentInput}
/>
{/if}
{/each}
</div>
</td>

View File

@@ -17,18 +17,36 @@
$: if (input && !deepEqual(input, lastInput)) {
lastInput = JSON.parse(JSON.stringify(input))
// Needed because of file uploads
if (input?.['value'] instanceof ArrayBuffer) {
lastInput.value = input?.['value']
}
}
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
$: state = $worldStore?.state
$: lastInput && $worldStore && handleConnection()
$: lastInput && lastInput.type == 'template' && $state && (value = getValue(lastInput))
$: lastInput && lastInput.type == 'eval' && $state && (value = evalExpr(lastInput))
let timeout: NodeJS.Timeout | undefined = undefined
const debounce_ms = 50
function debounce(cb: () => void) {
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(cb, debounce_ms)
}
$: lastInput && $worldStore && debounce(handleConnection)
$: lastInput &&
lastInput.type == 'template' &&
$state &&
debounce(() => (value = getValue(lastInput)))
$: lastInput &&
lastInput.type == 'eval' &&
$state &&
debounce(() => (value = evalExpr(lastInput)))
function handleConnection() {
// console.log('handle connection', id, lastInput)
if (lastInput.type === 'connected') {
$worldStore?.connect<any>(lastInput, onValueChange)
} else if (lastInput.type === 'static' || lastInput.type == 'template') {
@@ -110,7 +128,6 @@
function onValueChange(newValue: any): void {
if (lastInput.type === 'connected' && newValue !== undefined && newValue !== null) {
const { connection } = lastInput
if (!connection) {
// No connection
return

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import { getContext } from 'svelte'
import { findGridItem } from '../../editor/appUtils'
import type { AppEditorContext } from '../../types'
export let id: string
export let shouldWrap: boolean = false
const { app, breakpoint } = getContext<AppEditorContext>('AppEditorContext')
$: gridItem = findGridItem($app, id)
let wrapper: HTMLElement
$: {
if (wrapper && gridItem && shouldWrap) {
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}>
<slot />
</div>
{:else}
<slot />
{/if}

View File

@@ -2,10 +2,12 @@
import { goto } from '$app/navigation'
import type { Schema } from '$lib/common'
import Alert from '$lib/components/common/alert/Alert.svelte'
import Popover from '$lib/components/Popover.svelte'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
import { AppService, type CompletedJob } from '$lib/gen'
import { defaultIfEmptyString, emptySchema, sendUserToast } from '$lib/utils'
import { classNames, defaultIfEmptyString, emptySchema, sendUserToast } from '$lib/utils'
import { Bug } from 'lucide-svelte'
import { getContext, onMount } from 'svelte'
import type { AppInputs, Runnable } from '../../inputType'
import type { Output } from '../../rx'
@@ -50,12 +52,13 @@
}
})
let args: Record<string, any> = {}
let args: Record<string, any> | undefined = undefined
let testIsLoading = false
let runnableInputValues: Record<string, any> = {}
let executeTimeout: NodeJS.Timeout | undefined = undefined
function setDebouncedExecute() {
console.log('EXECUTE')
executeTimeout && clearTimeout(executeTimeout)
executeTimeout = setTimeout(() => {
executeComponent(true)
@@ -83,9 +86,8 @@
}
$: fields && (lazyStaticValues = computeStaticValues())
$: runnableInputValues &&
extraQueryParams &&
args &&
$: console.log(runnableInputValues, extraQueryParams, args, autoRefresh, testJobLoader)
$: (runnableInputValues || extraQueryParams || args) &&
autoRefresh &&
testJobLoader &&
setDebouncedExecute()
@@ -104,7 +106,6 @@
}
$: outputs?.loading?.set(testIsLoading)
$: schemaStripped = stripSchema(fields, $stateId)
function stripSchema(inputs: AppInputs, s: any): Schema {
@@ -163,7 +164,7 @@
if (field?.type == 'static' && fields[k]) {
staticRunnableInputs[k] = field.value
} else if (field?.type == 'user') {
nonStaticRunnableInputs[k] = args[k]
nonStaticRunnableInputs[k] = args?.[k]
} else {
nonStaticRunnableInputs[k] = runnableInputValues[k]
}
@@ -276,16 +277,28 @@
Please select a runnable
</Alert>
{:else if result?.error && $mode === 'preview'}
<div class="p-2">
<Alert type="error" title="Error during execution">
<div class="flex flex-col gap-2">
An error occured, please contact the app author.
<span class="font-semibold">Job id: {testJob?.id}</span>
<pre class=" whitespace-pre-wrap text-gray-900 bg-white border w-full p-4 text-xs"
>{JSON.stringify(result.error, null, 4)}
</pre>
</div>
</Alert>
<div
title="Error"
class={classNames(
'text-red-500 px-1 text-2xs py-0.5 font-bold w-fit absolute border border-red-500 -bottom-2 shadow left-1/2 transform -translate-x-1/2 z-50 cursor-pointer',
'bg-red-100/80'
)}
>
<Popover notClickable placement="bottom" popupClass="!bg-white border w-96">
<Bug size={14} />
<span slot="text">
<div class="bg-white">
<Alert type="error" title="Error during execution">
<div class="flex flex-col gap-2">
An error occured, please contact the app author.
<span class="font-semibold">Job id: {testJob?.id}</span>
</div>
</Alert>
</div>
</span>
</Popover>
</div>
<div class="block grow w-full max-h-full border border-red-300 bg-red-50 relative">
<slot />
</div>
{:else}

View File

@@ -61,12 +61,16 @@
<div class="app-select w-full mx-0.5" style="height: 34px" on:pointerdown|stopPropagation>
{#if !value || Array.isArray(value)}
<Select
--border-radius="0"
--border-color="#999"
multiple
on:change={(e) => e.stopPropagation()}
{items}
class={css?.input?.class ?? ''}
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
containerStyles={SELECT_INPUT_DEFAULT_STYLE.containerStyles + css?.input?.style}
containerStyles={'border-color: #999;' +
SELECT_INPUT_DEFAULT_STYLE.containerStyles +
css?.input?.style}
bind:value
{placeholder}
on:click={() => {

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
@@ -48,7 +49,10 @@
<AlignWrapper {verticalAlignment}>
<input
class="mx-0.5 {css?.input?.class ?? ''}"
class={twMerge(
'windmillapp w-full py-1.5 text-sm focus:ring-indigo-100 px-2 mx-0.5',
css?.input?.class ?? ''
)}
style={css?.input?.style ?? ''}
bind:this={input}
on:input={handleInput}

View File

@@ -54,7 +54,12 @@
<span class={css?.limits?.class ?? ''} style={css?.limits?.style ?? ''}>
{+min}
</span>
<div class="grow" on:pointerdown|stopPropagation>
<div
class="grow"
style="--range-handle-focus: {'#7e9abd'}; --range-handle: {'#7e9abd'}; {css?.bar?.style ??
''}"
on:pointerdown|stopPropagation
>
<RangeSlider
bind:slider
bind:values

View File

@@ -8,6 +8,8 @@
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
import { SELECT_INPUT_DEFAULT_STYLE } from '../../../../defaults'
import { twMerge } from 'tailwind-merge'
import Portal from 'svelte-portal'
export const staticOutputs: string[] = ['result']
export let id: string
@@ -97,16 +99,19 @@
<InputValue {id} input={configuration.create} bind:value={create} />
<AlignWrapper {horizontalAlignment} {verticalAlignment}>
<div class="app-select w-full mx-0.5" style="height: 34px" on:pointerdown|stopPropagation>
<div class="app-select w-full mx-0.5" style="height: 34px;" on:pointerdown|stopPropagation>
<Select
--border-radius="0"
--border-color="#999"
bind:filterText
on:filter={handleFilter}
on:clear={onChange}
on:change={onChange}
items={listItems}
class={css?.input?.class ?? ''}
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
containerStyles={SELECT_INPUT_DEFAULT_STYLE.containerStyles + css?.input?.style}
containerStyles={'border-color: #999;' +
SELECT_INPUT_DEFAULT_STYLE.containerStyles +
css?.input?.style}
{value}
{placeholder}
on:click={() => {

View File

@@ -13,7 +13,8 @@
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'> | undefined = undefined
export let customCss: ComponentCustomCSS<'handle' | 'limits' | 'value' | 'bar'> | undefined =
undefined
const { app, worldStore, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
let min = 0
@@ -60,7 +61,12 @@
<span class={css?.limits?.class ?? ''} style={css?.limits?.style ?? ''}>
{+min}
</span>
<div class="grow" on:pointerdown|stopPropagation={() => ($selectedComponent = id)}>
<div
class="grow"
style="--range-handle-focus: {'#7e9abd'}; --range-handle: {'#7e9abd'}; {css?.bar?.style ??
''}"
on:pointerdown|stopPropagation={() => ($selectedComponent = id)}
>
<RangeSlider bind:slider bind:values {step} min={+min} max={+max} />
</div>
<span class={css?.limits?.class ?? ''} style={css?.limits?.style ?? ''}>

View File

@@ -42,7 +42,10 @@
<AlignWrapper {verticalAlignment}>
<input
class={twMerge('mx-0.5', css?.input?.class ?? '')}
class={twMerge(
'windmillapp w-full py-1.5 text-sm focus:ring-indigo-100 px-2 mx-0.5',
css?.input?.class ?? ''
)}
style={css?.input?.style ?? ''}
on:focus={(e) => {
e?.stopPropagation()

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import type { AppInput } from '../../../inputType'
import type { Output } from '../../../rx'
import type { AppEditorContext, ComponentCustomCSS } from '../../../types'
@@ -54,9 +55,9 @@
{#key currency}
<CurrencyInput
inputClasses={{
formatted: 'p-0 ' + css?.input?.class,
wrapper: 'w-full',
formattedZero: 'text-black ' + css?.input?.class
formatted: twMerge('px-2 w-full py-1.5 windmillapp', css?.input?.class),
wrapper: 'w-full windmillapp',
formattedZero: twMerge('text-black windmillapp ', css?.input?.class)
}}
style={css?.input?.style}
bind:value

View File

@@ -36,6 +36,7 @@
import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte'
import { page } from '$app/stores'
import CssSettings from './componentsPanel/CssSettings.svelte'
import { initHistory } from '$lib/history'
export let app: App
export let path: string
@@ -56,6 +57,7 @@
input: undefined,
hoveredComponent: undefined
})
const history = initHistory(app)
const runnableComponents = writable<Record<string, () => Promise<void>>>({})
const errorByComponent = writable<Record<string, { error: string; componentId: string }>>({})
@@ -82,7 +84,8 @@
openDebugRun: writable(undefined),
focusedGrid,
stateId: writable(0),
parentWidth: writable(0)
parentWidth: writable(0),
history
})
let timeout: NodeJS.Timeout | undefined = undefined
@@ -201,20 +204,20 @@
</Pane>
<Pane size={21} minSize={5} maxSize={33}>
<div class="relative flex flex-col h-full">
<Tabs bind:selected={selectedTab} class="!border-b-2 !border-gray-200">
<Tab value="insert" size="xs" class="grow">
<Tabs bind:selected={selectedTab} class="!border-b-2 !border-gray-200 !h-full">
<Tab value="insert" size="xs">
<div class="m-1 center-center gap-2">
<Icon data={faPlus} />
<span>Insert</span>
</div>
</Tab>
<Tab value="settings" size="xs" class="grow">
<Tab value="settings" size="xs">
<div class="m-1 center-center gap-2">
<Icon data={faSliders} />
<span>Settings</span>
</div>
</Tab>
<Tab value="css" size="xs" class="grow">
<Tab value="css" size="xs">
<div class="m-1 center-center gap-2">
<Icon data={faCode} />
<span>CSS</span>

View File

@@ -1,7 +1,14 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import { Alert, Badge, Drawer, DrawerContent } from '$lib/components/common'
import {
Alert,
Badge,
ButtonPopup,
ButtonPopupItem,
Drawer,
DrawerContent
} from '$lib/components/common'
import Button from '$lib/components/common/button/Button.svelte'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
@@ -18,6 +25,7 @@
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { AppService, Job, Policy } from '$lib/gen'
import { redo, undo } from '$lib/history'
import { userStore, workspaceStore } from '$lib/stores'
import {
faBug,
@@ -35,8 +43,10 @@
Loader2,
MoreVertical,
Pencil,
Redo,
SlidersHorizontal,
Smartphone,
Undo,
X
} from 'lucide-svelte'
import { getContext } from 'svelte'
@@ -79,7 +89,8 @@
errorByComponent,
openDebugRun,
focusedGrid,
selectedComponent
selectedComponent,
history
} = getContext<AppEditorContext>('AppEditorContext')
const loading = {
@@ -417,6 +428,32 @@
<div class="min-w-64 w-64">
<input type="text" placeholder="App summary" class="text-sm w-full" bind:value={$summary} />
</div>
<div class="flex gap-1">
<Button
title="Undo"
disabled={$history.index == 0}
variant="border"
color="dark"
size="xs"
on:click={async () => {
$app = undo(history, $app)
}}
>
<Undo size={14} />
</Button>
<Button
title="Redo"
disabled={$history.index == $history.history.length - 1}
variant="border"
color="dark"
size="xs"
on:click={async () => {
$app = redo(history)
}}
>
<Redo size={14} />
</Button>
</div>
<div class="flex gap-4 items-center grow justify-center">
<div>
<ToggleButtonGroup bind:selected={$mode}>
@@ -537,14 +574,31 @@
>
<span class="hidden md:inline">Publish</span>
</Button>
<Button
loading={loading.save}
startIcon={{ icon: faSave }}
on:click={save}
color="dark"
size="xs"
>
<span class="hidden md:inline">Save</span>
</Button>
{#if appPath == ''}
<Button
loading={loading.save}
startIcon={{ icon: faSave }}
on:click={save}
color="dark"
size="xs"
>
<span class="hidden md:inline">Save</span>
</Button>
{:else}
<ButtonPopup
loading={loading.save}
startIcon={{ icon: faSave }}
on:click={save}
color="dark"
size="xs"
>
<svelte:fragment slot="main">Save</svelte:fragment>
<ButtonPopupItem
on:click={() => {
window.open(`/apps/add?template=${appPath}`)
}}>Fork</ButtonPopupItem
>
</ButtonPopup>
{/if}
</div>
</div>

View File

@@ -63,7 +63,8 @@
openDebugRun: writable(undefined),
focusedGrid: writable(undefined),
stateId: writable(0),
parentWidth: writable(0)
parentWidth: writable(0),
history: writable<any>(undefined)
})
let mounted = false
@@ -72,7 +73,7 @@
mounted = true
})
$: mounted && ($worldStore = buildWorld($staticOutputs, undefined, context))
$: mounted && ($worldStore = buildWorld($staticOutputs, $worldStore, context))
$: width = $breakpoint === 'sm' ? 'max-w-[640px]' : 'w-full '
$: lockedClasses = isLocked ? '!max-h-[400px] overflow-hidden pointer-events-none' : ''
</script>
@@ -94,7 +95,6 @@
{#if isLocked}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
transition:fade|local={{ duration: 200, easing: cubicOut }}
on:click={() => (isLocked = false)}
class="absolute inset-0 center-center bg-black/20 z-50 backdrop-blur-[1px] cursor-pointer"
>

View File

@@ -15,8 +15,7 @@
const dispatch = createEventDispatcher()
const { errorByComponent, openDebugRun, focusedGrid, worldStore } =
getContext<AppEditorContext>('AppEditorContext')
const { errorByComponent, openDebugRun } = getContext<AppEditorContext>('AppEditorContext')
$: error = Object.values($errorByComponent).find((e) => e.componentId === component.id)

View File

@@ -64,6 +64,8 @@
// @ts-ignore
let container
// $: containerHeight = subGrid.map((item) => columnConfiguration.map((c) => item[c[1]].h))
</script>
<div

View File

@@ -31,16 +31,14 @@ export function getNextGridItemId(app: App): string {
}
export function createNewGridItem(grid: GridItem[], id: string, data: AppComponent): GridItem {
const appComponent = data
appComponent.id = id
const newComponent = {
fixed: false,
resizable: true,
draggable: true,
customDragger: false,
customResizer: false,
x: 0,
y: 0
}
@@ -60,7 +58,10 @@ export function createNewGridItem(grid: GridItem[], id: string, data: AppCompone
min: { w: 1, h: 1 },
max: { w: column, h: 100 },
w: rec.w,
h: rec.h
h: rec.h,
customDragger: false,
customResizer: false,
fixed: false
}
const position = gridHelp.findSpace(newItem, grid, column) as { x: number; y: number }
newItem[column] = { ...newItem[column], ...position }

View File

@@ -37,7 +37,8 @@
AppAggridTable,
AppDrawer,
AppMap,
AppSplitpanes
AppSplitpanes,
AppPdf
} from '../../components'
import AppMultiSelect from '../../components/inputs/AppMultiSelect.svelte'
@@ -46,7 +47,7 @@
export let locked: boolean = false
export let pointerdown: boolean = false
const { staticOutputs, mode, connectingInput, app } =
const { staticOutputs, mode, connectingInput, app, errorByComponent } =
getContext<AppEditorContext>('AppEditorContext')
let hover = false
let initializing: boolean | undefined = undefined
@@ -56,6 +57,9 @@
// $: if (!deepEqual(component, pComponent)) {
// component = JSON.parse(JSON.stringify(pComponent))
// }
$: componentWithErrors = Object.values($errorByComponent).map((e) => e.componentId)
$: hasError = componentWithErrors.includes(component.id)
</script>
<div
@@ -81,7 +85,7 @@
selected && $mode !== 'preview' ? 'border border-blue-500' : '',
!selected && $mode !== 'preview' && !component.card ? 'border-gray-100' : '',
$mode !== 'preview' && !$connectingInput.opened ? 'hover:border-blue-500' : '',
component.softWrap ? '' : 'overflow-auto',
component.softWrap || hasError ? '' : 'overflow-auto',
$mode != 'preview' ? 'cursor-pointer' : '',
'relative z-auto',
$app.css?.['app']?.['component']?.class
@@ -194,6 +198,7 @@
customCss={component.customCss}
componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
recomputeIds={component.recomputeIds}
/>
{:else if component.type === 'selectcomponent'}
<AppSelect
@@ -221,6 +226,7 @@
customCss={component.customCss}
componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
recomputeIds={component.recomputeIds}
/>
{:else if component.type === 'formbuttoncomponent'}
<AppFormButton
@@ -231,6 +237,7 @@
customCss={component.customCss}
componentInput={component.componentInput}
bind:staticOutputs={$staticOutputs[component.id]}
recomputeIds={component.recomputeIds}
/>
{:else if component.type === 'checkboxcomponent'}
<AppCheckbox
@@ -386,13 +393,25 @@
customCss={component.customCss}
/>
{:else if component.type === 'mapcomponent'}
<AppMap {...component} bind:staticOutputs={$staticOutputs[component.id]} />
<AppMap
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{:else if component.type === 'pdfcomponent'}
<AppPdf
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:staticOutputs={$staticOutputs[component.id]}
/>
{/if}
</div>
</div>
{#if initializing}
<div
out:fade={{ duration: 200 }}
out:fade|local={{ duration: 200 }}
class="absolute inset-0 center-center flex-col bg-white text-gray-600 border"
>
<Loader2 class="animate-spin" size={16} />

View File

@@ -31,7 +31,8 @@ import {
SidebarClose,
MapPin,
FlipHorizontal,
FlipVertical
FlipVertical,
FileText
} from 'lucide-svelte'
import type { BaseAppComponent } from '../../types'
@@ -91,6 +92,7 @@ export type VerticalSplitPanesComponent = BaseComponent<'verticalsplitpanescompo
export type HorizontalSplitPanesComponent = BaseComponent<'horizontalsplitpanescomponent'> & {
panes: number[]
}
export type PdfComponent = BaseComponent<'pdfcomponent'>
export type AppComponent = BaseAppComponent &
(
@@ -129,6 +131,7 @@ export type AppComponent = BaseAppComponent &
| MapComponent
| VerticalSplitPanesComponent
| HorizontalSplitPanesComponent
| PdfComponent
)
export type AppComponentDimensions = `${IntRange<
@@ -228,6 +231,12 @@ export const components: Record<AppComponent['type'], AppComponentConfig> = {
value: false,
fieldType: 'boolean',
onlyStatic: true
},
fitContent: {
type: 'static',
value: false,
fieldType: 'boolean',
onlyStatic: true
}
},
customCss: {
@@ -912,9 +921,7 @@ Hello \${ctx.username}
type: 'static',
fieldType: 'array',
subFieldType: 'text',
value: [
"Foo", "Bar"
]
value: ['Foo', 'Bar']
},
placeholder: {
type: 'static',
@@ -1051,6 +1058,7 @@ Hello \${ctx.username}
}
},
customCss: {
bar: { style: '' },
handle: { style: '' },
limits: { class: '', style: '' },
value: { class: '', style: '' }
@@ -1097,6 +1105,7 @@ Hello \${ctx.username}
},
customCss: {
handles: { style: '' },
bar: { style: '' },
limits: { class: '', style: '' },
values: { class: '', style: '' }
} as const,
@@ -1528,5 +1537,35 @@ Hello \${ctx.username}
panes: [50, 50],
numberOfSubgrids: 2
}
},
pdfcomponent: {
name: 'PDF',
icon: FileText,
dims: '3:8-8:12',
data: {
id: '',
type: 'pdfcomponent',
componentInput: undefined,
configuration: {
source: {
type: 'static',
value: '/dummy.pdf',
fieldType: 'text',
fileUpload: {
accept: 'application/pdf',
convertTo: 'buffer'
}
},
zoom: {
fieldType: 'number',
type: 'static',
value: 100
}
},
customCss: {
container: { class: '', style: '' }
} as const,
card: false
}
}
}

View File

@@ -51,6 +51,7 @@ const display: ComponentSet = {
'plotlycomponent',
'scatterchartcomponent',
'timeseriescomponent',
'pdfcomponent',
'displaycomponent'
]
} as const

View File

@@ -1,16 +1,19 @@
<script lang="ts">
import type { AppEditorContext } from '../../types'
import { getContext, onMount } from 'svelte'
import { isOpenStore } from './store'
import { getContext } from 'svelte'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
import { components as componentsRecord, COMPONENT_SETS, type AppComponent } from '../component'
import ListItem from './ListItem.svelte'
import { insertNewGridItem } from '../appUtils'
import { X } from 'lucide-svelte'
import { push } from '$lib/history'
const TITLE_PREFIX = 'Component.' as const
const { app, selectedComponent, focusedGrid } = getContext<AppEditorContext>('AppEditorContext')
const { app, selectedComponent, focusedGrid, history } =
getContext<AppEditorContext>('AppEditorContext')
function addComponent(appComponentType: AppComponent['type']): void {
push(history, $app)
$dirtyStore = true
const data = componentsRecord[appComponentType].data
@@ -19,31 +22,62 @@
$selectedComponent = id
}
onMount(() => {
isOpenStore.addItems(COMPONENT_SETS.map((set) => ({ [TITLE_PREFIX + set.title]: true })))
})
let search = ''
// Filter COMPONENT_SETS by search
$: componentsFiltered = COMPONENT_SETS.map((set) => ({
...set,
components: set.components.filter((component) => {
const name = componentsRecord[component].name.toLowerCase()
return name.includes(search.toLowerCase())
})
}))
</script>
{#each COMPONENT_SETS as { title, components }, index (index)}
<ListItem {title} prefix={TITLE_PREFIX}>
{#if components.length}
<div class="flex flex-wrap gap-2 py-2">
{#each components as item}
<button
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
justify-center bg-white rounded-md hover:bg-gray-100 duration-200"
>
<svelte:component this={componentsRecord[item].icon} />
<div class="text-xs w-full text-center ellipsize">
{componentsRecord[item].name}
</div>
</button>
{/each}
</div>
{:else}
<div class="text-xs text-gray-500 py-1 px-2"> There are no components in this group yet </div>
<section class="p-2 sticky bg-white border-b w-full h-12 z-20 top-0">
<div class="relative">
<input
bind:value={search}
class="px-2 py-1 border border-gray-300 rounded-sm {search ? 'pr-8' : ''}"
placeholder="Search components..."
/>
{#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}
</ListItem>
{/each}
</div>
</section>
{#if componentsFiltered.reduce((acc, { components }) => acc + components.length, 0) === 0}
<div class="text-xs text-gray-500 py-1 px-2"> No components found </div>
{:else}
{#each componentsFiltered as { title, components }, index (index)}
<ListItem title={`${title} (${components.length})`}>
{#if components.length}
<div class="flex flex-wrap gap-2 py-2">
{#each components as item}
<button
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
justify-center bg-white rounded-md hover:bg-gray-100 duration-200"
>
<svelte:component this={componentsRecord[item].icon} />
<div class="text-xs w-full text-center ellipsize">
{componentsRecord[item].name}
</div>
</button>
{/each}
</div>
{:else}
<div class="text-xs text-gray-500 py-1 px-2">
There are no components in this group yet
</div>
{/if}
</ListItem>
{/each}
{/if}

View File

@@ -26,7 +26,7 @@
<input type="text" class="!pr-7" bind:value={value.style} on:focus />
{#if value?.style}
<button
transition:fade={{ duration: 100 }}
transition:fade|local={{ duration: 100 }}
class="absolute z-10 top-1.5 right-1 rounded-full p-1 duration-200 hover:bg-gray-200"
aria-label="Remove styles"
on:click|preventDefault|stopPropagation={() => reset('style')}
@@ -44,7 +44,7 @@
<input type="text" class="!pr-7" bind:value={value.class} on:focus />
{#if value?.class}
<button
transition:fade={{ duration: 100 }}
transition:fade|local={{ duration: 100 }}
class="absolute z-10 top-1.5 right-1 rounded-full p-1 duration-200 hover:bg-gray-200"
aria-label="Remove classes"
on:click|preventDefault|stopPropagation={() => reset('class')}

View File

@@ -4,27 +4,38 @@
import { isOpenStore } from './store'
export let title: string
export let prefix: string = ''
export let prefix: string | undefined = undefined
$: storeTitle = prefix + title
$: isOpen = $isOpenStore[storeTitle]
$: isOpen = prefix ? $isOpenStore[storeTitle] : true
</script>
<section class="pt-1 pb-2 px-1">
<button
on:click|preventDefault={() => isOpenStore.toggle(storeTitle)}
class="w-full flex justify-between items-center text-gray-700 px-2 py-1
{#if prefix !== undefined}
<button
on:click|preventDefault={() => isOpenStore.toggle(storeTitle)}
class="w-full flex justify-between items-center text-gray-700 px-2 py-1
rounded-sm duration-200 hover:bg-gray-100"
>
<h1 class="text-sm font-semibold text-left">
>
<h1 class="text-sm font-semibold text-left">
<slot name="title">
{title}
</slot>
</h1>
<ChevronDown class="rotate-0 duration-300 {isOpen ? '!rotate-180' : ''}" />
</button>
{#if isOpen}
<div transition:slide|local={{ duration: 300 }} class="px-2">
<slot />
</div>
{/if}
{:else}
<h1 class="text-sm font-semibold text-left px-2 py-1">
<slot name="title">
{title}
</slot>
</h1>
<ChevronDown class="rotate-0 duration-300 {isOpen ? '!rotate-180' : ''}" />
</button>
{#if isOpen}
<div transition:slide|local={{ duration: 300 }} class="px-2">
<div class="px-2">
<slot />
</div>
{/if}

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import { X } from 'lucide-svelte'
import { getContext } from 'svelte'
import type { AppEditorContext } from '../../types'
import { findGridItem } from '../appUtils'
@@ -42,13 +43,40 @@
$: panels = [['ctx', ['email', 'username', 'query']] 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())
)
})
</script>
<PanelSection noPadding titlePadding="px-4 pt-2" title="Outputs">
<PanelSection noPadding titlePadding="px-4 pt-2 pb-0.5" title="Outputs">
<div
class="overflow-auto min-w-[150px] border-t w-full relative flex flex-col gap-4 px-2 pt-4 pb-2"
>
{#each panels as [componentId, outputs] (componentId)}
<div class="relative {$connectingInput?.opened ? 'bg-white z-50' : ''}">
<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>
{#each filteredPanels as [componentId, outputs] (componentId)}
{#if outputs.length > 0 && $worldStore?.outputsById[componentId]}
{@const name = getComponentNameById(componentId)}
<div>
@@ -62,18 +90,18 @@
? undefined
: () => ($selectedComponent = componentId)}
class={classNames(
'px-2 text-2xs py-0.5 border border-gray-300 font-bold rounded-t-sm w-fit',
'px-2 text-2xs py-0.5 border-t border-x font-bold rounded-t-sm w-fit',
$selectedComponent === componentId
? ' bg-indigo-500 text-white'
: 'bg-gray-200 text-gray-500'
? ' bg-blue-500 text-white border-blue-500'
: 'bg-gray-100 text-gray-500 border-gray-200'
)}
>
{componentId}
</button>
<span
class={classNames(
'px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit',
'bg-gray-500 text-white'
'px-1 text-2xs py-0.5 font-semibold rounded-t-sm w-fit',
'bg-gray-700 text-white'
)}
>
{name}

View File

@@ -7,7 +7,7 @@
import { inferArgs } from '$lib/infer'
import { initialCode } from '$lib/script_helpers'
import { capitalize, emptySchema, getScriptByPath } from '$lib/utils'
import { faCodeBranch } from '@fortawesome/free-solid-svg-icons'
import { faCodeBranch, faTrash } from '@fortawesome/free-solid-svg-icons'
import { Building, Globe2 } from 'lucide-svelte'
import { createEventDispatcher, getContext } from 'svelte'
import { fly } from 'svelte/transition'
@@ -176,12 +176,22 @@
<div class="mt-4">
<Button
on:click={() => picker?.openDrawer()}
size="sm"
size="xs"
color="blue"
startIcon={{ icon: faCodeBranch }}
btnClasses="truncate"
>
or fork a detached/Workspace/Hub script
</Button>
<Button
on:click={() => dispatch('delete')}
size="xs"
color="red"
variant="border"
startIcon={{ icon: faTrash }}
btnClasses="truncate"
>
Delete
</Button>
</div>
</div>

View File

@@ -3,7 +3,7 @@
import type { Preview } from '$lib/gen'
import { createEventDispatcher, getContext, onMount } from 'svelte'
import type { AppEditorContext, InlineScript } from '../../types'
import { CheckCircle, Maximize2, Trash2, X } from 'lucide-svelte'
import { CornerDownLeft, Maximize2, Plus, Trash2, X } from 'lucide-svelte'
import InlineScriptEditorDrawer from './InlineScriptEditorDrawer.svelte'
import { inferArgs } from '$lib/infer'
import type { Schema } from '$lib/common'
@@ -11,11 +11,11 @@
import { fly } from 'svelte/transition'
import Editor from '$lib/components/Editor.svelte'
import { emptySchema, scriptLangToEditorLang } from '$lib/utils'
import Tooltip from '$lib/components/Tooltip.svelte'
import Popover from '../../../Popover.svelte'
import { computeFields } from './utils'
import { deepEqual } from 'fast-equals'
import type { AppInput } from '../../inputType'
import Kbd from '$lib/components/common/kbd/Kbd.svelte'
let inlineScriptEditorDrawer: InlineScriptEditorDrawer
@@ -73,37 +73,34 @@
}
}
}
let isMac = navigator.userAgent.indexOf('Mac OS X') !== -1
</script>
<InlineScriptEditorDrawer {editor} bind:this={inlineScriptEditorDrawer} bind:inlineScript />
<div class="h-full flex flex-col gap-1" transition:fly|local={{ duration: 50 }}>
<div class="h-full flex flex-col gap-1">
<div class="flex justify-between w-full gap-2 px-2 pt-1 flex-row items-center">
{#if name !== undefined}
<input bind:value={name} placeholder="Inline script name" />
<input bind:value={name} placeholder="Inline script name" class="!text-xs !rounded-xs" />
{/if}
<div class="flex w-full flex-row gap-2 items-center justify-end">
{#if validCode}
<Badge color="green" baseClass="!p-0 !h-[30px] aspect-square center-center">
<CheckCircle size={16} />
</Badge>
<Badge color="green" baseClass="!text-2xs">Valid</Badge>
{:else}
<Badge color="red" baseClass="!p-0 !h-[30px] aspect-square center-center">
<X size={16} />
</Badge>
<Badge color="red" baseClass="!text-2xs">Invalid</Badge>
{/if}
{#if id.startsWith('unused-') || id.startsWith('bg_')}
<Popover notClickable placement="bottom">
<Button
size="xs"
color="red"
variant="border"
btnClasses="!px-1.5"
color="light"
btnClasses="!px-2 !bg-red-100 hover:!bg-red-200"
aria-label="Delete"
on:click={() => dispatch('delete')}
>
<Trash2 size={16} />
<Trash2 size={14} class="text-red-800" />
</Button>
<svelte:fragment slot="text">Delete</svelte:fragment>
</Popover>
@@ -111,41 +108,60 @@
<Popover notClickable placement="bottom">
<Button
size="xs"
color="blue"
variant="border"
btnClasses="!px-1.5"
color="light"
btnClasses="!px-2 !bg-gray-100 hover:!bg-gray-200"
aria-label="Open full editor"
on:click={() => {
inlineScriptEditorDrawer?.openDrawer()
}}
>
<Maximize2 size={16} />
<Maximize2 size={14} />
</Button>
<svelte:fragment slot="text">Open full editor</svelte:fragment>
</Popover>
<Button
variant="border"
size="xs"
color="blue"
color="light"
btnClasses="!px-2 !py-1"
on:click={async () => {
editor.format()
}}
>
Format&nbsp;<Tooltip placement="bottom">Ctrl+S</Tooltip>
<div class="flex flex-row gap-1 items-center">
Format
<div class="flex flex-row items-center gap-1">
<Kbd>{isMac ? '⌘' : 'CTRL'}</Kbd>
<Kbd>S</Kbd>
</div>
</div>
</Button>
{#if $runnableComponents[id] != undefined}
<Button
loading={runLoading}
size="xs"
color="blue"
color="dark"
variant="border"
btnClasses="!px-2 !py-1 !bg-gray-700 !text-white hover:!bg-gray-900"
on:click={async () => {
runLoading = true
await $runnableComponents[id]?.()
runLoading = false
}}
>
Run&nbsp;
<Tooltip light placement="bottom">Ctrl+Enter</Tooltip>
<div class="flex flex-row gap-1 items-center">
Run
<div class="flex flex-row items-center gap-1">
<Kbd>{isMac ? '⌘' : 'CTRL'}</Kbd>
<Kbd>
<div class="h-4 flex items-center justify-center">
<CornerDownLeft size={10} />
</div>
</Kbd>
</div>
</div>
</Button>
{/if}
</div>

View File

@@ -62,6 +62,12 @@
) {
refreshScript(componentInput.runnable)
}
function deleteInlineScript() {
if (componentInput && componentInput.type == 'runnable') {
componentInput = clearResultAppInput(componentInput)
}
}
</script>
<Drawer bind:this={drawerFlowViewer} size="1200px">
@@ -80,16 +86,13 @@
bind:name={componentInput.runnable.name}
bind:fields={componentInput.fields}
syncFields
on:delete={() => {
if (componentInput && componentInput.type == 'runnable') {
componentInput = clearResultAppInput(componentInput)
}
}}
on:delete={deleteInlineScript}
/>
{:else}
<EmptyInlineScript
{id}
name={componentInput.runnable.name}
on:delete={deleteInlineScript}
on:new={(e) => {
if (
componentInput &&

View File

@@ -12,6 +12,16 @@
getContext<AppEditorContext>('AppEditorContext')
let selectedScriptComponentId: string | undefined = undefined
function deleteBackgroundScript(index: number) {
// remove the script from the array at the index
$app.hiddenInlineScripts.splice(index, 1)
$app.hiddenInlineScripts = [...$app.hiddenInlineScripts]
delete $staticOutputs[`bg_${index}`]
delete $runnableComponents[`bg_${index}`]
$staticOutputs = $staticOutputs
}
</script>
<SplitPanesWrapper>
@@ -97,20 +107,13 @@
bind:name={hiddenInlineScript.name}
bind:fields={hiddenInlineScript.fields}
syncFields
on:delete={() => {
// remove the script from the array at the index
$app.hiddenInlineScripts.splice(index, 1)
$app.hiddenInlineScripts = [...$app.hiddenInlineScripts]
delete $staticOutputs[`bg_${index}`]
delete $runnableComponents[`bg_${index}`]
$staticOutputs = $staticOutputs
}}
on:delete={() => deleteBackgroundScript(index)}
/>
{:else}
<EmptyInlineScript
id={`b_${index}`}
name={hiddenInlineScript.name}
on:delete={() => deleteBackgroundScript(index)}
on:new={(e) => {
hiddenInlineScript.inlineScript = e.detail
}}

View File

@@ -54,9 +54,9 @@
</script>
<div
class="sticky top-0 py-0.5 px-2 text-center bg-gray-200 text-gray-500 text-2xs font-bold tracking-wide border-b border-gray-300"
class="sticky top-0 py-0.5 px-2 text-left bg-gray-50 text-gray-800 text-xs font-semibold tracking-wide border-b border-gray-300"
>
RUNNABLES
Runnables
</div>
<div bind:this={list} class="grow flex flex-col gap-4">
<PanelSection title="Inline scripts" smallPadding>
@@ -129,9 +129,12 @@
smallPadding
>
<svelte:fragment slot="action">
<Button size="xs" color="dark" variant="border" on:click={createBackgroundScript}
><Plus size={14} />
</Button>
<button
class="rounded-full bg-gray-100 hover:bg-gray-200 p-1 border border-gray-200"
on:click={createBackgroundScript}
>
<Plus size={14} class="text-gray-500" />
</button>
</svelte:fragment>
<div class="flex flex-col gap-1 w-full">
{#if $app.hiddenInlineScripts?.length > 0}

View File

@@ -2,7 +2,7 @@
import Button from '$lib/components/common/button/Button.svelte'
import { faChevronDown, faChevronUp, faCopy, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { AppEditorContext, GridItem } from '../../types'
import type { AppEditorContext } from '../../types'
import PanelSection from './common/PanelSection.svelte'
import InputsSpecsEditor from './InputsSpecsEditor.svelte'
import TableActions from './TableActions.svelte'
@@ -26,6 +26,7 @@
import MoveToOtherGrid from './MoveToOtherGrid.svelte'
import GridPane from './GridPane.svelte'
import { slide } from 'svelte/transition'
import { push } from '$lib/history'
export let component: AppComponent
export let rowColumns = false
@@ -41,16 +42,19 @@
selectedComponent,
worldStore,
focusedGrid,
stateId
stateId,
history
} = getContext<AppEditorContext>('AppEditorContext')
function duplicateElement(id: string) {
push(history, $app)
$dirtyStore = true
const newId = duplicateGridItem($app, parent, id)
$selectedComponent = newId
}
function removeGridElement() {
push(history, $app)
$selectedComponent = undefined
$focusedGrid = undefined
if (component && !noGrid) {
@@ -191,7 +195,7 @@
</Button>
</div>
{#if viewCssOptions}
<div transition:slide>
<div transition:slide|local>
{#each Object.keys(component.customCss ?? {}) as name}
{#if component?.customCss?.[name]}
<div class="w-full mb-2">

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { Button } from '$lib/components/common'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
import { push } from '$lib/history'
import { faCopy } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { App, AppEditorContext } from '../../types'
@@ -18,7 +19,7 @@
let selectedOption: string
const { app } = getContext<AppEditorContext>('AppEditorContext')
const { app, history } = getContext<AppEditorContext>('AppEditorContext')
function listAllSubGrids(app: App) {
return app.subgrids ? Object.keys(app.subgrids) : []
@@ -46,6 +47,7 @@
targetId: string,
targetSubGridIndex: number
) {
push(history, $app)
const focusedGrid = {
parentComponentId: targetId,
subGridIndex: targetSubGridIndex
@@ -62,6 +64,7 @@
}
function onMove(component: AppComponent) {
push(history, $app)
const data = findAndDelete(component.id)
if (!data) {

View File

@@ -17,12 +17,12 @@
)}
>
<div class="flex justify-between flex-wrap items-center w-full gap-1">
<div class="text-sm inline-flex items-center font-extrabold {titlePadding}">
<div class="text-sm inline-flex items-center font-extrabold {titlePadding} gap-1">
<span class="truncate">
{title}
</span>
{#if tooltip}
<Tooltip>
<Tooltip light>
{tooltip}
</Tooltip>
{/if}

View File

@@ -1,5 +1,6 @@
import type { Schema } from '$lib/common'
import type { Preview } from '$lib/gen'
import type { History } from '$lib/history'
import type { FilledItem } from '@windmill-labs/svelte-grid'
import type { Writable } from 'svelte/store'
import type { AppComponent } from './editor/component/components'
@@ -42,14 +43,14 @@ export interface BaseAppComponent extends Partial<Aligned> {
configuration: Record<
string,
GeneralAppInput &
(
| StaticAppInput
| ConnectedAppInput
| UserAppInput
| RowAppInput
| EvalAppInput
| UploadAppInput
)
(
| StaticAppInput
| ConnectedAppInput
| UserAppInput
| RowAppInput
| EvalAppInput
| UploadAppInput
)
>
card: boolean | undefined
customCss?: ComponentCustomCSS
@@ -132,6 +133,7 @@ export type AppEditorContext = {
focusedGrid: Writable<FocusedGrid | undefined>
stateId: Writable<number>
parentWidth: Writable<number>
history: History<App>
}
export type FocusedGrid = { parentComponentId: string; subGridIndex: number }

View File

@@ -4,13 +4,11 @@ import { inferArgs } from '$lib/infer'
import { emptySchema } from '$lib/utils'
import type { AppComponent } from './editor/component'
import type { App, ComponentCssProperty, ComponentCustomCSS, GridItem } from './types'
import { getRecommendedDimensionsByComponent } from './editor/component'
import { gridColumns } from './gridUtils'
import gridHelp from '@windmill-labs/svelte-grid/src/utils/helper'
import { twMerge } from 'tailwind-merge'
import type { AppInput, InputType, ResultAppInput, StaticAppInput } from './inputType'
import type { Output } from './rx'
import { getNextId } from '../flows/flowStateUtils'
import { createNewGridItem } from './editor/appUtils'
export function allItems(
grid: GridItem[],
@@ -103,7 +101,7 @@ export function accessPropertyByPath<T>(object: T, path: string): T | undefined
for (let i = 0, depth = a.length; i < depth; ++i) {
let key = a[i]
if (object[key]) {
if (object[key] != undefined) {
object = object[key]
} else {
// Value not found
@@ -283,45 +281,6 @@ export function insertNewGridItem(
return root
}
// The grid is needed to find a space for the new component
export function createNewGridItem(grid: GridItem[], id: string, data: AppComponent): GridItem {
const appComponent = data
appComponent.id = id
const newComponent = {
fixed: false,
resizable: true,
draggable: true,
customDragger: false,
customResizer: false,
x: 0,
y: 0
}
let newData: AppComponent = JSON.parse(JSON.stringify(appComponent))
const newItem: GridItem = {
data: newData,
id: id
}
gridColumns.forEach((column) => {
const rec = getRecommendedDimensionsByComponent(appComponent.type, column)
newItem[column] = {
...newComponent,
min: { w: 1, h: 1 },
max: { w: column, h: 100 },
w: rec.w,
h: rec.h
}
const position = gridHelp.findSpace(newItem, grid, column) as { x: number; y: number }
newItem[column] = { ...newItem[column], ...position }
})
return newItem
}
function recursiveGetIds(gridItem: GridItem): string[] {
const subGrids = gridItem.data.subGrids ?? []

View File

@@ -15,6 +15,7 @@
export let startIcon: ButtonType.Icon | undefined = undefined
export let endIcon: ButtonType.Icon | undefined = undefined
export let spacingSize: ButtonType.Size = size
export let loading = false
let ref: ButtonType.Element
@@ -33,6 +34,7 @@
<div class="flex justy-start items-center">
{#if $$slots.main}
<Button
{loading}
{...commonProps}
{href}
{target}

View File

@@ -7,10 +7,10 @@
import { onMount } from 'svelte'
export let noMinW = false
let show = false
export let show = false
let menu: HTMLDivElement
type Alignment = 'start' | 'end'
type Alignment = 'start' | 'end' | 'center'
type Side = 'top' | 'bottom'
type Placement = `${Side}-${Alignment}`
@@ -45,6 +45,7 @@
})
const placementsClasses = {
'bottom-center': 'origin-top-left left-1/2 transform -translate-x-1/2',
'bottom-start': 'origin-top-left left-0',
'bottom-end': 'origin-top-right right-0',
'top-start': 'origin-bottom-left left-0 bottom-0',
@@ -62,7 +63,7 @@
}
show = !show
}}
class="cursor-pointer hover:bg-gray-100/30"
class="relative cursor-pointer hover:bg-gray-100/30"
>
<slot class="triggerable" name="trigger" />
</div>

View File

@@ -22,6 +22,7 @@
} from '@fortawesome/free-solid-svg-icons'
import { MoreVertical } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import Badge from '../badge/Badge.svelte'
import Button from '../button/Button.svelte'
import Row from './Row.svelte'
@@ -31,7 +32,7 @@
export let shareModal: ShareModal
export let moveDrawer: MoveDrawer
let { summary, path, extra_perms, canWrite, workspace_id } = flow
let { summary, path, extra_perms, canWrite, workspace_id, archived } = flow
const dispatch = createEventDispatcher()
@@ -70,11 +71,15 @@
>
<svelte:fragment slot="badges">
<SharedBadge {canWrite} extraPerms={extra_perms} />
{#if archived}
<Badge color="red" baseClass="border">archived</Badge>
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
<span class="hidden md:inline-flex gap-x-1">
{#if !$userStore?.operator}
{#if canWrite}
{#if canWrite && !archived}
<div>
<Button
color="light"
@@ -135,7 +140,7 @@
displayName: 'Edit',
icon: faEdit,
href: `/flows/edit/${path}?nodraft=true`,
disabled: !canWrite
disabled: !canWrite || archived
},
{
displayName: 'Use as template/Fork',
@@ -153,14 +158,15 @@
action: () => {
moveDrawer.openDrawer(path, summary, 'flow')
},
disabled: !canWrite
disabled: !canWrite || archived
},
{
displayName: 'Schedule',
icon: faCalendarAlt,
action: () => {
scheduleEditor.openNew(true, path)
}
},
disabled: archived
},
{
displayName: canWrite ? 'Share' : 'See Permissions',
@@ -176,7 +182,7 @@
path ? archiveFlow(path) : null
},
type: 'delete',
disabled: !canWrite
disabled: !canWrite || archived
},
{
displayName: 'Delete',

View File

@@ -42,7 +42,8 @@
canWrite,
lock_error_logs,
kind,
workspace_id
workspace_id,
archived
} = script
const dispatch = createEventDispatcher()
@@ -76,13 +77,17 @@
<Badge color="gray" baseClass="border">{capitalize(kind)}</Badge>
{/if}
{#if archived}
<Badge color="red" baseClass="border">archived</Badge>
{/if}
<LanguageBadge {language} />
</svelte:fragment>
<svelte:fragment slot="actions">
<span class="hidden md:inline-flex gap-x-1">
{#if !$userStore?.operator}
{#if canWrite}
{#if canWrite && !archived}
<div>
<Button
color="light"
@@ -143,13 +148,13 @@
displayName: 'Edit',
icon: faEdit,
href: `/scripts/edit/${hash}`,
disabled: !canWrite
disabled: !canWrite || archived
},
{
displayName: 'Edit code',
icon: faEdit,
href: `/scripts/edit/${hash}?step=2`,
disabled: !canWrite
disabled: !canWrite || archived
},
{
displayName: 'Use as template',
@@ -162,7 +167,7 @@
action: () => {
moveDrawer.openDrawer(path, summary, 'script')
},
disabled: !canWrite
disabled: !canWrite || archived
},
{
displayName: 'View runs',
@@ -174,14 +179,16 @@
icon: faCalendarAlt,
action: () => {
scheduleEditor.openNew(false, path)
}
},
disabled: archived
},
{
displayName: canWrite ? 'Share' : 'See Permissions',
icon: faShare,
action: () => {
shareModal.openDrawer && shareModal.openDrawer(path, 'script')
}
},
disabled: archived
},
{
displayName: 'Archive',
@@ -190,7 +197,7 @@
path ? archiveScript(path) : null
},
type: 'delete',
disabled: !canWrite
disabled: !canWrite || archived
}
]}
>

View File

@@ -2,17 +2,22 @@
import { Pane, Splitpanes } from 'svelte-splitpanes'
import FlowEditorPanel from './content/FlowEditorPanel.svelte'
import FlowModuleSchemaMap from './map/FlowModuleSchemaMap.svelte'
import { flowStore } from './flowStore'
import WindmillIcon from '../icons/WindmillIcon.svelte'
import { Skeleton } from '../common'
import { getContext } from 'svelte'
import type { FlowEditorContext } from './types'
export let initialPath: string
export let loading: boolean
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
let size = 50
</script>
<div class="h-full overflow-hidden border-t">
<Splitpanes>
<Pane size={20} minSize={15} class="h-full">
<Pane {size} minSize={15} class="h-full">
<div class="grow overflow-auto bg-gray h-full bg-gray-50 relative">
{#if loading}
<div class="p-2 pt-10">
@@ -21,11 +26,11 @@
{/each}
</div>
{:else if $flowStore.value.modules}
<FlowModuleSchemaMap bind:modules={$flowStore.value.modules} root />
<FlowModuleSchemaMap bind:modules={$flowStore.value.modules} />
{/if}
</div>
</Pane>
<Pane size={80} minSize={40}>
<Pane class="drop-shadow-2xl" size={100 - size} minSize={40}>
{#if loading}
<div class="w-full h-full ">
<div class="block m-auto mt-40 w-10">

View File

@@ -34,7 +34,7 @@
</script>
<div
class="flex items-center justify-between py-2 px-4 border-b border-gray-300 space-x-2 h-full max-h-12 flex-nowrap"
class="overflow-x-auto scrollbar-hidden flex items-center justify-between py-2 px-4 border-b border-gray-300 space-x-2 h-full max-h-12 flex-nowrap"
>
{#if flowModule}
<span class="text-sm w-full mr-4">

View File

@@ -1,11 +1,9 @@
<script lang="ts">
import type { FlowModule } from '$lib/gen'
import { getContext } from 'svelte'
import { flowStore } from '../flowStore'
import PropPickerWrapper from '../propPicker/PropPickerWrapper.svelte'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import { flowStateStore } from '../flowState'
import { getStepPropPicker } from '../previousResults'
import type { FlowEditorContext } from '../types'
@@ -17,7 +15,9 @@
export let parentModule: FlowModule
export let previousModule: FlowModule | undefined
const { previewArgs } = getContext<FlowEditorContext>('FlowEditorContext')
const { previewArgs, flowStateStore, flowStore } =
getContext<FlowEditorContext>('FlowEditorContext')
let editor: SimpleEditor | undefined = undefined
$: stepPropPicker = getStepPropPicker(

View File

@@ -8,7 +8,6 @@
import { CaptureService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { flowStore } from '../flowStore'
import { convert } from '@redocly/json-to-json-schema'
import SchemaViewer from '$lib/components/SchemaViewer.svelte'
@@ -19,7 +18,7 @@
import { faClipboard } from '@fortawesome/free-solid-svg-icons'
import SchemaForm from '$lib/components/SchemaForm.svelte'
const { previewArgs } = getContext<FlowEditorContext>('FlowEditorContext')
const { previewArgs, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
let drawer: Drawer
let interval: NodeJS.Timeout | undefined = undefined

View File

@@ -33,7 +33,7 @@
>
<div class="flex flex-col gap-y-4 py-2 w-full max-w-xl">
{#each value.branches as branch, i}
<div class="flex flex-row gap-x-4 w-full">
<div class="flex flex-row gap-x-4 w-full items-center">
<div class="grow">
<input type="text" bind:value={branch.summary} placeholder="Summary" />
</div>

View File

@@ -1,16 +1,17 @@
<script lang="ts">
import { dfs, flowStore } from '$lib/components/flows/flowStore'
import { dfs } from '$lib/components/flows/flowStore'
import FlowCard from '../common/FlowCard.svelte'
import { Alert, Badge } from '$lib/components/common'
import type { FlowModule, FlowModuleValue, InputTransform, PathScript, RawScript } from '$lib/gen'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import { flowStateStore } from '../flowState'
import { setContext } from 'svelte'
import { getContext, setContext } from 'svelte'
import type { PropPickerWrapperContext } from '../propPicker/PropPickerWrapper.svelte'
import { writable } from 'svelte/store'
import Toggle from '../../Toggle.svelte'
import InputTransformSchemaForm from '$lib/components/InputTransformSchemaForm.svelte'
import type { FlowEditorContext } from '../types'
let hideOptional = false
const { flowStateStore, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
$: steps = (
dfs($flowStore.value.modules, (x) => x)
@@ -75,9 +76,8 @@
<Badge large color="indigo">{m.id}</Badge>
</h2>
<SchemaForm
<InputTransformSchemaForm
noDynamicToggle
inputTransform
{filter}
class="mt-2"
schema={$flowStateStore[m.id]?.schema ?? {}}

View File

@@ -6,18 +6,19 @@
import FlowSettings from './FlowSettings.svelte'
import FlowInput from './FlowInput.svelte'
import FlowFailureModule from './FlowFailureModule.svelte'
import { flowStore } from '../flowStore'
import FlowConstants from './FlowConstants.svelte'
export let initialPath: string
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
const { selectedId, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
</script>
{#if $selectedId?.startsWith('settings')}
<FlowSettings {initialPath} />
{:else if $selectedId === 'inputs'}
{:else if $selectedId === 'Input'}
<FlowInput />
{:else if $selectedId === 'Result'}
<p class="p-4 text-gray-600">Nothing to show about the result node. Happy flow building!</p>
{:else if $selectedId === 'constants'}
<FlowConstants />
{:else if $selectedId === 'failure'}

View File

@@ -1,6 +1,9 @@
<script lang="ts">
import { flowStore } from '../flowStore'
import { getContext } from 'svelte'
import type { FlowEditorContext } from '../types'
import FlowModuleWrapper from './FlowModuleWrapper.svelte'
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
</script>
{#if $flowStore.value.failure_module}

View File

@@ -3,10 +3,14 @@
import SchemaEditor from '$lib/components/SchemaEditor.svelte'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import { getContext } from 'svelte'
import FlowCard from '../common/FlowCard.svelte'
import { copyFirstStepSchema, flowStore } from '../flowStore'
import { copyFirstStepSchema } from '../flowStore'
import type { FlowEditorContext } from '../types'
import CapturePayload from './CapturePayload.svelte'
const { flowStore, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
let capturePayload: CapturePayload
</script>
@@ -29,7 +33,7 @@
size="sm"
disabled={$flowStore.value.modules.length === 0 ||
$flowStore.value.modules[0].value.type == 'identity'}
on:click={copyFirstStepSchema}
on:click={() => copyFirstStepSchema($flowStateStore, flowStore)}
>
First step's inputs
</Button>

View File

@@ -13,11 +13,11 @@
import type { FlowModule } from '$lib/gen/models/FlowModule'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { getStepPropPicker } from '../previousResults'
import { flowStateStore } from '../flowState'
import { flowStore } from '../flowStore'
import FlowModuleSleep from './FlowModuleSleep.svelte'
const { previewArgs } = getContext<FlowEditorContext>('FlowEditorContext')
const { previewArgs, flowStateStore, flowStore } =
getContext<FlowEditorContext>('FlowEditorContext')
export let mod: FlowModule
export let parentModule: FlowModule | undefined

View File

@@ -6,12 +6,10 @@
import EditorBar from '$lib/components/EditorBar.svelte'
import ModulePreview from '$lib/components/ModulePreview.svelte'
import { createScriptFromInlineScript, fork } from '$lib/components/flows/flowStateUtils'
import { flowStore } from '$lib/components/flows/flowStore'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import { RawScript, type FlowModule, type PathFlow, type PathScript } from '$lib/gen'
import FlowCard from '../common/FlowCard.svelte'
import FlowModuleHeader from './FlowModuleHeader.svelte'
import { flowStateStore } from '../flowState'
import { getLatestHashForScript, schemaToObject, scriptLangToEditorLang } from '$lib/utils'
import PropPickerWrapper from '../propPicker/PropPickerWrapper.svelte'
import { afterUpdate, getContext } from 'svelte'
@@ -28,8 +26,10 @@
import Alert from '$lib/components/common/alert/Alert.svelte'
import FlowModuleSleep from './FlowModuleSleep.svelte'
import FlowPathViewer from './FlowPathViewer.svelte'
import InputTransformSchemaForm from '$lib/components/InputTransformSchemaForm.svelte'
const { selectedId, previewArgs } = getContext<FlowEditorContext>('FlowEditorContext')
const { selectedId, previewArgs, flowStateStore, flowStore } =
getContext<FlowEditorContext>('FlowEditorContext')
export let flowModule: FlowModule
export let failureModule: boolean = false
@@ -150,7 +150,8 @@
const [module, state] = await createScriptFromInlineScript(
flowModule,
$selectedId,
$flowStateStore[flowModule.id].schema
$flowStateStore[flowModule.id].schema,
$flowStore
)
flowModule = module
$flowStateStore[module.id] = state
@@ -165,7 +166,7 @@
{editor}
lang={value['language'] ?? 'deno'}
{websocketAlive}
iconOnly={width < 768}
iconOnly={width < 850}
/>
</div>
{/if}
@@ -176,7 +177,7 @@
style="max-height: calc(100% - {totalTopGap}px) !important;"
>
<Splitpanes horizontal>
<Pane size={isScript ? 30 : 50} minSize={20}>
<Pane class="-z-10" size={isScript ? 30 : 50} minSize={20}>
{#if value.type === 'rawscript'}
<div class="h-full">
<Editor
@@ -225,9 +226,8 @@
pickableProperties={stepPropPicker.pickableProperties}
error={failureModule}
>
<SchemaForm
<InputTransformSchemaForm
schema={$flowStateStore[$selectedId]?.schema ?? {}}
inputTransform={true}
previousModuleId={previousModule?.id}
bind:args={value.input_transforms}
bind:extraLib={stepPropPicker.extraLib}

View File

@@ -3,9 +3,12 @@
import Toggle from '$lib/components/Toggle.svelte'
import PropPickerWrapper from '$lib/components/flows/propPicker/PropPickerWrapper.svelte'
import type { FlowModule } from '$lib/gen'
import { flowStateStore } from '../flowState'
import Tooltip from '$lib/components/Tooltip.svelte'
import { NEVER_TESTED_THIS_FAR } from '../utils'
import type { FlowEditorContext } from '../types'
import { getContext } from 'svelte'
const { flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
export let flowModule: FlowModule

View File

@@ -7,14 +7,13 @@
import type { FlowModule } from '$lib/gen'
import { emptySchema } from '$lib/utils'
import { getContext } from 'svelte'
import { flowStateStore } from '../flowState'
import PropPickerWrapper from '../propPicker/PropPickerWrapper.svelte'
import type { FlowEditorContext } from '../types'
export let flowModule: FlowModule
export let previousModuleId: string | undefined
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
const { selectedId, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
let schema = emptySchema()
schema.properties['sleep'] = {

View File

@@ -13,13 +13,13 @@
pickScript
} from '$lib/components/flows/flowStateUtils'
import FlowInputs from './FlowInputs.svelte'
import { flowStateStore } from '../flowState'
import { Alert } from '$lib/components/common'
import FlowInputsFlow from './FlowInputsFlow.svelte'
import FlowBranchesAllWrapper from './FlowBranchesAllWrapper.svelte'
import FlowBranchesOneWrapper from './FlowBranchesOneWrapper.svelte'
const { selectedId, schedule } = getContext<FlowEditorContext>('FlowEditorContext')
const { selectedId, schedule, flowStateStore } =
getContext<FlowEditorContext>('FlowEditorContext')
export let flowModule: FlowModule
@@ -40,12 +40,7 @@
{:else if flowModule.value.type === 'identity'}
{#if $selectedId == 'failure'}
<Alert type="info" title="Error handlers are triggered upon non recovered errors">
If defined, the error handler will take as input, the result of the step that errored (which
has its error in the 'error field').
<br />
<br />
Steps are retried until they succeed, or until the maximum number of retries defined for that
spec is reached, at which point the error handler is called.
If defined, the error handler will take the error as input.
</Alert>
{/if}

View File

@@ -4,9 +4,9 @@
import Toggle from '$lib/components/Toggle.svelte'
import { emptyString } from '$lib/utils'
import { getContext } from 'svelte'
import { flowStore } from '../flowStore'
import type { FlowEditorContext } from '../types'
const { schedule } = getContext<FlowEditorContext>('FlowEditorContext')
const { schedule, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
</script>
<CronInput bind:schedule={$schedule.cron} />

View File

@@ -3,14 +3,12 @@
import Tab from '$lib/components/common/tabs/Tab.svelte'
import TabContent from '$lib/components/common/tabs/TabContent.svelte'
import { flowStore } from '$lib/components/flows/flowStore'
import Path from '$lib/components/Path.svelte'
import Required from '$lib/components/Required.svelte'
import FlowCard from '../common/FlowCard.svelte'
import FlowSchedules from './FlowSchedules.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import { Alert } from '$lib/components/common'
import { FlowGraph } from '$lib/components/graph'
import { getContext } from 'svelte'
import type { FlowEditorContext } from '../types'
import autosize from 'svelte-autosize'
@@ -21,12 +19,10 @@
import { Icon } from 'svelte-awesome'
import { faClipboard } from '@fortawesome/free-solid-svg-icons'
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
const { selectedId, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
export let initialPath: string
let topHeight = 0
$: url = `${$page.url.hostname}/api/w/${$workspaceStore}/jobs/run/f/${$flowStore?.path}`
</script>
@@ -37,7 +33,6 @@
<Tab value="settings-metadata">Metadata</Tab>
<Tab value="settings-schedule">Schedule</Tab>
<Tab value="settings-same-worker">Shared Directory</Tab>
<Tab value="settings-graph">Graph</Tab>
<svelte:fragment slot="content">
<TabContent value="settings-metadata" class="p-4 h-full">
@@ -89,18 +84,22 @@
</a>
</li>
<li class="mt-2">
<div class="flex flex-row gap-2">
2. Use a trigger script and schedule this flow to run as frequently as
needed and compare a state persisted in Windmill to the state of the
external system. If a difference is detected, then the rest of the flow is
triggered. Oftentimes, the second step of a flow is a for-loop that will
iterate over every elements. When using a trigger, a default schedule will
be created.
<img
class="shadow-lg border rounded"
alt="static button"
src="/trigger_button.png"
/>
<div class="flex flex-col gap-2">
<p>
2. Use a trigger script and schedule this flow to run as frequently as
needed and compare a state persisted in Windmill to the state of the
external system. If a difference is detected, then the rest of the flow is
triggered. Oftentimes, the second step of a flow is a for-loop that will
iterate over every elements. When using a trigger, a default schedule will
be created.
</p>
<div>
<img
class="shadow-lg border rounded"
alt="static button"
src="/trigger_button.png"
/>
</div>
</div>
</li></ul
>
@@ -132,26 +131,6 @@
}}
/>
</TabContent>
<TabContent value="settings-graph">
<div
bind:clientHeight={topHeight}
class="max-w-full w-full overflow-hidden h-screen bg-gray-50"
>
{#if $flowStore.value.modules}
<FlowGraph
on:click={(e) => {
if (e.detail.id) {
$selectedId = e.detail.id
}
}}
minHeight={topHeight}
modules={$flowStore.value.modules}
failureModule={$flowStore.value.failure_module}
notSelectable
/>
{/if}
</div>
</TabContent>
</svelte:fragment>
</Tabs>
</div>

View File

@@ -1,6 +1,6 @@
import type { Schema } from '$lib/common'
import type { Flow, FlowModule, FlowStatusModule } from '$lib/gen'
import { writable } from 'svelte/store'
import type { Writable } from 'svelte/store'
import { loadFlowModuleState } from './flowStateUtils'
import { emptyFlowModuleState } from './utils'
@@ -17,10 +17,9 @@ export type FlowState = Record<string, FlowModuleState>
* It contains data loaded that are not contained in a Flow object i.e. schemas.
* We also hold the data of the results of a test job, ran by the user.
*/
export const flowStateStore = writable<FlowState>({})
export const testStepStore = writable<Record<string, any>>({})
export async function initFlowState(flow: Flow) {
export async function initFlowState(flow: Flow, flowStateStore: Writable<FlowState>) {
const modulesState: FlowState = {}
await mapFlowModules(flow.value.modules, modulesState)

View File

@@ -2,6 +2,7 @@ import type { Schema } from '$lib/common'
import {
Script,
ScriptService,
type Flow,
type FlowModule,
type PathFlow,
type PathScript,
@@ -10,9 +11,8 @@ import {
import { initialCode } from '$lib/script_helpers'
import { userStore, workspaceStore } from '$lib/stores'
import { getScriptByPath } from '$lib/utils'
import { get } from 'svelte/store'
import { flowStateStore, type FlowModuleState } from './flowState'
import { flowStore } from './flowStore'
import { get, type Writable } from 'svelte/store'
import type { FlowModuleState, FlowState } from './flowState'
import {
charsToNumber,
emptyFlowModuleState,
@@ -56,8 +56,7 @@ export function getNextId(currentKeys: string[]): string {
}
// Computes the next available id
export function nextId(): string {
const flowState = get(flowStateStore)
export function nextId(flowState: FlowState): string {
const max = Object.keys(flowState).reduce((acc, key) => {
if (key === 'failure' || key.includes('branch') || key.includes('loop')) {
@@ -209,9 +208,9 @@ async function createInlineScriptModuleFromPath(
}
}
export function emptyModule(flow?: boolean): FlowModule {
export function emptyModule(flowState: FlowState, flow?: boolean): FlowModule {
return {
id: nextId(),
id: nextId(flowState),
value: { type: 'identity', flow }
}
}
@@ -219,9 +218,9 @@ export function emptyModule(flow?: boolean): FlowModule {
export async function createScriptFromInlineScript(
flowModule: FlowModule,
suffix: string,
schema: Schema
schema: Schema,
flow: Flow
): Promise<[FlowModule & { value: PathScript }, FlowModuleState]> {
const flow = get(flowStore)
const user = get(userStore)
if (flowModule.value.type != 'rawscript') {
@@ -259,7 +258,7 @@ export async function createScriptFromInlineScript(
return pickScript(availablePath, flowModule.summary ?? '', flowModule.id, hash)
}
export function deleteFlowStateById(id: string) {
export function deleteFlowStateById(id: string, flowStateStore: Writable<FlowState>) {
flowStateStore.update((fss) => {
delete fss[id]
return fss

View File

@@ -1,21 +1,12 @@
import type { Flow, FlowModule, ForloopFlow, InputTransform } from '$lib/gen'
import { sendUserToast } from '$lib/utils'
import { get, writable, derived } from 'svelte/store'
import { flowStateStore, initFlowState, type FlowState } from './flowState'
import { get, writable, type Writable } from 'svelte/store'
import { initFlowState, type FlowState } from './flowState'
import { numberToChars } from './utils'
export type FlowMode = 'push' | 'pull'
export const importFlowStore = writable<Flow | undefined>(undefined)
export const flowStore = writable<Flow>({
summary: '',
value: { modules: [] },
path: '',
edited_at: '',
edited_by: '',
archived: false,
extra_perms: {}
})
export function dfs<T>(modules: FlowModule[], f: (x: FlowModule) => T): T[] {
let result: T[] = []
@@ -37,7 +28,7 @@ export function dfs<T>(modules: FlowModule[], f: (x: FlowModule) => T): T[] {
}
export async function initFlow(flow: Flow) {
export async function initFlow(flow: Flow, flowStore: Writable<Flow>, flowStateStore: Writable<FlowState>) {
let counter = 0
for (const mod of flow.value.modules) {
migrateFlowModule(mod)
@@ -53,7 +44,7 @@ export async function initFlow(flow: Flow) {
}
}
await initFlowState(flow)
await initFlowState(flow, flowStateStore)
flowStore.set(flow)
function migrateFlowModule(mod: FlowModule) {
@@ -89,8 +80,7 @@ export async function initFlow(flow: Flow) {
}
}
export async function copyFirstStepSchema() {
const flowState = get(flowStateStore)
export async function copyFirstStepSchema(flowState: FlowState, flowStore: Writable<Flow>) {
flowStore.update((flow) => {
const firstModuleId = flow.value.modules[0]?.id

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