commit d31db8592086531e772a7f63aaf8c1ce162283fc
parent 0fb40a89d7e11234083cd89d7955042e08f38871
Author: triesap <tyson@radroots.org>
Date: Sat, 18 Apr 2026 07:27:21 +0000
home: add farm setup form and local save
Diffstat:
9 files changed, 933 insertions(+), 42 deletions(-)
diff --git a/crates/launchers/desktop/src/app.rs b/crates/launchers/desktop/src/app.rs
@@ -264,6 +264,7 @@ mod tests {
settings_account_projection: SettingsAccountProjection::default(),
startup_gate: AppStartupGate::SetupRequired,
home_route: HomeRoute::SetupRequired,
+ farm_setup_projection: Default::default(),
today_projection: TodayAgendaProjection::default(),
startup_issue: Some("desktop runtime roots require HOME for macos".to_owned()),
};
@@ -296,6 +297,7 @@ mod tests {
settings_account_projection: SettingsAccountProjection::default(),
startup_gate: AppStartupGate::Blocked,
home_route: HomeRoute::Blocked,
+ farm_setup_projection: Default::default(),
today_projection: TodayAgendaProjection::default(),
startup_issue: None,
};
@@ -304,6 +306,7 @@ mod tests {
settings_account_projection: SettingsAccountProjection::default(),
startup_gate: AppStartupGate::SetupRequired,
home_route: HomeRoute::SetupRequired,
+ farm_setup_projection: Default::default(),
today_projection: TodayAgendaProjection::default(),
startup_issue: None,
};
@@ -319,6 +322,7 @@ mod tests {
settings_account_projection: SettingsAccountProjection::default(),
startup_gate: AppStartupGate::Personal,
home_route: HomeRoute::Personal,
+ farm_setup_projection: Default::default(),
today_projection: TodayAgendaProjection::default(),
startup_issue: None,
};
@@ -327,6 +331,7 @@ mod tests {
settings_account_projection: SettingsAccountProjection::default(),
startup_gate: AppStartupGate::Farmer,
home_route: HomeRoute::FarmSetupOnboarding,
+ farm_setup_projection: Default::default(),
today_projection: TodayAgendaProjection::default(),
startup_issue: None,
};
@@ -342,6 +347,7 @@ mod tests {
settings_account_projection: SettingsAccountProjection::default(),
startup_gate: AppStartupGate::Personal,
home_route: HomeRoute::Personal,
+ farm_setup_projection: Default::default(),
today_projection: TodayAgendaProjection::default(),
startup_issue: Some("runtime unavailable".to_owned()),
};
@@ -356,6 +362,7 @@ mod tests {
settings_account_projection: SettingsAccountProjection::default(),
startup_gate: AppStartupGate::SetupRequired,
home_route: HomeRoute::SetupRequired,
+ farm_setup_projection: Default::default(),
today_projection: TodayAgendaProjection::default(),
startup_issue: None,
};
@@ -364,6 +371,7 @@ mod tests {
settings_account_projection: SettingsAccountProjection::default(),
startup_gate: AppStartupGate::Personal,
home_route: HomeRoute::Personal,
+ farm_setup_projection: Default::default(),
today_projection: TodayAgendaProjection::default(),
startup_issue: None,
};
@@ -372,6 +380,7 @@ mod tests {
settings_account_projection: SettingsAccountProjection::default(),
startup_gate: AppStartupGate::Farmer,
home_route: HomeRoute::FarmSetupOnboarding,
+ farm_setup_projection: Default::default(),
today_projection: TodayAgendaProjection::default(),
startup_issue: None,
};
@@ -380,6 +389,7 @@ mod tests {
settings_account_projection: SettingsAccountProjection::default(),
startup_gate: AppStartupGate::Farmer,
home_route: HomeRoute::FarmSetupOnboarding,
+ farm_setup_projection: Default::default(),
today_projection: TodayAgendaProjection::default(),
startup_issue: Some("runtime unavailable".to_owned()),
};
diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs
@@ -3,8 +3,9 @@ use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use radroots_app_core::{AppDesktopRuntimePaths, AppRuntimePathsError, AppSharedAccountsPaths};
use radroots_app_models::{
- ActiveSurface, AppActivityContext, AppActivityKind, AppStartupGate, SettingsAccountProjection,
- SettingsPreference, SettingsSection, TodayAgendaProjection,
+ ActiveSurface, AppActivityContext, AppActivityKind, AppIdentityProjection, AppStartupGate,
+ FarmId, FarmReadiness, FarmSetupDraft, FarmSetupProjection, FarmSummary,
+ SettingsAccountProjection, SettingsPreference, SettingsSection, TodayAgendaProjection,
};
use radroots_app_sqlite::{
APP_ACTIVITY_CONTEXT_LIMIT, AppSqliteError, AppSqliteStore, DatabaseTarget,
@@ -49,6 +50,7 @@ impl DesktopAppRuntime {
settings_account_projection: state.state_store.settings_account_projection(),
startup_gate: state.state_store.startup_gate(),
home_route: state.state_store.home_route(),
+ farm_setup_projection: state.state_store.farm_setup_projection().clone(),
today_projection: state.state_store.today_projection().clone(),
startup_issue: state.startup_issue.clone(),
}
@@ -146,6 +148,19 @@ impl DesktopAppRuntime {
.apply_in_memory(AppStateCommand::select_farm_setup_flow_stage(stage))
}
+ pub fn save_farm_setup_draft(
+ &self,
+ draft: FarmSetupDraft,
+ ) -> Result<FarmSetupProjection, DesktopAppRuntimeFarmSetupError> {
+ self.lock_state_mut().save_farm_setup_draft(draft)
+ }
+
+ pub fn finish_farm_setup(
+ &self,
+ ) -> Result<FarmSetupProjection, DesktopAppRuntimeFarmSetupError> {
+ self.lock_state_mut().finish_farm_setup()
+ }
+
pub fn record_home_opened(&self) -> bool {
self.record_activity(AppActivityKind::HomeOpened)
}
@@ -200,10 +215,17 @@ pub struct DesktopAppRuntimeSummary {
pub settings_account_projection: SettingsAccountProjection,
pub startup_gate: AppStartupGate,
pub home_route: HomeRoute,
+ pub farm_setup_projection: FarmSetupProjection,
pub today_projection: TodayAgendaProjection,
pub startup_issue: Option<String>,
}
+#[derive(Clone, Debug, Default)]
+struct DesktopSelectedAccountContext {
+ farm_setup_projection: FarmSetupProjection,
+ today_projection: TodayAgendaProjection,
+}
+
struct DesktopAppRuntimeState {
state_store: AppStateStore<InMemoryAppStateRepository>,
default_nostr_relay_url: String,
@@ -244,12 +266,17 @@ impl DesktopAppRuntimeState {
let sqlite_store = AppSqliteStore::open(DatabaseTarget::Path(database_path.clone()))?;
let mut state_store = AppStateStore::load(InMemoryAppStateRepository::default())?;
let accounts_bootstrap = bootstrap_desktop_accounts(&paths.shared_accounts, &sqlite_store)?;
- let today_projection = sqlite_store.load_today_agenda(None)?;
- let _ =
- state_store.apply_in_memory(AppStateCommand::replace_today_agenda(today_projection));
+ let selected_account_context =
+ load_selected_account_context(&sqlite_store, &accounts_bootstrap.identity_projection)?;
let _ = state_store.apply_in_memory(AppStateCommand::replace_identity_projection(
accounts_bootstrap.identity_projection,
));
+ let _ = state_store.apply_in_memory(AppStateCommand::replace_farm_setup_projection(
+ selected_account_context.farm_setup_projection,
+ ));
+ let _ = state_store.apply_in_memory(AppStateCommand::replace_today_agenda(
+ selected_account_context.today_projection,
+ ));
Ok(Self {
state_store,
@@ -282,7 +309,7 @@ impl DesktopAppRuntimeState {
generate_local_account(accounts_manager, sqlite_store, label)?
};
- Ok(self.replace_identity_projection(projection))
+ self.replace_identity_projection(projection)
}
fn import_local_account(
@@ -295,7 +322,7 @@ impl DesktopAppRuntimeState {
import_local_account(accounts_manager, sqlite_store, request)?
};
- Ok(self.replace_identity_projection(projection))
+ self.replace_identity_projection(projection)
}
fn select_local_account(
@@ -308,7 +335,7 @@ impl DesktopAppRuntimeState {
select_local_account(accounts_manager, sqlite_store, account_id)?
};
- Ok(self.replace_identity_projection(projection))
+ self.replace_identity_projection(projection)
}
fn select_active_surface(
@@ -321,7 +348,7 @@ impl DesktopAppRuntimeState {
select_active_surface(accounts_manager, sqlite_store, active_surface)?
};
- Ok(self.replace_identity_projection(projection))
+ self.replace_identity_projection(projection)
}
fn remove_selected_local_key(&mut self) -> Result<bool, DesktopAppRuntimeCommandError> {
@@ -331,7 +358,7 @@ impl DesktopAppRuntimeState {
remove_selected_local_key(accounts_manager, sqlite_store)?
};
- Ok(self.replace_identity_projection(projection))
+ self.replace_identity_projection(projection)
}
fn reset_local_device_state(&mut self) -> Result<bool, DesktopAppRuntimeCommandError> {
@@ -342,7 +369,7 @@ impl DesktopAppRuntimeState {
reset_local_device_state(accounts_manager, sqlite_store, shared_accounts_paths)?
};
- Ok(self.replace_identity_projection(projection))
+ self.replace_identity_projection(projection)
}
fn record_activity(&self, kind: AppActivityKind) -> Result<(), AppSqliteError> {
@@ -352,12 +379,103 @@ impl DesktopAppRuntimeState {
}
}
+ fn save_farm_setup_draft(
+ &mut self,
+ draft: FarmSetupDraft,
+ ) -> Result<FarmSetupProjection, DesktopAppRuntimeFarmSetupError> {
+ let account_id = self.selected_account_id()?;
+ let sqlite_store = self.sqlite_store_for_farm_setup()?;
+ let projection = FarmSetupProjection::from_draft(draft);
+ sqlite_store.save_farm_setup(account_id.as_str(), &projection)?;
+
+ let selected_account_context = self.refresh_selected_account_context()?;
+ self.apply_selected_account_context(&selected_account_context);
+
+ Ok(selected_account_context.farm_setup_projection)
+ }
+
+ fn finish_farm_setup(
+ &mut self,
+ ) -> Result<FarmSetupProjection, DesktopAppRuntimeFarmSetupError> {
+ let account = self.selected_account_for_farm_setup()?;
+ let sqlite_store = self.sqlite_store_for_farm_setup()?;
+ let draft = self.state_store.farm_setup_projection().draft.clone();
+
+ if !draft.blockers().is_empty() {
+ return Err(DesktopAppRuntimeFarmSetupError::IncompleteDraft);
+ }
+
+ let saved_farm = FarmSummary {
+ farm_id: account
+ .farmer_activation
+ .farm_id
+ .unwrap_or_else(FarmId::new),
+ display_name: draft.farm_name.trim().to_owned(),
+ readiness: FarmReadiness::Incomplete,
+ };
+ let projection = FarmSetupProjection::new(draft, Some(saved_farm.clone()));
+
+ sqlite_store.save_farm_summary(&saved_farm)?;
+ sqlite_store.save_farm_setup(account.account.account_id.as_str(), &projection)?;
+
+ let selected_account_context = self.refresh_selected_account_context()?;
+ self.apply_selected_account_context(&selected_account_context);
+
+ Ok(selected_account_context.farm_setup_projection)
+ }
+
fn replace_identity_projection(
&mut self,
- projection: radroots_app_models::AppIdentityProjection,
- ) -> bool {
+ projection: AppIdentityProjection,
+ ) -> Result<bool, DesktopAppRuntimeCommandError> {
+ let selected_account_context =
+ load_selected_account_context(self.sqlite_store()?, &projection)?;
+ let identity_changed = self
+ .state_store
+ .apply_in_memory(AppStateCommand::replace_identity_projection(projection));
+ let context_changed = self.apply_selected_account_context(&selected_account_context);
+
+ Ok(identity_changed || context_changed)
+ }
+
+ fn refresh_selected_account_context(
+ &self,
+ ) -> Result<DesktopSelectedAccountContext, DesktopAppRuntimeFarmSetupError> {
+ Ok(load_selected_account_context(
+ self.sqlite_store_for_farm_setup()?,
+ self.state_store.identity_projection(),
+ )?)
+ }
+
+ fn apply_selected_account_context(&mut self, context: &DesktopSelectedAccountContext) -> bool {
+ let farm_setup_changed =
+ self.state_store
+ .apply_in_memory(AppStateCommand::replace_farm_setup_projection(
+ context.farm_setup_projection.clone(),
+ ));
+ let today_changed =
+ self.state_store
+ .apply_in_memory(AppStateCommand::replace_today_agenda(
+ context.today_projection.clone(),
+ ));
+
+ farm_setup_changed || today_changed
+ }
+
+ fn selected_account_id(&self) -> Result<String, DesktopAppRuntimeFarmSetupError> {
+ self.selected_account_for_farm_setup()
+ .map(|account| account.account.account_id.clone())
+ }
+
+ fn selected_account_for_farm_setup(
+ &self,
+ ) -> Result<&radroots_app_models::SelectedAccountProjection, DesktopAppRuntimeFarmSetupError>
+ {
self.state_store
- .apply_in_memory(AppStateCommand::replace_identity_projection(projection))
+ .identity_projection()
+ .selected_account
+ .as_ref()
+ .ok_or(DesktopAppRuntimeFarmSetupError::AccountRequired)
}
fn accounts_manager(
@@ -374,6 +492,14 @@ impl DesktopAppRuntimeState {
.ok_or(DesktopAppRuntimeCommandError::RuntimeUnavailable)
}
+ fn sqlite_store_for_farm_setup(
+ &self,
+ ) -> Result<&AppSqliteStore, DesktopAppRuntimeFarmSetupError> {
+ self.sqlite_store
+ .as_ref()
+ .ok_or(DesktopAppRuntimeFarmSetupError::RuntimeUnavailable)
+ }
+
fn shared_accounts_paths(
&self,
) -> Result<&AppSharedAccountsPaths, DesktopAppRuntimeCommandError> {
@@ -394,6 +520,20 @@ pub enum DesktopAppRuntimeCommandError {
RuntimeUnavailable,
#[error(transparent)]
Accounts(#[from] DesktopAccountsCommandError),
+ #[error(transparent)]
+ Sqlite(#[from] AppSqliteError),
+}
+
+#[derive(Debug, Error)]
+pub enum DesktopAppRuntimeFarmSetupError {
+ #[error("desktop runtime commands are unavailable while the runtime is degraded")]
+ RuntimeUnavailable,
+ #[error("farm setup requires a selected account")]
+ AccountRequired,
+ #[error("farm setup is incomplete")]
+ IncompleteDraft,
+ #[error(transparent)]
+ Sqlite(#[from] AppSqliteError),
}
#[derive(Debug, Error)]
@@ -408,6 +548,33 @@ enum DesktopAppRuntimeBootstrapError {
State(#[from] AppStateStoreError),
}
+fn load_selected_account_context(
+ sqlite_store: &AppSqliteStore,
+ identity_projection: &AppIdentityProjection,
+) -> Result<DesktopSelectedAccountContext, AppSqliteError> {
+ let Some(selected_account) = identity_projection.selected_account.as_ref() else {
+ return Ok(DesktopSelectedAccountContext::default());
+ };
+ let farm_setup_projection =
+ sqlite_store.load_farm_setup(&selected_account.account.account_id)?;
+ let today_farm_id = selected_account
+ .farmer_activation
+ .farm_id
+ .or(farm_setup_projection
+ .saved_farm
+ .as_ref()
+ .map(|farm| farm.farm_id));
+ let today_projection = match today_farm_id {
+ Some(farm_id) => sqlite_store.load_today_agenda(Some(farm_id))?,
+ None => TodayAgendaProjection::default(),
+ };
+
+ Ok(DesktopSelectedAccountContext {
+ farm_setup_projection,
+ today_projection,
+ })
+}
+
#[cfg(test)]
mod tests {
use std::{
@@ -423,9 +590,9 @@ mod tests {
};
use radroots_app_models::{
AccountSurfaceActivationProjection, ActiveSurface, AppActivityKind, AppStartupGate, FarmId,
- FarmReadiness, FarmSummary, FarmerActivationProjection, SelectedSurfaceProjection,
- SettingsPreference, SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask,
- TodaySetupTaskKind, TodaySummary,
+ FarmOrderMethod, FarmReadiness, FarmSetupDraft, FarmSetupProjection, FarmSummary,
+ FarmerActivationProjection, SelectedSurfaceProjection, SettingsPreference, SettingsSection,
+ ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary,
};
use radroots_app_sqlite::{AppSqliteStore, DatabaseTarget};
use radroots_app_state::{
@@ -864,6 +1031,133 @@ mod tests {
}
#[test]
+ fn selecting_farmer_account_loads_persisted_farm_setup_draft() {
+ let runtime = memory_runtime();
+
+ assert!(
+ runtime
+ .generate_local_account(Some("Farmer".to_owned()))
+ .expect("account should generate")
+ );
+ let account_id = runtime
+ .summary()
+ .settings_account_projection
+ .selected_account
+ .as_ref()
+ .expect("selected account")
+ .account
+ .account_id
+ .clone();
+ let projection = FarmSetupProjection::from_draft(FarmSetupDraft::new(
+ "North field farm",
+ "Stockholm County",
+ [FarmOrderMethod::Pickup],
+ ));
+ runtime
+ .lock_state()
+ .sqlite_store
+ .as_ref()
+ .expect("sqlite store")
+ .save_farm_setup(account_id.as_str(), &projection)
+ .expect("farm setup should save");
+ save_surface_activation(&runtime, account_id.as_str(), ActiveSurface::Farmer, true);
+
+ assert!(
+ runtime
+ .select_local_account(account_id.as_str())
+ .expect("account should select")
+ );
+ let summary = runtime.summary();
+
+ assert_eq!(summary.startup_gate, AppStartupGate::Farmer);
+ assert_eq!(summary.home_route, HomeRoute::FarmSetupForm);
+ assert_eq!(summary.farm_setup_projection, projection);
+ }
+
+ #[test]
+ fn finishing_farm_setup_persists_saved_farm_and_today_projection() {
+ let runtime = memory_runtime();
+
+ assert!(
+ runtime
+ .generate_local_account(Some("Farmer".to_owned()))
+ .expect("account should generate")
+ );
+ let account_id = runtime
+ .summary()
+ .settings_account_projection
+ .selected_account
+ .as_ref()
+ .expect("selected account")
+ .account
+ .account_id
+ .clone();
+ let farm_id =
+ save_farmer_surface_activation(&runtime, account_id.as_str(), ActiveSurface::Farmer);
+ assert!(
+ runtime
+ .select_local_account(account_id.as_str())
+ .expect("account should select")
+ );
+ assert_eq!(runtime.summary().home_route, HomeRoute::FarmSetupOnboarding);
+
+ let draft = FarmSetupDraft::new(
+ "North field farm",
+ "Stockholm County",
+ [FarmOrderMethod::Pickup, FarmOrderMethod::Delivery],
+ );
+ assert_eq!(
+ runtime
+ .save_farm_setup_draft(draft.clone())
+ .expect("draft should save")
+ .draft,
+ draft
+ );
+ assert_eq!(runtime.summary().home_route, HomeRoute::FarmSetupForm);
+
+ let finished_projection = runtime
+ .finish_farm_setup()
+ .expect("farm setup should finish");
+ let summary = runtime.summary();
+
+ assert_eq!(summary.home_route, HomeRoute::Today);
+ assert_eq!(
+ finished_projection.saved_farm,
+ Some(FarmSummary {
+ farm_id,
+ display_name: "North field farm".to_owned(),
+ readiness: FarmReadiness::Incomplete,
+ })
+ );
+ assert_eq!(
+ summary.today_projection.farm,
+ finished_projection.saved_farm.clone()
+ );
+ assert_eq!(summary.today_projection.setup_checklist.len(), 2);
+ assert_eq!(
+ runtime
+ .lock_state()
+ .sqlite_store
+ .as_ref()
+ .expect("sqlite store")
+ .load_farm_setup(account_id.as_str())
+ .expect("farm setup should load"),
+ finished_projection
+ );
+ assert_eq!(
+ runtime
+ .lock_state()
+ .sqlite_store
+ .as_ref()
+ .expect("sqlite store")
+ .load_today_agenda(Some(farm_id))
+ .expect("today agenda should load")
+ .farm,
+ finished_projection.saved_farm
+ );
+ }
+
+ #[test]
fn runtime_reset_local_device_state_clears_store_file_and_projection() {
let (runtime, paths) = file_backed_runtime("reset");
@@ -1064,6 +1358,27 @@ mod tests {
.expect("surface activation should save");
}
+ fn save_farmer_surface_activation(
+ runtime: &DesktopAppRuntime,
+ account_id: &str,
+ active_surface: ActiveSurface,
+ ) -> FarmId {
+ let farm_id = FarmId::new();
+ let activation = AccountSurfaceActivationProjection::new(
+ account_id,
+ SelectedSurfaceProjection::new(active_surface),
+ FarmerActivationProjection::active(farm_id),
+ );
+ runtime
+ .lock_state()
+ .sqlite_store
+ .as_ref()
+ .expect("sqlite store")
+ .save_surface_activation(&activation)
+ .expect("surface activation should save");
+ farm_id
+ }
+
fn cleanup_paths(paths: &AppSharedAccountsPaths) {
let Some(base) = paths.data_root.ancestors().nth(3).map(PathBuf::from) else {
return;
diff --git a/crates/launchers/desktop/src/source_guards.rs b/crates/launchers/desktop/src/source_guards.rs
@@ -9,6 +9,10 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[
"account-more",
"failed to add relay `{relay_url}`: {error}",
"home-create-account",
+ "home-farm-setup-delivery",
+ "home-farm-setup-finish",
+ "home-farm-setup-pickup",
+ "home-farm-setup-shipping",
"home-farm-setup-start",
"home-today-scroll",
"settings-allow-relay-connections",
@@ -29,6 +33,21 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[
"AppTextKey::HomeFarmSetupOnboardingTitle",
"AppTextKey::HomeFarmSetupOnboardingBody",
"AppTextKey::HomeFarmSetupOnboardingAction",
+ "AppTextKey::HomeFarmSetupSectionFarm",
+ "AppTextKey::HomeFarmSetupSectionLocation",
+ "AppTextKey::HomeFarmSetupSectionOrderMethods",
+ "AppTextKey::HomeFarmSetupFieldFarmName",
+ "AppTextKey::HomeFarmSetupFieldLocationOrServiceArea",
+ "AppTextKey::HomeFarmSetupOrderMethodPickup",
+ "AppTextKey::HomeFarmSetupOrderMethodDelivery",
+ "AppTextKey::HomeFarmSetupOrderMethodShipping",
+ "AppTextKey::HomeFarmSetupBlockerAddFarmName",
+ "AppTextKey::HomeFarmSetupBlockerAddLocationOrServiceArea",
+ "AppTextKey::HomeFarmSetupBlockerChooseOrderMethod",
+ "AppTextKey::HomeFarmSetupSaveAutosavesLocally",
+ "AppTextKey::HomeFarmSetupSaveSavedLocally",
+ "AppTextKey::HomeFarmSetupSaveFailedLocally",
+ "AppTextKey::HomeFarmSetupFinishAction",
"AppTextKey::SettingsAccountNoSelectionTitle",
"AppTextKey::SettingsAccountNoSelectionBody",
"AppTextKey::SettingsAccountStatusLoggedOut",
diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs
@@ -1,15 +1,18 @@
use gpui::{
- Animation, AnimationExt, AnyElement, App, AppContext, Bounds, ClickEvent, Context,
+ Animation, AnimationExt, AnyElement, App, AppContext, Bounds, ClickEvent, Context, Entity,
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
- StatefulInteractiveElement, Styled, Timer, Window, WindowBackgroundAppearance, WindowBounds,
- WindowOptions, div, prelude::FluentBuilder, px, relative, rgb, size,
+ StatefulInteractiveElement, Styled, Subscription, Timer, Window, WindowBackgroundAppearance,
+ WindowBounds, WindowOptions, div, prelude::FluentBuilder, px, relative, rgb, size,
+};
+use gpui_component::{
+ IconName, Root, Sizable, Size as ComponentSize,
+ input::{Input, InputEvent, InputState},
};
-use gpui_component::{IconName, Root};
use radroots_app_i18n::AppTextKey;
pub use radroots_app_models::SettingsSection as SettingsPanelViewKey;
use radroots_app_models::{
- AppStartupGate, FulfillmentWindowSummary, OrderListRow, ProductListRow, TodayAgendaProjection,
- TodaySetupTaskKind,
+ AppStartupGate, FarmOrderMethod, FarmSetupBlocker, FarmSetupDraft, FulfillmentWindowSummary,
+ OrderListRow, ProductListRow, TodayAgendaProjection, TodaySetupTaskKind,
};
use radroots_app_state::{FarmSetupFlowStage, HomeRoute};
use radroots_app_ui::{
@@ -140,6 +143,7 @@ pub struct HomeView {
runtime: DesktopAppRuntime,
startup_view: StartupHomeView,
logged_in_view: LoggedInHomeView,
+ farm_setup_form: Option<FarmSetupFormState>,
relay_client: Option<RadrootsNostrClient>,
}
@@ -149,6 +153,7 @@ impl HomeView {
runtime,
startup_view: StartupHomeView::new(),
logged_in_view: LoggedInHomeView::new(),
+ farm_setup_form: None,
relay_client: None,
}
}
@@ -207,11 +212,132 @@ impl HomeView {
cx.notify();
}
}
+
+ fn sync_farm_setup_form(
+ &mut self,
+ runtime_summary: &DesktopAppRuntimeSummary,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(account_id) = runtime_summary
+ .settings_account_projection
+ .selected_account
+ .as_ref()
+ .map(|account| account.account.account_id.clone())
+ else {
+ self.farm_setup_form = None;
+ return;
+ };
+
+ if runtime_summary.home_route != HomeRoute::FarmSetupForm {
+ self.farm_setup_form = None;
+ return;
+ }
+
+ let draft = runtime_summary.farm_setup_projection.draft.clone();
+ let should_reset = self
+ .farm_setup_form
+ .as_ref()
+ .map(|form| form.account_id != account_id)
+ .unwrap_or(true);
+
+ if should_reset {
+ self.farm_setup_form = Some(FarmSetupFormState::new(account_id, draft, window, cx));
+ }
+ }
+
+ fn handle_farm_name_input_event(
+ &mut self,
+ state: &Entity<InputState>,
+ event: &InputEvent,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if matches!(event, InputEvent::Change) {
+ let value = state.read(cx).value().to_string();
+ self.update_farm_setup_draft(cx, |draft| {
+ draft.farm_name = value;
+ });
+ }
+ }
+
+ fn handle_location_input_event(
+ &mut self,
+ state: &Entity<InputState>,
+ event: &InputEvent,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if matches!(event, InputEvent::Change) {
+ let value = state.read(cx).value().to_string();
+ self.update_farm_setup_draft(cx, |draft| {
+ draft.location_or_service_area = value;
+ });
+ }
+ }
+
+ fn toggle_farm_order_method(
+ &mut self,
+ method: FarmOrderMethod,
+ enabled: bool,
+ cx: &mut Context<Self>,
+ ) {
+ self.update_farm_setup_draft(cx, |draft| {
+ if enabled {
+ draft.order_methods.insert(method);
+ } else {
+ draft.order_methods.remove(&method);
+ }
+ });
+ }
+
+ fn update_farm_setup_draft(
+ &mut self,
+ cx: &mut Context<Self>,
+ update: impl FnOnce(&mut FarmSetupDraft),
+ ) {
+ let Some(form) = self.farm_setup_form.as_mut() else {
+ return;
+ };
+
+ update(&mut form.draft);
+
+ match self.runtime.save_farm_setup_draft(form.draft.clone()) {
+ Ok(projection) => {
+ form.draft = projection.draft;
+ form.save_state = FarmSetupSaveState::SavedLocally;
+ }
+ Err(_) => {
+ form.save_state = FarmSetupSaveState::SaveFailed;
+ }
+ }
+
+ cx.notify();
+ }
+
+ fn finish_farm_setup(&mut self, cx: &mut Context<Self>) {
+ let Some(form) = self.farm_setup_form.as_mut() else {
+ return;
+ };
+
+ match self.runtime.finish_farm_setup() {
+ Ok(_) => {
+ form.save_state = FarmSetupSaveState::SavedLocally;
+ self.farm_setup_form = None;
+ }
+ Err(_) => {
+ form.save_state = FarmSetupSaveState::SaveFailed;
+ }
+ }
+
+ cx.notify();
+ }
}
impl Render for HomeView {
- fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let runtime_summary = self.runtime.summary();
+ self.sync_farm_setup_form(&runtime_summary, window, cx);
match home_stage(&runtime_summary) {
HomeStage::Setup => self
.startup_view
@@ -231,6 +357,31 @@ impl Render for HomeView {
.logged_in_view
.render_farmer(
&runtime_summary,
+ self.farm_setup_form.as_ref().map(|form| {
+ home_farm_setup_form_card(
+ form,
+ cx.listener(|this, checked: &bool, _, cx| {
+ this.toggle_farm_order_method(FarmOrderMethod::Pickup, *checked, cx)
+ }),
+ cx.listener(|this, checked: &bool, _, cx| {
+ this.toggle_farm_order_method(
+ FarmOrderMethod::Delivery,
+ *checked,
+ cx,
+ )
+ }),
+ cx.listener(|this, checked: &bool, _, cx| {
+ this.toggle_farm_order_method(
+ FarmOrderMethod::Shipping,
+ *checked,
+ cx,
+ )
+ }),
+ cx.listener(|this, _, _, cx| this.finish_farm_setup(cx)),
+ cx,
+ )
+ .into_any_element()
+ }),
cx.listener(|this, _, _, cx| this.open_farm_setup(cx)),
cx,
)
@@ -239,6 +390,63 @@ impl Render for HomeView {
}
}
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum FarmSetupSaveState {
+ AutosavesLocally,
+ SavedLocally,
+ SaveFailed,
+}
+
+struct FarmSetupFormState {
+ account_id: String,
+ draft: FarmSetupDraft,
+ farm_name_input: Entity<InputState>,
+ location_input: Entity<InputState>,
+ _farm_name_subscription: Subscription,
+ _location_subscription: Subscription,
+ save_state: FarmSetupSaveState,
+}
+
+impl FarmSetupFormState {
+ fn new(
+ account_id: String,
+ draft: FarmSetupDraft,
+ window: &mut Window,
+ cx: &mut Context<HomeView>,
+ ) -> Self {
+ let farm_name_input =
+ cx.new(|cx| InputState::new(window, cx).default_value(draft.farm_name.clone()));
+ let location_input = cx.new(|cx| {
+ InputState::new(window, cx).default_value(draft.location_or_service_area.clone())
+ });
+ let farm_name_subscription = cx.subscribe_in(
+ &farm_name_input,
+ window,
+ HomeView::handle_farm_name_input_event,
+ );
+ let location_subscription = cx.subscribe_in(
+ &location_input,
+ window,
+ HomeView::handle_location_input_event,
+ );
+ let save_state = if draft.is_empty() {
+ FarmSetupSaveState::AutosavesLocally
+ } else {
+ FarmSetupSaveState::SavedLocally
+ };
+
+ Self {
+ account_id,
+ draft,
+ farm_name_input,
+ location_input,
+ _farm_name_subscription: farm_name_subscription,
+ _location_subscription: location_subscription,
+ save_state,
+ }
+ }
+}
+
#[derive(Clone, Debug, Eq, PartialEq)]
enum StartupPhase {
Idle,
@@ -313,10 +521,11 @@ impl LoggedInHomeView {
fn render_farmer(
&self,
runtime: &DesktopAppRuntimeSummary,
+ farm_setup_form: Option<AnyElement>,
on_open_farm_setup: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
cx: &App,
) -> AnyElement {
- farmer_home_shell(runtime, on_open_farm_setup, cx).into_any_element()
+ farmer_home_shell(runtime, farm_setup_form, on_open_farm_setup, cx).into_any_element()
}
}
@@ -896,6 +1105,7 @@ struct FarmSetupOnboardingCardSpec {
fn farmer_home_shell(
runtime: &DesktopAppRuntimeSummary,
+ farm_setup_form: Option<AnyElement>,
on_open_farm_setup: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
cx: &App,
) -> impl IntoElement {
@@ -905,7 +1115,12 @@ fn farmer_home_shell(
.id("home-today-scroll")
.size_full()
.overflow_y_scroll()
- .child(home_view_content(runtime, on_open_farm_setup, cx))
+ .child(home_view_content(
+ runtime,
+ farm_setup_form,
+ on_open_farm_setup,
+ cx,
+ ))
.into_any_element(),
)
}
@@ -1210,6 +1425,7 @@ fn home_sidebar(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement {
fn home_view_content(
runtime: &DesktopAppRuntimeSummary,
+ farm_setup_form: Option<AnyElement>,
on_open_farm_setup: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
cx: &App,
) -> impl IntoElement {
@@ -1232,7 +1448,11 @@ fn home_view_content(
);
}
- if let Some(spec) = setup_onboarding {
+ if runtime.home_route == HomeRoute::FarmSetupForm {
+ if let Some(farm_setup_form) = farm_setup_form {
+ sections.push(farm_setup_form);
+ }
+ } else if let Some(spec) = setup_onboarding {
sections
.push(home_farm_setup_onboarding_card(spec, on_open_farm_setup, cx).into_any_element());
} else if projection.needs_setup() {
@@ -1382,6 +1602,205 @@ fn home_farm_setup_onboarding_card(
)
}
+fn home_farm_setup_form_card(
+ form: &FarmSetupFormState,
+ on_pickup_change: impl Fn(&bool, &mut Window, &mut App) + 'static,
+ on_delivery_change: impl Fn(&bool, &mut Window, &mut App) + 'static,
+ on_shipping_change: impl Fn(&bool, &mut Window, &mut App) + 'static,
+ on_finish_setup: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ cx: &App,
+) -> impl IntoElement {
+ let blockers = form.draft.blockers();
+ let finish_ready = blockers.is_empty();
+
+ home_card(
+ app_shared_text(AppTextKey::HomeFarmSetupOnboardingTitle),
+ div()
+ .w_full()
+ .flex()
+ .flex_col()
+ .items_start()
+ .gap(px(APP_UI_THEME.layout.home_stack_gap_px))
+ .child(home_body_text(app_shared_text(
+ AppTextKey::HomeFarmSetupOnboardingBody,
+ )))
+ .child(home_farm_setup_text_field(
+ AppTextKey::HomeFarmSetupSectionFarm,
+ AppTextKey::HomeFarmSetupFieldFarmName,
+ &form.farm_name_input,
+ blockers
+ .contains(&FarmSetupBlocker::AddFarmName)
+ .then_some(AppTextKey::HomeFarmSetupBlockerAddFarmName),
+ ))
+ .child(home_farm_setup_text_field(
+ AppTextKey::HomeFarmSetupSectionLocation,
+ AppTextKey::HomeFarmSetupFieldLocationOrServiceArea,
+ &form.location_input,
+ blockers
+ .contains(&FarmSetupBlocker::AddLocationOrServiceArea)
+ .then_some(AppTextKey::HomeFarmSetupBlockerAddLocationOrServiceArea),
+ ))
+ .child(home_farm_setup_order_method_section(
+ form,
+ blockers
+ .contains(&FarmSetupBlocker::ChooseOrderMethod)
+ .then_some(AppTextKey::HomeFarmSetupBlockerChooseOrderMethod),
+ on_pickup_change,
+ on_delivery_change,
+ on_shipping_change,
+ cx,
+ ))
+ .child(
+ div()
+ .w_full()
+ .flex()
+ .flex_col()
+ .items_start()
+ .gap(px(APP_UI_THEME.layout.home_stack_gap_px))
+ .child(home_body_text(app_shared_text(farm_setup_save_state_key(
+ form.save_state,
+ ))))
+ .child(div().child(if finish_ready {
+ action_button_primary(
+ "home-farm-setup-finish",
+ app_shared_text(AppTextKey::HomeFarmSetupFinishAction),
+ on_finish_setup,
+ cx,
+ )
+ .into_any_element()
+ } else {
+ action_button_primary_disabled(
+ "home-farm-setup-finish",
+ app_shared_text(AppTextKey::HomeFarmSetupFinishAction),
+ cx,
+ )
+ .into_any_element()
+ })),
+ ),
+ )
+}
+
+fn home_farm_setup_text_field(
+ section_key: AppTextKey,
+ field_label_key: AppTextKey,
+ input: &Entity<InputState>,
+ blocker_key: Option<AppTextKey>,
+) -> impl IntoElement {
+ div()
+ .w_full()
+ .flex()
+ .flex_col()
+ .items_start()
+ .gap(px(8.0))
+ .child(home_farm_setup_section_label(section_key))
+ .child(
+ div()
+ .w_full()
+ .flex()
+ .flex_col()
+ .items_start()
+ .gap(px(6.0))
+ .child(home_farm_setup_field_label(field_label_key))
+ .child(
+ Input::new(input)
+ .with_size(ComponentSize::Large)
+ .w_full()
+ .into_any_element(),
+ )
+ .when_some(blocker_key, |this, blocker_key| {
+ this.child(home_farm_setup_blocker(blocker_key))
+ }),
+ )
+}
+
+fn home_farm_setup_order_method_section(
+ form: &FarmSetupFormState,
+ blocker_key: Option<AppTextKey>,
+ on_pickup_change: impl Fn(&bool, &mut Window, &mut App) + 'static,
+ on_delivery_change: impl Fn(&bool, &mut Window, &mut App) + 'static,
+ on_shipping_change: impl Fn(&bool, &mut Window, &mut App) + 'static,
+ cx: &App,
+) -> impl IntoElement {
+ div()
+ .w_full()
+ .flex()
+ .flex_col()
+ .items_start()
+ .gap(px(8.0))
+ .child(home_farm_setup_section_label(
+ AppTextKey::HomeFarmSetupSectionOrderMethods,
+ ))
+ .child(
+ div()
+ .w_full()
+ .flex()
+ .flex_col()
+ .items_start()
+ .gap(px(8.0))
+ .child(app_checkbox_field(
+ AppCheckboxFieldSpec::new(
+ "home-farm-setup-pickup",
+ app_shared_text(AppTextKey::HomeFarmSetupOrderMethodPickup),
+ Option::<SharedString>::None,
+ ),
+ form.draft.order_methods.contains(&FarmOrderMethod::Pickup),
+ cx,
+ move |checked, window, cx| on_pickup_change(&checked, window, cx),
+ ))
+ .child(app_checkbox_field(
+ AppCheckboxFieldSpec::new(
+ "home-farm-setup-delivery",
+ app_shared_text(AppTextKey::HomeFarmSetupOrderMethodDelivery),
+ Option::<SharedString>::None,
+ ),
+ form.draft
+ .order_methods
+ .contains(&FarmOrderMethod::Delivery),
+ cx,
+ move |checked, window, cx| on_delivery_change(&checked, window, cx),
+ ))
+ .child(app_checkbox_field(
+ AppCheckboxFieldSpec::new(
+ "home-farm-setup-shipping",
+ app_shared_text(AppTextKey::HomeFarmSetupOrderMethodShipping),
+ Option::<SharedString>::None,
+ ),
+ form.draft
+ .order_methods
+ .contains(&FarmOrderMethod::Shipping),
+ cx,
+ move |checked, window, cx| on_shipping_change(&checked, window, cx),
+ ))
+ .when_some(blocker_key, |this, blocker_key| {
+ this.child(home_farm_setup_blocker(blocker_key))
+ }),
+ )
+}
+
+fn home_farm_setup_section_label(key: AppTextKey) -> impl IntoElement {
+ div()
+ .text_size(px(APP_UI_THEME.typography.utility_title_text_px))
+ .font_weight(gpui::FontWeight::SEMIBOLD)
+ .text_color(rgb(APP_UI_THEME.text.secondary))
+ .child(app_shared_text(key))
+}
+
+fn home_farm_setup_field_label(key: AppTextKey) -> impl IntoElement {
+ div()
+ .text_size(px(APP_UI_THEME.typography.body_text_px))
+ .font_weight(gpui::FontWeight::MEDIUM)
+ .text_color(rgb(APP_UI_THEME.text.primary))
+ .child(app_shared_text(key))
+}
+
+fn home_farm_setup_blocker(key: AppTextKey) -> impl IntoElement {
+ div()
+ .text_size(px(APP_UI_THEME.typography.utility_title_text_px))
+ .line_height(relative(1.2))
+ .text_color(rgb(APP_UI_THEME.text.secondary))
+ .child(app_shared_text(key))
+}
+
fn home_card(title: impl Into<SharedString>, body: impl IntoElement) -> impl IntoElement {
div()
.w_full()
@@ -1679,6 +2098,14 @@ fn farm_setup_onboarding_card_spec(home_route: HomeRoute) -> Option<FarmSetupOnb
}
}
+fn farm_setup_save_state_key(state: FarmSetupSaveState) -> AppTextKey {
+ match state {
+ FarmSetupSaveState::AutosavesLocally => AppTextKey::HomeFarmSetupSaveAutosavesLocally,
+ FarmSetupSaveState::SavedLocally => AppTextKey::HomeFarmSetupSaveSavedLocally,
+ FarmSetupSaveState::SaveFailed => AppTextKey::HomeFarmSetupSaveFailedLocally,
+ }
+}
+
fn home_status_presentation(runtime: &DesktopAppRuntimeSummary) -> HomeStatusPresentation {
if runtime.startup_issue.is_some() || runtime.startup_gate == AppStartupGate::Blocked {
return HomeStatusPresentation {
diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs
@@ -45,6 +45,21 @@ define_app_text_keys! {
HomeFarmSetupOnboardingTitle => "home.farm_setup.onboarding.title",
HomeFarmSetupOnboardingBody => "home.farm_setup.onboarding.body",
HomeFarmSetupOnboardingAction => "home.farm_setup.onboarding.action",
+ HomeFarmSetupSectionFarm => "home.farm_setup.section.farm",
+ HomeFarmSetupSectionLocation => "home.farm_setup.section.location",
+ HomeFarmSetupSectionOrderMethods => "home.farm_setup.section.order_methods",
+ HomeFarmSetupFieldFarmName => "home.farm_setup.field.farm_name",
+ HomeFarmSetupFieldLocationOrServiceArea => "home.farm_setup.field.location_or_service_area",
+ HomeFarmSetupOrderMethodPickup => "home.farm_setup.order_method.pickup",
+ HomeFarmSetupOrderMethodDelivery => "home.farm_setup.order_method.delivery",
+ HomeFarmSetupOrderMethodShipping => "home.farm_setup.order_method.shipping",
+ HomeFarmSetupBlockerAddFarmName => "home.farm_setup.blocker.add_farm_name",
+ HomeFarmSetupBlockerAddLocationOrServiceArea => "home.farm_setup.blocker.add_location_or_service_area",
+ HomeFarmSetupBlockerChooseOrderMethod => "home.farm_setup.blocker.choose_order_method",
+ HomeFarmSetupSaveAutosavesLocally => "home.farm_setup.save.autosaves_locally",
+ HomeFarmSetupSaveSavedLocally => "home.farm_setup.save.saved_locally",
+ HomeFarmSetupSaveFailedLocally => "home.farm_setup.save.failed_locally",
+ HomeFarmSetupFinishAction => "home.farm_setup.finish_action",
HomeTodayEmptySetupTitle => "home.today.empty.setup.title",
HomeTodayEmptySetupBody => "home.today.empty.setup.body",
HomeTodayEmptyNoFarmTitle => "home.today.empty.no_farm.title",
diff --git a/crates/shared/sqlite/src/lib.rs b/crates/shared/sqlite/src/lib.rs
@@ -11,7 +11,7 @@ use std::{fs, path::PathBuf, time::Duration};
use radroots_app_models::{
AccountSurfaceActivationProjection, AppActivityContext, AppActivityEvent, AppActivityKind,
- FarmId, FarmSetupProjection, TodayAgendaProjection,
+ FarmId, FarmSetupProjection, FarmSummary, TodayAgendaProjection,
};
use rusqlite::Connection;
@@ -81,6 +81,10 @@ impl AppSqliteStore {
self.today_agenda_repository().load(farm_id)
}
+ pub fn save_farm_summary(&self, farm: &FarmSummary) -> Result<(), AppSqliteError> {
+ self.today_agenda_repository().save_farm_summary(farm)
+ }
+
pub fn record_activity_event(&self, kind: &AppActivityKind) -> Result<(), AppSqliteError> {
self.activity_repository().record(kind)
}
diff --git a/crates/shared/sqlite/src/today.rs b/crates/shared/sqlite/src/today.rs
@@ -35,6 +35,35 @@ impl<'a> AppTodayAgendaRepository<'a> {
})
}
+ pub fn save_farm_summary(&self, farm: &FarmSummary) -> Result<(), AppSqliteError> {
+ self.connection
+ .execute(
+ "insert into farms (id, display_name, readiness, created_at, updated_at)
+ values (
+ ?1,
+ ?2,
+ ?3,
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
+ )
+ on conflict(id) do update set
+ display_name = excluded.display_name,
+ readiness = excluded.readiness,
+ updated_at = excluded.updated_at",
+ params![
+ farm.farm_id.to_string(),
+ farm.display_name,
+ farm_readiness_storage_key(farm.readiness),
+ ],
+ )
+ .map_err(|source| AppSqliteError::Query {
+ operation: "save today farm summary",
+ source,
+ })?;
+
+ Ok(())
+ }
+
fn load_farm_summary(
&self,
farm_id: Option<FarmId>,
@@ -393,6 +422,13 @@ fn parse_farm_readiness(
}
}
+fn farm_readiness_storage_key(readiness: FarmReadiness) -> &'static str {
+ match readiness {
+ FarmReadiness::Incomplete => "incomplete",
+ FarmReadiness::Ready => "ready",
+ }
+}
+
#[cfg(test)]
mod tests {
use radroots_app_models::{FarmId, FulfillmentWindowId, ProductId, TodaySetupTaskKind};
@@ -632,6 +668,31 @@ mod tests {
assert!(projection.next_fulfillment_window.is_none());
}
+ #[test]
+ fn saved_farm_summary_round_trips_into_today_projection() {
+ let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
+ let farm = radroots_app_models::FarmSummary {
+ farm_id: FarmId::new(),
+ display_name: "North field farm".to_owned(),
+ readiness: radroots_app_models::FarmReadiness::Incomplete,
+ };
+
+ store
+ .save_farm_summary(&farm)
+ .expect("farm summary should save");
+
+ let projection = store
+ .load_today_agenda(Some(farm.farm_id))
+ .expect("today agenda should load");
+
+ assert_eq!(projection.farm, Some(farm));
+ assert_eq!(
+ projection.summary.expect("summary").orders_needing_action,
+ 0
+ );
+ assert_eq!(projection.setup_checklist.len(), 2);
+ }
+
fn insert_farm(
connection: &Connection,
farm_id: FarmId,
diff --git a/crates/shared/state/src/lib.rs b/crates/shared/state/src/lib.rs
@@ -204,17 +204,14 @@ impl AppProjection {
AppStartupGate::Blocked => HomeRoute::Blocked,
AppStartupGate::SetupRequired => HomeRoute::SetupRequired,
AppStartupGate::Personal => HomeRoute::Personal,
- AppStartupGate::Farmer => match self.farm_setup.readiness {
- FarmSetupReadiness::Ready => HomeRoute::Today,
- FarmSetupReadiness::NotStarted
- if self.farm_setup_flow_stage == FarmSetupFlowStage::Onboarding =>
- {
- HomeRoute::FarmSetupOnboarding
- }
- FarmSetupReadiness::NotStarted | FarmSetupReadiness::InProgress => {
- HomeRoute::FarmSetupForm
- }
- },
+ AppStartupGate::Farmer if self.farm_setup.has_saved_farm() => HomeRoute::Today,
+ AppStartupGate::Farmer
+ if self.farm_setup.readiness == FarmSetupReadiness::NotStarted
+ && self.farm_setup_flow_stage == FarmSetupFlowStage::Onboarding =>
+ {
+ HomeRoute::FarmSetupOnboarding
+ }
+ AppStartupGate::Farmer => HomeRoute::FarmSetupForm,
}
}
}
@@ -541,7 +538,7 @@ fn sync_projection(projection: &mut AppProjection) {
sync_farm_setup_flow_stage(
&mut projection.farm_setup_flow_stage,
projection.startup_gate,
- projection.farm_setup.readiness,
+ projection.farm_setup.has_saved_farm(),
);
}
@@ -573,9 +570,9 @@ fn sync_farm_setup_to_today(farm_setup: &mut FarmSetupProjection, today: &TodayA
fn sync_farm_setup_flow_stage(
flow_stage: &mut FarmSetupFlowStage,
startup_gate: AppStartupGate,
- readiness: FarmSetupReadiness,
+ has_saved_farm: bool,
) {
- if startup_gate != AppStartupGate::Farmer || readiness == FarmSetupReadiness::Ready {
+ if startup_gate != AppStartupGate::Farmer || has_saved_farm {
*flow_stage = FarmSetupFlowStage::Onboarding;
}
}
@@ -957,6 +954,34 @@ mod tests {
}
#[test]
+ fn complete_draft_without_saved_farm_stays_on_farm_setup_form() {
+ let mut store = AppStateStore::load(InMemoryAppStateRepository::default())
+ .expect("in-memory repository should load");
+
+ assert_eq!(
+ store.apply(AppStateCommand::replace_identity_projection(
+ ready_identity(ActiveSurface::Farmer),
+ )),
+ Ok(true)
+ );
+
+ let changed = store.apply(AppStateCommand::replace_farm_setup_projection(
+ FarmSetupProjection::from_draft(FarmSetupDraft::new(
+ "North field farm",
+ "Stockholm County",
+ [FarmOrderMethod::Pickup],
+ )),
+ ));
+
+ assert_eq!(changed, Ok(true));
+ assert_eq!(store.home_route(), HomeRoute::FarmSetupForm);
+ assert_eq!(
+ store.projection().farm_setup_flow_stage,
+ FarmSetupFlowStage::Onboarding
+ );
+ }
+
+ #[test]
fn saved_farm_in_today_projection_synchronizes_ready_home_route() {
let mut store = AppStateStore::load(InMemoryAppStateRepository::default())
.expect("in-memory repository should load");
diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json
@@ -24,6 +24,21 @@
"home.farm_setup.onboarding.title": "Set up your farm",
"home.farm_setup.onboarding.body": "Add the basics now. You can change them later.",
"home.farm_setup.onboarding.action": "Set up your farm",
+ "home.farm_setup.section.farm": "Farm",
+ "home.farm_setup.section.location": "Location",
+ "home.farm_setup.section.order_methods": "How customers get orders",
+ "home.farm_setup.field.farm_name": "Farm name",
+ "home.farm_setup.field.location_or_service_area": "Location or service area",
+ "home.farm_setup.order_method.pickup": "Pickup",
+ "home.farm_setup.order_method.delivery": "Delivery",
+ "home.farm_setup.order_method.shipping": "Shipping",
+ "home.farm_setup.blocker.add_farm_name": "Add a farm name.",
+ "home.farm_setup.blocker.add_location_or_service_area": "Add a location or service area.",
+ "home.farm_setup.blocker.choose_order_method": "Choose at least one way customers get orders.",
+ "home.farm_setup.save.autosaves_locally": "Saves locally as you type.",
+ "home.farm_setup.save.saved_locally": "Saved locally.",
+ "home.farm_setup.save.failed_locally": "Couldn't save locally. Try again.",
+ "home.farm_setup.finish_action": "Finish setup",
"home.today.empty.setup.title": "Account setup required",
"home.today.empty.setup.body": "Add a local account to start using Radroots on this device.",
"home.today.empty.no_farm.title": "No farm yet",