From: Taran Nathan Date: Fri, 25 Sep 2026 03:47:28 +0000 (-0700) Subject: sort by subteam and person search X-Git-Url: https://git.taranathan.com/?a=commitdiff_plain;h=2345711da532dd88ac959e4edbd5ca85cb7c3b1a;p=robotics-task-manager.git sort by subteam and person search --- diff --git a/client/src/client.gleam b/client/src/client.gleam index a47dc99..4e14378 100644 --- a/client/src/client.gleam +++ b/client/src/client.gleam @@ -3,6 +3,7 @@ import gleam/json import gleam/list import gleam/option.{type Option, None, Some} import gleam/result +import gleam/string import lustre import lustre/attribute import lustre/effect.{type Effect} @@ -20,6 +21,10 @@ pub fn main() -> Nil { Nil } +fn join_subteams(subteams: List(String)) -> String { + string.join(subteams, ", ") +} + type Model { Model( page: String, @@ -34,7 +39,10 @@ type Model { task_description: String, task_assignee: String, task_assignees: String, + task_subteams: List(String), task_due_date: String, + subteam_filter: String, + task_search: String, ) } @@ -56,6 +64,9 @@ pub type Message { TaskAssigneeChanged(String) TaskAssigneesChanged(String) TaskDueDateChanged(String) + TaskSubteamChanged(String, Bool) + SubteamFilterChanged(String) + TaskSearchChanged(String) InvitePasswordChanged(String) DeleteTask(Int) DeleteProject(Int) @@ -84,7 +95,10 @@ fn init(_flags: Option(Nil)) -> #(Model, Effect(Message)) { task_description: "", task_assignee: "", task_assignees: "", + task_subteams: [], task_due_date: "", + subteam_filter: "all", + task_search: "", ) case page { @@ -151,6 +165,21 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) { Model(..model, task_due_date: value), effect.none(), ) + TaskSubteamChanged(subteam, checked) -> #( + Model( + ..model, + task_subteams: toggle_subteam(model.task_subteams, subteam, checked), + ), + effect.none(), + ) + SubteamFilterChanged(value) -> #( + Model(..model, subteam_filter: value), + effect.none(), + ) + TaskSearchChanged(value) -> #( + Model(..model, task_search: value), + effect.none(), + ) LoginSubmitted(fields) -> auth_request(model, fields, "/api/login", "/") RegisterSubmitted(fields) -> auth_request(model, fields, "/api/register", "/") @@ -176,6 +205,7 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) { field(fields, "description"), field(fields, "assignee"), field(fields, "assignees"), + model.task_subteams, field(fields, "due_date"), ) |> shared.new_task_json @@ -186,6 +216,7 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) { task_description: "", task_assignee: "", task_assignees: "", + task_subteams: [], task_due_date: "", ), rsvp.post( @@ -251,6 +282,7 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) { task_description: "", task_assignee: "", task_assignees: "", + task_subteams: [], task_due_date: "", invite_password: "", ), @@ -326,6 +358,38 @@ fn remove_project(model: Model, project_id: Int) -> Model { } } +fn task_matches(model: Model, task: shared.Task) -> Bool { + let search = string.lowercase(string.trim(model.task_search)) + let people = string.lowercase(task.assignee <> " " <> task.assignees) + let search_matches = search == "" || string.contains(people, search) + let subteam_matches = + model.subteam_filter == "all" + || list.contains(task.subteams, model.subteam_filter) + search_matches && subteam_matches +} + +fn toggle_subteam( + selected: List(String), + subteam: String, + checked: Bool, +) -> List(String) { + case checked { + True -> + case subteam { + "none" -> ["none"] + _ -> + case list.contains(selected, subteam) { + True -> list.filter(selected, fn(item) { item != "none" }) + False -> [ + subteam, + ..list.filter(selected, fn(item) { item != "none" }) + ] + } + } + False -> list.filter(selected, fn(item) { item != subteam }) + } +} + fn auth_request( model: Model, fields: List(#(String, String)), @@ -476,6 +540,44 @@ fn dashboard_view(model: Model) -> Element(Message) { html.section([attribute.class("hero")], [ html.text("wassup"), ]), + html.section([attribute.class("panel")], [ + html.h2([], [html.text("Find tasks")]), + html.input([ + attribute.class("task-search"), + attribute.styles([ + #("width", "100%"), + #("box-sizing", "border-box"), + ]), + attribute.placeholder("Search owner or assignees"), + event.on_input(TaskSearchChanged), + ]), + html.select( + [ + attribute.class("subteam-filter"), + attribute.styles([ + #("width", "100%"), + #("box-sizing", "border-box"), + #("padding", "12px 13px"), + #("border", "1px solid #d9cfc2"), + #("border-radius", "0"), + #("background", "#fffcf8"), + #("color", "#28231e"), + #("font", "inherit"), + #("appearance", "auto"), + ]), + event.on_change(SubteamFilterChanged), + ], + [ + html.option([attribute.value("all")], "All subteams"), + html.option([attribute.value("none")], "No subteam"), + html.option([attribute.value("electrical")], "Electrical"), + html.option([attribute.value("software")], "Software"), + html.option([attribute.value("CAD")], "CAD"), + html.option([attribute.value("machining")], "Machining"), + html.option([attribute.value("operations")], "Operations"), + ], + ), + ]), html.section([attribute.class("panel")], [ html.h2([], [html.text("New project")]), html.form([event.on_submit(ProjectSubmitted)], [ @@ -491,10 +593,11 @@ fn dashboard_view(model: Model) -> Element(Message) { } } -fn project_view(_model: Model, project: shared.Project) -> Element(Message) { - let backlog = list.filter(project.tasks, fn(task) { task.status == "todo" }) - let doing = list.filter(project.tasks, fn(task) { task.status == "doing" }) - let done = list.filter(project.tasks, fn(task) { task.status == "done" }) +fn project_view(model: Model, project: shared.Project) -> Element(Message) { + let tasks = list.filter(project.tasks, fn(task) { task_matches(model, task) }) + let backlog = list.filter(tasks, fn(task) { task.status == "todo" }) + let doing = list.filter(tasks, fn(task) { task.status == "doing" }) + let done = list.filter(tasks, fn(task) { task.status == "done" }) html.article( [attribute.class("panel")], list.append( @@ -522,6 +625,7 @@ fn project_view(_model: Model, project: shared.Project) -> Element(Message) { #("display", "grid"), #("grid-template-columns", "repeat(4, minmax(0, 1fr))"), #("align-items", "start"), + #("gap", "10px"), ]), event.on_submit(fn(fields) { TaskSubmitted(project.id, fields) }), ], @@ -530,6 +634,7 @@ fn project_view(_model: Model, project: shared.Project) -> Element(Message) { text_input("assignee", "Owner"), text_input("assignees", "Assignees"), text_input("due_date", "Due (MM/DD/YY)"), + subteam_checkboxes(model.task_subteams), html.textarea( [ attribute.class("task-description-input"), @@ -576,6 +681,75 @@ fn kanban_column(title: String, tasks: List(shared.Task)) -> Element(Message) { ) } +fn subteam_checkboxes(selected: List(String)) -> Element(Message) { + html.fieldset( + [ + attribute.class("subteam-picker"), + attribute.styles([ + #("grid-column", "1 / -1"), + #("display", "block"), + #("margin", "0"), + #("padding", "12px"), + #("border", "1px solid #d9cfc2"), + #("background", "#fffcf8"), + ]), + ], + [ + html.legend([], [html.text("Subteams")]), + html.div( + [ + attribute.class("subteam-options"), + attribute.styles([ + #("display", "grid"), + #("grid-template-columns", "repeat(3, minmax(0, 1fr))"), + #("column-gap", "32px"), + #("row-gap", "12px"), + #("align-items", "center"), + ]), + ], + [ + subteam_checkbox(selected, "none", "No subteam"), + subteam_checkbox(selected, "electrical", "Electrical"), + subteam_checkbox(selected, "software", "Software"), + subteam_checkbox(selected, "CAD", "CAD"), + subteam_checkbox(selected, "machining", "Machining"), + subteam_checkbox(selected, "operations", "Operations"), + ], + ), + ], + ) +} + +fn subteam_checkbox( + selected: List(String), + value: String, + label: String, +) -> Element(Message) { + html.label( + [ + attribute.class("subteam-option"), + attribute.styles([ + #("display", "flex"), + #("align-items", "center"), + #("gap", "8px"), + #("color", "#4e463e"), + #("font-size", ".82rem"), + #("line-height", "1.2"), + #("cursor", "pointer"), + ]), + ], + [ + html.input([ + attribute.type_("checkbox"), + attribute.value(value), + attribute.checked(list.contains(selected, value)), + event.on_check(fn(checked) { TaskSubteamChanged(value, checked) }), + ]), + html.text(label), + ], + ) +} + fn task_view(task: shared.Task) -> Element(Message) { html.div([attribute.class("task")], [ html.div([attribute.class("task-header")], [ @@ -603,6 +777,10 @@ fn task_view(task: shared.Task) -> Element(Message) { html.small([], [html.text("Assignees")]), html.text(if_blank(task.assignees, "None")), ]), + html.div([attribute.class("task-person")], [ + html.small([], [html.text("Subteams")]), + html.text(if_blank(join_subteams(task.subteams), "No subteam")), + ]), html.div([attribute.class("task-due")], [ html.small([], [html.text("Due")]), html.text(case task.due_date { @@ -668,7 +846,7 @@ fn text_input(name: String, placeholder: String) -> Element(Message) { fn styles() -> Element(Message) { html.style( [], - "body{margin:0;background:#f4f0e8;color:#28231e;font-family:Inter,ui-sans-serif,system-ui,sans-serif}.page{max-width:1120px;margin:auto;padding:28px 22px 72px}.auth{max-width:430px;margin:10vh auto;padding:42px;background:#fff;border:1px solid #e2d9cc;box-shadow:0 20px 60px #2d241414}.auth h1{font-size:clamp(2.4rem,8vw,4.8rem);line-height:.95;letter-spacing:-.06em;margin:14px 0}.auth form{display:grid;gap:12px;margin:26px 0 14px}.auth a{color:rgb(255, 100, 0);font-weight:700}.eyebrow{font-size:.72rem;letter-spacing:.16em;color:rgb(255, 100, 0);font-weight:800}header{display:flex;justify-content:space-between;gap:24px;align-items:start;margin-bottom:42px}header h1{font-size:clamp(2.7rem,7vw,5.5rem);line-height:.9;letter-spacing:-.07em;margin:12px 0}.hero{padding:42px;background:linear-gradient(120deg,#29241f,#594538);color:#fff;margin-bottom:20px;font-size:clamp(1.5rem,3vw,2.5rem);font-weight:800;letter-spacing:-.04em}.panel{background:#fff;border:1px solid #e2d9cc;padding:24px;margin:18px 0;box-shadow:0 12px 30px #2d24140b}.panel h2{margin:5px 0 8px;letter-spacing:-.03em}.panel>p{color:#796f64}.kanban{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:22px}.kanban-column{background:#f7f3ed;border:1px solid #ebe1d4;padding:13px}.kanban-column h3{font-size:.8rem;letter-spacing:.08em;text-transform:uppercase;color:#796f64;margin:0 0 5px}.task{display:flex;flex-direction:column;gap:13px;background:#fff;border:1px solid #e8dfd4;padding:16px;margin:9px 0;box-shadow:0 4px 12px #2d24140a;min-width:0}.task-header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.task-title{font-size:1rem;line-height:1.25;overflow-wrap:anywhere}.delete-task{flex:0 0 auto;padding:5px 8px;font-size:.68rem;background:#f0ebe3;color:#796f64}.delete-task:hover{background:#e8dfd4;color:#28231e}.task-description{margin:0;color:#796f64;line-height:1.45;overflow-wrap:anywhere}.task-details{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px 14px;padding-top:12px;border-top:1px solid #eee6dc;color:#4e463e;font-size:.8rem}.task-person,.task-due{display:flex;flex-direction:column;gap:4px;min-width:0;overflow-wrap:anywhere}.task-details small{color:#000000;font-size:.68rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase}.task-due{grid-column:1/-1}.task-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:2px}.task-status{padding:5px 8px;background:#f0ebe3;color:#796f64;font-size:.68rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase}.task-actions{display:flex;gap:6px;margin-left:auto}.task-actions form{margin:0}.move-task{width:31px;height:29px;padding:0;font-size:1.05rem;line-height:1}.move-task:hover{background:#902e1d}form{display:flex;gap:10px;flex-wrap:wrap;margin-top:22px}input{min-width:150px;flex:1;padding:12px 13px;border:1px solid #d9cfc2;background:#fffcf8;color:#28231e;font:inherit}button{border:0;background:rgb(255, 100, 0);color:#fff;padding:12px 17px;font:inherit;font-weight:800;cursor:pointer}button:hover{background:#902e1d}.error{color:rgb(255, 100, 0);font-weight:700}a{color:inherit;text-decoration:none}@media(max-width:800px){.kanban{grid-template-columns:1fr 1fr}}@media(max-width:650px){.kanban{grid-template-columns:1fr}.task-details{grid-template-columns:1fr}.task-due{grid-column:auto}header{display:block}.hero{padding:28px}}", + "body{margin:0;background:#f4f0e8;color:#28231e;font-family:Inter,ui-sans-serif,system-ui,sans-serif}.page{max-width:1120px;margin:auto;padding:28px 22px 72px}.auth{max-width:430px;margin:10vh auto;padding:42px;background:#fff;border:1px solid #e2d9cc;box-shadow:0 20px 60px #2d241414}.auth h1{font-size:clamp(2.4rem,8vw,4.8rem);line-height:.95;letter-spacing:-.06em;margin:14px 0}.auth form{display:grid;gap:12px;margin:26px 0 14px}.auth a{color:rgb(255,100,0);font-weight:700}.eyebrow{font-size:.72rem;letter-spacing:.16em;color:rgb(255,100,0);font-weight:800}header{display:flex;justify-content:space-between;gap:24px;align-items:start;margin-bottom:42px}header h1{font-size:clamp(2.7rem,7vw,5.5rem);line-height:.9;letter-spacing:-.07em;margin:12px 0}.hero{padding:42px;background:linear-gradient(120deg,#29241f,#594538);color:#fff;margin-bottom:20px;font-size:clamp(1.5rem,3vw,2.5rem);font-weight:800;letter-spacing:-.04em}.panel{background:#fff;border:1px solid #e2d9cc;padding:24px;margin:18px 0;box-shadow:0 12px 30px #2d24140b}.panel h2{margin:5px 0 8px;letter-spacing:-.03em}.panel>p{color:#796f64}.kanban{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:22px}.kanban-column{background:#f7f3ed;border:1px solid #ebe1d4;padding:13px}.kanban-column h3{font-size:.8rem;letter-spacing:.08em;text-transform:uppercase;color:#796f64;margin:0 0 5px}.task{display:flex;flex-direction:column;gap:13px;background:#fff;border:1px solid #e8dfd4;padding:16px;margin:9px 0;box-shadow:0 4px 12px #2d24140a;min-width:0}.task-header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.task-title{font-size:1rem;line-height:1.25;overflow-wrap:anywhere}.delete-task{flex:0 0 auto;padding:5px 8px;font-size:.68rem;background:#f0ebe3;color:#796f64}.delete-task:hover{background:#e8dfd4;color:#28231e}.task-description{margin:0;color:#796f64;line-height:1.45;overflow-wrap:anywhere}.task-details{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px 14px;padding-top:12px;border-top:1px solid #eee6dc;color:#4e463e;font-size:.8rem}.task-person,.task-due{display:flex;flex-direction:column;gap:4px;min-width:0;overflow-wrap:anywhere}.task-details small{color:#000000;font-size:.68rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase}.task-due{grid-column:1/-1}.task-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:2px}.task-status{padding:5px 8px;background:#f0ebe3;color:#796f64;font-size:.68rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase}.task-actions{display:flex;gap:6px;margin-left:auto}.task-actions form{margin:0}.move-task{width:31px;height:29px;padding:0;font-size:1.05rem;line-height:1}.move-task:hover{background:#902e1d}form{display:flex;gap:10px;flex-wrap:wrap;margin-top:22px}input:not([type=\"checkbox\"]){min-width:150px;flex:1;padding:12px 13px;border:1px solid #d9cfc2;background:#fffcf8;color:#28231e;font:inherit}input[type=\"checkbox\"]{min-width:16px;width:16px;height:16px;flex:0 0 16px;margin:0;padding:0}button{border:0;background:rgb(255,100,0);color:#fff;padding:12px 17px;font:inherit;font-weight:800;cursor:pointer}button:hover{background:#902e1d}.error{color:rgb(255,100,0);font-weight:700}a{color:inherit;text-decoration:none}@media(max-width:800px){.kanban{grid-template-columns:1fr 1fr}}@media(max-width:650px){.kanban{grid-template-columns:1fr}.task-details{grid-template-columns:1fr}.task-due{grid-column:auto}header{display:block}.hero{padding:28px}}", ) } diff --git a/server/db/init.sql b/server/db/init.sql index af1fa9d..5f044b2 100644 --- a/server/db/init.sql +++ b/server/db/init.sql @@ -20,11 +20,13 @@ create table if not exists task ( status text not null default 'todo' check (status in ('todo', 'doing', 'done')), assignee text not null default '', assignees text not null default '', + subteams subteam[] not null default '{}'::subteam[], due_date date, created_at timestamptz not null default now() ); alter table task add column if not exists assignees text not null default ''; +alter table task add column if not exists subteams subteam[] not null default '{}'::subteam[]; create table if not exists session ( token text primary key, diff --git a/server/src/server.gleam b/server/src/server.gleam index 2cfde07..350a2e8 100644 --- a/server/src/server.gleam +++ b/server/src/server.gleam @@ -23,6 +23,7 @@ const signup_password = "cremebrulee" pub fn main() -> Nil { wisp.configure_logger() let assert Ok(db) = database.start() + let assert Ok(_) = create_subteam_type(db) let assert Ok(schema) = simplifile.read("./db/init.sql") let assert Ok(_) = initialize_schema(db, schema) let secret = @@ -42,6 +43,18 @@ pub fn main() -> Nil { process.sleep_forever() } +fn create_subteam_type(db: Database) -> Result(Nil, pog.QueryError) { + case + "create type subteam as enum ('electrical', 'software', 'CAD', 'machining', 'operations', 'none')" + |> pog.query + |> pog.execute(db.connection) + { + Ok(_) -> Ok(Nil) + Error(pog.PostgresqlError("42710", _, _)) -> Ok(Nil) + Error(error) -> Error(error) + } +} + fn initialize_schema( db: Database, schema: String, @@ -259,6 +272,7 @@ fn add_task( task.description, task.assignee, task.assignees, + task.subteams, task.due_date, ) wisp.json_response("{\"ok\":true}", 201) @@ -317,6 +331,7 @@ fn to_shared_task(task: sql.Task) -> shared.Task { status: task.status, assignee: task.assignee, assignees: task.assignees, + subteams: task.subteams, due_date: task.due_date, ) } diff --git a/server/src/sql.gleam b/server/src/sql.gleam index 165e473..f6033d0 100644 --- a/server/src/sql.gleam +++ b/server/src/sql.gleam @@ -1,4 +1,5 @@ import gleam/dynamic/decode +import gleam/string import pog pub type User { @@ -18,6 +19,7 @@ pub type Task { status: String, assignee: String, assignees: String, + subteams: List(String), due_date: String, ) } @@ -83,7 +85,7 @@ pub fn create_project(db: pog.Connection, name: String, description: String) { } pub fn list_tasks(db: pog.Connection, project_id: Int) { - "select id, project_id, title, description, status, assignee, assignees, coalesce(due_date::text, '') from task where project_id = $1 order by case status when 'doing' then 0 when 'todo' then 1 else 2 end, created_at desc" + "select id, project_id, title, description, status, assignee, assignees, coalesce(array_to_string(subteams, ','), ''), coalesce(due_date::text, '') from task where project_id = $1 order by case status when 'doing' then 0 when 'todo' then 1 else 2 end, created_at desc" |> pog.query |> pog.parameter(pog.int(project_id)) |> pog.returning({ @@ -94,7 +96,8 @@ pub fn list_tasks(db: pog.Connection, project_id: Int) { use status <- decode.field(4, decode.string) use assignee <- decode.field(5, decode.string) use assignees <- decode.field(6, decode.string) - use due_date <- decode.field(7, decode.string) + use subteams <- decode.field(7, decode.string) + use due_date <- decode.field(8, decode.string) decode.success(Task( id:, project_id:, @@ -103,6 +106,7 @@ pub fn list_tasks(db: pog.Connection, project_id: Int) { status:, assignee:, assignees:, + subteams: string.split(subteams, ","), due_date:, )) }) @@ -116,15 +120,17 @@ pub fn create_task( description: String, assignee: String, assignees: String, + subteams: List(String), due_date: String, ) { - "insert into task (project_id, title, description, assignee, assignees, due_date) values ($1, $2, $3, $4, $5, nullif($6, '')::date) returning id" + "insert into task (project_id, title, description, assignee, assignees, subteams, due_date) values ($1, $2, $3, $4, $5, $6::subteam[], nullif($7, '')::date) returning id" |> pog.query |> pog.parameter(pog.int(project_id)) |> pog.parameter(pog.text(title)) |> pog.parameter(pog.text(description)) |> pog.parameter(pog.text(assignee)) |> pog.parameter(pog.text(assignees)) + |> pog.parameter(pog.array(pog.text, subteams)) |> pog.parameter(pog.text(due_date)) |> pog.returning(decode.field(0, decode.int, decode.success)) |> pog.execute(db) diff --git a/server/static/client.js b/server/static/client.js index cfce598..86e9b76 100644 --- a/server/static/client.js +++ b/server/static/client.js @@ -805,6 +805,24 @@ function reverse_and_prepend(loop$prefix, loop$suffix) { function reverse(list) { return reverse_and_prepend(list, List$Empty$const); } +function contains(loop$list, loop$elem) { + while (true) { + let list = loop$list; + let elem = loop$elem; + if (list instanceof Empty) { + return false; + } else { + let first$1 = list.head; + if (isEqual(first$1, elem)) { + return true; + } else { + let rest$1 = list.tail; + loop$list = rest$1; + loop$elem = elem; + } + } + } +} function filter_loop(loop$list, loop$fun, loop$acc) { while (true) { let list = loop$list; @@ -1324,6 +1342,7 @@ class Decoder extends CustomType { var float2 = /* @__PURE__ */ new Decoder(decode_float); var int2 = /* @__PURE__ */ new Decoder(decode_int); var string3 = /* @__PURE__ */ new Decoder(decode_string); +var bool = /* @__PURE__ */ new Decoder(decode_bool); function run(data, decoder) { let $ = decoder.function(data); let maybe_invalid_data = $[0]; @@ -1502,9 +1521,27 @@ function success(data) { return [data, List$Empty$const]; }); } +function decode_error(expected, found) { + return toList([ + new DecodeError(expected, classify_dynamic(found), List$Empty$const) + ]); +} function field(field_name, field_decoder, next) { return subfield(toList([field_name]), field_decoder, next); } +function decode_bool(data) { + let $ = isEqual(identity(true), data); + if ($) { + return [true, List$Empty$const]; + } else { + let $1 = isEqual(identity(false), data); + if ($1) { + return [false, List$Empty$const]; + } else { + return [false, decode_error("Bool", data)]; + } + } +} // build/dev/javascript/gleam_stdlib/gleam_stdlib.mjs function identity(x) { @@ -1540,6 +1577,9 @@ function split(xs, pattern) { function string_codeunit_slice(str, from2, length2) { return str.slice(from2, from2 + length2); } +function contains_string(haystack, needle) { + return haystack.indexOf(needle) >= 0; +} function starts_with(haystack, needle) { return haystack.startsWith(needle); } @@ -1557,6 +1597,12 @@ var unicode_whitespaces = [ ].join(""); var trim_start_regex = /* @__PURE__ */ new RegExp(`^[${unicode_whitespaces}]*`); var trim_end_regex = /* @__PURE__ */ new RegExp(`[${unicode_whitespaces}]*$`); +function trim_start(string4) { + return string4.replace(trim_start_regex, ""); +} +function trim_end(string4) { + return string4.replace(trim_end_regex, ""); +} function classify_dynamic(data) { if (typeof data === "string") { return "String"; @@ -1889,6 +1935,36 @@ function concat_loop(loop$strings, loop$accumulator) { function concat2(strings) { return concat_loop(strings, ""); } +function join_loop(loop$strings, loop$separator, loop$accumulator) { + while (true) { + let strings = loop$strings; + let separator = loop$separator; + let accumulator = loop$accumulator; + if (strings instanceof Empty) { + return accumulator; + } else { + let string4 = strings.head; + let strings$1 = strings.tail; + loop$strings = strings$1; + loop$separator = separator; + loop$accumulator = accumulator + separator + string4; + } + } +} +function join(strings, separator) { + if (strings instanceof Empty) { + return ""; + } else { + let first$1 = strings.head; + let rest = strings.tail; + return join_loop(rest, separator, first$1); + } +} +function trim(string4) { + let _pipe = string4; + let _pipe$1 = trim_start(_pipe); + return trim_end(_pipe$1); +} // build/dev/javascript/gleam_stdlib/gleam/result.mjs function map4(result, fun) { @@ -1945,6 +2021,14 @@ function object(entries) { function identity2(x) { return x; } +function array(list3) { + const array2 = []; + while (List$isNonEmpty(list3)) { + array2.push(List$NonEmpty$first(list3)); + list3 = List$NonEmpty$rest(list3); + } + return array2; +} function decode(string4) { try { const result = JSON.parse(string4); @@ -2067,9 +2151,20 @@ function to_string2(json) { function string4(input) { return identity2(input); } +function bool2(input) { + return identity2(input); +} function object2(entries) { return object(entries); } +function preprocessed_array(from2) { + return array(from2); +} +function array2(entries, inner_type) { + let _pipe = entries; + let _pipe$1 = map2(_pipe, inner_type); + return preprocessed_array(_pipe$1); +} // build/dev/javascript/gleam_erlang/gleam/erlang/process.mjs class Normal extends CustomType { } @@ -2378,6 +2473,13 @@ function attribute2(name, value) { function property2(name, value) { return property(name, value); } +function boolean_attribute(name, value) { + if (value) { + return attribute2(name, ""); + } else { + return property2(name, bool2(false)); + } +} function class$(name) { return attribute2("class", name); } @@ -2425,6 +2527,9 @@ function styles(properties) { function href(url) { return attribute2("href", url); } +function checked(is_checked) { + return boolean_attribute("checked", is_checked); +} function name(element_name) { return attribute2("name", element_name); } @@ -2437,6 +2542,9 @@ function rows(value) { function type_(control_type) { return attribute2("type", control_type); } +function value(control_value) { + return attribute2("value", control_value); +} // build/dev/javascript/lustre/lustre/effect.mjs class Effect extends CustomType { @@ -2496,9 +2604,9 @@ function get_or_compute(map5, key, compute) { function has_key(map5, key) { return map5 && map5.has(key); } -function insert2(map5, key, value) { +function insert2(map5, key, value2) { map5 ??= new Map; - map5.set(key, value); + map5.set(key, value2); return map5; } function remove(map5, key) { @@ -2727,8 +2835,8 @@ function unsafe_raw_html(namespace, tag, attributes, inner_html) { function memo2(dependencies, view) { return memo("", dependencies, view); } -function ref(value) { - return identity3(value); +function ref(value2) { + return identity3(value2); } function map6(element3, f) { return map5(element3, f); @@ -2783,12 +2891,27 @@ function strong(attrs, children) { function button(attrs, children) { return element2("button", attrs, children); } +function fieldset(attrs, children) { + return element2("fieldset", attrs, children); +} function form(attrs, children) { return element2("form", attrs, children); } function input(attrs) { return element2("input", attrs, empty_list); } +function label(attrs, children) { + return element2("label", attrs, children); +} +function legend(attrs, children) { + return element2("legend", attrs, children); +} +function option(attrs, label2) { + return element2("option", attrs, toList([text2(label2)])); +} +function select(attrs, children) { + return element2("select", attrs, children); +} function textarea(attrs, content) { return element2("textarea", prepend(property2("value", string4(content)), attrs), toList([text2(content)])); } @@ -2924,11 +3047,11 @@ class Emit extends CustomType { } } class Provide extends CustomType { - constructor(kind, key, value) { + constructor(kind, key, value2) { super(); this.kind = kind; this.key = key; - this.value = value; + this.value = value2; } } class Subscribe extends CustomType { @@ -2952,25 +3075,25 @@ class Batch extends CustomType { this.messages = messages; } } -var ServerMessage$isBatch = (value) => value instanceof Batch; +var ServerMessage$isBatch = (value2) => value2 instanceof Batch; class AttributeChanged extends CustomType { - constructor(kind, name2, value) { + constructor(kind, name2, value2) { super(); this.kind = kind; this.name = name2; - this.value = value; + this.value = value2; } } -var ServerMessage$isAttributeChanged = (value) => value instanceof AttributeChanged; +var ServerMessage$isAttributeChanged = (value2) => value2 instanceof AttributeChanged; class PropertyChanged extends CustomType { - constructor(kind, name2, value) { + constructor(kind, name2, value2) { super(); this.kind = kind; this.name = name2; - this.value = value; + this.value = value2; } } -var ServerMessage$isPropertyChanged = (value) => value instanceof PropertyChanged; +var ServerMessage$isPropertyChanged = (value2) => value2 instanceof PropertyChanged; class EventFired extends CustomType { constructor(kind, path, name2, event2) { super(); @@ -2980,16 +3103,16 @@ class EventFired extends CustomType { this.event = event2; } } -var ServerMessage$isEventFired = (value) => value instanceof EventFired; +var ServerMessage$isEventFired = (value2) => value2 instanceof EventFired; class ContextProvided extends CustomType { - constructor(kind, key, value) { + constructor(kind, key, value2) { super(); this.kind = kind; this.key = key; - this.value = value; + this.value = value2; } } -var ServerMessage$isContextProvided = (value) => value instanceof ContextProvided; +var ServerMessage$isContextProvided = (value2) => value2 instanceof ContextProvided; var mount_kind = 0; var reconcile_kind = 1; var emit_kind = 2; @@ -3005,8 +3128,8 @@ function reconcile(patch, memos) { function emit(name2, data) { return new Emit(emit_kind, name2, data); } -function provide(key, value) { - return new Provide(provide_kind, key, value); +function provide(key, value2) { + return new Provide(provide_kind, key, value2); } function subscribe(key) { return new Subscribe(subscribe_kind, key); @@ -3582,21 +3705,21 @@ class ClientDispatchedMessage extends CustomType { this.message = message; } } -var Message$isClientDispatchedMessage = (value) => value instanceof ClientDispatchedMessage; +var Message$isClientDispatchedMessage = (value2) => value2 instanceof ClientDispatchedMessage; class ClientRegisteredCallback extends CustomType { constructor(callback) { super(); this.callback = callback; } } -var Message$isClientRegisteredCallback = (value) => value instanceof ClientRegisteredCallback; +var Message$isClientRegisteredCallback = (value2) => value2 instanceof ClientRegisteredCallback; class ClientDeregisteredCallback extends CustomType { constructor(callback) { super(); this.callback = callback; } } -var Message$isClientDeregisteredCallback = (value) => value instanceof ClientDeregisteredCallback; +var Message$isClientDeregisteredCallback = (value2) => value2 instanceof ClientDeregisteredCallback; class EffectDispatchedMessage extends CustomType { constructor(message) { super(); @@ -3604,7 +3727,7 @@ class EffectDispatchedMessage extends CustomType { } } var Message$EffectDispatchedMessage = (message) => new EffectDispatchedMessage(message); -var Message$isEffectDispatchedMessage = (value) => value instanceof EffectDispatchedMessage; +var Message$isEffectDispatchedMessage = (value2) => value2 instanceof EffectDispatchedMessage; class EffectEmitEvent extends CustomType { constructor(name2, data) { super(); @@ -3613,16 +3736,16 @@ class EffectEmitEvent extends CustomType { } } var Message$EffectEmitEvent = (name2, data) => new EffectEmitEvent(name2, data); -var Message$isEffectEmitEvent = (value) => value instanceof EffectEmitEvent; +var Message$isEffectEmitEvent = (value2) => value2 instanceof EffectEmitEvent; class EffectProvidedValue extends CustomType { - constructor(key, value) { + constructor(key, value2) { super(); this.key = key; - this.value = value; + this.value = value2; } } -var Message$EffectProvidedValue = (key, value) => new EffectProvidedValue(key, value); -var Message$isEffectProvidedValue = (value) => value instanceof EffectProvidedValue; +var Message$EffectProvidedValue = (key, value2) => new EffectProvidedValue(key, value2); +var Message$isEffectProvidedValue = (value2) => value2 instanceof EffectProvidedValue; class EffectRequestedContextSubscription extends CustomType { constructor(key, decoder) { super(); @@ -3631,7 +3754,7 @@ class EffectRequestedContextSubscription extends CustomType { } } var Message$EffectRequestedContextSubscription = (key, decoder) => new EffectRequestedContextSubscription(key, decoder); -var Message$isEffectRequestedContextSubscription = (value) => value instanceof EffectRequestedContextSubscription; +var Message$isEffectRequestedContextSubscription = (value2) => value2 instanceof EffectRequestedContextSubscription; class EffectRemovedContextSubscription extends CustomType { constructor(key) { super(); @@ -3639,11 +3762,11 @@ class EffectRemovedContextSubscription extends CustomType { } } var Message$EffectRemovedContextSubscription = (key) => new EffectRemovedContextSubscription(key); -var Message$isEffectRemovedContextSubscription = (value) => value instanceof EffectRemovedContextSubscription; +var Message$isEffectRemovedContextSubscription = (value2) => value2 instanceof EffectRemovedContextSubscription; class SystemRequestedShutdown extends CustomType { } var Message$SystemRequestedShutdown$const = new SystemRequestedShutdown; -var Message$isSystemRequestedShutdown = (value) => value instanceof SystemRequestedShutdown; +var Message$isSystemRequestedShutdown = (value2) => value2 instanceof SystemRequestedShutdown; // build/dev/javascript/lustre/lustre/runtime/app.mjs class App extends CustomType { @@ -4703,7 +4826,7 @@ var insertBefore = (parent, node, reference) => parent.insertBefore(node, refere var moveBefore = SUPPORTS_MOVE_BEFORE ? (parent, node, reference) => parent.moveBefore(node, reference) : insertBefore; var removeChild = (parent, child2) => parent.removeChild(child2); var getAttribute = (node, name2) => node.getAttribute(name2); -var setAttribute = (node, name2, value) => node.setAttribute(name2, value); +var setAttribute = (node, name2, value2) => node.setAttribute(name2, value2); var removeAttribute = (node, name2) => node.removeAttribute(name2); var addEventListener = (node, name2, handler, options) => node.addEventListener(name2, handler, options); var removeEventListener = (node, name2, handler) => node.removeEventListener(name2, handler); @@ -4992,14 +5115,14 @@ class Reconciler { const { kind, name: name2, - value, + value: value2, prevent_default: prevent, debounce: debounceDelay, throttle: throttleDelay } = attribute3; switch (kind) { case attribute_kind: { - const valueOrDefault = value ?? ""; + const valueOrDefault = value2 ?? ""; if (name2 === "virtual:defaultValue") { node.defaultValue = valueOrDefault; return; @@ -5017,7 +5140,7 @@ class Reconciler { break; } case property_kind: - node[name2] = value; + node[name2] = value2; break; case event_kind: { if (handlers.has(name2)) { @@ -5116,8 +5239,8 @@ var syncedBooleanAttribute = (name2) => { }; var syncedAttribute = (name2) => { return { - added(node, value) { - node[name2] = value; + added(node, value2) { + node[name2] = value2; } }; }; @@ -5320,17 +5443,17 @@ var virtualiseAttributes = (node) => { }; var INPUT_ELEMENTS = ["input", "select", "textarea"]; var virtualiseInputEvents = (tag, node) => { - const value = node.value; - const checked = node.checked; - if (tag === "input" && node.type === "checkbox" && !checked) + const value2 = node.value; + const checked2 = node.checked; + if (tag === "input" && node.type === "checkbox" && !checked2) return; - if (tag === "input" && node.type === "radio" && !checked) + if (tag === "input" && node.type === "radio" && !checked2) return; - if (node.type !== "checkbox" && node.type !== "radio" && !value) + if (node.type !== "checkbox" && node.type !== "radio" && !value2) return; queueMicrotask(() => { - node.value = value; - node.checked = checked; + node.value = value2; + node.checked = checked2; node.dispatchEvent(new Event("input", { bubbles: true })); node.dispatchEvent(new Event("change", { bubbles: true })); if (globalThis.document.activeElement !== node) { @@ -5408,22 +5531,22 @@ class Runtime { const target = this.root.host ?? this.root; target.dispatchEvent(new LustreEvent(event3, data)); } - provide(key, value) { + provide(key, value2) { if (!this.#contexts.has(key)) { - this.#contexts.set(key, { value, subscribers: [] }); + this.#contexts.set(key, { value: value2, subscribers: [] }); } else { const context = this.#contexts.get(key); - if (isEqual2(context.value, value)) { + if (isEqual2(context.value, value2)) { return; } - context.value = value; + context.value = value2; for (let i = context.subscribers.length - 1;i >= 0; i--) { const [subscriber, unsubscribe2] = context.subscribers[i]; if (!subscriber) { context.subscribers.splice(i, 1); continue; } - subscriber(value, unsubscribe2); + subscriber(value2, unsubscribe2); } } } @@ -5432,12 +5555,12 @@ class Runtime { return; this.#contextSubscriptions.get(key)?.(); const target = this.root.host ?? this.root; - target.dispatchEvent(new ContextRequestEvent(key, (value, unsubscribe2) => { + target.dispatchEvent(new ContextRequestEvent(key, (value2, unsubscribe2) => { const previousUnsubscribe = this.#contextSubscriptions.get(key); if (previousUnsubscribe !== unsubscribe2) { previousUnsubscribe?.(); } - const decoded = run(value, decoder); + const decoded = run(value2, decoder); this.#contextSubscriptions.set(key, unsubscribe2); if (Result$isOk(decoded)) { this.dispatch(Result$Ok$0(decoded), true); @@ -5475,7 +5598,7 @@ class Runtime { emit: (event3, data) => this.emit(event3, data), select: () => {}, root: () => this.root, - provide: (key, value) => this.provide(key, value), + provide: (key, value2) => this.provide(key, value2), subscribe: (key, decoder) => this.subscribe(key, decoder), unsubscribe: (key) => this.unsubscribe(key) }; @@ -5637,13 +5760,13 @@ class Runtime2 { const { name: name2, data } = message; this.broadcast(emit(name2, data)); } else if (Message$isEffectProvidedValue(message)) { - const { key, value } = message; + const { key, value: value2 } = message; const existing = get(this.#providers, key); - if (Result$isOk(existing) && isEqual2(Result$Ok$0(existing), value)) { + if (Result$isOk(existing) && isEqual2(Result$Ok$0(existing), value2)) { return; } - this.#providers = insert(this.#providers, key, value); - this.broadcast(provide(key, value)); + this.#providers = insert(this.#providers, key, value2); + this.broadcast(provide(key, value2)); } else if (Message$isEffectRequestedContextSubscription(message)) { const { key, decoder } = message; this.broadcast(subscribe(key)); @@ -5685,15 +5808,15 @@ class Runtime2 { this.#model = model; return this.#view(model); } else if (ServerMessage$isAttributeChanged(message)) { - const { name: name2, value } = message; - const result = this.#handle_attribute_change(name2, value); + const { name: name2, value: value2 } = message; + const result = this.#handle_attribute_change(name2, value2); if (!Result$isOk(result)) { return this.#vdom; } return this.#dispatch(Result$Ok$0(result)); } else if (ServerMessage$isPropertyChanged(message)) { - const { name: name2, value } = message; - const result = this.#handle_properties_change(name2, value); + const { name: name2, value: value2 } = message; + const result = this.#handle_properties_change(name2, value2); if (!Result$isOk(result)) { return this.#vdom; } @@ -5708,12 +5831,12 @@ class Runtime2 { const { message: message2 } = Result$Ok$0(result); return this.#dispatch(message2); } else if (ServerMessage$isContextProvided(message)) { - const { key, value } = message; + const { key, value: value2 } = message; let result = get(this.#config.contexts, key); if (!Result$isOk(result)) { return this.#vdom; } - result = run(value, Result$Ok$0(result)); + result = run(value2, Result$Ok$0(result)); if (!Result$isOk(result)) { return this.#vdom; } @@ -5726,34 +5849,34 @@ class Runtime2 { this.#model = model; return this.#view(this.#model); } - #handle_attribute_change(name2, value) { + #handle_attribute_change(name2, value2) { const result = get(this.#config.attributes, name2); if (!Result$isOk(result)) { return result; } - return Result$Ok$0(result)(value); + return Result$Ok$0(result)(value2); } - #handle_properties_change(name2, value) { + #handle_properties_change(name2, value2) { const result = get(this.#config.properties, name2); if (!Result$isOk(result)) { return result; } - return Result$Ok$0(result)(value); + return Result$Ok$0(result)(value2); } #handle_effect(effect) { const dispatch2 = (message) => this.send(Message$EffectDispatchedMessage(message)); const emit2 = (name2, data) => this.send(Message$EffectEmitEvent(name2, data)); - const select = () => { + const select2 = () => { return; }; const internals = () => { return; }; - const provide2 = (key, value) => this.send(Message$EffectProvidedValue(key, value)); + const provide2 = (key, value2) => this.send(Message$EffectProvidedValue(key, value2)); const subscribe2 = (key, decoder) => this.send(Message$EffectRequestedContextSubscription(key, decoder)); const unsubscribe2 = (key) => this.send(Message$EffectRemovedContextSubscription(key)); globalThis.queueMicrotask(() => { - perform(effect, dispatch2, emit2, select, internals, provide2, subscribe2, unsubscribe2); + perform(effect, dispatch2, emit2, select2, internals, provide2, subscribe2, unsubscribe2); }); } } @@ -5801,16 +5924,26 @@ function on_click(message) { return on("click", success(message)); } function on_input(message) { - return on("input", subfield(toList(["target", "value"]), string3, (value) => { - return success(message(value)); + return on("input", subfield(toList(["target", "value"]), string3, (value2) => { + return success(message(value2)); + })); +} +function on_change(message) { + return on("change", subfield(toList(["target", "value"]), string3, (value2) => { + return success(message(value2)); + })); +} +function on_check(message) { + return on("change", subfield(toList(["target", "checked"]), bool, (checked2) => { + return success(message(checked2)); })); } function formdata_decoder() { let string_value_decoder = field(0, string3, (key) => { return field(1, one_of(map3(string3, (var0) => { return new Ok(var0); - }), toList([success(error_nil)])), (value) => { - let _pipe2 = value; + }), toList([success(error_nil)])), (value2) => { + let _pipe2 = value2; let _pipe$12 = map4(_pipe2, (_capture) => { return new$5(key, _capture); }); @@ -5838,21 +5971,21 @@ class PromiseLayer { constructor(promise) { this.promise = promise; } - static wrap(value) { - return value instanceof Promise ? new PromiseLayer(value) : value; + static wrap(value2) { + return value2 instanceof Promise ? new PromiseLayer(value2) : value2; } - static unwrap(value) { - return value instanceof PromiseLayer ? value.promise : value; + static unwrap(value2) { + return value2 instanceof PromiseLayer ? value2.promise : value2; } } -function resolve(value) { - return Promise.resolve(PromiseLayer.wrap(value)); +function resolve(value2) { + return Promise.resolve(PromiseLayer.wrap(value2)); } function then_await(promise, fn) { - return promise.then((value) => fn(PromiseLayer.unwrap(value))); + return promise.then((value2) => fn(PromiseLayer.unwrap(value2))); } function map_promise(promise, fn) { - return promise.then((value) => PromiseLayer.wrap(fn(PromiseLayer.unwrap(value)))); + return promise.then((value2) => PromiseLayer.wrap(fn(PromiseLayer.unwrap(value2)))); } function rescue(promise, fn) { return promise.catch((error) => fn(error)); @@ -6652,8 +6785,8 @@ function from_uri(uri) { }); }); } -function set_header(request, key2, value2) { - let headers = key_set(request.headers, lowercase(key2), value2); +function set_header(request, key2, value3) { + let headers = key_set(request.headers, lowercase(key2), value3); return new Request(request.method, headers, request.body, request.scheme, request.host, request.port, request.path, request.query); } function set_body(req, body2) { @@ -6768,7 +6901,7 @@ var from_relative_url = (url_string) => { return new Ok(uri); }; var uri_from_url = (url) => { - const optional = (value2) => value2 ? new Some(value2) : new None; + const optional = (value3) => value3 ? new Some(value3) : new None; return new Uri(optional(url.protocol?.slice(0, -1)), new None, optional(url.hostname), optional(url.port && Number(url.port)), url.pathname, optional(url.search?.slice(1)), optional(url.hash?.slice(1))); }; @@ -6977,7 +7110,7 @@ class Project extends CustomType { } } class Task extends CustomType { - constructor(id, project_id, title2, description, status, assignee, assignees, due_date) { + constructor(id, project_id, title2, description, status, assignee, assignees, subteams, due_date) { super(); this.id = id; this.project_id = project_id; @@ -6986,6 +7119,7 @@ class Task extends CustomType { this.status = status; this.assignee = assignee; this.assignees = assignees; + this.subteams = subteams; this.due_date = due_date; } } @@ -7012,12 +7146,13 @@ class NewProject extends CustomType { } } class NewTask extends CustomType { - constructor(title2, description, assignee, assignees, due_date) { + constructor(title2, description, assignee, assignees, subteams, due_date) { super(); this.title = title2; this.description = description; this.assignee = assignee; this.assignees = assignees; + this.subteams = subteams; this.due_date = due_date; } } @@ -7035,8 +7170,10 @@ function task_decoder() { return field("status", string3, (status) => { return field("assignee", string3, (assignee) => { return field("assignees", string3, (assignees) => { - return field("due_date", string3, (due_date) => { - return success(new Task(id, project_id, title2, description, status, assignee, assignees, due_date)); + return field("subteams", list2(string3), (subteams) => { + return field("due_date", string3, (due_date) => { + return success(new Task(id, project_id, title2, description, status, assignee, assignees, subteams, due_date)); + }); }); }); }); @@ -7064,36 +7201,37 @@ function dashboard_decoder() { }); }); } -function credentials_json(value2) { +function credentials_json(value3) { return object2(toList([ - ["username", string4(value2.username)], - ["password", string4(value2.password)], - ["invite_password", string4(value2.invite_password)] + ["username", string4(value3.username)], + ["password", string4(value3.password)], + ["invite_password", string4(value3.invite_password)] ])); } -function new_project_json(value2) { +function new_project_json(value3) { return object2(toList([ - ["name", string4(value2.name)], - ["description", string4(value2.description)] + ["name", string4(value3.name)], + ["description", string4(value3.description)] ])); } -function new_task_json(value2) { +function new_task_json(value3) { return object2(toList([ - ["title", string4(value2.title)], - ["description", string4(value2.description)], - ["assignee", string4(value2.assignee)], - ["assignees", string4(value2.assignees)], - ["due_date", string4(value2.due_date)] + ["title", string4(value3.title)], + ["description", string4(value3.description)], + ["assignee", string4(value3.assignee)], + ["assignees", string4(value3.assignees)], + ["subteams", array2(value3.subteams, string4)], + ["due_date", string4(value3.due_date)] ])); } -function status_change_json(value2) { - return object2(toList([["status", string4(value2.status)]])); +function status_change_json(value3) { + return object2(toList([["status", string4(value3.status)]])); } // build/dev/javascript/client/client.mjs var FILEPATH = "src/client.gleam"; class Model extends CustomType { - constructor(page, dashboard, error, username, password, invite_password, project_name, project_description, task_title, task_description, task_assignee, task_assignees, task_due_date) { + constructor(page, dashboard, error, username, password, invite_password, project_name, project_description, task_title, task_description, task_assignee, task_assignees, task_subteams, task_due_date, subteam_filter, task_search) { super(); this.page = page; this.dashboard = dashboard; @@ -7107,7 +7245,10 @@ class Model extends CustomType { this.task_description = task_description; this.task_assignee = task_assignee; this.task_assignees = task_assignees; + this.task_subteams = task_subteams; this.task_due_date = task_due_date; + this.subteam_filter = subteam_filter; + this.task_search = task_search; } } @@ -7215,6 +7356,25 @@ class TaskDueDateChanged extends CustomType { this[0] = $0; } } +class TaskSubteamChanged extends CustomType { + constructor($0, $1) { + super(); + this[0] = $0; + this[1] = $1; + } +} +class SubteamFilterChanged extends CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} +class TaskSearchChanged extends CustomType { + constructor($0) { + super(); + this[0] = $0; + } +} class InvitePasswordChanged extends CustomType { constructor($0) { super(); @@ -7239,14 +7399,70 @@ var Message$LogOut$const = new LogOut; class Nothing extends CustomType { } var Message$Nothing$const = new Nothing; +function subteam_checkbox(selected, value3, label2) { + return label(toList([ + class$("subteam-option"), + styles(toList([ + ["display", "flex"], + ["align-items", "center"], + ["gap", "8px"], + ["color", "#4e463e"], + ["font-size", ".82rem"], + ["line-height", "1.2"], + ["cursor", "pointer"] + ])) + ]), toList([ + input(toList([ + type_("checkbox"), + value(value3), + checked(contains(selected, value3)), + on_check((checked2) => { + return new TaskSubteamChanged(value3, checked2); + }) + ])), + text3(label2) + ])); +} +function subteam_checkboxes(selected) { + return fieldset(toList([ + class$("subteam-picker"), + styles(toList([ + ["grid-column", "1 / -1"], + ["display", "block"], + ["margin", "0"], + ["padding", "12px"], + ["border", "1px solid #d9cfc2"], + ["background", "#fffcf8"] + ])) + ]), toList([ + legend(List$Empty$const, toList([text3("Subteams")])), + div(toList([ + class$("subteam-options"), + styles(toList([ + ["display", "grid"], + ["grid-template-columns", "repeat(3, minmax(0, 1fr))"], + ["column-gap", "32px"], + ["row-gap", "12px"], + ["align-items", "center"] + ])) + ]), toList([ + subteam_checkbox(selected, "none", "No subteam"), + subteam_checkbox(selected, "electrical", "Electrical"), + subteam_checkbox(selected, "software", "Software"), + subteam_checkbox(selected, "CAD", "CAD"), + subteam_checkbox(selected, "machining", "Machining"), + subteam_checkbox(selected, "operations", "Operations") + ])) + ])); +} function text_input(name2, placeholder2) { return input(toList([name(name2), placeholder(placeholder2)])); } -function status_button(task_id, status, label) { +function status_button(task_id, status, label2) { return button(toList([ class$("move-task"), on_click(new StatusSubmitted(task_id, toList([["status", status]]))) - ]), toList([text3(label)])); + ]), toList([text3(label2)])); } function right_status_button(task) { let $ = task.status; @@ -7268,12 +7484,15 @@ function left_status_button(task) { return none2(); } } -function if_blank(value2, fallback) { - let $ = value2 === ""; +function join_subteams(subteams) { + return join(subteams, ", "); +} +function if_blank(value3, fallback) { + let $ = value3 === ""; if ($) { return fallback; } else { - return value2; + return value3; } } function task_view(task) { @@ -7298,6 +7517,10 @@ function task_view(task) { small(List$Empty$const, toList([text3("Assignees")])), text3(if_blank(task.assignees, "None")) ])), + div(toList([class$("task-person")]), toList([ + small(List$Empty$const, toList([text3("Subteams")])), + text3(if_blank(join_subteams(task.subteams), "No subteam")) + ])), div(toList([class$("task-due")]), toList([ small(List$Empty$const, toList([text3("Due")])), text3((() => { @@ -7323,14 +7546,24 @@ function kanban_column(title2, tasks) { ])) ]), map2(tasks, task_view))); } -function project_view(_, project) { - let backlog = filter(project.tasks, (task) => { +function task_matches(model, task) { + let search2 = lowercase(trim(model.task_search)); + let people = lowercase(task.assignee + " " + task.assignees); + let search_matches = search2 === "" || contains_string(people, search2); + let subteam_matches = model.subteam_filter === "all" || contains(task.subteams, model.subteam_filter); + return search_matches && subteam_matches; +} +function project_view(model, project) { + let tasks = filter(project.tasks, (task) => { + return task_matches(model, task); + }); + let backlog = filter(tasks, (task) => { return task.status === "todo"; }); - let doing = filter(project.tasks, (task) => { + let doing = filter(tasks, (task) => { return task.status === "doing"; }); - let done = filter(project.tasks, (task) => { + let done = filter(tasks, (task) => { return task.status === "done"; }); return article(toList([class$("panel")]), append(toList([ @@ -7350,7 +7583,8 @@ function project_view(_, project) { styles(toList([ ["display", "grid"], ["grid-template-columns", "repeat(4, minmax(0, 1fr))"], - ["align-items", "start"] + ["align-items", "start"], + ["gap", "10px"] ])), on_submit((fields) => { return new TaskSubmitted(project.id, fields); @@ -7360,6 +7594,7 @@ function project_view(_, project) { text_input("assignee", "Owner"), text_input("assignees", "Assignees"), text_input("due_date", "Due (MM/DD/YY)"), + subteam_checkboxes(model.task_subteams), textarea(toList([ class$("task-description-input"), styles(toList([ @@ -7383,7 +7618,7 @@ function project_view(_, project) { ])))); } function styles2() { - return style2(List$Empty$const, "body{margin:0;background:#f4f0e8;color:#28231e;font-family:Inter,ui-sans-serif,system-ui,sans-serif}.page{max-width:1120px;margin:auto;padding:28px 22px 72px}.auth{max-width:430px;margin:10vh auto;padding:42px;background:#fff;border:1px solid #e2d9cc;box-shadow:0 20px 60px #2d241414}.auth h1{font-size:clamp(2.4rem,8vw,4.8rem);line-height:.95;letter-spacing:-.06em;margin:14px 0}.auth form{display:grid;gap:12px;margin:26px 0 14px}.auth a{color:rgb(255, 100, 0);font-weight:700}.eyebrow{font-size:.72rem;letter-spacing:.16em;color:rgb(255, 100, 0);font-weight:800}header{display:flex;justify-content:space-between;gap:24px;align-items:start;margin-bottom:42px}header h1{font-size:clamp(2.7rem,7vw,5.5rem);line-height:.9;letter-spacing:-.07em;margin:12px 0}.hero{padding:42px;background:linear-gradient(120deg,#29241f,#594538);color:#fff;margin-bottom:20px;font-size:clamp(1.5rem,3vw,2.5rem);font-weight:800;letter-spacing:-.04em}.panel{background:#fff;border:1px solid #e2d9cc;padding:24px;margin:18px 0;box-shadow:0 12px 30px #2d24140b}.panel h2{margin:5px 0 8px;letter-spacing:-.03em}.panel>p{color:#796f64}.kanban{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:22px}.kanban-column{background:#f7f3ed;border:1px solid #ebe1d4;padding:13px}.kanban-column h3{font-size:.8rem;letter-spacing:.08em;text-transform:uppercase;color:#796f64;margin:0 0 5px}.task{display:flex;flex-direction:column;gap:13px;background:#fff;border:1px solid #e8dfd4;padding:16px;margin:9px 0;box-shadow:0 4px 12px #2d24140a;min-width:0}.task-header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.task-title{font-size:1rem;line-height:1.25;overflow-wrap:anywhere}.delete-task{flex:0 0 auto;padding:5px 8px;font-size:.68rem;background:#f0ebe3;color:#796f64}.delete-task:hover{background:#e8dfd4;color:#28231e}.task-description{margin:0;color:#796f64;line-height:1.45;overflow-wrap:anywhere}.task-details{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px 14px;padding-top:12px;border-top:1px solid #eee6dc;color:#4e463e;font-size:.8rem}.task-person,.task-due{display:flex;flex-direction:column;gap:4px;min-width:0;overflow-wrap:anywhere}.task-details small{color:#000000;font-size:.68rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase}.task-due{grid-column:1/-1}.task-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:2px}.task-status{padding:5px 8px;background:#f0ebe3;color:#796f64;font-size:.68rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase}.task-actions{display:flex;gap:6px;margin-left:auto}.task-actions form{margin:0}.move-task{width:31px;height:29px;padding:0;font-size:1.05rem;line-height:1}.move-task:hover{background:#902e1d}form{display:flex;gap:10px;flex-wrap:wrap;margin-top:22px}input{min-width:150px;flex:1;padding:12px 13px;border:1px solid #d9cfc2;background:#fffcf8;color:#28231e;font:inherit}button{border:0;background:rgb(255, 100, 0);color:#fff;padding:12px 17px;font:inherit;font-weight:800;cursor:pointer}button:hover{background:#902e1d}.error{color:rgb(255, 100, 0);font-weight:700}a{color:inherit;text-decoration:none}@media(max-width:800px){.kanban{grid-template-columns:1fr 1fr}}@media(max-width:650px){.kanban{grid-template-columns:1fr}.task-details{grid-template-columns:1fr}.task-due{grid-column:auto}header{display:block}.hero{padding:28px}}"); + return style2(List$Empty$const, "body{margin:0;background:#f4f0e8;color:#28231e;font-family:Inter,ui-sans-serif,system-ui,sans-serif}.page{max-width:1120px;margin:auto;padding:28px 22px 72px}.auth{max-width:430px;margin:10vh auto;padding:42px;background:#fff;border:1px solid #e2d9cc;box-shadow:0 20px 60px #2d241414}.auth h1{font-size:clamp(2.4rem,8vw,4.8rem);line-height:.95;letter-spacing:-.06em;margin:14px 0}.auth form{display:grid;gap:12px;margin:26px 0 14px}.auth a{color:rgb(255,100,0);font-weight:700}.eyebrow{font-size:.72rem;letter-spacing:.16em;color:rgb(255,100,0);font-weight:800}header{display:flex;justify-content:space-between;gap:24px;align-items:start;margin-bottom:42px}header h1{font-size:clamp(2.7rem,7vw,5.5rem);line-height:.9;letter-spacing:-.07em;margin:12px 0}.hero{padding:42px;background:linear-gradient(120deg,#29241f,#594538);color:#fff;margin-bottom:20px;font-size:clamp(1.5rem,3vw,2.5rem);font-weight:800;letter-spacing:-.04em}.panel{background:#fff;border:1px solid #e2d9cc;padding:24px;margin:18px 0;box-shadow:0 12px 30px #2d24140b}.panel h2{margin:5px 0 8px;letter-spacing:-.03em}.panel>p{color:#796f64}.kanban{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:22px}.kanban-column{background:#f7f3ed;border:1px solid #ebe1d4;padding:13px}.kanban-column h3{font-size:.8rem;letter-spacing:.08em;text-transform:uppercase;color:#796f64;margin:0 0 5px}.task{display:flex;flex-direction:column;gap:13px;background:#fff;border:1px solid #e8dfd4;padding:16px;margin:9px 0;box-shadow:0 4px 12px #2d24140a;min-width:0}.task-header{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.task-title{font-size:1rem;line-height:1.25;overflow-wrap:anywhere}.delete-task{flex:0 0 auto;padding:5px 8px;font-size:.68rem;background:#f0ebe3;color:#796f64}.delete-task:hover{background:#e8dfd4;color:#28231e}.task-description{margin:0;color:#796f64;line-height:1.45;overflow-wrap:anywhere}.task-details{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px 14px;padding-top:12px;border-top:1px solid #eee6dc;color:#4e463e;font-size:.8rem}.task-person,.task-due{display:flex;flex-direction:column;gap:4px;min-width:0;overflow-wrap:anywhere}.task-details small{color:#000000;font-size:.68rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase}.task-due{grid-column:1/-1}.task-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:2px}.task-status{padding:5px 8px;background:#f0ebe3;color:#796f64;font-size:.68rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase}.task-actions{display:flex;gap:6px;margin-left:auto}.task-actions form{margin:0}.move-task{width:31px;height:29px;padding:0;font-size:1.05rem;line-height:1}.move-task:hover{background:#902e1d}form{display:flex;gap:10px;flex-wrap:wrap;margin-top:22px}input:not([type=\"checkbox\"]){min-width:150px;flex:1;padding:12px 13px;border:1px solid #d9cfc2;background:#fffcf8;color:#28231e;font:inherit}input[type=\"checkbox\"]{min-width:16px;width:16px;height:16px;flex:0 0 16px;margin:0;padding:0}button{border:0;background:rgb(255,100,0);color:#fff;padding:12px 17px;font:inherit;font-weight:800;cursor:pointer}button:hover{background:#902e1d}.error{color:rgb(255,100,0);font-weight:700}a{color:inherit;text-decoration:none}@media(max-width:800px){.kanban{grid-template-columns:1fr 1fr}}@media(max-width:650px){.kanban{grid-template-columns:1fr}.task-details{grid-template-columns:1fr}.task-due{grid-column:auto}header{display:block}.hero{padding:28px}}"); } function error_view(error) { if (error instanceof Some) { @@ -7407,6 +7642,42 @@ function dashboard_view(model) { button(toList([on_click(Message$LogOut$const)]), toList([text3("Sign out")])) ])), section(toList([class$("hero")]), toList([text3("wassup")])), + section(toList([class$("panel")]), toList([ + h2(List$Empty$const, toList([text3("Find tasks")])), + input(toList([ + class$("task-search"), + styles(toList([["width", "100%"], ["box-sizing", "border-box"]])), + placeholder("Search owner or assignees"), + on_input((var0) => { + return new TaskSearchChanged(var0); + }) + ])), + select(toList([ + class$("subteam-filter"), + styles(toList([ + ["width", "100%"], + ["box-sizing", "border-box"], + ["padding", "12px 13px"], + ["border", "1px solid #d9cfc2"], + ["border-radius", "0"], + ["background", "#fffcf8"], + ["color", "#28231e"], + ["font", "inherit"], + ["appearance", "auto"] + ])), + on_change((var0) => { + return new SubteamFilterChanged(var0); + }) + ]), toList([ + option(toList([value("all")]), "All subteams"), + option(toList([value("none")]), "No subteam"), + option(toList([value("electrical")]), "Electrical"), + option(toList([value("software")]), "Software"), + option(toList([value("CAD")]), "CAD"), + option(toList([value("machining")]), "Machining"), + option(toList([value("operations")]), "Operations") + ])) + ])), section(toList([class$("panel")]), toList([ h2(List$Empty$const, toList([text3("New project")])), form(toList([ @@ -7548,7 +7819,7 @@ function remove_project(model, project_id) { let projects = filter(board.projects, (project) => { return project.id !== project_id; }); - return new Model(model.page, new Some(new Dashboard(board.username, projects)), model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date); + return new Model(model.page, new Some(new Dashboard(board.username, projects)), model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search); } else { return model; } @@ -7563,7 +7834,7 @@ function remove_task(model, task_id) { }); return new Project(project.id, project.name, project.description, tasks); }); - return new Model(model.page, new Some(new Dashboard(board.username, projects)), model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date); + return new Model(model.page, new Some(new Dashboard(board.username, projects)), model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search); } else { return model; } @@ -7576,14 +7847,14 @@ function update_task_status(model, task_id, status) { let tasks = map2(project.tasks, (task) => { let $1 = task.id === task_id; if ($1) { - return new Task(task.id, task.project_id, task.title, task.description, status, task.assignee, task.assignees, task.due_date); + return new Task(task.id, task.project_id, task.title, task.description, status, task.assignee, task.assignees, task.subteams, task.due_date); } else { return task; } }); return new Project(project.id, project.name, project.description, tasks); }); - return new Model(model.page, new Some(new Dashboard(board.username, projects)), model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date); + return new Model(model.page, new Some(new Dashboard(board.username, projects)), model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search); } else { return model; } @@ -7612,18 +7883,40 @@ function auth_request(model, fields, url, _) { })) ]; } +function toggle_subteam(selected, subteam, checked2) { + if (checked2) { + if (subteam === "none") { + return toList(["none"]); + } else { + let $ = contains(selected, subteam); + if ($) { + return filter(selected, (item) => { + return item !== "none"; + }); + } else { + return prepend(subteam, filter(selected, (item) => { + return item !== "none"; + })); + } + } + } else { + return filter(selected, (item) => { + return item !== subteam; + }); + } +} function update2(model, message2) { if (message2 instanceof DashboardLoaded) { let $ = message2[0]; if ($ instanceof Ok) { - let value2 = $[0]; + let value3 = $[0]; return [ - new Model("dashboard", new Some(value2), Option$None$const, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model("dashboard", new Some(value3), Option$None$const, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), none() ]; } else { return [ - new Model("login", model.dashboard, new Some("Please sign in to continue."), model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model("login", model.dashboard, new Some("Please sign in to continue."), model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), none() ]; } @@ -7640,7 +7933,7 @@ function update2(model, message2) { _block = new_project_json(_pipe); let body2 = _block; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, "", "", model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, "", "", model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), post("/api/projects", body2, expect_any_response((result) => { return new Saved((() => { let _pipe$1 = result; @@ -7654,11 +7947,11 @@ function update2(model, message2) { let project_id = message2[0]; let fields = message2[1]; let _block; - let _pipe = new NewTask(field2(fields, "title"), field2(fields, "description"), field2(fields, "assignee"), field2(fields, "assignees"), field2(fields, "due_date")); + let _pipe = new NewTask(field2(fields, "title"), field2(fields, "description"), field2(fields, "assignee"), field2(fields, "assignees"), model.task_subteams, field2(fields, "due_date")); _block = new_task_json(_pipe); let body2 = _block; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, "", "", "", "", ""), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, "", "", "", "", List$Empty$const, "", model.subteam_filter, model.task_search), post("/api/projects/" + to_string(project_id) + "/tasks", body2, expect_any_response((result) => { return new Saved((() => { let _pipe$1 = result; @@ -7691,14 +7984,14 @@ function update2(model, message2) { let $ = message2[0]; if ($ instanceof Ok) { return [ - new Model("dashboard", model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model("dashboard", model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), get3("/api/dashboard", expect_json(dashboard_decoder(), (var0) => { return new DashboardLoaded(var0); })) ]; } else { return [ - new Model(model.page, model.dashboard, new Some("Invalid username or password."), model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, new Some("Invalid username or password."), model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), none() ]; } @@ -7706,75 +7999,94 @@ function update2(model, message2) { let $ = message2[0]; if ($ instanceof Ok) { return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, "", "", "", "", "", "", "", ""), + new Model(model.page, model.dashboard, model.error, model.username, model.password, "", "", "", "", "", "", "", List$Empty$const, "", model.subteam_filter, model.task_search), get3("/api/dashboard", expect_json(dashboard_decoder(), (var0) => { return new DashboardLoaded(var0); })) ]; } else { return [ - new Model(model.page, model.dashboard, new Some("The request could not be saved."), model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, new Some("The request could not be saved."), model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), none() ]; } } else if (message2 instanceof UsernameChanged) { - let value2 = message2[0]; + let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, value2, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, model.error, value3, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), none() ]; } else if (message2 instanceof PasswordChanged) { - let value2 = message2[0]; + let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, value2, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, model.error, model.username, value3, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), none() ]; } else if (message2 instanceof ProjectNameChanged) { - let value2 = message2[0]; + let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, value2, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, value3, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), none() ]; } else if (message2 instanceof ProjectDescriptionChanged) { - let value2 = message2[0]; + let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, value2, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, value3, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), none() ]; } else if (message2 instanceof TaskTitleChanged) { - let value2 = message2[0]; + let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, value2, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, value3, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), none() ]; } else if (message2 instanceof TaskDescriptionChanged) { - let value2 = message2[0]; + let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, value2, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, value3, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), none() ]; } else if (message2 instanceof TaskAssigneeChanged) { - let value2 = message2[0]; + let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, value2, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, value3, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), none() ]; } else if (message2 instanceof TaskAssigneesChanged) { - let value2 = message2[0]; + let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, value2, model.task_due_date), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, value3, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), none() ]; } else if (message2 instanceof TaskDueDateChanged) { - let value2 = message2[0]; + let value3 = message2[0]; + return [ + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, value3, model.subteam_filter, model.task_search), + none() + ]; + } else if (message2 instanceof TaskSubteamChanged) { + let subteam = message2[0]; + let checked2 = message2[1]; + return [ + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, toggle_subteam(model.task_subteams, subteam, checked2), model.task_due_date, model.subteam_filter, model.task_search), + none() + ]; + } else if (message2 instanceof SubteamFilterChanged) { + let value3 = message2[0]; + return [ + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, value3, model.task_search), + none() + ]; + } else if (message2 instanceof TaskSearchChanged) { + let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, value2), + new Model(model.page, model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, value3), none() ]; } else if (message2 instanceof InvitePasswordChanged) { - let value2 = message2[0]; + let value3 = message2[0]; return [ - new Model(model.page, model.dashboard, model.error, model.username, model.password, value2, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model(model.page, model.dashboard, model.error, model.username, model.password, value3, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), none() ]; } else if (message2 instanceof DeleteTask) { @@ -7805,7 +8117,7 @@ function update2(model, message2) { ]; } else if (message2 instanceof LogOut) { return [ - new Model("login", model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_due_date), + new Model("login", model.dashboard, model.error, model.username, model.password, model.invite_password, model.project_name, model.project_description, model.task_title, model.task_description, model.task_assignee, model.task_assignees, model.task_subteams, model.task_due_date, model.subteam_filter, model.task_search), get3("/api/logout", expect_ok_response((_) => { return Message$Nothing$const; })) @@ -7830,7 +8142,7 @@ function init(_) { _block = "dashboard"; } let page = _block; - let model = new Model(page, Option$None$const, Option$None$const, "", "", "", "", "", "", "", "", "", ""); + let model = new Model(page, Option$None$const, Option$None$const, "", "", "", "", "", "", "", "", "", List$Empty$const, "", "all", ""); if (page === "dashboard") { return [ model, @@ -7853,7 +8165,7 @@ function main2() { let app = application(init, update2, view); let $ = start4(app, "#app", Option$None$const); if (!($ instanceof Ok)) { - throw makeError("let_assert", FILEPATH, "client", 19, "main", "Pattern match failed, no pattern matched the value.", { value: $, start: 439, end: 489, pattern_start: 450, pattern_end: 455 }); + throw makeError("let_assert", FILEPATH, "client", 20, "main", "Pattern match failed, no pattern matched the value.", { value: $, start: 459, end: 509, pattern_start: 470, pattern_end: 475 }); } return; } diff --git a/shared/src/shared.gleam b/shared/src/shared.gleam index 96e4d6e..196f3f5 100644 --- a/shared/src/shared.gleam +++ b/shared/src/shared.gleam @@ -14,6 +14,7 @@ pub type Task { status: String, assignee: String, assignees: String, + subteams: List(String), due_date: String, ) } @@ -36,6 +37,7 @@ pub type NewTask { description: String, assignee: String, assignees: String, + subteams: List(String), due_date: String, ) } @@ -66,8 +68,9 @@ fn task_decoder() -> decode.Decoder(Task) { use status <- decode.field("status", decode.string) use assignee <- decode.field("assignee", decode.string) use assignees <- decode.field("assignees", decode.string) + use subteams <- decode.field("subteams", decode.list(decode.string)) use due_date <- decode.field("due_date", decode.string) - decode.success(Task(id:, project_id:, title:, description:, status:, assignee:, assignees:, due_date:)) + decode.success(Task(id:, project_id:, title:, description:, status:, assignee:, assignees:, subteams:, due_date:)) } pub fn credentials_decoder() -> decode.Decoder(Credentials) { @@ -88,8 +91,9 @@ pub fn new_task_decoder() -> decode.Decoder(NewTask) { use description <- decode.field("description", decode.string) use assignee <- decode.field("assignee", decode.string) use assignees <- decode.field("assignees", decode.string) + use subteams <- decode.field("subteams", decode.list(decode.string)) use due_date <- decode.field("due_date", decode.string) - decode.success(NewTask(title:, description:, assignee:, assignees:, due_date:)) + decode.success(NewTask(title:, description:, assignee:, assignees:, subteams:, due_date:)) } pub fn status_change_decoder() -> decode.Decoder(StatusChange) { @@ -123,6 +127,7 @@ fn task_json(value: Task) -> json.Json { #("status", json.string(value.status)), #("assignee", json.string(value.assignee)), #("assignees", json.string(value.assignees)), + #("subteams", json.array(value.subteams, json.string)), #("due_date", json.string(value.due_date)), ]) } @@ -148,6 +153,7 @@ pub fn new_task_json(value: NewTask) -> json.Json { #("description", json.string(value.description)), #("assignee", json.string(value.assignee)), #("assignees", json.string(value.assignees)), + #("subteams", json.array(value.subteams, json.string)), #("due_date", json.string(value.due_date)), ]) }