diff --git a/Cargo.lock b/Cargo.lock index 371ab812a66652f6e6915a0b4198dfcba9b0bd78..e91833f9965ea23f0178aed1d9d166457407e4d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10826,6 +10826,7 @@ dependencies = [ "editor", "feature_flags", "gpui", + "project", "settings", "settings_ui", "theme", diff --git a/crates/onboarding_ui/Cargo.toml b/crates/onboarding_ui/Cargo.toml index cdfaad21f6745c2864448086b95670a579d3952b..b1591e916708804df3863ba1bb77f824d48db2e6 100644 --- a/crates/onboarding_ui/Cargo.toml +++ b/crates/onboarding_ui/Cargo.toml @@ -22,6 +22,7 @@ component.workspace = true db.workspace = true feature_flags.workspace = true gpui.workspace = true +project.workspace = true settings.workspace = true settings_ui.workspace = true theme.workspace = true diff --git a/crates/onboarding_ui/src/onboarding_ui.rs b/crates/onboarding_ui/src/onboarding_ui.rs index 17c6851b157b75675c5510d44ee20903bd356eec..3a5853edfb7f8aeee55e27f883b69179cd0260ec 100644 --- a/crates/onboarding_ui/src/onboarding_ui.rs +++ b/crates/onboarding_ui/src/onboarding_ui.rs @@ -1,15 +1,22 @@ #![allow(unused, dead_code)] +mod persistence; + use client::Client; use command_palette_hooks::CommandPaletteFilter; use feature_flags::FeatureFlagAppExt as _; -use gpui::{Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, actions, prelude::*}; +use gpui::{ + Entity, EventEmitter, FocusHandle, Focusable, KeyBinding, Task, WeakEntity, actions, prelude::*, +}; +use persistence::ONBOARDING_DB; + +use project::Project; use settings_ui::SettingsUiFeatureFlag; use std::sync::Arc; -use ui::{KeyBinding, ListItem, Vector, VectorName, prelude::*}; +use ui::{ListItem, Vector, VectorName, prelude::*}; use util::ResultExt; use workspace::{ Workspace, WorkspaceId, - item::{Item, ItemEvent}, + item::{Item, ItemEvent, SerializableItem}, notifications::NotifyResultExt, }; @@ -38,6 +45,8 @@ pub fn init(cx: &mut App) { }) .detach(); + workspace::register_serializable_item::(cx); + feature_gate_onboarding_ui_actions(cx); } @@ -104,6 +113,7 @@ pub struct OnboardingUI { // Workspace reference for Item trait workspace: WeakEntity, + workspace_id: Option, client: Arc, } @@ -166,10 +176,26 @@ impl OnboardingUI { current_focus: OnboardingFocus::Page, completed_pages: [false; 4], workspace: workspace.weak_handle(), + workspace_id: workspace.database_id(), client, } } + fn completed_pages_to_string(&self) -> String { + self.completed_pages + .iter() + .map(|&completed| if completed { '1' } else { '0' }) + .collect() + } + + fn completed_pages_from_string(s: &str) -> [bool; 4] { + let mut result = [false; 4]; + for (i, ch) in s.chars().take(4).enumerate() { + result[i] = ch == '1'; + } + result + } + fn jump_to_page( &mut self, page: OnboardingPage, @@ -350,7 +376,12 @@ impl OnboardingUI { }, ) .style(ButtonStyle::Filled) - .key_binding(KeyBinding::for_action(&NextPage, window, cx)) + .key_binding(ui::KeyBinding::for_action_in( + &NextPage, + &self.focus_handle, + window, + cx, + )) .on_click(cx.listener(|this, _, window, cx| { this.next_page(window, cx); })), @@ -432,6 +463,15 @@ impl Item for OnboardingUI { f(event.clone()) } + fn added_to_workspace( + &mut self, + workspace: &mut Workspace, + _window: &mut Window, + _cx: &mut Context, + ) { + self.workspace_id = workspace.database_id(); + } + fn show_toolbar(&self) -> bool { false } @@ -453,3 +493,90 @@ impl Item for OnboardingUI { } } } + +impl SerializableItem for OnboardingUI { + fn serialized_item_kind() -> &'static str { + "OnboardingUI" + } + + fn deserialize( + _project: Entity, + workspace: WeakEntity, + workspace_id: WorkspaceId, + item_id: u64, + window: &mut Window, + cx: &mut App, + ) -> Task>> { + window.spawn(cx, async move |cx| { + let (current_page, completed_pages) = if let Some((page_str, completed_str)) = + ONBOARDING_DB.get_state(item_id, workspace_id)? + { + let page = match page_str.as_str() { + "basics" => OnboardingPage::Basics, + "editing" => OnboardingPage::Editing, + "ai_setup" => OnboardingPage::AiSetup, + "welcome" => OnboardingPage::Welcome, + _ => OnboardingPage::Basics, + }; + let completed = OnboardingUI::completed_pages_from_string(&completed_str); + (page, completed) + } else { + (OnboardingPage::Basics, [false; 4]) + }; + + cx.update(|window, cx| { + let workspace = workspace + .upgrade() + .ok_or_else(|| anyhow::anyhow!("workspace dropped"))?; + + workspace.update(cx, |workspace, cx| { + let client = workspace.client().clone(); + Ok(cx.new(|cx| { + let mut onboarding = OnboardingUI::new(workspace, client, cx); + onboarding.current_page = current_page; + onboarding.completed_pages = completed_pages; + onboarding + })) + }) + })? + }) + } + + fn serialize( + &mut self, + _workspace: &mut Workspace, + item_id: u64, + _closing: bool, + _window: &mut Window, + cx: &mut Context, + ) -> Option>> { + let workspace_id = self.workspace_id?; + let current_page = match self.current_page { + OnboardingPage::Basics => "basics", + OnboardingPage::Editing => "editing", + OnboardingPage::AiSetup => "ai_setup", + OnboardingPage::Welcome => "welcome", + } + .to_string(); + let completed_pages = self.completed_pages_to_string(); + + Some(cx.background_spawn(async move { + ONBOARDING_DB + .save_state(item_id, workspace_id, current_page, completed_pages) + .await + })) + } + + fn cleanup( + _workspace_id: WorkspaceId, + _item_ids: Vec, + _window: &mut Window, + _cx: &mut App, + ) -> Task> { + Task::ready(Ok(())) + } + + fn should_serialize(&self, _event: &ItemEvent) -> bool { + true + } +} diff --git a/crates/onboarding_ui/src/persistence.rs b/crates/onboarding_ui/src/persistence.rs new file mode 100644 index 0000000000000000000000000000000000000000..944d5c58aefd81dd19239d913a6545b17d1521ab --- /dev/null +++ b/crates/onboarding_ui/src/persistence.rs @@ -0,0 +1,53 @@ +use anyhow::Result; +use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; + +use workspace::{WorkspaceDb, WorkspaceId}; + +define_connection! { + pub static ref ONBOARDING_DB: OnboardingDb = + &[sql!( + CREATE TABLE onboarding_state ( + workspace_id INTEGER, + item_id INTEGER UNIQUE, + current_page TEXT, + completed_pages TEXT, + PRIMARY KEY(workspace_id, item_id), + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ) STRICT; + )]; +} + +impl OnboardingDb { + pub async fn save_state( + &self, + item_id: u64, + workspace_id: WorkspaceId, + current_page: String, + completed_pages: String, + ) -> Result<()> { + let query = + "INSERT INTO onboarding_state(item_id, workspace_id, current_page, completed_pages) + VALUES (?1, ?2, ?3, ?4) + ON CONFLICT DO UPDATE SET + current_page = ?3, + completed_pages = ?4"; + self.write(move |conn| { + let mut statement = Statement::prepare(conn, query)?; + let mut next_index = statement.bind(&item_id, 1)?; + next_index = statement.bind(&workspace_id, next_index)?; + next_index = statement.bind(¤t_page, next_index)?; + statement.bind(&completed_pages, next_index)?; + statement.exec() + }) + .await + } + + query! { + pub fn get_state(item_id: u64, workspace_id: WorkspaceId) -> Result> { + SELECT current_page, completed_pages + FROM onboarding_state + WHERE item_id = ? AND workspace_id = ? + } + } +}