Roadmap lays initiatives across months in lanes: each a bar from its first day through its last, filled as far as it has come, under the months' names and a line at today; hover a bar for its dates. Bars sit at fractions of the span, so nothing is measured, and a strip too narrow for its months scrolls sideways at a least width per month. GanttChart and TimelineView are the charts GanttChart. BurndownChart lives with the charts, on their scales, labels, tiles and paint: the ideal line from the scope down to nothing, what was left at each day's end so far, and a line at today; hover a day to read it. MilestoneProgress shows a milestone's issues done as a bar and a count, when it is due, and whether it keeps pace: on track, at risk when the work trails the time gone by more than a tenth, late past its day, or done. WorkloadView sets each person's points against what they can take, the bar turning red past it, in columns that line up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
157 lines
4.7 KiB
Rust
157 lines
4.7 KiB
Rust
use ely_gpui_component::{
|
|
mail::Label,
|
|
project::{IssueId, Person, Priority, Status, Task},
|
|
theme::{ActiveTheme, TextSize},
|
|
};
|
|
use gpui::{
|
|
AnyElement, App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div,
|
|
};
|
|
use jiff::{Timestamp, ToSpan, civil::Date, tz::TimeZone};
|
|
|
|
mod boards;
|
|
mod plans;
|
|
mod tasks;
|
|
|
|
use super::Page;
|
|
use crate::{script::Step, ui::keep};
|
|
|
|
pub const PAGE: Page = Page {
|
|
number: 31,
|
|
slug: "project",
|
|
title: "Project",
|
|
summary: "Tasks and issues, who has them, where they stand, and when they are due.",
|
|
render,
|
|
script: SCRIPT,
|
|
};
|
|
|
|
const SCRIPT: &[Step] = &[
|
|
Step::DownAt("project-fields", 150.0, 16.0),
|
|
Step::UpAt("project-fields", 150.0, 16.0),
|
|
Step::Wait(250),
|
|
Step::Shot("status-open"),
|
|
Step::Key("escape"),
|
|
Step::DownAt("project-tasks", 26.0, 22.0),
|
|
Step::UpAt("project-tasks", 26.0, 22.0),
|
|
Step::Wait(200),
|
|
Step::Shot("task-ticked"),
|
|
Step::DownAt("project-board", 80.0, 80.0),
|
|
Step::DragTo("project-board", 200.0, 90.0),
|
|
Step::DragTo("project-board", 360.0, 110.0),
|
|
Step::Wait(250),
|
|
Step::Shot("board-drag"),
|
|
Step::UpAt("project-board", 360.0, 110.0),
|
|
Step::Wait(300),
|
|
Step::Shot("board-dropped"),
|
|
];
|
|
|
|
/// What a demo says it did, under it.
|
|
fn quiet(text: SharedString, cx: &App) -> impl IntoElement + use<> {
|
|
let theme = cx.theme();
|
|
div()
|
|
.text_size(theme.text_size(TextSize::Sm))
|
|
.text_color(theme.colors.fg_muted)
|
|
.child(text)
|
|
}
|
|
|
|
/// Today in the gallery's zone.
|
|
fn today() -> Date {
|
|
let zone = TimeZone::try_system().expect("the gallery reads the system time zone");
|
|
Timestamp::now().to_zoned(zone).date()
|
|
}
|
|
|
|
fn team() -> Vec<Person> {
|
|
vec![
|
|
Person::new("chloe", "Chloé Martin"),
|
|
Person::new("ana", "Ana Lima"),
|
|
Person::new("ben", "Ben Ito"),
|
|
Person::new("dev", "Dev Rao"),
|
|
]
|
|
}
|
|
|
|
/// The studio's labels.
|
|
fn studio_labels() -> Vec<Label> {
|
|
vec![
|
|
Label::new("client", "Client", 0),
|
|
Label::new("site", "Site", 5),
|
|
Label::new("render", "Render", 4),
|
|
Label::new("budget", "Budget", 2),
|
|
]
|
|
}
|
|
|
|
/// One of the studio's labels, by key.
|
|
fn tag(key: &str) -> Label {
|
|
studio_labels()
|
|
.into_iter()
|
|
.find(|label| label.key.as_ref() == key)
|
|
.expect("one of the studio's labels")
|
|
}
|
|
|
|
/// A studio's open work around today: late, due soon, in review, done.
|
|
fn work(today: Date) -> Vec<Task> {
|
|
let day = |offset: i64| today.checked_add(offset.days()).expect("a day nearby");
|
|
let who = |key: &str| {
|
|
team()
|
|
.into_iter()
|
|
.find(|person| person.key.as_ref() == key)
|
|
.expect("one of the team")
|
|
};
|
|
vec![
|
|
Task::new("brief", "Write the client brief")
|
|
.issue(IssueId::new("ARC", 41))
|
|
.status(Status::InProgress)
|
|
.priority(Priority::Urgent)
|
|
.due(day(-1))
|
|
.assignee(who("ana"))
|
|
.label(tag("client")),
|
|
Task::new("samples", "Order plaster samples")
|
|
.issue(IssueId::new("ARC", 44))
|
|
.priority(Priority::High)
|
|
.due(day(0))
|
|
.assignee(who("ben"))
|
|
.label(tag("budget")),
|
|
Task::new("model", "Photograph the site model")
|
|
.issue(IssueId::new("ARC", 38))
|
|
.status(Status::InReview)
|
|
.priority(Priority::Medium)
|
|
.due(day(3))
|
|
.assignee(who("chloe"))
|
|
.label(tag("site"))
|
|
.label(tag("render")),
|
|
Task::new("budget", "Revise the lighting budget")
|
|
.issue(IssueId::new("ARC", 47))
|
|
.priority(Priority::Low)
|
|
.due(day(12)),
|
|
Task::new("survey", "Book the land survey")
|
|
.issue(IssueId::new("ARC", 29))
|
|
.status(Status::Done)
|
|
.priority(Priority::Medium)
|
|
.due(day(-4))
|
|
.assignee(who("dev"))
|
|
.label(tag("site")),
|
|
Task::new("archive", "Archive last year's renders")
|
|
.issue(IssueId::new("ARC", 17))
|
|
.status(Status::Backlog),
|
|
]
|
|
}
|
|
|
|
/// The page's tasks, which ticks and picks change.
|
|
fn board(window: &mut Window, cx: &mut App) -> Entity<Vec<Task>> {
|
|
keep("project-work", || work(today()), window, cx)
|
|
}
|
|
|
|
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
|
div()
|
|
.child(tasks::marks(cx))
|
|
.child(tasks::fields(window, cx))
|
|
.child(tasks::list(window, cx))
|
|
.child(tasks::subtasks(window, cx))
|
|
.child(boards::kanban(window, cx))
|
|
.child(boards::panel(window, cx))
|
|
.child(boards::labels(window, cx))
|
|
.child(plans::roadmap(cx))
|
|
.child(plans::burndown(cx))
|
|
.child(plans::milestones(cx))
|
|
.child(plans::workload(cx))
|
|
.into_any_element()
|
|
}
|