feat: impl dashboard & issue list
This commit is contained in:
@@ -10,7 +10,10 @@ futures-lite = "2.6.1"
|
||||
gpui = { version = "*" }
|
||||
paste = "1.0"
|
||||
rand = "0.10.1"
|
||||
reqwest = { version = "0.13.2", features = ["form"] }
|
||||
reqwest = { version = "0.13.2", features = ["form", "query"] }
|
||||
serde = "1.0.228"
|
||||
serde_json = "1.0.149"
|
||||
tokio = { version = "1.52.1", features = ["rt-multi-thread", "net", "time"] }
|
||||
|
||||
[build-dependencies]
|
||||
serde_json = "1.0.149"
|
||||
|
||||
82
build.rs
82
build.rs
@@ -13,9 +13,13 @@ fn main() {
|
||||
let manifest_dir =
|
||||
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("missing CARGO_MANIFEST_DIR"));
|
||||
let asset_root = manifest_dir.join("src/asset");
|
||||
let out_file = PathBuf::from(env::var("OUT_DIR").expect("missing OUT_DIR")).join("asset.rs");
|
||||
let fixture_root = manifest_dir.join("fixtures/github");
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("missing OUT_DIR"));
|
||||
let asset_out_file = out_dir.join("asset.rs");
|
||||
let fixture_out_file = out_dir.join("github_fixtures.rs");
|
||||
|
||||
println!("cargo::rerun-if-changed={}", asset_root.display());
|
||||
println!("cargo::rerun-if-changed={}", fixture_root.display());
|
||||
|
||||
let mut directory_entries = BTreeMap::<String, BTreeSet<String>>::new();
|
||||
directory_entries
|
||||
@@ -32,8 +36,11 @@ fn main() {
|
||||
);
|
||||
|
||||
let generated = render_assets(&asset_files, &directory_entries);
|
||||
let fixture_module = render_github_fixtures(&fixture_root);
|
||||
|
||||
fs::write(out_file, generated).expect("failed to write generated assets module");
|
||||
fs::write(asset_out_file, generated).expect("failed to write generated assets module");
|
||||
fs::write(fixture_out_file, fixture_module)
|
||||
.expect("failed to write generated github fixtures module");
|
||||
}
|
||||
|
||||
fn collect_assets(
|
||||
@@ -117,6 +124,77 @@ fn render_assets(
|
||||
output
|
||||
}
|
||||
|
||||
fn render_github_fixtures(fixture_root: &Path) -> String {
|
||||
let user_fetch = read_json_fixture(&fixture_root.join("user.fetch.json"));
|
||||
let repo_list = read_json_fixture(&fixture_root.join("repo.list.json"));
|
||||
|
||||
let mut issue_fixtures = BTreeMap::<(String, u32), String>::new();
|
||||
let mut entries = fs::read_dir(fixture_root)
|
||||
.unwrap_or_else(|err| panic!("failed to read {}: {err}", fixture_root.display()))
|
||||
.map(|entry| entry.expect("failed to read github fixture entry"))
|
||||
.collect::<Vec<_>>();
|
||||
entries.sort_by_key(|entry| entry.file_name());
|
||||
|
||||
for entry in entries {
|
||||
let file_name = entry
|
||||
.file_name()
|
||||
.into_string()
|
||||
.unwrap_or_else(|_| panic!("non-utf8 fixture name in {}", fixture_root.display()));
|
||||
|
||||
let Some((filter, page)) = parse_issue_fixture_name(&file_name) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
issue_fixtures.insert((filter, page), read_json_fixture(&entry.path()));
|
||||
}
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
output.push_str("pub fn user_fetch() -> &'static str {\n");
|
||||
output.push_str(" ");
|
||||
output.push_str(&string_literal(&user_fetch));
|
||||
output.push_str("\n}\n\n");
|
||||
|
||||
output.push_str("pub fn repo_list() -> &'static str {\n");
|
||||
output.push_str(" ");
|
||||
output.push_str(&string_literal(&repo_list));
|
||||
output.push_str("\n}\n\n");
|
||||
|
||||
output.push_str("pub fn issues_pull_requests(filter: &str, page: u32) -> Option<&'static str> {\n");
|
||||
output.push_str(" match (filter, page) {\n");
|
||||
for ((filter, page), json) in issue_fixtures {
|
||||
output.push_str(" (");
|
||||
output.push_str(&string_literal(&filter));
|
||||
output.push_str(", ");
|
||||
output.push_str(&page.to_string());
|
||||
output.push_str(") => Some(");
|
||||
output.push_str(&string_literal(&json));
|
||||
output.push_str("),\n");
|
||||
}
|
||||
output.push_str(" _ => None,\n");
|
||||
output.push_str(" }\n");
|
||||
output.push_str("}\n");
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
fn read_json_fixture(path: &Path) -> String {
|
||||
let raw = fs::read_to_string(path)
|
||||
.unwrap_or_else(|err| panic!("failed to read fixture {}: {err}", path.display()));
|
||||
let value: serde_json::Value = serde_json::from_str(&raw)
|
||||
.unwrap_or_else(|err| panic!("invalid json fixture {}: {err}", path.display()));
|
||||
serde_json::to_string(&value)
|
||||
.unwrap_or_else(|err| panic!("failed to serialize fixture {}: {err}", path.display()))
|
||||
}
|
||||
|
||||
fn parse_issue_fixture_name(file_name: &str) -> Option<(String, u32)> {
|
||||
let name = file_name.strip_suffix(".json")?;
|
||||
let rest = name.strip_prefix("issues.pull_requests.")?;
|
||||
let (filter, page) = rest.rsplit_once(".page")?;
|
||||
let page = page.parse::<u32>().ok()?;
|
||||
Some((filter.to_owned(), page))
|
||||
}
|
||||
|
||||
fn string_literal(value: &str) -> String {
|
||||
format!("{value:?}")
|
||||
}
|
||||
|
||||
439
fixtures/github/issues.pull_requests.all.page1.json
Normal file
439
fixtures/github/issues.pull_requests.all.page1.json
Normal file
@@ -0,0 +1,439 @@
|
||||
[
|
||||
{
|
||||
"id": 9001,
|
||||
"node_id": "PR_kwDONovem84",
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/issues/84",
|
||||
"repository_url": "https://api.github.com/repos/kennethnym/novem",
|
||||
"labels_url": "https://api.github.com/repos/kennethnym/novem/issues/84/labels{/name}",
|
||||
"comments_url": "https://api.github.com/repos/kennethnym/novem/issues/84/comments",
|
||||
"events_url": "https://api.github.com/repos/kennethnym/novem/issues/84/events",
|
||||
"html_url": "https://github.com/kennethnym/novem/pull/84",
|
||||
"number": 84,
|
||||
"state": "open",
|
||||
"state_reason": null,
|
||||
"title": "feat(dashboard): hydrate issue pane from cached query state",
|
||||
"body": "Wires the dashboard issue list to the query store and keeps selection stable while refetching.",
|
||||
"body_text": "Wires the dashboard issue list to the query store and keeps selection stable while refetching.",
|
||||
"body_html": null,
|
||||
"user": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"labels": [
|
||||
{
|
||||
"id": 11001,
|
||||
"node_id": "LA_kwDONovem_feature",
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/labels/feature",
|
||||
"name": "feature",
|
||||
"description": "New product capability.",
|
||||
"color": "0E8A16",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": 11002,
|
||||
"node_id": "LA_kwDONovem_dashboard",
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/labels/dashboard",
|
||||
"name": "dashboard",
|
||||
"description": "Dashboard experience.",
|
||||
"color": "1D76DB",
|
||||
"default": false
|
||||
}
|
||||
],
|
||||
"assignee": null,
|
||||
"assignees": [],
|
||||
"milestone": null,
|
||||
"locked": false,
|
||||
"active_lock_reason": null,
|
||||
"comments": 3,
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/pulls/84",
|
||||
"html_url": "https://github.com/kennethnym/novem/pull/84",
|
||||
"diff_url": "https://github.com/kennethnym/novem/pull/84.diff",
|
||||
"patch_url": "https://github.com/kennethnym/novem/pull/84.patch",
|
||||
"merged_at": null
|
||||
},
|
||||
"closed_at": null,
|
||||
"created_at": "2026-05-01T09:12:00Z",
|
||||
"updated_at": "2026-05-05T02:40:00Z",
|
||||
"closed_by": null,
|
||||
"author_association": "OWNER",
|
||||
"draft": false,
|
||||
"timeline_url": "https://api.github.com/repos/kennethnym/novem/issues/84/timeline",
|
||||
"repository": {
|
||||
"id": 101,
|
||||
"node_id": "R_kgDONovem",
|
||||
"name": "novem",
|
||||
"full_name": "kennethnym/novem",
|
||||
"owner": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"private": false,
|
||||
"html_url": "https://github.com/kennethnym/novem",
|
||||
"description": "Desktop workspace for triaging GitHub work.",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/kennethnym/novem"
|
||||
},
|
||||
"performed_via_github_app": null,
|
||||
"reactions": null,
|
||||
"pinned_comment": null,
|
||||
"type": null,
|
||||
"sub_issues_summary": null
|
||||
},
|
||||
{
|
||||
"id": 9005,
|
||||
"node_id": "PR_kwDOSprint62",
|
||||
"url": "https://api.github.com/repos/kennethnym/sprint-planner/issues/62",
|
||||
"repository_url": "https://api.github.com/repos/kennethnym/sprint-planner",
|
||||
"labels_url": "https://api.github.com/repos/kennethnym/sprint-planner/issues/62/labels{/name}",
|
||||
"comments_url": "https://api.github.com/repos/kennethnym/sprint-planner/issues/62/comments",
|
||||
"events_url": "https://api.github.com/repos/kennethnym/sprint-planner/issues/62/events",
|
||||
"html_url": "https://github.com/kennethnym/sprint-planner/pull/62",
|
||||
"number": 62,
|
||||
"state": "closed",
|
||||
"state_reason": "completed",
|
||||
"title": "feat(calendar): ship release handoff checklist in weekly planner",
|
||||
"body": "Adds the release checklist views and marks the handoff flow complete for the May rollout.",
|
||||
"body_text": "Adds the release checklist views and marks the handoff flow complete for the May rollout.",
|
||||
"body_html": null,
|
||||
"user": {
|
||||
"login": "rorycraft",
|
||||
"id": 7171,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7171?v=4",
|
||||
"html_url": "https://github.com/rorycraft",
|
||||
"name": "Rory Craft",
|
||||
"email": "rory@example.com"
|
||||
},
|
||||
"labels": [
|
||||
{
|
||||
"id": 14001,
|
||||
"node_id": "LA_kwDOSprint_release_blocker",
|
||||
"url": "https://api.github.com/repos/kennethnym/sprint-planner/labels/release-blocker",
|
||||
"name": "release-blocker",
|
||||
"description": "Required before the next release can ship.",
|
||||
"color": "B60205",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": 14002,
|
||||
"node_id": "LA_kwDOSprint_planning",
|
||||
"url": "https://api.github.com/repos/kennethnym/sprint-planner/labels/planning",
|
||||
"name": "planning",
|
||||
"description": "Roadmap and planning workflow.",
|
||||
"color": "0E8A16",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": 14003,
|
||||
"node_id": "LA_kwDOSprint_tests",
|
||||
"url": "https://api.github.com/repos/kennethnym/sprint-planner/labels/tests",
|
||||
"name": "tests",
|
||||
"description": "Verification and test coverage.",
|
||||
"color": "FBCA04",
|
||||
"default": false
|
||||
}
|
||||
],
|
||||
"assignee": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"assignees": [
|
||||
{
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
{
|
||||
"login": "mariahops",
|
||||
"id": 6161,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6161?v=4",
|
||||
"html_url": "https://github.com/mariahops",
|
||||
"name": "Maria Hops",
|
||||
"email": "maria@example.com"
|
||||
}
|
||||
],
|
||||
"milestone": null,
|
||||
"locked": false,
|
||||
"active_lock_reason": null,
|
||||
"comments": 12,
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/kennethnym/sprint-planner/pulls/62",
|
||||
"html_url": "https://github.com/kennethnym/sprint-planner/pull/62",
|
||||
"diff_url": "https://github.com/kennethnym/sprint-planner/pull/62.diff",
|
||||
"patch_url": "https://github.com/kennethnym/sprint-planner/pull/62.patch",
|
||||
"merged_at": "2026-05-04T18:10:00Z"
|
||||
},
|
||||
"closed_at": "2026-05-04T18:15:00Z",
|
||||
"created_at": "2026-04-28T10:20:00Z",
|
||||
"updated_at": "2026-05-05T01:10:00Z",
|
||||
"closed_by": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"author_association": "MEMBER",
|
||||
"draft": false,
|
||||
"timeline_url": "https://api.github.com/repos/kennethnym/sprint-planner/issues/62/timeline",
|
||||
"repository": {
|
||||
"id": 104,
|
||||
"node_id": "R_kgDOSprint",
|
||||
"name": "sprint-planner",
|
||||
"full_name": "kennethnym/sprint-planner",
|
||||
"owner": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"private": false,
|
||||
"html_url": "https://github.com/kennethnym/sprint-planner",
|
||||
"description": "Weekly planning board and release calendar.",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/kennethnym/sprint-planner"
|
||||
},
|
||||
"performed_via_github_app": null,
|
||||
"reactions": null,
|
||||
"pinned_comment": null,
|
||||
"type": null,
|
||||
"sub_issues_summary": null
|
||||
},
|
||||
{
|
||||
"id": 9004,
|
||||
"node_id": "PR_kwDONovem85",
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/issues/85",
|
||||
"repository_url": "https://api.github.com/repos/kennethnym/novem",
|
||||
"labels_url": "https://api.github.com/repos/kennethnym/novem/issues/85/labels{/name}",
|
||||
"comments_url": "https://api.github.com/repos/kennethnym/novem/issues/85/comments",
|
||||
"events_url": "https://api.github.com/repos/kennethnym/novem/issues/85/events",
|
||||
"html_url": "https://github.com/kennethnym/novem/pull/85",
|
||||
"number": 85,
|
||||
"state": "open",
|
||||
"state_reason": null,
|
||||
"title": "feat(repo): add cached repository query for titlebar picker",
|
||||
"body": "Introduces a repository list query so the titlebar can switch context without hitting GitHub repeatedly.",
|
||||
"body_text": "Introduces a repository list query so the titlebar can switch context without hitting GitHub repeatedly.",
|
||||
"body_html": null,
|
||||
"user": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"labels": [
|
||||
{
|
||||
"id": 11003,
|
||||
"node_id": "LA_kwDONovem_performance",
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/labels/performance",
|
||||
"name": "performance",
|
||||
"description": "Performance-sensitive change.",
|
||||
"color": "5319E7",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": 11004,
|
||||
"node_id": "LA_kwDONovem_cache",
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/labels/cache",
|
||||
"name": "cache",
|
||||
"description": "Caching and persistence work.",
|
||||
"color": "0052CC",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": 11005,
|
||||
"node_id": "LA_kwDONovem_needs_review",
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/labels/needs-review",
|
||||
"name": "needs-review",
|
||||
"description": "Awaiting reviewer attention.",
|
||||
"color": "FBCA04",
|
||||
"default": false
|
||||
}
|
||||
],
|
||||
"assignee": {
|
||||
"login": "leaferiksen",
|
||||
"id": 5151,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5151?v=4",
|
||||
"html_url": "https://github.com/leaferiksen",
|
||||
"name": "Leaf Eriksen",
|
||||
"email": "leaf@example.com"
|
||||
},
|
||||
"assignees": [
|
||||
{
|
||||
"login": "leaferiksen",
|
||||
"id": 5151,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5151?v=4",
|
||||
"html_url": "https://github.com/leaferiksen",
|
||||
"name": "Leaf Eriksen",
|
||||
"email": "leaf@example.com"
|
||||
}
|
||||
],
|
||||
"milestone": null,
|
||||
"locked": false,
|
||||
"active_lock_reason": null,
|
||||
"comments": 2,
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/pulls/85",
|
||||
"html_url": "https://github.com/kennethnym/novem/pull/85",
|
||||
"diff_url": "https://github.com/kennethnym/novem/pull/85.diff",
|
||||
"patch_url": "https://github.com/kennethnym/novem/pull/85.patch",
|
||||
"merged_at": null
|
||||
},
|
||||
"closed_at": null,
|
||||
"created_at": "2026-05-03T07:40:00Z",
|
||||
"updated_at": "2026-05-05T00:15:00Z",
|
||||
"closed_by": null,
|
||||
"author_association": "OWNER",
|
||||
"draft": false,
|
||||
"timeline_url": "https://api.github.com/repos/kennethnym/novem/issues/85/timeline",
|
||||
"repository": {
|
||||
"id": 101,
|
||||
"node_id": "R_kgDONovem",
|
||||
"name": "novem",
|
||||
"full_name": "kennethnym/novem",
|
||||
"owner": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"private": false,
|
||||
"html_url": "https://github.com/kennethnym/novem",
|
||||
"description": "Desktop workspace for triaging GitHub work.",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/kennethnym/novem"
|
||||
},
|
||||
"performed_via_github_app": null,
|
||||
"reactions": null,
|
||||
"pinned_comment": null,
|
||||
"type": null,
|
||||
"sub_issues_summary": null
|
||||
},
|
||||
{
|
||||
"id": 9002,
|
||||
"node_id": "PR_kwDOAgent47",
|
||||
"url": "https://api.github.com/repos/kennethnym/agent-tooling/issues/47",
|
||||
"repository_url": "https://api.github.com/repos/kennethnym/agent-tooling",
|
||||
"labels_url": "https://api.github.com/repos/kennethnym/agent-tooling/issues/47/labels{/name}",
|
||||
"comments_url": "https://api.github.com/repos/kennethnym/agent-tooling/issues/47/comments",
|
||||
"events_url": "https://api.github.com/repos/kennethnym/agent-tooling/issues/47/events",
|
||||
"html_url": "https://github.com/kennethnym/agent-tooling/pull/47",
|
||||
"number": 47,
|
||||
"state": "open",
|
||||
"state_reason": null,
|
||||
"title": "feat(prompts): split context loading from execution workers",
|
||||
"body": "Separates prompt packing from worker orchestration to make delegation easier to reason about.",
|
||||
"body_text": "Separates prompt packing from worker orchestration to make delegation easier to reason about.",
|
||||
"body_html": null,
|
||||
"user": {
|
||||
"login": "leaferiksen",
|
||||
"id": 5151,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5151?v=4",
|
||||
"html_url": "https://github.com/leaferiksen",
|
||||
"name": "Leaf Eriksen",
|
||||
"email": "leaf@example.com"
|
||||
},
|
||||
"labels": [
|
||||
{
|
||||
"id": 12001,
|
||||
"node_id": "LA_kwDOAgent_dx",
|
||||
"url": "https://api.github.com/repos/kennethnym/agent-tooling/labels/developer-experience",
|
||||
"name": "developer-experience",
|
||||
"description": "Improves contributor ergonomics.",
|
||||
"color": "7057FF",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": 12002,
|
||||
"node_id": "LA_kwDOAgent_prompts",
|
||||
"url": "https://api.github.com/repos/kennethnym/agent-tooling/labels/prompts",
|
||||
"name": "prompts",
|
||||
"description": "Prompt and agent coordination.",
|
||||
"color": "1D76DB",
|
||||
"default": false
|
||||
}
|
||||
],
|
||||
"assignee": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"assignees": [
|
||||
{
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
}
|
||||
],
|
||||
"milestone": null,
|
||||
"locked": false,
|
||||
"active_lock_reason": null,
|
||||
"comments": 1,
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/kennethnym/agent-tooling/pulls/47",
|
||||
"html_url": "https://github.com/kennethnym/agent-tooling/pull/47",
|
||||
"diff_url": "https://github.com/kennethnym/agent-tooling/pull/47.diff",
|
||||
"patch_url": "https://github.com/kennethnym/agent-tooling/pull/47.patch",
|
||||
"merged_at": null
|
||||
},
|
||||
"closed_at": null,
|
||||
"created_at": "2026-04-30T14:22:00Z",
|
||||
"updated_at": "2026-05-04T23:10:00Z",
|
||||
"closed_by": null,
|
||||
"author_association": "COLLABORATOR",
|
||||
"draft": true,
|
||||
"timeline_url": "https://api.github.com/repos/kennethnym/agent-tooling/issues/47/timeline",
|
||||
"repository": {
|
||||
"id": 102,
|
||||
"node_id": "R_kgDOAgent",
|
||||
"name": "agent-tooling",
|
||||
"full_name": "kennethnym/agent-tooling",
|
||||
"owner": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"private": false,
|
||||
"html_url": "https://github.com/kennethnym/agent-tooling",
|
||||
"description": "Experiments for agent-driven developer workflows.",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/kennethnym/agent-tooling"
|
||||
},
|
||||
"performed_via_github_app": null,
|
||||
"reactions": null,
|
||||
"pinned_comment": null,
|
||||
"type": null,
|
||||
"sub_issues_summary": null
|
||||
}
|
||||
]
|
||||
203
fixtures/github/issues.pull_requests.all.page2.json
Normal file
203
fixtures/github/issues.pull_requests.all.page2.json
Normal file
@@ -0,0 +1,203 @@
|
||||
[
|
||||
{
|
||||
"id": 9003,
|
||||
"node_id": "PR_kwDODesign31",
|
||||
"url": "https://api.github.com/repos/kennethnym/design-notes/issues/31",
|
||||
"repository_url": "https://api.github.com/repos/kennethnym/design-notes",
|
||||
"labels_url": "https://api.github.com/repos/kennethnym/design-notes/issues/31/labels{/name}",
|
||||
"comments_url": "https://api.github.com/repos/kennethnym/design-notes/issues/31/comments",
|
||||
"events_url": "https://api.github.com/repos/kennethnym/design-notes/issues/31/events",
|
||||
"html_url": "https://github.com/kennethnym/design-notes/pull/31",
|
||||
"number": 31,
|
||||
"state": "open",
|
||||
"state_reason": null,
|
||||
"title": "chore(tokens): tighten dashboard spacing scale",
|
||||
"body": "Normalizes horizontal gutters and sidebar section padding before the visual refresh.",
|
||||
"body_text": "Normalizes horizontal gutters and sidebar section padding before the visual refresh.",
|
||||
"body_html": null,
|
||||
"user": {
|
||||
"login": "mariahops",
|
||||
"id": 6161,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6161?v=4",
|
||||
"html_url": "https://github.com/mariahops",
|
||||
"name": "Maria Hops",
|
||||
"email": "maria@example.com"
|
||||
},
|
||||
"labels": [
|
||||
{
|
||||
"id": 13001,
|
||||
"node_id": "LA_kwDODesign_system",
|
||||
"url": "https://api.github.com/repos/kennethnym/design-notes/labels/design-system",
|
||||
"name": "design-system",
|
||||
"description": "Shared UI language and tokens.",
|
||||
"color": "C5DEF5",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": 13002,
|
||||
"node_id": "LA_kwDODesign_spacing",
|
||||
"url": "https://api.github.com/repos/kennethnym/design-notes/labels/spacing",
|
||||
"name": "spacing",
|
||||
"description": "Layout rhythm and spacing.",
|
||||
"color": "BFDADC",
|
||||
"default": false
|
||||
}
|
||||
],
|
||||
"assignee": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"assignees": [
|
||||
{
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
}
|
||||
],
|
||||
"milestone": null,
|
||||
"locked": false,
|
||||
"active_lock_reason": null,
|
||||
"comments": 0,
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/kennethnym/design-notes/pulls/31",
|
||||
"html_url": "https://github.com/kennethnym/design-notes/pull/31",
|
||||
"diff_url": "https://github.com/kennethnym/design-notes/pull/31.diff",
|
||||
"patch_url": "https://github.com/kennethnym/design-notes/pull/31.patch",
|
||||
"merged_at": null
|
||||
},
|
||||
"closed_at": null,
|
||||
"created_at": "2026-05-02T11:05:00Z",
|
||||
"updated_at": "2026-05-03T18:30:00Z",
|
||||
"closed_by": null,
|
||||
"author_association": "CONTRIBUTOR",
|
||||
"draft": false,
|
||||
"timeline_url": "https://api.github.com/repos/kennethnym/design-notes/issues/31/timeline",
|
||||
"repository": {
|
||||
"id": 103,
|
||||
"node_id": "R_kgDODesign",
|
||||
"name": "design-notes",
|
||||
"full_name": "kennethnym/design-notes",
|
||||
"owner": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"private": false,
|
||||
"html_url": "https://github.com/kennethnym/design-notes",
|
||||
"description": "Product and UI explorations.",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/kennethnym/design-notes"
|
||||
},
|
||||
"performed_via_github_app": null,
|
||||
"reactions": null,
|
||||
"pinned_comment": null,
|
||||
"type": null,
|
||||
"sub_issues_summary": null
|
||||
},
|
||||
{
|
||||
"id": 9007,
|
||||
"node_id": "PR_kwDOInfra19",
|
||||
"url": "https://api.github.com/repos/kennethnym/infra-scripts/issues/19",
|
||||
"repository_url": "https://api.github.com/repos/kennethnym/infra-scripts",
|
||||
"labels_url": "https://api.github.com/repos/kennethnym/infra-scripts/issues/19/labels{/name}",
|
||||
"comments_url": "https://api.github.com/repos/kennethnym/infra-scripts/issues/19/comments",
|
||||
"events_url": "https://api.github.com/repos/kennethnym/infra-scripts/issues/19/events",
|
||||
"html_url": "https://github.com/kennethnym/infra-scripts/pull/19",
|
||||
"number": 19,
|
||||
"state": "closed",
|
||||
"state_reason": "not_planned",
|
||||
"title": "docs(deploy): document manual failover steps",
|
||||
"body": null,
|
||||
"body_text": null,
|
||||
"body_html": null,
|
||||
"user": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"labels": [
|
||||
{
|
||||
"id": 15001,
|
||||
"node_id": "LA_kwDOInfra_docs",
|
||||
"url": "https://api.github.com/repos/kennethnym/infra-scripts/labels/docs",
|
||||
"name": "docs",
|
||||
"description": "Documentation updates.",
|
||||
"color": "0075CA",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"id": 15002,
|
||||
"node_id": "LA_kwDOInfra_infra",
|
||||
"url": "https://api.github.com/repos/kennethnym/infra-scripts/labels/infra",
|
||||
"name": "infra",
|
||||
"description": "Infrastructure or deployment work.",
|
||||
"color": "D4C5F9",
|
||||
"default": false
|
||||
}
|
||||
],
|
||||
"assignee": null,
|
||||
"assignees": [],
|
||||
"milestone": null,
|
||||
"locked": true,
|
||||
"active_lock_reason": "resolved",
|
||||
"comments": 5,
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/kennethnym/infra-scripts/pulls/19",
|
||||
"html_url": "https://github.com/kennethnym/infra-scripts/pull/19",
|
||||
"diff_url": "https://github.com/kennethnym/infra-scripts/pull/19.diff",
|
||||
"patch_url": "https://github.com/kennethnym/infra-scripts/pull/19.patch",
|
||||
"merged_at": null
|
||||
},
|
||||
"closed_at": "2026-05-02T12:05:00Z",
|
||||
"created_at": "2026-04-24T06:40:00Z",
|
||||
"updated_at": "2026-05-02T12:05:00Z",
|
||||
"closed_by": {
|
||||
"login": "piperlane",
|
||||
"id": 8181,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8181?v=4",
|
||||
"html_url": "https://github.com/piperlane",
|
||||
"name": "Piper Lane",
|
||||
"email": "piper@example.com"
|
||||
},
|
||||
"author_association": "OWNER",
|
||||
"draft": false,
|
||||
"timeline_url": "https://api.github.com/repos/kennethnym/infra-scripts/issues/19/timeline",
|
||||
"repository": {
|
||||
"id": 105,
|
||||
"node_id": "R_kgDOInfra",
|
||||
"name": "infra-scripts",
|
||||
"full_name": "kennethnym/infra-scripts",
|
||||
"owner": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"private": true,
|
||||
"html_url": "https://github.com/kennethnym/infra-scripts",
|
||||
"description": "Deployment and environment automation.",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/kennethnym/infra-scripts"
|
||||
},
|
||||
"performed_via_github_app": null,
|
||||
"reactions": null,
|
||||
"pinned_comment": null,
|
||||
"type": null,
|
||||
"sub_issues_summary": null
|
||||
}
|
||||
]
|
||||
1
fixtures/github/issues.pull_requests.all.page3.json
Normal file
1
fixtures/github/issues.pull_requests.all.page3.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
341
fixtures/github/issues.pull_requests.assigned.page1.json
Normal file
341
fixtures/github/issues.pull_requests.assigned.page1.json
Normal file
@@ -0,0 +1,341 @@
|
||||
[
|
||||
{
|
||||
"id": 9005,
|
||||
"node_id": "PR_kwDOSprint62",
|
||||
"url": "https://api.github.com/repos/kennethnym/sprint-planner/issues/62",
|
||||
"repository_url": "https://api.github.com/repos/kennethnym/sprint-planner",
|
||||
"labels_url": "https://api.github.com/repos/kennethnym/sprint-planner/issues/62/labels{/name}",
|
||||
"comments_url": "https://api.github.com/repos/kennethnym/sprint-planner/issues/62/comments",
|
||||
"events_url": "https://api.github.com/repos/kennethnym/sprint-planner/issues/62/events",
|
||||
"html_url": "https://github.com/kennethnym/sprint-planner/pull/62",
|
||||
"number": 62,
|
||||
"state": "closed",
|
||||
"state_reason": "completed",
|
||||
"title": "feat(calendar): ship release handoff checklist in weekly planner",
|
||||
"body": "Adds the release checklist views and marks the handoff flow complete for the May rollout.",
|
||||
"body_text": "Adds the release checklist views and marks the handoff flow complete for the May rollout.",
|
||||
"body_html": null,
|
||||
"user": {
|
||||
"login": "rorycraft",
|
||||
"id": 7171,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7171?v=4",
|
||||
"html_url": "https://github.com/rorycraft",
|
||||
"name": "Rory Craft",
|
||||
"email": "rory@example.com"
|
||||
},
|
||||
"labels": [
|
||||
{
|
||||
"id": 14001,
|
||||
"node_id": "LA_kwDOSprint_release_blocker",
|
||||
"url": "https://api.github.com/repos/kennethnym/sprint-planner/labels/release-blocker",
|
||||
"name": "release-blocker",
|
||||
"description": "Required before the next release can ship.",
|
||||
"color": "B60205",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": 14002,
|
||||
"node_id": "LA_kwDOSprint_planning",
|
||||
"url": "https://api.github.com/repos/kennethnym/sprint-planner/labels/planning",
|
||||
"name": "planning",
|
||||
"description": "Roadmap and planning workflow.",
|
||||
"color": "0E8A16",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": 14003,
|
||||
"node_id": "LA_kwDOSprint_tests",
|
||||
"url": "https://api.github.com/repos/kennethnym/sprint-planner/labels/tests",
|
||||
"name": "tests",
|
||||
"description": "Verification and test coverage.",
|
||||
"color": "FBCA04",
|
||||
"default": false
|
||||
}
|
||||
],
|
||||
"assignee": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"assignees": [
|
||||
{
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
{
|
||||
"login": "mariahops",
|
||||
"id": 6161,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6161?v=4",
|
||||
"html_url": "https://github.com/mariahops",
|
||||
"name": "Maria Hops",
|
||||
"email": "maria@example.com"
|
||||
}
|
||||
],
|
||||
"milestone": null,
|
||||
"locked": false,
|
||||
"active_lock_reason": null,
|
||||
"comments": 12,
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/kennethnym/sprint-planner/pulls/62",
|
||||
"html_url": "https://github.com/kennethnym/sprint-planner/pull/62",
|
||||
"diff_url": "https://github.com/kennethnym/sprint-planner/pull/62.diff",
|
||||
"patch_url": "https://github.com/kennethnym/sprint-planner/pull/62.patch",
|
||||
"merged_at": "2026-05-04T18:10:00Z"
|
||||
},
|
||||
"closed_at": "2026-05-04T18:15:00Z",
|
||||
"created_at": "2026-04-28T10:20:00Z",
|
||||
"updated_at": "2026-05-05T01:10:00Z",
|
||||
"closed_by": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"author_association": "MEMBER",
|
||||
"draft": false,
|
||||
"timeline_url": "https://api.github.com/repos/kennethnym/sprint-planner/issues/62/timeline",
|
||||
"repository": {
|
||||
"id": 104,
|
||||
"node_id": "R_kgDOSprint",
|
||||
"name": "sprint-planner",
|
||||
"full_name": "kennethnym/sprint-planner",
|
||||
"owner": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"private": false,
|
||||
"html_url": "https://github.com/kennethnym/sprint-planner",
|
||||
"description": "Weekly planning board and release calendar.",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/kennethnym/sprint-planner"
|
||||
},
|
||||
"performed_via_github_app": null,
|
||||
"reactions": null,
|
||||
"pinned_comment": null,
|
||||
"type": null,
|
||||
"sub_issues_summary": null
|
||||
},
|
||||
{
|
||||
"id": 9002,
|
||||
"node_id": "PR_kwDOAgent47",
|
||||
"url": "https://api.github.com/repos/kennethnym/agent-tooling/issues/47",
|
||||
"repository_url": "https://api.github.com/repos/kennethnym/agent-tooling",
|
||||
"labels_url": "https://api.github.com/repos/kennethnym/agent-tooling/issues/47/labels{/name}",
|
||||
"comments_url": "https://api.github.com/repos/kennethnym/agent-tooling/issues/47/comments",
|
||||
"events_url": "https://api.github.com/repos/kennethnym/agent-tooling/issues/47/events",
|
||||
"html_url": "https://github.com/kennethnym/agent-tooling/pull/47",
|
||||
"number": 47,
|
||||
"state": "open",
|
||||
"state_reason": null,
|
||||
"title": "feat(prompts): split context loading from execution workers",
|
||||
"body": "Separates prompt packing from worker orchestration to make delegation easier to reason about.",
|
||||
"body_text": "Separates prompt packing from worker orchestration to make delegation easier to reason about.",
|
||||
"body_html": null,
|
||||
"user": {
|
||||
"login": "leaferiksen",
|
||||
"id": 5151,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5151?v=4",
|
||||
"html_url": "https://github.com/leaferiksen",
|
||||
"name": "Leaf Eriksen",
|
||||
"email": "leaf@example.com"
|
||||
},
|
||||
"labels": [
|
||||
{
|
||||
"id": 12001,
|
||||
"node_id": "LA_kwDOAgent_dx",
|
||||
"url": "https://api.github.com/repos/kennethnym/agent-tooling/labels/developer-experience",
|
||||
"name": "developer-experience",
|
||||
"description": "Improves contributor ergonomics.",
|
||||
"color": "7057FF",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": 12002,
|
||||
"node_id": "LA_kwDOAgent_prompts",
|
||||
"url": "https://api.github.com/repos/kennethnym/agent-tooling/labels/prompts",
|
||||
"name": "prompts",
|
||||
"description": "Prompt and agent coordination.",
|
||||
"color": "1D76DB",
|
||||
"default": false
|
||||
}
|
||||
],
|
||||
"assignee": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"assignees": [
|
||||
{
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
}
|
||||
],
|
||||
"milestone": null,
|
||||
"locked": false,
|
||||
"active_lock_reason": null,
|
||||
"comments": 1,
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/kennethnym/agent-tooling/pulls/47",
|
||||
"html_url": "https://github.com/kennethnym/agent-tooling/pull/47",
|
||||
"diff_url": "https://github.com/kennethnym/agent-tooling/pull/47.diff",
|
||||
"patch_url": "https://github.com/kennethnym/agent-tooling/pull/47.patch",
|
||||
"merged_at": null
|
||||
},
|
||||
"closed_at": null,
|
||||
"created_at": "2026-04-30T14:22:00Z",
|
||||
"updated_at": "2026-05-04T23:10:00Z",
|
||||
"closed_by": null,
|
||||
"author_association": "COLLABORATOR",
|
||||
"draft": true,
|
||||
"timeline_url": "https://api.github.com/repos/kennethnym/agent-tooling/issues/47/timeline",
|
||||
"repository": {
|
||||
"id": 102,
|
||||
"node_id": "R_kgDOAgent",
|
||||
"name": "agent-tooling",
|
||||
"full_name": "kennethnym/agent-tooling",
|
||||
"owner": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"private": false,
|
||||
"html_url": "https://github.com/kennethnym/agent-tooling",
|
||||
"description": "Experiments for agent-driven developer workflows.",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/kennethnym/agent-tooling"
|
||||
},
|
||||
"performed_via_github_app": null,
|
||||
"reactions": null,
|
||||
"pinned_comment": null,
|
||||
"type": null,
|
||||
"sub_issues_summary": null
|
||||
},
|
||||
{
|
||||
"id": 9003,
|
||||
"node_id": "PR_kwDODesign31",
|
||||
"url": "https://api.github.com/repos/kennethnym/design-notes/issues/31",
|
||||
"repository_url": "https://api.github.com/repos/kennethnym/design-notes",
|
||||
"labels_url": "https://api.github.com/repos/kennethnym/design-notes/issues/31/labels{/name}",
|
||||
"comments_url": "https://api.github.com/repos/kennethnym/design-notes/issues/31/comments",
|
||||
"events_url": "https://api.github.com/repos/kennethnym/design-notes/issues/31/events",
|
||||
"html_url": "https://github.com/kennethnym/design-notes/pull/31",
|
||||
"number": 31,
|
||||
"state": "open",
|
||||
"state_reason": null,
|
||||
"title": "chore(tokens): tighten dashboard spacing scale",
|
||||
"body": "Normalizes horizontal gutters and sidebar section padding before the visual refresh.",
|
||||
"body_text": "Normalizes horizontal gutters and sidebar section padding before the visual refresh.",
|
||||
"body_html": null,
|
||||
"user": {
|
||||
"login": "mariahops",
|
||||
"id": 6161,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6161?v=4",
|
||||
"html_url": "https://github.com/mariahops",
|
||||
"name": "Maria Hops",
|
||||
"email": "maria@example.com"
|
||||
},
|
||||
"labels": [
|
||||
{
|
||||
"id": 13001,
|
||||
"node_id": "LA_kwDODesign_system",
|
||||
"url": "https://api.github.com/repos/kennethnym/design-notes/labels/design-system",
|
||||
"name": "design-system",
|
||||
"description": "Shared UI language and tokens.",
|
||||
"color": "C5DEF5",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": 13002,
|
||||
"node_id": "LA_kwDODesign_spacing",
|
||||
"url": "https://api.github.com/repos/kennethnym/design-notes/labels/spacing",
|
||||
"name": "spacing",
|
||||
"description": "Layout rhythm and spacing.",
|
||||
"color": "BFDADC",
|
||||
"default": false
|
||||
}
|
||||
],
|
||||
"assignee": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"assignees": [
|
||||
{
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
}
|
||||
],
|
||||
"milestone": null,
|
||||
"locked": false,
|
||||
"active_lock_reason": null,
|
||||
"comments": 0,
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/kennethnym/design-notes/pulls/31",
|
||||
"html_url": "https://github.com/kennethnym/design-notes/pull/31",
|
||||
"diff_url": "https://github.com/kennethnym/design-notes/pull/31.diff",
|
||||
"patch_url": "https://github.com/kennethnym/design-notes/pull/31.patch",
|
||||
"merged_at": null
|
||||
},
|
||||
"closed_at": null,
|
||||
"created_at": "2026-05-02T11:05:00Z",
|
||||
"updated_at": "2026-05-03T18:30:00Z",
|
||||
"closed_by": null,
|
||||
"author_association": "CONTRIBUTOR",
|
||||
"draft": false,
|
||||
"timeline_url": "https://api.github.com/repos/kennethnym/design-notes/issues/31/timeline",
|
||||
"repository": {
|
||||
"id": 103,
|
||||
"node_id": "R_kgDODesign",
|
||||
"name": "design-notes",
|
||||
"full_name": "kennethnym/design-notes",
|
||||
"owner": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"private": false,
|
||||
"html_url": "https://github.com/kennethnym/design-notes",
|
||||
"description": "Product and UI explorations.",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/kennethnym/design-notes"
|
||||
},
|
||||
"performed_via_github_app": null,
|
||||
"reactions": null,
|
||||
"pinned_comment": null,
|
||||
"type": null,
|
||||
"sub_issues_summary": null
|
||||
}
|
||||
]
|
||||
1
fixtures/github/issues.pull_requests.assigned.page2.json
Normal file
1
fixtures/github/issues.pull_requests.assigned.page2.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
205
fixtures/github/issues.pull_requests.created.page1.json
Normal file
205
fixtures/github/issues.pull_requests.created.page1.json
Normal file
@@ -0,0 +1,205 @@
|
||||
[
|
||||
{
|
||||
"id": 9004,
|
||||
"node_id": "PR_kwDONovem85",
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/issues/85",
|
||||
"repository_url": "https://api.github.com/repos/kennethnym/novem",
|
||||
"labels_url": "https://api.github.com/repos/kennethnym/novem/issues/85/labels{/name}",
|
||||
"comments_url": "https://api.github.com/repos/kennethnym/novem/issues/85/comments",
|
||||
"events_url": "https://api.github.com/repos/kennethnym/novem/issues/85/events",
|
||||
"html_url": "https://github.com/kennethnym/novem/pull/85",
|
||||
"number": 85,
|
||||
"state": "open",
|
||||
"state_reason": null,
|
||||
"title": "feat(repo): add cached repository query for titlebar picker",
|
||||
"body": "Introduces a repository list query so the titlebar can switch context without hitting GitHub repeatedly.",
|
||||
"body_text": "Introduces a repository list query so the titlebar can switch context without hitting GitHub repeatedly.",
|
||||
"body_html": null,
|
||||
"user": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"labels": [
|
||||
{
|
||||
"id": 11003,
|
||||
"node_id": "LA_kwDONovem_performance",
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/labels/performance",
|
||||
"name": "performance",
|
||||
"description": "Performance-sensitive change.",
|
||||
"color": "5319E7",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": 11004,
|
||||
"node_id": "LA_kwDONovem_cache",
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/labels/cache",
|
||||
"name": "cache",
|
||||
"description": "Caching and persistence work.",
|
||||
"color": "0052CC",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": 11005,
|
||||
"node_id": "LA_kwDONovem_needs_review",
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/labels/needs-review",
|
||||
"name": "needs-review",
|
||||
"description": "Awaiting reviewer attention.",
|
||||
"color": "FBCA04",
|
||||
"default": false
|
||||
}
|
||||
],
|
||||
"assignee": {
|
||||
"login": "leaferiksen",
|
||||
"id": 5151,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5151?v=4",
|
||||
"html_url": "https://github.com/leaferiksen",
|
||||
"name": "Leaf Eriksen",
|
||||
"email": "leaf@example.com"
|
||||
},
|
||||
"assignees": [
|
||||
{
|
||||
"login": "leaferiksen",
|
||||
"id": 5151,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5151?v=4",
|
||||
"html_url": "https://github.com/leaferiksen",
|
||||
"name": "Leaf Eriksen",
|
||||
"email": "leaf@example.com"
|
||||
}
|
||||
],
|
||||
"milestone": null,
|
||||
"locked": false,
|
||||
"active_lock_reason": null,
|
||||
"comments": 2,
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/pulls/85",
|
||||
"html_url": "https://github.com/kennethnym/novem/pull/85",
|
||||
"diff_url": "https://github.com/kennethnym/novem/pull/85.diff",
|
||||
"patch_url": "https://github.com/kennethnym/novem/pull/85.patch",
|
||||
"merged_at": null
|
||||
},
|
||||
"closed_at": null,
|
||||
"created_at": "2026-05-03T07:40:00Z",
|
||||
"updated_at": "2026-05-05T00:15:00Z",
|
||||
"closed_by": null,
|
||||
"author_association": "OWNER",
|
||||
"draft": false,
|
||||
"timeline_url": "https://api.github.com/repos/kennethnym/novem/issues/85/timeline",
|
||||
"repository": {
|
||||
"id": 101,
|
||||
"node_id": "R_kgDONovem",
|
||||
"name": "novem",
|
||||
"full_name": "kennethnym/novem",
|
||||
"owner": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"private": false,
|
||||
"html_url": "https://github.com/kennethnym/novem",
|
||||
"description": "Desktop workspace for triaging GitHub work.",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/kennethnym/novem"
|
||||
},
|
||||
"performed_via_github_app": null,
|
||||
"reactions": null,
|
||||
"pinned_comment": null,
|
||||
"type": null,
|
||||
"sub_issues_summary": null
|
||||
},
|
||||
{
|
||||
"id": 9001,
|
||||
"node_id": "PR_kwDONovem84",
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/issues/84",
|
||||
"repository_url": "https://api.github.com/repos/kennethnym/novem",
|
||||
"labels_url": "https://api.github.com/repos/kennethnym/novem/issues/84/labels{/name}",
|
||||
"comments_url": "https://api.github.com/repos/kennethnym/novem/issues/84/comments",
|
||||
"events_url": "https://api.github.com/repos/kennethnym/novem/issues/84/events",
|
||||
"html_url": "https://github.com/kennethnym/novem/pull/84",
|
||||
"number": 84,
|
||||
"state": "open",
|
||||
"state_reason": null,
|
||||
"title": "feat(dashboard): hydrate issue pane from cached query state",
|
||||
"body": "Wires the dashboard issue list to the query store and keeps selection stable while refetching.",
|
||||
"body_text": "Wires the dashboard issue list to the query store and keeps selection stable while refetching.",
|
||||
"body_html": null,
|
||||
"user": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"labels": [
|
||||
{
|
||||
"id": 11001,
|
||||
"node_id": "LA_kwDONovem_feature",
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/labels/feature",
|
||||
"name": "feature",
|
||||
"description": "New product capability.",
|
||||
"color": "0E8A16",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": 11002,
|
||||
"node_id": "LA_kwDONovem_dashboard",
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/labels/dashboard",
|
||||
"name": "dashboard",
|
||||
"description": "Dashboard experience.",
|
||||
"color": "1D76DB",
|
||||
"default": false
|
||||
}
|
||||
],
|
||||
"assignee": null,
|
||||
"assignees": [],
|
||||
"milestone": null,
|
||||
"locked": false,
|
||||
"active_lock_reason": null,
|
||||
"comments": 3,
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/kennethnym/novem/pulls/84",
|
||||
"html_url": "https://github.com/kennethnym/novem/pull/84",
|
||||
"diff_url": "https://github.com/kennethnym/novem/pull/84.diff",
|
||||
"patch_url": "https://github.com/kennethnym/novem/pull/84.patch",
|
||||
"merged_at": null
|
||||
},
|
||||
"closed_at": null,
|
||||
"created_at": "2026-05-01T09:12:00Z",
|
||||
"updated_at": "2026-05-05T02:40:00Z",
|
||||
"closed_by": null,
|
||||
"author_association": "OWNER",
|
||||
"draft": false,
|
||||
"timeline_url": "https://api.github.com/repos/kennethnym/novem/issues/84/timeline",
|
||||
"repository": {
|
||||
"id": 101,
|
||||
"node_id": "R_kgDONovem",
|
||||
"name": "novem",
|
||||
"full_name": "kennethnym/novem",
|
||||
"owner": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"private": false,
|
||||
"html_url": "https://github.com/kennethnym/novem",
|
||||
"description": "Desktop workspace for triaging GitHub work.",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/kennethnym/novem"
|
||||
},
|
||||
"performed_via_github_app": null,
|
||||
"reactions": null,
|
||||
"pinned_comment": null,
|
||||
"type": null,
|
||||
"sub_issues_summary": null
|
||||
}
|
||||
]
|
||||
98
fixtures/github/issues.pull_requests.created.page2.json
Normal file
98
fixtures/github/issues.pull_requests.created.page2.json
Normal file
@@ -0,0 +1,98 @@
|
||||
[
|
||||
{
|
||||
"id": 9007,
|
||||
"node_id": "PR_kwDOInfra19",
|
||||
"url": "https://api.github.com/repos/kennethnym/infra-scripts/issues/19",
|
||||
"repository_url": "https://api.github.com/repos/kennethnym/infra-scripts",
|
||||
"labels_url": "https://api.github.com/repos/kennethnym/infra-scripts/issues/19/labels{/name}",
|
||||
"comments_url": "https://api.github.com/repos/kennethnym/infra-scripts/issues/19/comments",
|
||||
"events_url": "https://api.github.com/repos/kennethnym/infra-scripts/issues/19/events",
|
||||
"html_url": "https://github.com/kennethnym/infra-scripts/pull/19",
|
||||
"number": 19,
|
||||
"state": "closed",
|
||||
"state_reason": "not_planned",
|
||||
"title": "docs(deploy): document manual failover steps",
|
||||
"body": null,
|
||||
"body_text": null,
|
||||
"body_html": null,
|
||||
"user": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"labels": [
|
||||
{
|
||||
"id": 15001,
|
||||
"node_id": "LA_kwDOInfra_docs",
|
||||
"url": "https://api.github.com/repos/kennethnym/infra-scripts/labels/docs",
|
||||
"name": "docs",
|
||||
"description": "Documentation updates.",
|
||||
"color": "0075CA",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"id": 15002,
|
||||
"node_id": "LA_kwDOInfra_infra",
|
||||
"url": "https://api.github.com/repos/kennethnym/infra-scripts/labels/infra",
|
||||
"name": "infra",
|
||||
"description": "Infrastructure or deployment work.",
|
||||
"color": "D4C5F9",
|
||||
"default": false
|
||||
}
|
||||
],
|
||||
"assignee": null,
|
||||
"assignees": [],
|
||||
"milestone": null,
|
||||
"locked": true,
|
||||
"active_lock_reason": "resolved",
|
||||
"comments": 5,
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/kennethnym/infra-scripts/pulls/19",
|
||||
"html_url": "https://github.com/kennethnym/infra-scripts/pull/19",
|
||||
"diff_url": "https://github.com/kennethnym/infra-scripts/pull/19.diff",
|
||||
"patch_url": "https://github.com/kennethnym/infra-scripts/pull/19.patch",
|
||||
"merged_at": null
|
||||
},
|
||||
"closed_at": "2026-05-02T12:05:00Z",
|
||||
"created_at": "2026-04-24T06:40:00Z",
|
||||
"updated_at": "2026-05-02T12:05:00Z",
|
||||
"closed_by": {
|
||||
"login": "piperlane",
|
||||
"id": 8181,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8181?v=4",
|
||||
"html_url": "https://github.com/piperlane",
|
||||
"name": "Piper Lane",
|
||||
"email": "piper@example.com"
|
||||
},
|
||||
"author_association": "OWNER",
|
||||
"draft": false,
|
||||
"timeline_url": "https://api.github.com/repos/kennethnym/infra-scripts/issues/19/timeline",
|
||||
"repository": {
|
||||
"id": 105,
|
||||
"node_id": "R_kgDOInfra",
|
||||
"name": "infra-scripts",
|
||||
"full_name": "kennethnym/infra-scripts",
|
||||
"owner": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
},
|
||||
"private": true,
|
||||
"html_url": "https://github.com/kennethnym/infra-scripts",
|
||||
"description": "Deployment and environment automation.",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/kennethnym/infra-scripts"
|
||||
},
|
||||
"performed_via_github_app": null,
|
||||
"reactions": null,
|
||||
"pinned_comment": null,
|
||||
"type": null,
|
||||
"sub_issues_summary": null
|
||||
}
|
||||
]
|
||||
1
fixtures/github/issues.pull_requests.created.page3.json
Normal file
1
fixtures/github/issues.pull_requests.created.page3.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
77
fixtures/github/repo.list.json
Normal file
77
fixtures/github/repo.list.json
Normal file
@@ -0,0 +1,77 @@
|
||||
[
|
||||
{
|
||||
"id": 101,
|
||||
"name": "novem",
|
||||
"full_name": "kennethnym/novem",
|
||||
"private": false,
|
||||
"html_url": "https://github.com/kennethnym/novem",
|
||||
"description": "Desktop workspace for triaging GitHub work.",
|
||||
"default_branch": "main",
|
||||
"owner": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 104,
|
||||
"name": "sprint-planner",
|
||||
"full_name": "kennethnym/sprint-planner",
|
||||
"private": false,
|
||||
"html_url": "https://github.com/kennethnym/sprint-planner",
|
||||
"description": "Weekly planning board and release calendar.",
|
||||
"default_branch": "main",
|
||||
"owner": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"name": "agent-tooling",
|
||||
"full_name": "kennethnym/agent-tooling",
|
||||
"private": false,
|
||||
"html_url": "https://github.com/kennethnym/agent-tooling",
|
||||
"description": "Experiments for agent-driven developer workflows.",
|
||||
"default_branch": "main",
|
||||
"owner": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 103,
|
||||
"name": "design-notes",
|
||||
"full_name": "kennethnym/design-notes",
|
||||
"private": false,
|
||||
"html_url": "https://github.com/kennethnym/design-notes",
|
||||
"description": "Product and UI explorations.",
|
||||
"default_branch": "main",
|
||||
"owner": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 105,
|
||||
"name": "infra-scripts",
|
||||
"full_name": "kennethnym/infra-scripts",
|
||||
"private": true,
|
||||
"html_url": "https://github.com/kennethnym/infra-scripts",
|
||||
"description": "Deployment and environment automation.",
|
||||
"default_branch": "trunk",
|
||||
"owner": {
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym"
|
||||
}
|
||||
}
|
||||
]
|
||||
8
fixtures/github/user.fetch.json
Normal file
8
fixtures/github/user.fetch.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"login": "kennethnym",
|
||||
"id": 4242,
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4242?v=4",
|
||||
"html_url": "https://github.com/kennethnym",
|
||||
"name": "Kenneth Ng",
|
||||
"email": "kenneth@example.com"
|
||||
}
|
||||
21
src/api.rs
21
src/api.rs
@@ -1,11 +1,11 @@
|
||||
use std::fmt::format;
|
||||
|
||||
use reqwest::{Response, dns::Resolving};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::query;
|
||||
|
||||
pub(crate) mod auth;
|
||||
pub(crate) mod issues;
|
||||
#[cfg(debug_assertions)]
|
||||
mod mock;
|
||||
pub(crate) mod repo;
|
||||
pub(crate) mod user;
|
||||
|
||||
@@ -14,6 +14,9 @@ pub struct QueryContext {
|
||||
pub(crate) http: reqwest::Client,
|
||||
pub(crate) auth: Option<AuthTokens>,
|
||||
pub(crate) github: GithubCredentials,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) should_use_fixtures: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
@@ -30,6 +33,8 @@ pub(crate) struct GithubCredentials {
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Error {
|
||||
Unauthenticated,
|
||||
#[cfg(debug_assertions)]
|
||||
MissingMockFixture(String),
|
||||
Github(GithubError),
|
||||
MalformedResponse(serde_json::Error),
|
||||
HttpError(reqwest::Error),
|
||||
@@ -63,6 +68,16 @@ impl QueryContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn use_github_fixtures() -> bool {
|
||||
mock::is_enabled()
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub(crate) fn use_github_fixtures() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
impl query::Context for QueryContext {}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
|
||||
@@ -2,10 +2,7 @@ use std::collections::HashMap;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
api,
|
||||
query::{self, use_query},
|
||||
};
|
||||
use crate::{api, query};
|
||||
|
||||
pub(crate) const DEVICE_LOGIN_FLOW_URL: &str = "https://github.com/login/device";
|
||||
|
||||
@@ -28,8 +25,8 @@ impl query::QueryFn for CreateDeviceCode {
|
||||
type Error = api::Error;
|
||||
type Context = api::QueryContext;
|
||||
|
||||
fn key(&self) -> &'static str {
|
||||
"auth.device_code"
|
||||
fn key(&self) -> query::Key {
|
||||
"auth/device_code".into()
|
||||
}
|
||||
|
||||
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
|
||||
@@ -64,8 +61,8 @@ impl query::QueryFn for RequestAccessToken {
|
||||
type Error = api::Error;
|
||||
type Context = api::QueryContext;
|
||||
|
||||
fn key(&self) -> &'static str {
|
||||
"auth.access_token"
|
||||
fn key(&self) -> query::Key {
|
||||
"auth.access_token".into()
|
||||
}
|
||||
|
||||
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
|
||||
|
||||
226
src/api/issues.rs
Normal file
226
src/api/issues.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use reqwest::Method;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
api::{self, user},
|
||||
query,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
#[repr(transparent)]
|
||||
pub(crate) struct Id(u64);
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct Issue {
|
||||
pub(crate) id: Id,
|
||||
pub(crate) node_id: String,
|
||||
pub(crate) url: String,
|
||||
pub(crate) repository_url: String,
|
||||
pub(crate) labels_url: String,
|
||||
pub(crate) comments_url: String,
|
||||
pub(crate) events_url: String,
|
||||
pub(crate) html_url: String,
|
||||
pub(crate) number: u64,
|
||||
pub(crate) state: String,
|
||||
pub(crate) state_reason: Option<String>,
|
||||
pub(crate) title: String,
|
||||
pub(crate) body: Option<String>,
|
||||
pub(crate) body_text: Option<String>,
|
||||
pub(crate) body_html: Option<String>,
|
||||
pub(crate) user: Option<user::User>,
|
||||
pub(crate) labels: Vec<Label>,
|
||||
pub(crate) assignee: Option<user::User>,
|
||||
#[serde(default)]
|
||||
pub(crate) assignees: Vec<user::User>,
|
||||
pub(crate) milestone: Option<Milestone>,
|
||||
pub(crate) locked: bool,
|
||||
pub(crate) active_lock_reason: Option<String>,
|
||||
pub(crate) comments: u64,
|
||||
pub(crate) pull_request: Option<PullRequest>,
|
||||
pub(crate) closed_at: Option<String>,
|
||||
pub(crate) created_at: String,
|
||||
pub(crate) updated_at: String,
|
||||
pub(crate) closed_by: Option<user::User>,
|
||||
pub(crate) author_association: String,
|
||||
pub(crate) draft: Option<bool>,
|
||||
pub(crate) timeline_url: Option<String>,
|
||||
pub(crate) repository: Option<Repository>,
|
||||
pub(crate) performed_via_github_app: Option<serde_json::Value>,
|
||||
pub(crate) reactions: Option<Reactions>,
|
||||
pub(crate) pinned_comment: Option<serde_json::Value>,
|
||||
#[serde(rename = "type")]
|
||||
pub(crate) issue_type: Option<IssueType>,
|
||||
pub(crate) sub_issues_summary: Option<SubIssuesSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum Label {
|
||||
Name(String),
|
||||
Detail(LabelDetail),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct LabelDetail {
|
||||
pub(crate) id: u64,
|
||||
pub(crate) node_id: String,
|
||||
pub(crate) url: String,
|
||||
pub(crate) name: String,
|
||||
pub(crate) description: Option<String>,
|
||||
pub(crate) color: String,
|
||||
pub(crate) default: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct Milestone {
|
||||
pub(crate) url: String,
|
||||
pub(crate) html_url: String,
|
||||
pub(crate) labels_url: String,
|
||||
pub(crate) id: u64,
|
||||
pub(crate) node_id: String,
|
||||
pub(crate) number: u64,
|
||||
pub(crate) state: String,
|
||||
pub(crate) title: String,
|
||||
pub(crate) description: Option<String>,
|
||||
pub(crate) creator: Option<user::User>,
|
||||
pub(crate) open_issues: u64,
|
||||
pub(crate) closed_issues: u64,
|
||||
pub(crate) created_at: String,
|
||||
pub(crate) updated_at: String,
|
||||
pub(crate) closed_at: Option<String>,
|
||||
pub(crate) due_on: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct PullRequest {
|
||||
pub(crate) url: String,
|
||||
pub(crate) html_url: String,
|
||||
pub(crate) diff_url: String,
|
||||
pub(crate) patch_url: String,
|
||||
pub(crate) merged_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct Repository {
|
||||
pub(crate) id: u64,
|
||||
pub(crate) node_id: String,
|
||||
pub(crate) name: String,
|
||||
pub(crate) full_name: String,
|
||||
pub(crate) owner: user::User,
|
||||
pub(crate) private: bool,
|
||||
pub(crate) html_url: String,
|
||||
pub(crate) description: Option<String>,
|
||||
pub(crate) fork: bool,
|
||||
pub(crate) url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct Reactions {
|
||||
pub(crate) url: String,
|
||||
pub(crate) total_count: u64,
|
||||
#[serde(rename = "+1")]
|
||||
pub(crate) plus_one: u64,
|
||||
#[serde(rename = "-1")]
|
||||
pub(crate) minus_one: u64,
|
||||
pub(crate) laugh: u64,
|
||||
pub(crate) confused: u64,
|
||||
pub(crate) heart: u64,
|
||||
pub(crate) hooray: u64,
|
||||
pub(crate) rocket: u64,
|
||||
pub(crate) eyes: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct IssueType {
|
||||
pub(crate) id: u64,
|
||||
pub(crate) node_id: String,
|
||||
pub(crate) name: String,
|
||||
pub(crate) description: Option<String>,
|
||||
pub(crate) color: Option<String>,
|
||||
pub(crate) created_at: Option<String>,
|
||||
pub(crate) updated_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct SubIssuesSummary {
|
||||
pub(crate) total: u64,
|
||||
pub(crate) completed: u64,
|
||||
pub(crate) percent_completed: f64,
|
||||
}
|
||||
|
||||
impl Deref for Id {
|
||||
type Target = u64;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for Id {
|
||||
fn from(id: u64) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Id {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Issue {
|
||||
pub(crate) const FILTER_ASSIGNED: &'static str = "assigned";
|
||||
pub(crate) const FILTER_CREATED: &'static str = "created";
|
||||
pub(crate) const FILTER_MENTIONED: &'static str = "mentioned";
|
||||
pub(crate) const FILTER_SUBSCRIBED: &'static str = "subscribed";
|
||||
pub(crate) const FILTER_REPOS: &'static str = "repos";
|
||||
pub(crate) const FILTER_ALL: &'static str = "all";
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ListPullRequests {
|
||||
pub filter: Option<&'static str>,
|
||||
pub page: u32,
|
||||
}
|
||||
|
||||
impl query::QueryFn for ListPullRequests {
|
||||
type Data = Vec<Issue>;
|
||||
type Error = api::Error;
|
||||
type Context = api::QueryContext;
|
||||
|
||||
fn key(&self) -> query::Key {
|
||||
format!(
|
||||
"issues/list?pulls=true&page={}&filter={}",
|
||||
self.page,
|
||||
self.filter.unwrap_or_default()
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
|
||||
#[cfg(debug_assertions)]
|
||||
if c.should_use_fixtures {
|
||||
return super::mock::list_pull_requests(self.filter, self.page);
|
||||
}
|
||||
|
||||
let page_string = self.page.to_string();
|
||||
let mut params: Vec<(&str, &str)> = Vec::with_capacity(4);
|
||||
params.push(("pulls", "true"));
|
||||
params.push(("page", &page_string));
|
||||
params.push(("direction", "desc"));
|
||||
if let Some(filter) = self.filter {
|
||||
params.push(("filter", filter))
|
||||
}
|
||||
|
||||
let res = c
|
||||
.github_request(Method::GET, "/issues")?
|
||||
.query(¶ms)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
api::parse_response::<Vec<Issue>>(res).await
|
||||
}
|
||||
}
|
||||
47
src/api/mock.rs
Normal file
47
src/api/mock.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use crate::api::{self, issues, repo, user};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/github_fixtures.rs"));
|
||||
|
||||
pub(super) fn is_enabled() -> bool {
|
||||
std::env::var("NOVEM_GITHUB_FIXTURES")
|
||||
.ok()
|
||||
.map(|value| {
|
||||
!matches!(
|
||||
value.trim().to_ascii_lowercase().as_str(),
|
||||
"" | "0" | "false" | "off"
|
||||
)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub(crate) fn fetch_user() -> Result<user::User, api::Error> {
|
||||
parse_fixture("user.fetch", user_fetch())
|
||||
}
|
||||
|
||||
pub(crate) fn list_repos() -> Result<Vec<repo::Repository>, api::Error> {
|
||||
parse_fixture("repo.list", repo_list())
|
||||
}
|
||||
|
||||
pub(crate) fn list_pull_requests(
|
||||
filter: Option<&str>,
|
||||
page: u32,
|
||||
) -> Result<Vec<issues::Issue>, api::Error> {
|
||||
let filter = filter.unwrap_or_default();
|
||||
let json = issues_pull_requests(filter, page).ok_or_else(|| {
|
||||
api::Error::MissingMockFixture(format!("issues.pull_requests filter={filter} page={page}"))
|
||||
})?;
|
||||
|
||||
parse_fixture(&format!("issues.pull_requests.{filter}.page{page}"), json)
|
||||
}
|
||||
|
||||
fn parse_fixture<T>(name: &str, json: &'static str) -> Result<T, api::Error>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
serde_json::from_str(json).map_err(|err| {
|
||||
println!("[mock fixture] failed to parse {name}: {err}");
|
||||
api::Error::MalformedResponse(err)
|
||||
})
|
||||
}
|
||||
@@ -1,19 +1,52 @@
|
||||
use crate::api::QueryContext;
|
||||
use crate::query;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{api, query};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Repository {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
pub full_name: String,
|
||||
pub private: bool,
|
||||
pub html_url: String,
|
||||
pub description: Option<String>,
|
||||
pub default_branch: String,
|
||||
pub owner: Owner,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Owner {
|
||||
pub login: String,
|
||||
pub id: api::user::Id,
|
||||
pub avatar_url: String,
|
||||
pub html_url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct List;
|
||||
|
||||
impl query::QueryFn for List {
|
||||
type Data = ();
|
||||
type Error = ();
|
||||
type Context = QueryContext;
|
||||
type Data = Vec<Repository>;
|
||||
type Error = api::Error;
|
||||
type Context = api::QueryContext;
|
||||
|
||||
fn key(&self) -> &'static str {
|
||||
"repo.list"
|
||||
fn key(&self) -> query::Key {
|
||||
"repo/list".into()
|
||||
}
|
||||
|
||||
async fn run(&self, _c: &Self::Context) -> Result<Self::Data, Self::Error> {
|
||||
Ok(())
|
||||
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
|
||||
#[cfg(debug_assertions)]
|
||||
if c.should_use_fixtures {
|
||||
return super::mock::list_repos();
|
||||
}
|
||||
|
||||
let params = [("sort", "updated"), ("per_page", "100")];
|
||||
let res = c
|
||||
.github_request(reqwest::Method::GET, "/user/repos")?
|
||||
.query(¶ms)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
api::parse_response(res).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,11 +48,16 @@ impl query::QueryFn for Fetch {
|
||||
type Error = api::Error;
|
||||
type Context = api::QueryContext;
|
||||
|
||||
fn key(&self) -> &'static str {
|
||||
"user"
|
||||
fn key(&self) -> query::Key {
|
||||
"user".into()
|
||||
}
|
||||
|
||||
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
|
||||
#[cfg(debug_assertions)]
|
||||
if c.should_use_fixtures {
|
||||
return super::mock::fetch_user();
|
||||
}
|
||||
|
||||
let res = c.github_request(Method::GET, "/user")?.send().await?;
|
||||
api::parse_response(res).await
|
||||
}
|
||||
|
||||
28
src/app.rs
28
src/app.rs
@@ -1,3 +1,6 @@
|
||||
use gpui::AppContext;
|
||||
use gpui::BorrowAppContext;
|
||||
|
||||
use crate::api;
|
||||
use crate::query;
|
||||
use crate::theme;
|
||||
@@ -22,3 +25,28 @@ pub fn rng(cx: &mut gpui::App) -> &mut rand::prelude::ThreadRng {
|
||||
pub fn query_store(cx: &gpui::App) -> &query::Store<api::QueryContext> {
|
||||
cx.global::<query::Store<api::QueryContext>>()
|
||||
}
|
||||
|
||||
pub fn open_window<V>(
|
||||
cx: &mut gpui::App,
|
||||
options: gpui::WindowOptions,
|
||||
screen: impl FnOnce(&mut gpui::Window, &mut gpui::Context<V>) -> V,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
V: gpui::Render,
|
||||
{
|
||||
cx.open_window(options, |window, cx| {
|
||||
cx.new(|cx| {
|
||||
cx.observe_window_appearance(window, |_, window, cx| {
|
||||
cx.update_global::<Global, ()>(|global, cx| {
|
||||
global.current_theme = global
|
||||
.theme_family
|
||||
.theme_for_appearance(window.appearance());
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
screen(window, cx)
|
||||
})
|
||||
})
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
1
src/asset/font_icon/list.svg
Normal file
1
src/asset/font_icon/list.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-icon lucide-list"><path d="M3 5h.01"/><path d="M3 12h.01"/><path d="M3 19h.01"/><path d="M8 5h13"/><path d="M8 12h13"/><path d="M8 19h13"/></svg>
|
||||
|
After Width: | Height: | Size: 353 B |
1
src/asset/font_icon/pencil_line.svg
Normal file
1
src/asset/font_icon/pencil_line.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pencil-line-icon lucide-pencil-line"><path d="M13 21h8"/><path d="m15 5 4 4"/><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/></svg>
|
||||
|
After Width: | Height: | Size: 427 B |
1
src/asset/font_icon/pull_request_arrow.svg
Normal file
1
src/asset/font_icon/pull_request_arrow.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-git-pull-request-arrow-icon lucide-git-pull-request-arrow"><circle cx="5" cy="6" r="3"/><path d="M5 9v12"/><circle cx="19" cy="18" r="3"/><path d="m15 9-3-3 3-3"/><path d="M12 6h5a2 2 0 0 1 2 2v7"/></svg>
|
||||
|
After Width: | Height: | Size: 407 B |
1
src/asset/font_icon/pull_request_closed.svg
Normal file
1
src/asset/font_icon/pull_request_closed.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-git-pull-request-closed-icon lucide-git-pull-request-closed"><circle cx="6" cy="6" r="3"/><path d="M6 9v12"/><path d="m21 3-6 6"/><path d="m21 9-6-6"/><path d="M18 11.5V15"/><circle cx="18" cy="18" r="3"/></svg>
|
||||
|
After Width: | Height: | Size: 414 B |
1
src/asset/font_icon/pull_request_draft.svg
Normal file
1
src/asset/font_icon/pull_request_draft.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-git-pull-request-draft-icon lucide-git-pull-request-draft"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M18 6V5"/><path d="M18 11v-1"/><line x1="6" x2="6" y1="9" y2="21"/></svg>
|
||||
|
After Width: | Height: | Size: 404 B |
1
src/asset/font_icon/user_plus.svg
Normal file
1
src/asset/font_icon/user_plus.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-plus-icon lucide-user-plus"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" x2="19" y1="8" y2="14"/><line x1="22" x2="16" y1="11" y2="11"/></svg>
|
||||
|
After Width: | Height: | Size: 401 B |
@@ -26,7 +26,20 @@ macro_rules! define_font_icons {
|
||||
};
|
||||
}
|
||||
|
||||
define_font_icons!(Check, ChevronDown, FolderGit, Github, ArrowRight, Cat);
|
||||
define_font_icons!(
|
||||
Check,
|
||||
ChevronDown,
|
||||
FolderGit,
|
||||
Github,
|
||||
ArrowRight,
|
||||
Cat,
|
||||
PencilLine,
|
||||
UserPlus,
|
||||
List,
|
||||
PullRequestArrow,
|
||||
PullRequestClosed,
|
||||
PullRequestDraft
|
||||
);
|
||||
|
||||
#[derive(gpui::IntoElement)]
|
||||
pub struct FontIconSvg {
|
||||
|
||||
@@ -8,6 +8,7 @@ pub(crate) struct Text {
|
||||
opacity: f32,
|
||||
text_align: gpui::TextAlign,
|
||||
text_size: Option<gpui::AbsoluteLength>,
|
||||
text_color: Option<gpui::Hsla>,
|
||||
line_height: gpui::DefiniteLength,
|
||||
styled: Option<Box<dyn Fn(gpui::Div) -> gpui::Div>>,
|
||||
}
|
||||
@@ -25,6 +26,7 @@ pub(crate) fn text(content: impl TextContent) -> Text {
|
||||
opacity: 1.,
|
||||
text_align: gpui::TextAlign::Left,
|
||||
text_size: None,
|
||||
text_color: None,
|
||||
line_height: gpui::relative(1.5),
|
||||
styled: None,
|
||||
}
|
||||
@@ -84,6 +86,11 @@ impl Text {
|
||||
self.text_size(gpui::rems(1.875))
|
||||
}
|
||||
|
||||
pub(crate) fn text_color(mut self, color: impl Into<gpui::Hsla>) -> Self {
|
||||
self.text_color = Some(color.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn line_height(mut self, line_height: impl Into<gpui::DefiniteLength>) -> Self {
|
||||
self.line_height = line_height.into();
|
||||
self
|
||||
@@ -160,7 +167,7 @@ impl gpui::RenderOnce for Text {
|
||||
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl gpui::IntoElement {
|
||||
let theme = app::current_theme(cx);
|
||||
let mut div = div()
|
||||
.text_color(theme.colors.text)
|
||||
.text_color(self.text_color.unwrap_or(theme.colors.text.into()))
|
||||
.font_weight(self.font_weight)
|
||||
.opacity(self.opacity)
|
||||
.text_align(self.text_align)
|
||||
|
||||
33
src/main.rs
33
src/main.rs
@@ -1,6 +1,6 @@
|
||||
use gpui::{bounds, point, prelude::*, px, size};
|
||||
|
||||
use crate::{query::fetch_query, screen::dashboard, screen::setup_wizard};
|
||||
use crate::screen::{dashboard, setup_wizard};
|
||||
|
||||
mod api;
|
||||
mod app;
|
||||
@@ -38,6 +38,7 @@ fn main() {
|
||||
}
|
||||
|
||||
fn setup_application(cx: &mut gpui::App) {
|
||||
let use_github_fixtures = api::use_github_fixtures();
|
||||
let query_store = query::Store::new(api::QueryContext {
|
||||
http: reqwest::Client::new(),
|
||||
auth: None,
|
||||
@@ -45,6 +46,9 @@ fn setup_application(cx: &mut gpui::App) {
|
||||
base_url: "https://api.github.com",
|
||||
client_id: "Iv23liZD4bMQpGJICsR7",
|
||||
},
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
should_use_fixtures: false,
|
||||
});
|
||||
|
||||
let theme_family = theme::ThemeFamily::default();
|
||||
@@ -61,6 +65,11 @@ fn setup_application(cx: &mut gpui::App) {
|
||||
// TODO: handle failure
|
||||
_ = storage::ensure_data_dir();
|
||||
|
||||
if use_github_fixtures {
|
||||
_ = dashboard::open_window(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let start = resume_application_state(cx);
|
||||
|
||||
match start {
|
||||
@@ -75,8 +84,7 @@ fn setup_application(cx: &mut gpui::App) {
|
||||
}
|
||||
|
||||
Start::FromSaved(_) => {
|
||||
let screen = dashboard::new(cx);
|
||||
_ = dashboard::open_window(screen, cx);
|
||||
_ = dashboard::open_window(cx);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -87,12 +95,14 @@ fn resume_application_state(cx: &mut gpui::App) -> Start {
|
||||
return Start::FromScratch;
|
||||
};
|
||||
|
||||
let auth_tokens = if cfg!(debug_assertions) {
|
||||
state.debug_auth_tokens.take()
|
||||
} else {
|
||||
cx.background_executor()
|
||||
.block(storage::load_auth_tokens(cx, state.selected_account))
|
||||
};
|
||||
#[cfg(debug_assertions)]
|
||||
let auth_tokens = state.debug_auth_tokens.take();
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
let auth_tokens = cx
|
||||
.background_executor()
|
||||
.block(storage::load_auth_tokens(cx, state.selected_account));
|
||||
|
||||
let Some(auth_tokens) = auth_tokens else {
|
||||
return Start::FromScratch;
|
||||
};
|
||||
@@ -100,6 +110,11 @@ fn resume_application_state(cx: &mut gpui::App) -> Start {
|
||||
_ = cx.update_global::<query::Store<api::QueryContext>, _>(|store, _| {
|
||||
store.update_query_context(|cx| {
|
||||
cx.auth = Some(auth_tokens);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
cx.should_use_fixtures = state.debug_should_use_fixtures.unwrap_or(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
58
src/query.rs
58
src/query.rs
@@ -1,12 +1,12 @@
|
||||
use gpui::{AppContext, BorrowAppContext};
|
||||
use std::{any::Any, collections::HashMap, marker::PhantomData, ops::Deref};
|
||||
use std::{any::Any, borrow::Cow, collections::HashMap, marker::PhantomData, ops::Deref};
|
||||
|
||||
pub(crate) trait QueryFn: Clone + 'static {
|
||||
type Data: 'static;
|
||||
type Error: std::fmt::Debug + 'static;
|
||||
type Context: Context;
|
||||
|
||||
fn key(&self) -> &'static str;
|
||||
fn key(&self) -> Key;
|
||||
|
||||
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error>;
|
||||
}
|
||||
@@ -17,8 +17,10 @@ pub(crate) trait QueryAppContext: gpui::AppContext {
|
||||
fn map_result<T, U>(result: Self::Result<T>, f: impl FnOnce(T) -> U) -> Self::Result<U>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub(crate) struct Key(Cow<'static, str>);
|
||||
|
||||
pub(crate) struct Query {
|
||||
key: &'static str,
|
||||
data: QueryData,
|
||||
}
|
||||
|
||||
@@ -193,7 +195,7 @@ pub struct Store<C>
|
||||
where
|
||||
C: Context,
|
||||
{
|
||||
query_data: HashMap<String, gpui::Entity<Query>>,
|
||||
query_data: HashMap<Key, gpui::Entity<Query>>,
|
||||
query_context: C,
|
||||
}
|
||||
|
||||
@@ -217,22 +219,21 @@ where
|
||||
Q: QueryFn<Context = C>,
|
||||
CX: QueryAppContext,
|
||||
{
|
||||
if let Some(raw) = self.query_data.get(query.key()) {
|
||||
let query_key = query.key();
|
||||
|
||||
if let Some(raw) = self.query_data.get(&query_key) {
|
||||
return CX::ready(Entity {
|
||||
raw: raw.clone(),
|
||||
_marker: PhantomData,
|
||||
});
|
||||
}
|
||||
|
||||
let key = query.key();
|
||||
|
||||
CX::map_result(
|
||||
cx.new(|_| Query {
|
||||
key: query.key(),
|
||||
data: QueryData::Pending,
|
||||
}),
|
||||
|raw| {
|
||||
self.query_data.insert(key.into(), raw.clone());
|
||||
self.query_data.insert(query_key, raw.clone());
|
||||
Entity {
|
||||
raw,
|
||||
_marker: PhantomData,
|
||||
@@ -332,15 +333,12 @@ where
|
||||
E: 'static,
|
||||
F: QueryFn<Context = C>,
|
||||
{
|
||||
let entity = entity.raw.read(cx);
|
||||
if let Some(entity) = self.query_data.get(entity.key) {
|
||||
entity.update(cx, |query, cx| {
|
||||
if !matches!(query.data, QueryData::Loading) {
|
||||
query.data = QueryData::Stale;
|
||||
cx.notify()
|
||||
}
|
||||
})
|
||||
}
|
||||
entity.update(cx, |query, cx| {
|
||||
if !matches!(query.data, QueryData::Loading) {
|
||||
query.data = QueryData::Stale;
|
||||
cx.notify()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,4 +372,28 @@ impl<'a, E> QueryAppContext for gpui::Context<'a, E> {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Key {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Key {
|
||||
fn from(key: &'static str) -> Self {
|
||||
Key(Cow::Borrowed(key))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Key {
|
||||
fn from(key: String) -> Self {
|
||||
Key(Cow::Owned(key))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Key {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> gpui::Global for Store<C> where C: Context + 'static {}
|
||||
|
||||
165
src/screen/dashboard/issue_list.rs
Normal file
165
src/screen/dashboard/issue_list.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use gpui::{IntoElement, ParentElement, Styled, div, list, prelude::FluentBuilder, px};
|
||||
|
||||
use crate::{
|
||||
api::{self},
|
||||
app,
|
||||
component::{
|
||||
font_icon::{FontIcon, font_icon},
|
||||
text::text,
|
||||
},
|
||||
query::{self, QueryStatus, read_query, use_query},
|
||||
};
|
||||
|
||||
pub(crate) struct IssueList {
|
||||
pr_query: query::Entity<api::issues::ListPullRequests>,
|
||||
|
||||
list_state: gpui::ListState,
|
||||
list_items: Vec<IssueListItem>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum IssueStatus {
|
||||
Draft,
|
||||
Open,
|
||||
Closed,
|
||||
}
|
||||
|
||||
#[derive(gpui::IntoElement, Clone)]
|
||||
struct IssueListItem {
|
||||
repo_name: Option<gpui::SharedString>,
|
||||
title: gpui::SharedString,
|
||||
description: Option<gpui::SharedString>,
|
||||
status: IssueStatus,
|
||||
is_last: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn new(cx: &mut gpui::Context<IssueList>) -> IssueList {
|
||||
let mut list = IssueList {
|
||||
pr_query: use_query(
|
||||
api::issues::ListPullRequests {
|
||||
filter: Some(api::issues::Issue::FILTER_ALL),
|
||||
page: 1,
|
||||
},
|
||||
cx,
|
||||
),
|
||||
|
||||
list_state: gpui::ListState::new(0, gpui::ListAlignment::Top, px(100.)),
|
||||
list_items: Vec::new(),
|
||||
};
|
||||
list.on_create(cx);
|
||||
list
|
||||
}
|
||||
|
||||
impl IssueList {
|
||||
fn on_create(&mut self, cx: &mut gpui::Context<Self>) {
|
||||
cx.observe(&self.pr_query, |this, _, cx| {
|
||||
let data = read_query(&this.pr_query, cx);
|
||||
if let QueryStatus::Loaded(issues) = data {
|
||||
let old_len = this.list_state.item_count();
|
||||
let new_len = issues.len();
|
||||
|
||||
this.list_items = issues
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, it)| IssueListItem {
|
||||
repo_name: it.repository.as_ref().map(|it| {
|
||||
gpui::SharedString::new(format!("{}/{}", it.owner.login, it.name))
|
||||
}),
|
||||
title: gpui::SharedString::new(it.title.as_str()),
|
||||
description: it
|
||||
.body_text
|
||||
.as_ref()
|
||||
.map(|it| gpui::SharedString::new(it.as_str())),
|
||||
status: if it.state == "open" {
|
||||
IssueStatus::Open
|
||||
} else if it.state == "closed" {
|
||||
IssueStatus::Closed
|
||||
} else {
|
||||
IssueStatus::Draft
|
||||
},
|
||||
is_last: i == new_len - 1,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
this.list_state.splice(old_len..old_len, new_len);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::Render for IssueList {
|
||||
fn render(
|
||||
&mut self,
|
||||
_window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl gpui::IntoElement {
|
||||
let this = cx.entity();
|
||||
list(self.list_state.clone(), move |i, _, cx| {
|
||||
let this = this.read(cx);
|
||||
this.list_items[i].clone().into_any_element()
|
||||
})
|
||||
.size_full()
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::RenderOnce for IssueListItem {
|
||||
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement {
|
||||
let theme = app::current_theme(cx);
|
||||
|
||||
let repo_name_text = match self.repo_name {
|
||||
Some(name) => text(name),
|
||||
None => text("Unknown repo"),
|
||||
}
|
||||
.text_xs()
|
||||
.opacity(0.5);
|
||||
|
||||
let icon = match self.status {
|
||||
IssueStatus::Draft => font_icon(FontIcon::PullRequestDraft)
|
||||
.text_color(theme.colors.text)
|
||||
.opacity(0.5),
|
||||
IssueStatus::Open => {
|
||||
font_icon(FontIcon::PullRequestArrow).text_color(theme.colors.success)
|
||||
}
|
||||
IssueStatus::Closed => {
|
||||
font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.danger)
|
||||
}
|
||||
}
|
||||
.flex_shrink_0()
|
||||
.size_4();
|
||||
|
||||
let description_text = match self.description {
|
||||
Some(description) => text(description).text_xs(),
|
||||
None => text("No description provided").opacity(0.5).text_xs(),
|
||||
};
|
||||
|
||||
div()
|
||||
.w_full()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.child(icon)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.w_full()
|
||||
.pr_2()
|
||||
.child(repo_name_text)
|
||||
.child(
|
||||
text(self.title)
|
||||
.text_sm()
|
||||
.leading_tight()
|
||||
.medium()
|
||||
.styled(|it| it.w_full().min_w_0().line_clamp(2)),
|
||||
)
|
||||
.child(description_text),
|
||||
)
|
||||
.when(!self.is_last, |it| {
|
||||
it.border_b_1().border_color(theme.colors.border)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
mod issue_list;
|
||||
mod screen;
|
||||
mod sidebar;
|
||||
mod titlebar;
|
||||
|
||||
use gpui::{AppContext, BorrowAppContext, point, px, size};
|
||||
pub(crate) use screen::new;
|
||||
|
||||
use crate::{app, screen::dashboard::screen::Screen};
|
||||
use crate::app;
|
||||
|
||||
pub fn open_window(screen: Screen, cx: &mut gpui::App) -> anyhow::Result<()> {
|
||||
pub fn open_window(cx: &mut gpui::App) -> anyhow::Result<()> {
|
||||
let (top_left, window_bounds) = cx.read_global::<app::Global, _>(|global, cx| {
|
||||
(
|
||||
global.safe_area.origin,
|
||||
@@ -14,7 +16,8 @@ pub fn open_window(screen: Screen, cx: &mut gpui::App) -> anyhow::Result<()> {
|
||||
)
|
||||
});
|
||||
|
||||
cx.open_window(
|
||||
app::open_window(
|
||||
cx,
|
||||
gpui::WindowOptions {
|
||||
window_bounds: Some(gpui::WindowBounds::Windowed(window_bounds)),
|
||||
titlebar: Some(gpui::TitlebarOptions {
|
||||
@@ -24,20 +27,7 @@ pub fn open_window(screen: Screen, cx: &mut gpui::App) -> anyhow::Result<()> {
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| {
|
||||
cx.new(|cx| {
|
||||
cx.observe_window_appearance(window, |_, window, cx| {
|
||||
cx.update_global::<app::Global, ()>(|global, cx| {
|
||||
global.current_theme = global
|
||||
.theme_family
|
||||
.theme_for_appearance(window.appearance());
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
screen
|
||||
})
|
||||
},
|
||||
|_window, cx| new(cx),
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
@@ -1,14 +1,54 @@
|
||||
use gpui::{AppContext, ParentElement, Styled, div};
|
||||
use gpui::{AppContext, BorrowAppContext, ParentElement, Styled, div};
|
||||
|
||||
use crate::{app, screen::dashboard::titlebar};
|
||||
use crate::{
|
||||
app,
|
||||
screen::dashboard::{
|
||||
issue_list::{self, IssueList},
|
||||
sidebar::{self, Sidebar, SidebarItemValue},
|
||||
titlebar::{self, TitleBar},
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) struct Screen {
|
||||
titlebar: gpui::Entity<titlebar::TitleBar>,
|
||||
titlebar: gpui::Entity<TitleBar>,
|
||||
issue_list: gpui::Entity<IssueList>,
|
||||
sidebar: gpui::Entity<Sidebar>,
|
||||
|
||||
issue_filter: Option<&'static str>,
|
||||
}
|
||||
|
||||
pub(crate) fn new(cx: &mut gpui::App) -> Screen {
|
||||
Screen {
|
||||
titlebar: cx.new(|cx| titlebar::new(cx)),
|
||||
pub(crate) fn new(cx: &mut gpui::Context<Screen>) -> Screen {
|
||||
let mut screen = Screen {
|
||||
titlebar: cx.new(titlebar::new),
|
||||
issue_list: cx.new(issue_list::new),
|
||||
sidebar: cx.new(|_| sidebar::new()),
|
||||
issue_filter: None,
|
||||
};
|
||||
screen.on_create(cx);
|
||||
screen
|
||||
}
|
||||
|
||||
impl Screen {
|
||||
fn on_create(&mut self, cx: &mut gpui::Context<Self>) {
|
||||
let on_item_change = cx.listener(|this, value, _, cx| {
|
||||
this.handle_sidebar_item_change(value, cx);
|
||||
});
|
||||
self.sidebar.update(cx, |sidebar, _| {
|
||||
sidebar.on_item_change(on_item_change);
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_sidebar_item_change(
|
||||
&mut self,
|
||||
value: &SidebarItemValue,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
match value {
|
||||
SidebarItemValue::PullRequest { filter } => {
|
||||
self.issue_filter = Some(*filter);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +71,26 @@ impl gpui::Render for Screen {
|
||||
.flex_row()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.px_3()
|
||||
.pb_3()
|
||||
.child(div().w_1_4().h_full().rounded_lg().bg(theme.colors.surface))
|
||||
.child(div().w_3_4().h_full().rounded_lg().bg(theme.colors.surface)),
|
||||
.child(div().w_40().h_full().child(self.sidebar.clone()))
|
||||
.child(
|
||||
div()
|
||||
.w_80()
|
||||
.h_full()
|
||||
.bg(theme.colors.surface)
|
||||
.border_x_1()
|
||||
.border_color(theme.colors.border)
|
||||
.mr_2()
|
||||
.overflow_hidden()
|
||||
.child(self.issue_list.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.bg(theme.colors.surface)
|
||||
.border_l_1()
|
||||
.border_color(theme.colors.border),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
195
src/screen/dashboard/sidebar.rs
Normal file
195
src/screen/dashboard/sidebar.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
use gpui::{
|
||||
InteractiveElement, ParentElement, StatefulInteractiveElement, Styled, div,
|
||||
prelude::FluentBuilder,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
api, app,
|
||||
component::{
|
||||
font_icon::{FontIcon, font_icon},
|
||||
text::text,
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) struct Sidebar {
|
||||
selected_item_id: Option<&'static str>,
|
||||
|
||||
on_item_change: Option<Box<dyn Fn(&SidebarItemValue, &mut gpui::Window, &mut gpui::App)>>,
|
||||
}
|
||||
|
||||
#[derive(gpui::IntoElement)]
|
||||
struct SidebarItem {
|
||||
id: &'static str,
|
||||
title: &'static str,
|
||||
icon: FontIcon,
|
||||
value: SidebarItemValue,
|
||||
is_selected: bool,
|
||||
|
||||
on_click: Option<Box<dyn Fn(&gpui::ClickEvent, &mut gpui::Window, &mut gpui::App)>>,
|
||||
}
|
||||
|
||||
pub enum SidebarItemValue {
|
||||
PullRequest { filter: &'static str },
|
||||
}
|
||||
|
||||
pub fn new() -> Sidebar {
|
||||
Sidebar {
|
||||
selected_item_id: Some("all"),
|
||||
on_item_change: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl Sidebar {
|
||||
const ALL_ITEMS: [SidebarItem; 3] = [
|
||||
SidebarItem {
|
||||
id: "all",
|
||||
title: "All",
|
||||
icon: FontIcon::List,
|
||||
value: SidebarItemValue::PullRequest {
|
||||
filter: api::issues::Issue::FILTER_ALL,
|
||||
},
|
||||
is_selected: false,
|
||||
on_click: None,
|
||||
},
|
||||
SidebarItem {
|
||||
id: "authored",
|
||||
title: "Authored",
|
||||
icon: FontIcon::PencilLine,
|
||||
value: SidebarItemValue::PullRequest {
|
||||
filter: api::issues::Issue::FILTER_CREATED,
|
||||
},
|
||||
is_selected: false,
|
||||
on_click: None,
|
||||
},
|
||||
SidebarItem {
|
||||
id: "assigned",
|
||||
title: "Assigned",
|
||||
icon: FontIcon::UserPlus,
|
||||
value: SidebarItemValue::PullRequest {
|
||||
filter: api::issues::Issue::FILTER_ASSIGNED,
|
||||
},
|
||||
is_selected: false,
|
||||
on_click: None,
|
||||
},
|
||||
];
|
||||
|
||||
fn select_sidebar_item(&mut self, id: &str, cx: &mut gpui::Context<Self>) {
|
||||
let Some(item) = Sidebar::ALL_ITEMS.iter().find(|item| item.id == id) else {
|
||||
return;
|
||||
};
|
||||
self.selected_item_id = Some(item.id);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Sidebar {
|
||||
pub fn on_item_change(
|
||||
&mut self,
|
||||
f: impl Fn(&SidebarItemValue, &mut gpui::Window, &mut gpui::App) + 'static,
|
||||
) {
|
||||
self.on_item_change = Some(Box::new(f));
|
||||
}
|
||||
|
||||
pub fn handle_item_change(
|
||||
&mut self,
|
||||
id: &'static str,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
let Some(item) = Sidebar::ALL_ITEMS.iter().find(|item| item.id == id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.selected_item_id = Some(id);
|
||||
if let Some(ref f) = self.on_item_change {
|
||||
f(&item.value, window, cx)
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::Render for Sidebar {
|
||||
fn render(
|
||||
&mut self,
|
||||
_window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl gpui::IntoElement {
|
||||
let pull_request_sidebar_items = Sidebar::ALL_ITEMS
|
||||
.into_iter()
|
||||
.map(|it| {
|
||||
let id = it.id;
|
||||
let selected = id == self.selected_item_id.unwrap_or_default();
|
||||
it.selected(selected)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.handle_item_change(id, window, cx);
|
||||
}))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
div().flex().flex_col().size_full().pt_1().child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.w_full()
|
||||
.child(
|
||||
text("PULL REQUESTS")
|
||||
.text_xs()
|
||||
.medium()
|
||||
.opacity(0.5)
|
||||
.styled(|it| it.pl_3().py_1()),
|
||||
)
|
||||
.children(pull_request_sidebar_items),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl SidebarItem {
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
f: impl Fn(&gpui::ClickEvent, &mut gpui::Window, &mut gpui::App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Box::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(mut self, selected: bool) -> Self {
|
||||
self.is_selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::RenderOnce for SidebarItem {
|
||||
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl gpui::IntoElement {
|
||||
let theme = app::current_theme(cx);
|
||||
div()
|
||||
.id(self.id)
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.child(div().h_full().w_1())
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.gap_2()
|
||||
.child(font_icon(self.icon).size_3().when(self.is_selected, |it| {
|
||||
it.text_color(theme.colors.accent_text)
|
||||
}))
|
||||
.child(
|
||||
text(self.title)
|
||||
.text_sm()
|
||||
.leading_tight()
|
||||
.when(self.is_selected, |it| {
|
||||
it.text_color(theme.colors.accent_text)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.when_some(self.on_click, |it, f| it.on_click(f))
|
||||
.when(self.is_selected, |it| it.bg(theme.colors.accent))
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::{ParentElement, Styled, TitlebarOptions, div};
|
||||
|
||||
use crate::component::button::button;
|
||||
use crate::query::{self, QueryStatus, read_query, use_query};
|
||||
use crate::query::{self, QueryStatus, read_query, use_lazy_query, use_query};
|
||||
use crate::{
|
||||
api, app,
|
||||
component::{
|
||||
@@ -18,7 +18,7 @@ pub struct RepoSelector {}
|
||||
|
||||
pub fn new(cx: &mut gpui::Context<TitleBar>) -> TitleBar {
|
||||
TitleBar {
|
||||
fetch_user_query: use_query(api::user::Fetch, cx),
|
||||
fetch_user_query: use_lazy_query(api::user::Fetch, cx),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ impl gpui::Render for TitleBar {
|
||||
.bg(g.current_theme.colors.background)
|
||||
.text_color(g.current_theme.colors.text)
|
||||
.relative()
|
||||
.border_b_1()
|
||||
.border_color(g.current_theme.colors.border)
|
||||
.child(repo_selector(cx))
|
||||
.child(user_avatar)
|
||||
}
|
||||
@@ -60,9 +62,6 @@ impl gpui::Render for TitleBar {
|
||||
|
||||
impl RepoSelector {
|
||||
pub fn new(cx: &mut gpui::Context<Self>) -> Self {
|
||||
use_query(api::repo::List, cx);
|
||||
use_query(api::user::Fetch, cx);
|
||||
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ pub fn open_window(screen: Screen, cx: &mut gpui::App) -> anyhow::Result<()> {
|
||||
)
|
||||
});
|
||||
|
||||
cx.open_window(
|
||||
app::open_window(
|
||||
cx,
|
||||
gpui::WindowOptions {
|
||||
window_bounds: Some(gpui::WindowBounds::Windowed(window_bounds)),
|
||||
titlebar: Some(gpui::TitlebarOptions {
|
||||
@@ -43,22 +44,8 @@ pub fn open_window(screen: Screen, cx: &mut gpui::App) -> anyhow::Result<()> {
|
||||
is_resizable: false,
|
||||
..Default::default()
|
||||
},
|
||||
|window, cx| {
|
||||
cx.new(|cx| {
|
||||
cx.observe_window_appearance(window, |_, window, cx| {
|
||||
cx.update_global::<app::Global, ()>(|global, cx| {
|
||||
global.current_theme = global
|
||||
.theme_family
|
||||
.theme_for_appearance(window.appearance());
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
screen
|
||||
})
|
||||
},
|
||||
|_window, _cx| screen,
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
impl Step {
|
||||
|
||||
@@ -9,6 +9,9 @@ pub(crate) struct PersistedState {
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub debug_auth_tokens: Option<api::AuthTokens>,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub debug_should_use_fixtures: Option<bool>,
|
||||
}
|
||||
|
||||
pub(crate) fn data_dir_path() -> std::path::PathBuf {
|
||||
@@ -50,11 +53,16 @@ pub(crate) async fn load_auth_tokens(
|
||||
cx: &gpui::App,
|
||||
user_id: api::user::Id,
|
||||
) -> Option<api::AuthTokens> {
|
||||
if cfg!(debug_assertions) {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let _ = (cx, user_id);
|
||||
// in debug mode, credentials are loaded from persisted state
|
||||
// to avoid being prompted for permission to access keychain on macos
|
||||
None
|
||||
} else {
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
cx.read_credentials(&format!("https://github.com/user/{}", user_id))
|
||||
.await
|
||||
.ok()?
|
||||
@@ -68,12 +76,18 @@ pub(crate) fn store_auth_tokens(
|
||||
user: &api::user::User,
|
||||
cx: &gpui::App,
|
||||
) -> gpui::Task<anyhow::Result<()>> {
|
||||
if cfg!(debug_assertions) {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let _ = (user, cx);
|
||||
let r = update_persisted_state(|state| {
|
||||
state.debug_auth_tokens = Some(tokens.clone());
|
||||
});
|
||||
gpui::Task::ready(r)
|
||||
} else {
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
let _ = tokens;
|
||||
cx.write_credentials(
|
||||
&format!("https://github.com/user/{}", user.id),
|
||||
&format!("{}", user.id),
|
||||
|
||||
Reference in New Issue
Block a user