Repository navigation
[ASNT-15] Project-creation wizard fixes + Google Sheet import (cloud-sync) - #20
Conversation
…ect flow Implements the Claude Design handoff "Qrate Wizard Flow": a Photoshop-style startup launcher (Recent Projects + Create New cards for Blank / CSV+folder / Google Sheet), and a New Project wizard window with Name, Files (CSV or Sheet fork), Link-files, Columns (with recently-used and load-from-file/Sheet sub-dialogs), Review, and Success steps. - New crates/project-wizard crate: wizard shell + per-step render impls, real CSV parsing (csv crate) and folder matching validated against sample/, column-config CSV loading with the design's error variants, recent-projects persistence through AppSettings, and project.json manifest written on create. Google Sheets validation is mocked pending a real Sheets API integration (documented in data.rs). - Launcher replaces the ASNT-13 debug startup window; crates/app/src/debug.rs and its call sites are removed as its comments instructed. - NewProject action wired to the wizard (menu item + existing ctrl-n binding). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LNjLXbAcnnHeTZvk3gbvhh
New minimal crate for importing a public ("anyone with the link") Google
Sheet, no OAuth or Sheets API v4. `fetch_sheet` pulls the built-in
`?format=xlsx` export once and derives both the first tab's rows (via
calamine) and its cell notes (zip + quick-xml over `xl/comments*.xml`).
A private sheet serves an HTML sign-in page and yields a clear error.
Threaded comments aren't reachable this way — they survive no export and
would need the Drive API + OAuth; deferred. The older CSV-export path is
kept as marked-legacy dead code until the wizard is rewired onto
`fetch_sheet`.
- Blank projects now flow through the Files and Columns steps (folder
picker + a "skip columns for now" card) instead of jumping past them.
- Add an "I'll add a files folder later" toggle that also skips the Link
step (skip_files / skips_link).
- Move the custom-pattern input inside its Link option card, revealed on
select, rather than floating outside it.
- Fix a gpui panic ("cannot read ProjectWizard while it is already being
updated") — the recent/load-config dialogs now open via window.defer,
outside the click listener's own entity update.
- Wire the Google Sheet entry path to cloud-sync for real fetching, and
drop the now-unused success step.
There was a problem hiding this comment.
Pull request overview
Adds a full “New Project” launcher + multi-step project-creation wizard, and introduces a new cloud-sync crate for importing public Google Sheets (CSV today, XLSX+notes groundwork), wiring the app to start on the launcher and adding a “New Project…” menu action.
Changes:
- Introduces
project-wizardcrate (launcher, wizard steps, CSV parsing, folder matching, recent projects, andproject.jsonmanifest creation). - Introduces
cloud-synccrate for fetching public Google Sheets (CSV export + XLSX parsing for rows/notes). - Updates
appstartup/menu/actions and workspace members (removes debug scaffolding; launcher becomes the real startup window).
Reviewed changes
Copilot reviewed 21 out of 22 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/project-wizard/src/wizard.rs | Core wizard state machine, navigation, and shared UI scaffolding. |
| crates/project-wizard/src/steps/mod.rs | Registers wizard step modules. |
| crates/project-wizard/src/steps/name.rs | Name step UI + live validation. |
| crates/project-wizard/src/steps/files.rs | Files step variants (CSV/Sheet/Blank) + folder matching + sheet check. |
| crates/project-wizard/src/steps/link.rs | Link step UI including advanced pattern input within option card. |
| crates/project-wizard/src/steps/columns.rs | Columns step UI + dialogs for recent/load config; sheet-config fetch path. |
| crates/project-wizard/src/steps/review.rs | Review + “Create Project” manifest write and handoff to main window. |
| crates/project-wizard/src/data.rs | CSV preview parsing, folder matching, and column-config loading (+ tests). |
| crates/project-wizard/src/recent.rs | Persisted recent-project list and relative-time formatting. |
| crates/project-wizard/src/project.rs | Writes on-disk project.json manifest and project directory creation. |
| crates/project-wizard/src/launcher.rs | Startup launcher window (recents + create-new cards). |
| crates/project-wizard/src/lib.rs | Crate entry points and exports. |
| crates/project-wizard/Cargo.toml | New crate manifest + dependencies (incl. cloud-sync). |
| crates/cloud-sync/src/lib.rs | Google Sheet fetch (CSV/XLSX) + XLSX notes parsing (+ tests). |
| crates/cloud-sync/Cargo.toml | New crate manifest + deps (reqwest, calamine, zip, quick-xml). |
| crates/app/src/main.rs | Makes launcher the startup window; adds LauncherHooks; registers New Project action. |
| crates/app/src/debug.rs | Removes prior debug scaffolding window/actions. |
| crates/app/src/app_menus.rs | Replaces debug menu item with “New Project…”. |
| crates/app/src/actions.rs | Updates handler documentation (NewProject now registered in main.rs). |
| crates/app/Cargo.toml | Adds dependency on project-wizard. |
| Cargo.toml | Adds new workspace members and adds csv to workspace deps. |
| Cargo.lock | Updates lockfile for new crates/dependencies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let files_matched = self | ||
| .folder_match | ||
| .as_ref() | ||
| .map(|m| (m.matched_rows, m.total_rows)); | ||
| let link_method = (self.entry_kind != EntryKind::Blank).then(|| match self.link_method { | ||
| LinkMethod::ExactFilename => "exact filename".to_string(), | ||
| LinkMethod::CustomPattern => "custom pattern".to_string(), | ||
| }); |
| let files_line = self.folder_match.as_ref().map(|m| { | ||
| let method = match self.link_method { | ||
| LinkMethod::ExactFilename => "exact filename", | ||
| LinkMethod::CustomPattern => "custom pattern", | ||
| }; | ||
| let extra = if m.extra_files > 0 { | ||
| format!(" · {} not linked", m.extra_files) | ||
| } else { | ||
| String::new() | ||
| }; | ||
| format!( | ||
| "{} of {} matched ({method}){extra}", | ||
| m.matched_rows, m.total_rows | ||
| ) | ||
| }); |
| .when(self.entry_kind != EntryKind::Blank, |el| { | ||
| el.child(summary_row("Columns", &columns_line)) | ||
| }), |
| if delta < 60 { | ||
| "just now".into() | ||
| } else if delta < 3600 { | ||
| format!("{} minutes ago", delta / 60) | ||
| } else if delta < 86_400 { | ||
| format!("{} hours ago", delta / 3600) | ||
| } else if delta < 86_400 * 7 { | ||
| format!("{} days ago", delta / 86_400) | ||
| } else if delta < 86_400 * 30 { | ||
| format!("{} weeks ago", delta / (86_400 * 7)) | ||
| } else { | ||
| format!("{} months ago", delta / (86_400 * 30)) | ||
| } |
| let dir = std::env::temp_dir().join("qrate-sheets"); | ||
| fs::create_dir_all(&dir)?; | ||
| let path = dir.join(format!("{id}.csv")); | ||
| fs::write(&path, &body)?; | ||
| Ok(path) | ||
| } | ||
|
|
||
| /// Where a fetched sheet lands, without fetching — handy for callers/tests. | ||
| pub fn cache_path_for(link: &str) -> Option<PathBuf> { | ||
| let SheetRef { id, .. } = parse_sheet_ref(link).ok()?; | ||
| Some( | ||
| std::env::temp_dir() | ||
| .join("qrate-sheets") | ||
| .join(format!("{id}.csv")), | ||
| ) | ||
| } |
| url.push_str(&format!("&gid={gid}")); | ||
| } | ||
|
|
||
| let resp = reqwest::blocking::get(&url)?.error_for_status()?; |
| pub fn fetch_sheet(link: &str) -> Result<SheetData, SheetSyncError> { | ||
| let SheetRef { id, .. } = parse_sheet_ref(link)?; | ||
| let url = format!("https://docs.google.com/spreadsheets/d/{id}/export?format=xlsx"); | ||
| let resp = reqwest::blocking::get(&url)?.error_for_status()?; |
| LinkMethod::CustomPattern => "custom pattern".to_string(), | ||
| }); | ||
| let columns = match self.entry_kind { | ||
| EntryKind::Blank => Vec::new(), |
| // ponytail: blocking fetch on the UI thread — fine for a "Check" click; | ||
| // move to the background executor if it ever drags. | ||
| fn load_config_from_sheet(&mut self, cx: &mut Context<Self>) { | ||
| let link = self.sheet_link_input.read(cx).value().to_string(); | ||
| let fetched = cloud_sync::fetch_sheet_csv(&link) | ||
| .map_err(|e| e.message()) | ||
| .and_then(|path| { | ||
| data::load_csv_preview(&path.to_string_lossy()).map_err(|e| e.message()) | ||
| }); |
Unformatted since ec7d4ad; caught by `cargo fmt --all --check` (the CI gate).
Address Copilot review on PR #20: - When "add files later" is checked, the folder/link state is stale, so don't populate files_matched / link_method or show a Files line that claims a match happened. - Blank projects now flow through the Columns step, so show the Columns row in review and keep any config they loaded there instead of forcing an empty column list. - Pluralize relative_time ("1 minute ago", not "1 minutes ago").
Address Copilot review on PR #20: a bare blocking request has no timeout, so a stalled connection hangs the background worker forever. Use a client with a 30s timeout so it errors out instead.
|
Audited the Copilot review — pushed Applied (5):
Dismissed (4):
Also ran the full CI suite locally before pushing ( |
Runs `cargo fmt --all --check` (CI's fmt gate) on every commit so unformatted code can't land — the check that missed ec7d4ad. Shared via core.hooksPath; enable per clone with `git config core.hooksPath .githooks`.
`cargo clippy --all-targets` and `cargo test --workspace` already compile every target (incl. non-test bin code), so a separate `cargo build` adds nothing but CI time.
Address Copilot review on PR #20: - When "add files later" is checked, the folder/link state is stale, so don't populate files_matched / link_method or show a Files line that claims a match happened. - Blank projects now flow through the Columns step, so show the Columns row in review and keep any config they loaded there instead of forcing an empty column list. - Pluralize relative_time ("1 minute ago", not "1 minutes ago").
Address Copilot review on PR #20: a bare blocking request has no timeout, so a stalled connection hangs the background worker forever. Use a client with a 30s timeout so it errors out instead.
Builds on the project-creation wizard (ASNT-15) with a round of fixes and a new
cloud-synccrate for Google Sheet import (ASNT-17).Wizard fixes
skip_files/skips_link).cannot read ProjectWizard while it is already being updatedwhen clicking any Columns option other than Auto. The recent/load-config dialogs now open viawindow.defer, outside the click listener's own entity update.cloud-synccrate (Google Sheet import)fetch_sheet(link)imports a public ("anyone with the link") sheet via the built-in?format=xlsxexport — no OAuth, no Sheets API v4. One download yields both the first tab's rows (viacalamine) and its cell notes (zip + quick-xml overxl/comments*.xml). Private sheets get a clear error.fetch_sheet(tracked as a follow-up task).Verification
cargo fmt --check,cargo clippy -D warnings,cargo build, andcargo test -p cloud-syncall clean on the affected crates.🤖 Generated with Claude Code