Compare commits
37 Commits
v1.72.0
...
app-previe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4140117b6a | ||
|
|
f20ea37eb0 | ||
|
|
67d8009dcf | ||
|
|
87d31ee63b | ||
|
|
fb2d52f99b | ||
|
|
95ccc9edf8 | ||
|
|
9e4d90ad37 | ||
|
|
2e3271257f | ||
|
|
c638897fdc | ||
|
|
71305e5154 | ||
|
|
9e9f8efb8e | ||
|
|
3e5d09ef0b | ||
|
|
b013996585 | ||
|
|
614fb5022a | ||
|
|
0beadfd1ac | ||
|
|
0a76e3533c | ||
|
|
7648b382ff | ||
|
|
25580c1272 | ||
|
|
e2747e824f | ||
|
|
2060445d20 | ||
|
|
2557e136bd | ||
|
|
200cb69d82 | ||
|
|
9ee261fe1a | ||
|
|
8e563a42f5 | ||
|
|
a999eb2112 | ||
|
|
e5dbe7076c | ||
|
|
94e30052bf | ||
|
|
68add7932e | ||
|
|
2ac51b0af0 | ||
|
|
f3232062c3 | ||
|
|
b11a5a2df6 | ||
|
|
e2c4545240 | ||
|
|
70dd6f759c | ||
|
|
dcfb29fb80 | ||
|
|
94f1aadef2 | ||
|
|
58300eb6ac | ||
|
|
304dea4b74 |
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -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;
|
||||
@@ -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": [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -670,6 +670,7 @@ async fn execute_component(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -125,7 +125,7 @@ pub enum FlowStatusModule {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum JobResult {
|
||||
SingleJob(Uuid),
|
||||
ListJob(Vec<Uuid>),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -85,6 +85,7 @@ pub async fn push_scheduled_job<'c>(
|
||||
Some(next),
|
||||
Some(schedule.path.clone()),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
|
||||
@@ -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()));
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
1508
frontend/package-lock.json
generated
1508
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <input type="checkbox" bind:checked={forceJson} /></div
|
||||
as JSON <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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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 ?? []}
|
||||
|
||||
@@ -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 <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'
|
||||
{'${} '}
|
||||
<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"
|
||||
> <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}
|
||||
|
||||
114
frontend/src/lib/components/InputTransformSchemaForm.svelte
Normal file
114
frontend/src/lib/components/InputTransformSchemaForm.svelte
Normal 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} />
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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 ?? ''}>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -64,6 +64,8 @@
|
||||
|
||||
// @ts-ignore
|
||||
let container
|
||||
|
||||
// $: containerHeight = subGrid.map((item) => columnConfiguration.map((c) => item[c[1]].h))
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ const display: ComponentSet = {
|
||||
'plotlycomponent',
|
||||
'scatterchartcomponent',
|
||||
'timeseriescomponent',
|
||||
'pdfcomponent',
|
||||
'displaycomponent'
|
||||
]
|
||||
} as const
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 <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
|
||||
<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>
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 ?? []
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ?? {}}
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'] = {
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user