app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit f2dfac1519fb35ee12ca781233eb69912be931d2
parent 7182ab6e7b1927528d7129910b064d1ae0f77e4f
Author: triesap <tyson@radroots.org>
Date:   Sat, 18 Apr 2026 23:55:30 +0000

settings: add farm rules utility window structure

Diffstat:
Mcrates/launchers/desktop/src/source_guards.rs | 31+++++++++++++++++++++++++++++++
Mcrates/launchers/desktop/src/window.rs | 387++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mcrates/shared/i18n/src/keys.rs | 30++++++++++++++++++++++++++++++
Mcrates/shared/i18n/src/lib.rs | 33+++++++++++++++++++++++++++++++++
Mcrates/shared/models/src/lib.rs | 34++++++++++++++++++++--------------
Acrates/shared/sqlite/migrations/0007_activity_farm_settings_section.sql | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/shared/sqlite/src/activity.rs | 6++++--
Mcrates/shared/sqlite/src/migrations.rs | 4++++
Mi18n/locales/en/messages.json | 30++++++++++++++++++++++++++++++
9 files changed, 512 insertions(+), 97 deletions(-)

diff --git a/crates/launchers/desktop/src/source_guards.rs b/crates/launchers/desktop/src/source_guards.rs @@ -101,6 +101,7 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "settings-manage-media-servers", "settings-nav-about", "settings-nav-accounts", + "settings-nav-farm", "settings-nav-settings", "settings-panel-scroll", "settings-use-media-servers", @@ -220,6 +221,36 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::SettingsAccountAddAction", "AppTextKey::SettingsAccountLogOutAction", "AppTextKey::SettingsAccountOpenWorkspaceAction", + "AppTextKey::SettingsNavFarm", + "AppTextKey::SettingsFarmPanelBody", + "AppTextKey::SettingsFarmFieldTimezone", + "AppTextKey::SettingsFarmFieldCurrency", + "AppTextKey::SettingsPickupLocationsSectionLabel", + "AppTextKey::SettingsPickupLocationsFieldLabel", + "AppTextKey::SettingsPickupLocationsFieldAddress", + "AppTextKey::SettingsPickupLocationsFieldDirections", + "AppTextKey::SettingsPickupLocationsFieldDefault", + "AppTextKey::SettingsSettingsPanelBody", + "AppTextKey::SettingsOperatingRulesSectionLabel", + "AppTextKey::SettingsOperatingRulesFieldPromiseLeadTime", + "AppTextKey::SettingsOperatingRulesFieldSubstitutionPolicy", + "AppTextKey::SettingsOperatingRulesFieldMissedPickupPolicy", + "AppTextKey::SettingsFulfillmentWindowsSectionLabel", + "AppTextKey::SettingsFulfillmentWindowsFieldLabel", + "AppTextKey::SettingsFulfillmentWindowsFieldPickupLocation", + "AppTextKey::SettingsFulfillmentWindowsFieldStartsAt", + "AppTextKey::SettingsFulfillmentWindowsFieldEndsAt", + "AppTextKey::SettingsFulfillmentWindowsFieldOrderCutoff", + "AppTextKey::SettingsBlackoutPeriodsSectionLabel", + "AppTextKey::SettingsBlackoutPeriodsFieldLabel", + "AppTextKey::SettingsBlackoutPeriodsFieldStartsAt", + "AppTextKey::SettingsBlackoutPeriodsFieldEndsAt", + "AppTextKey::SettingsReadinessSectionLabel", + "AppTextKey::SettingsReadinessFieldMissingProfileBasics", + "AppTextKey::SettingsReadinessFieldMissingPickupLocation", + "AppTextKey::SettingsReadinessFieldMissingFulfillmentWindow", + "AppTextKey::SettingsReadinessFieldMissingOperatingRules", + "AppTextKey::SettingsReadinessFieldInvalidTimingConflicts", ]; #[test] diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs @@ -2233,8 +2233,6 @@ impl SettingsWindowView { } fn settings_panel(&mut self, cx: &mut Context<Self>) -> impl IntoElement { - let section_label_width_px = 72.0; - let form_max_width_px = 420.0; let runtime_summary = self.runtime.summary(); let general_settings = runtime_summary.shell_projection.settings.general; let general_allow_relay_connections = general_settings.allow_relay_connections; @@ -2242,79 +2240,78 @@ impl SettingsWindowView { let general_use_nip05 = general_settings.use_nip05; let general_launch_at_login = general_settings.launch_at_login; - div() - .size_full() - .p(px(APP_UI_THEME.layout.settings_content_padding_px)) - .flex() - .flex_col() - .items_center() - .child( + let mut cards = SETTINGS_OPERATIONS_PANEL_SECTIONS + .iter() + .copied() + .map(settings_inventory_card) + .map(IntoElement::into_any_element) + .collect::<Vec<_>>(); + + cards.push( + home_card( + app_shared_text(AppTextKey::SettingsGeneralSectionLabel), div() - .h_full() .w_full() - .max_w(px(form_max_width_px)) .flex() - .items_start() - .gap(px(APP_UI_THEME.layout.settings_section_gap_px)) - .child( - div() - .w(px(section_label_width_px)) - .text_size(px(APP_UI_THEME.typography.body_text_px)) - .font_weight(gpui::FontWeight::MEDIUM) - .text_color(rgb(APP_UI_THEME.text.secondary)) - .child(app_shared_label_text( - AppTextKey::SettingsGeneralSectionLabel, - )), - ) - .child( - div() - .flex_1() - .min_w_0() - .flex() - .flex_col() - .gap(px(16.0)) - .child(self.settings_checkbox_row( - "settings-allow-relay-connections", - general_allow_relay_connections, - AppTextKey::SettingsGeneralAllowRelayConnections, - None, - None, - None, - |_, _, _| {}, - cx, - )) - .child(self.settings_checkbox_row( - "settings-use-media-servers", - general_use_media_servers, - AppTextKey::SettingsGeneralUseMediaServers, - Some("settings-manage-media-servers"), - Some(AppTextKey::SettingsGeneralManageAction), - None, - |_, _, _| {}, - cx, - )) - .child(self.settings_checkbox_row( - "settings-use-nip05", - general_use_nip05, - AppTextKey::SettingsGeneralUseNip05, - None, - None, - Some(AppTextKey::SettingsGeneralUseNip05Note), - |_, _, _| {}, - cx, - )) - .child(self.settings_checkbox_row( - "settings-launch-at-login", - general_launch_at_login, - AppTextKey::SettingsGeneralLaunchAtLogin, - None, - None, - None, - |_, _, _| {}, - cx, - )), - ), + .flex_col() + .gap(px(16.0)) + .child(self.settings_checkbox_row( + "settings-allow-relay-connections", + general_allow_relay_connections, + AppTextKey::SettingsGeneralAllowRelayConnections, + None, + None, + None, + |_, _, _| {}, + cx, + )) + .child(self.settings_checkbox_row( + "settings-use-media-servers", + general_use_media_servers, + AppTextKey::SettingsGeneralUseMediaServers, + Some("settings-manage-media-servers"), + Some(AppTextKey::SettingsGeneralManageAction), + None, + |_, _, _| {}, + cx, + )) + .child(self.settings_checkbox_row( + "settings-use-nip05", + general_use_nip05, + AppTextKey::SettingsGeneralUseNip05, + None, + None, + Some(AppTextKey::SettingsGeneralUseNip05Note), + |_, _, _| {}, + cx, + )) + .child(self.settings_checkbox_row( + "settings-launch-at-login", + general_launch_at_login, + AppTextKey::SettingsGeneralLaunchAtLogin, + None, + None, + None, + |_, _, _| {}, + cx, + )), ) + .into_any_element(), + ); + + settings_inventory_panel(AppTextKey::SettingsSettingsPanelBody, cards) + } + + fn farm_panel(&self) -> impl IntoElement { + settings_inventory_panel( + AppTextKey::SettingsFarmPanelBody, + SETTINGS_FARM_PANEL_SECTIONS + .iter() + .copied() + .map(settings_inventory_card) + .map(IntoElement::into_any_element) + .collect(), + ) } fn about_panel(&self) -> impl IntoElement { @@ -2372,6 +2369,7 @@ impl SettingsWindowView { fn settings_panel_content(&mut self, cx: &mut Context<Self>) -> AnyElement { match self.selected_view() { SettingsPanelViewKey::Account => self.account_panel(cx).into_any_element(), + SettingsPanelViewKey::Farm => self.farm_panel().into_any_element(), SettingsPanelViewKey::Settings => self.settings_panel(cx).into_any_element(), SettingsPanelViewKey::About => self.about_panel().into_any_element(), } @@ -2380,6 +2378,12 @@ impl SettingsWindowView { impl Render for SettingsWindowView { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { + let navigation_buttons = SETTINGS_NAVIGATION_ORDER + .iter() + .copied() + .map(|view| self.navigation_button(view, cx).into_any_element()) + .collect::<Vec<_>>(); + app_window_shell( APP_UI_THEME.surfaces.panel_background, div() @@ -2406,9 +2410,7 @@ impl Render for SettingsWindowView { .pt(px(APP_UI_THEME.layout.settings_navigation_row_padding_px)) .pb(px(APP_UI_THEME.layout.settings_navigation_row_padding_px)) .gap(px(APP_UI_THEME.layout.settings_navigation_row_gap_px)) - .child(self.navigation_button(SettingsPanelViewKey::Account, cx)) - .child(self.navigation_button(SettingsPanelViewKey::Settings, cx)) - .child(self.navigation_button(SettingsPanelViewKey::About, cx)), + .children(navigation_buttons), ), ) .child(section_divider()) @@ -2425,6 +2427,7 @@ impl Render for SettingsWindowView { fn settings_panel_label_key(view: SettingsPanelViewKey) -> AppTextKey { match view { SettingsPanelViewKey::Account => AppTextKey::SettingsNavAccounts, + SettingsPanelViewKey::Farm => AppTextKey::SettingsNavFarm, SettingsPanelViewKey::Settings => AppTextKey::SettingsNavSettings, SettingsPanelViewKey::About => AppTextKey::SettingsNavAbout, } @@ -2433,6 +2436,7 @@ fn settings_panel_label_key(view: SettingsPanelViewKey) -> AppTextKey { fn settings_panel_spec(view: SettingsPanelViewKey) -> (&'static str, IconName) { match view { SettingsPanelViewKey::Account => ("settings-nav-accounts", IconName::CircleUser), + SettingsPanelViewKey::Farm => ("settings-nav-farm", IconName::Map), SettingsPanelViewKey::Settings => ("settings-nav-settings", IconName::Settings2), SettingsPanelViewKey::About => ("settings-nav-about", IconName::Info), } @@ -2451,6 +2455,90 @@ struct FarmSetupOnboardingCardSpec { action_key: Option<AppTextKey>, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct SettingsInventorySectionSpec { + title_key: AppTextKey, + field_keys: &'static [AppTextKey], +} + +const SETTINGS_NAVIGATION_ORDER: &[SettingsPanelViewKey] = &[ + SettingsPanelViewKey::Account, + SettingsPanelViewKey::Farm, + SettingsPanelViewKey::Settings, + SettingsPanelViewKey::About, +]; + +const SETTINGS_FARM_SECTION_FIELDS: &[AppTextKey] = &[ + AppTextKey::HomeFarmSetupFieldFarmName, + AppTextKey::SettingsFarmFieldTimezone, + AppTextKey::SettingsFarmFieldCurrency, +]; + +const SETTINGS_PICKUP_LOCATIONS_SECTION_FIELDS: &[AppTextKey] = &[ + AppTextKey::SettingsPickupLocationsFieldLabel, + AppTextKey::SettingsPickupLocationsFieldAddress, + AppTextKey::SettingsPickupLocationsFieldDirections, + AppTextKey::SettingsPickupLocationsFieldDefault, +]; + +const SETTINGS_OPERATING_RULES_SECTION_FIELDS: &[AppTextKey] = &[ + AppTextKey::SettingsOperatingRulesFieldPromiseLeadTime, + AppTextKey::SettingsOperatingRulesFieldSubstitutionPolicy, + AppTextKey::SettingsOperatingRulesFieldMissedPickupPolicy, +]; + +const SETTINGS_FULFILLMENT_WINDOWS_SECTION_FIELDS: &[AppTextKey] = &[ + AppTextKey::SettingsFulfillmentWindowsFieldLabel, + AppTextKey::SettingsFulfillmentWindowsFieldPickupLocation, + AppTextKey::SettingsFulfillmentWindowsFieldStartsAt, + AppTextKey::SettingsFulfillmentWindowsFieldEndsAt, + AppTextKey::SettingsFulfillmentWindowsFieldOrderCutoff, +]; + +const SETTINGS_BLACKOUT_PERIODS_SECTION_FIELDS: &[AppTextKey] = &[ + AppTextKey::SettingsBlackoutPeriodsFieldLabel, + AppTextKey::SettingsBlackoutPeriodsFieldStartsAt, + AppTextKey::SettingsBlackoutPeriodsFieldEndsAt, +]; + +const SETTINGS_READINESS_SECTION_FIELDS: &[AppTextKey] = &[ + AppTextKey::SettingsReadinessFieldMissingProfileBasics, + AppTextKey::SettingsReadinessFieldMissingPickupLocation, + AppTextKey::SettingsReadinessFieldMissingFulfillmentWindow, + AppTextKey::SettingsReadinessFieldMissingOperatingRules, + AppTextKey::SettingsReadinessFieldInvalidTimingConflicts, +]; + +const SETTINGS_FARM_PANEL_SECTIONS: &[SettingsInventorySectionSpec] = &[ + SettingsInventorySectionSpec { + title_key: AppTextKey::HomeFarmSetupSectionFarm, + field_keys: SETTINGS_FARM_SECTION_FIELDS, + }, + SettingsInventorySectionSpec { + title_key: AppTextKey::SettingsPickupLocationsSectionLabel, + field_keys: SETTINGS_PICKUP_LOCATIONS_SECTION_FIELDS, + }, +]; + +const SETTINGS_OPERATIONS_PANEL_SECTIONS: &[SettingsInventorySectionSpec] = &[ + SettingsInventorySectionSpec { + title_key: AppTextKey::SettingsOperatingRulesSectionLabel, + field_keys: SETTINGS_OPERATING_RULES_SECTION_FIELDS, + }, + SettingsInventorySectionSpec { + title_key: AppTextKey::SettingsFulfillmentWindowsSectionLabel, + field_keys: SETTINGS_FULFILLMENT_WINDOWS_SECTION_FIELDS, + }, + SettingsInventorySectionSpec { + title_key: AppTextKey::SettingsBlackoutPeriodsSectionLabel, + field_keys: SETTINGS_BLACKOUT_PERIODS_SECTION_FIELDS, + }, + SettingsInventorySectionSpec { + title_key: AppTextKey::SettingsReadinessSectionLabel, + field_keys: SETTINGS_READINESS_SECTION_FIELDS, + }, +]; + fn holding_home_shell(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement { let home_status = home_status_presentation(runtime); let (title_key, body_key) = match home_stage(runtime) { @@ -4543,6 +4631,61 @@ fn home_farm_setup_blocker(key: AppTextKey) -> impl IntoElement { .child(app_shared_text(key)) } +fn settings_inventory_panel(intro_key: AppTextKey, cards: Vec<AnyElement>) -> impl IntoElement { + let content_max_width_px = 560.0; + + div() + .id("settings-panel-scroll") + .size_full() + .overflow_y_scroll() + .child( + div() + .w_full() + .p(px(APP_UI_THEME.layout.settings_content_padding_px)) + .flex() + .flex_col() + .items_center() + .child( + div() + .w_full() + .max_w(px(content_max_width_px)) + .flex() + .flex_col() + .gap(px(APP_UI_THEME.layout.home_stack_gap_px)) + .child(home_body_text(app_shared_text(intro_key))) + .children(cards), + ), + ) +} + +fn settings_inventory_card(spec: SettingsInventorySectionSpec) -> impl IntoElement { + home_card( + app_shared_text(spec.title_key), + div().w_full().flex().flex_col().gap(px(8.0)).children( + spec.field_keys + .iter() + .copied() + .map(settings_inventory_field_row) + .map(IntoElement::into_any_element) + .collect::<Vec<_>>(), + ), + ) +} + +fn settings_inventory_field_row(key: AppTextKey) -> impl IntoElement { + div() + .w_full() + .bg(rgb(APP_UI_THEME.surfaces.chrome_background)) + .rounded(px(APP_UI_THEME + .controls + .action_button + .sizing + .corner_radius_px)) + .px(px(12.0)) + .py(px(10.0)) + .child(home_farm_setup_field_label(key)) +} + fn home_saved_farm_summary_card(runtime: &DesktopAppRuntimeSummary) -> Option<AnyElement> { let saved_farm = home_saved_farm(runtime)?; let location_or_service_area = if runtime @@ -4998,13 +5141,15 @@ fn home_farm_order_method_label_key(method: FarmOrderMethod) -> AppTextKey { #[cfg(test)] mod tests { use super::{ - AppTextKey, FarmerHomeFarmState, StartupHomeSurface, StartupSignerConnectState, - farm_setup_onboarding_card_spec, farmer_home_farm_state, home_saved_farm, - home_window_launch_size_px, home_window_minimum_size_px, - parse_optional_product_editor_stock_input, parse_product_editor_price_input, - product_display_title, startup_home_surface, startup_signer_preview_summary, - startup_signer_preview_summary_for_connect_state, startup_signer_source_input_is_editable, - startup_signer_status_spec, startup_signer_transport_failure_requires_notice, + AppTextKey, FarmerHomeFarmState, SETTINGS_FARM_PANEL_SECTIONS, SETTINGS_NAVIGATION_ORDER, + SETTINGS_OPERATIONS_PANEL_SECTIONS, SettingsInventorySectionSpec, SettingsPanelViewKey, + StartupHomeSurface, StartupSignerConnectState, farm_setup_onboarding_card_spec, + farmer_home_farm_state, home_saved_farm, home_window_launch_size_px, + home_window_minimum_size_px, parse_optional_product_editor_stock_input, + parse_product_editor_price_input, product_display_title, startup_home_surface, + startup_signer_preview_summary, startup_signer_preview_summary_for_connect_state, + startup_signer_source_input_is_editable, startup_signer_status_spec, + startup_signer_transport_failure_requires_notice, }; use crate::runtime::DesktopAppRuntimeSummary; use radroots_app_models::SettingsAccountProjection; @@ -5043,6 +5188,86 @@ mod tests { } #[test] + fn settings_navigation_order_keeps_farm_between_account_and_settings() { + assert_eq!( + SETTINGS_NAVIGATION_ORDER, + &[ + SettingsPanelViewKey::Account, + SettingsPanelViewKey::Farm, + SettingsPanelViewKey::Settings, + SettingsPanelViewKey::About, + ] + ); + } + + #[test] + fn settings_inventory_sections_follow_the_frozen_farm_rules_order() { + assert_eq!( + SETTINGS_FARM_PANEL_SECTIONS, + &[ + SettingsInventorySectionSpec { + title_key: AppTextKey::HomeFarmSetupSectionFarm, + field_keys: &[ + AppTextKey::HomeFarmSetupFieldFarmName, + AppTextKey::SettingsFarmFieldTimezone, + AppTextKey::SettingsFarmFieldCurrency, + ], + }, + SettingsInventorySectionSpec { + title_key: AppTextKey::SettingsPickupLocationsSectionLabel, + field_keys: &[ + AppTextKey::SettingsPickupLocationsFieldLabel, + AppTextKey::SettingsPickupLocationsFieldAddress, + AppTextKey::SettingsPickupLocationsFieldDirections, + AppTextKey::SettingsPickupLocationsFieldDefault, + ], + }, + ] + ); + assert_eq!( + SETTINGS_OPERATIONS_PANEL_SECTIONS, + &[ + SettingsInventorySectionSpec { + title_key: AppTextKey::SettingsOperatingRulesSectionLabel, + field_keys: &[ + AppTextKey::SettingsOperatingRulesFieldPromiseLeadTime, + AppTextKey::SettingsOperatingRulesFieldSubstitutionPolicy, + AppTextKey::SettingsOperatingRulesFieldMissedPickupPolicy, + ], + }, + SettingsInventorySectionSpec { + title_key: AppTextKey::SettingsFulfillmentWindowsSectionLabel, + field_keys: &[ + AppTextKey::SettingsFulfillmentWindowsFieldLabel, + AppTextKey::SettingsFulfillmentWindowsFieldPickupLocation, + AppTextKey::SettingsFulfillmentWindowsFieldStartsAt, + AppTextKey::SettingsFulfillmentWindowsFieldEndsAt, + AppTextKey::SettingsFulfillmentWindowsFieldOrderCutoff, + ], + }, + SettingsInventorySectionSpec { + title_key: AppTextKey::SettingsBlackoutPeriodsSectionLabel, + field_keys: &[ + AppTextKey::SettingsBlackoutPeriodsFieldLabel, + AppTextKey::SettingsBlackoutPeriodsFieldStartsAt, + AppTextKey::SettingsBlackoutPeriodsFieldEndsAt, + ], + }, + SettingsInventorySectionSpec { + title_key: AppTextKey::SettingsReadinessSectionLabel, + field_keys: &[ + AppTextKey::SettingsReadinessFieldMissingProfileBasics, + AppTextKey::SettingsReadinessFieldMissingPickupLocation, + AppTextKey::SettingsReadinessFieldMissingFulfillmentWindow, + AppTextKey::SettingsReadinessFieldMissingOperatingRules, + AppTextKey::SettingsReadinessFieldInvalidTimingConflicts, + ], + }, + ] + ); + } + + #[test] fn today_route_has_no_setup_onboarding_card() { assert!(farm_setup_onboarding_card_spec(HomeRoute::Today).is_none()); } diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs @@ -153,6 +153,7 @@ define_app_text_keys! { MenuServices => "menu.services", SettingsTitle => "settings.title", SettingsNavAccounts => "settings.nav.accounts", + SettingsNavFarm => "settings.nav.farm", SettingsNavSettings => "settings.nav.settings", SettingsNavAbout => "settings.nav.about", SettingsAccountNoSelectionTitle => "settings.account.no_selection.title", @@ -178,6 +179,35 @@ define_app_text_keys! { SettingsViewAccount => "settings.view.account", SettingsViewSettings => "settings.view.settings", SettingsViewAbout => "settings.view.about", + SettingsFarmPanelBody => "settings.farm.panel.body", + SettingsFarmFieldTimezone => "settings.farm.field.timezone", + SettingsFarmFieldCurrency => "settings.farm.field.currency", + SettingsPickupLocationsSectionLabel => "settings.pickup_locations.section.label", + SettingsPickupLocationsFieldLabel => "settings.pickup_locations.field.label", + SettingsPickupLocationsFieldAddress => "settings.pickup_locations.field.address", + SettingsPickupLocationsFieldDirections => "settings.pickup_locations.field.directions", + SettingsPickupLocationsFieldDefault => "settings.pickup_locations.field.default", + SettingsSettingsPanelBody => "settings.settings.panel.body", + SettingsOperatingRulesSectionLabel => "settings.operating_rules.section.label", + SettingsOperatingRulesFieldPromiseLeadTime => "settings.operating_rules.field.promise_lead_time", + SettingsOperatingRulesFieldSubstitutionPolicy => "settings.operating_rules.field.substitution_policy", + SettingsOperatingRulesFieldMissedPickupPolicy => "settings.operating_rules.field.missed_pickup_policy", + SettingsFulfillmentWindowsSectionLabel => "settings.fulfillment_windows.section.label", + SettingsFulfillmentWindowsFieldLabel => "settings.fulfillment_windows.field.label", + SettingsFulfillmentWindowsFieldPickupLocation => "settings.fulfillment_windows.field.pickup_location", + SettingsFulfillmentWindowsFieldStartsAt => "settings.fulfillment_windows.field.starts_at", + SettingsFulfillmentWindowsFieldEndsAt => "settings.fulfillment_windows.field.ends_at", + SettingsFulfillmentWindowsFieldOrderCutoff => "settings.fulfillment_windows.field.order_cutoff", + SettingsBlackoutPeriodsSectionLabel => "settings.blackout_periods.section.label", + SettingsBlackoutPeriodsFieldLabel => "settings.blackout_periods.field.label", + SettingsBlackoutPeriodsFieldStartsAt => "settings.blackout_periods.field.starts_at", + SettingsBlackoutPeriodsFieldEndsAt => "settings.blackout_periods.field.ends_at", + SettingsReadinessSectionLabel => "settings.readiness.section.label", + SettingsReadinessFieldMissingProfileBasics => "settings.readiness.field.missing_profile_basics", + SettingsReadinessFieldMissingPickupLocation => "settings.readiness.field.missing_pickup_location", + SettingsReadinessFieldMissingFulfillmentWindow => "settings.readiness.field.missing_fulfillment_window", + SettingsReadinessFieldMissingOperatingRules => "settings.readiness.field.missing_operating_rules", + SettingsReadinessFieldInvalidTimingConflicts => "settings.readiness.field.invalid_timing_conflicts", SettingsGeneralSectionLabel => "settings.general.section.label", SettingsGeneralAllowRelayConnections => "settings.general.allow_relay_connections", SettingsGeneralUseMediaServers => "settings.general.use_media_servers", diff --git a/crates/shared/i18n/src/lib.rs b/crates/shared/i18n/src/lib.rs @@ -168,6 +168,39 @@ mod tests { } #[test] + fn english_farm_rules_host_copy_matches_the_frozen_utility_window_inventory() { + assert_eq!(app_text(AppTextKey::SettingsNavFarm), "Farm"); + assert_eq!( + app_text(AppTextKey::SettingsFarmPanelBody), + "Farm profile and pickup details stay local on this device." + ); + assert_eq!( + app_text(AppTextKey::SettingsPickupLocationsSectionLabel), + "Pickup locations" + ); + assert_eq!( + app_text(AppTextKey::SettingsOperatingRulesSectionLabel), + "Operating rules" + ); + assert_eq!( + app_text(AppTextKey::SettingsFulfillmentWindowsSectionLabel), + "Fulfillment windows" + ); + assert_eq!( + app_text(AppTextKey::SettingsBlackoutPeriodsSectionLabel), + "Blackout periods" + ); + assert_eq!( + app_text(AppTextKey::SettingsReadinessSectionLabel), + "Readiness" + ); + assert_eq!( + app_text(AppTextKey::SettingsReadinessFieldInvalidTimingConflicts), + "Invalid timing conflicts" + ); + } + + #[test] fn startup_identity_choice_keys_remain_defined_in_the_typed_registry_source() { let source = include_str!("keys.rs"); diff --git a/crates/shared/models/src/lib.rs b/crates/shared/models/src/lib.rs @@ -50,6 +50,7 @@ impl FarmerSection { pub enum SettingsSection { #[default] Account, + Farm, Settings, About, } @@ -58,6 +59,7 @@ impl SettingsSection { pub const fn storage_key(self) -> &'static str { match self { Self::Account => "settings.account", + Self::Farm => "settings.farm", Self::Settings => "settings.settings", Self::About => "settings.about", } @@ -140,6 +142,7 @@ impl FromStr for ShellSection { "farmer.pack_day" => Ok(Self::Farmer(FarmerSection::PackDay)), "farmer.farm" => Ok(Self::Farmer(FarmerSection::Farm)), "settings.account" => Ok(Self::Settings(SettingsSection::Account)), + "settings.farm" => Ok(Self::Settings(SettingsSection::Farm)), "settings.settings" => Ok(Self::Settings(SettingsSection::Settings)), "settings.about" => Ok(Self::Settings(SettingsSection::About)), _ => Err(ParseShellSectionError), @@ -1437,19 +1440,18 @@ mod tests { use super::{ AccountCustody, AccountSummary, AccountSurfaceActivationProjection, ActiveSurface, ActivityEventId, AppActivityContext, AppActivityEvent, AppActivityKind, - AppIdentityProjection, AppStartupGate, BlackoutPeriodId, FarmId, - FarmOrderMethod, FarmReadinessBlocker, FarmRulesProjection, FarmRulesReadiness, - FarmSetupBlocker, FarmSetupDraft, FarmSetupProjection, FarmSetupReadiness, - FarmSetupSection, FarmTimingConflict, FarmTimingConflictKind, - FarmerActivationProjection, FarmerSection, IdentityBlockedReason, IdentityReadiness, - LoggedOutStartupPhase, LoggedOutStartupProjection, OrderListRow, - ParseStartupSignerSourceError, PickupLocationId, ProductAttentionState, - ProductAvailabilityState, ProductAvailabilitySummary, ProductEditorDraft, ProductListRow, - ProductPricePresentation, ProductPublishBlocker, ProductStatus, ProductStockState, - ProductStockSummary, ProductsFilter, ProductsListProjection, ProductsListRow, - ProductsListSummary, ProductsSort, SelectedAccountProjection, - SelectedSurfaceProjection, SettingsPreference, SettingsSection, ShellSection, - StartupSignerEntryProjection, StartupSignerSource, StartupSignerSourceKind, + AppIdentityProjection, AppStartupGate, BlackoutPeriodId, FarmId, FarmOrderMethod, + FarmReadinessBlocker, FarmRulesProjection, FarmRulesReadiness, FarmSetupBlocker, + FarmSetupDraft, FarmSetupProjection, FarmSetupReadiness, FarmSetupSection, + FarmTimingConflict, FarmTimingConflictKind, FarmerActivationProjection, FarmerSection, + IdentityBlockedReason, IdentityReadiness, LoggedOutStartupPhase, + LoggedOutStartupProjection, OrderListRow, ParseStartupSignerSourceError, PickupLocationId, + ProductAttentionState, ProductAvailabilityState, ProductAvailabilitySummary, + ProductEditorDraft, ProductListRow, ProductPricePresentation, ProductPublishBlocker, + ProductStatus, ProductStockState, ProductStockSummary, ProductsFilter, + ProductsListProjection, ProductsListRow, ProductsListSummary, ProductsSort, + SelectedAccountProjection, SelectedSurfaceProjection, SettingsPreference, SettingsSection, + ShellSection, StartupSignerEntryProjection, StartupSignerSource, StartupSignerSourceKind, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary, }; use std::{collections::BTreeSet, str::FromStr}; @@ -1465,6 +1467,7 @@ mod tests { ShellSection::Farmer(FarmerSection::PackDay), ShellSection::Farmer(FarmerSection::Farm), ShellSection::Settings(SettingsSection::Account), + ShellSection::Settings(SettingsSection::Farm), ShellSection::Settings(SettingsSection::Settings), ShellSection::Settings(SettingsSection::About), ]; @@ -2178,7 +2181,10 @@ mod tests { .map(|profile| profile.display_name.as_str()), Some(saved_farm.display_name.as_str()) ); - assert_eq!(projection.pickup_locations[0].pickup_location_id, pickup_location_id); + assert_eq!( + projection.pickup_locations[0].pickup_location_id, + pickup_location_id + ); assert_eq!( projection.fulfillment_windows[0].pickup_location_id, pickup_location_id diff --git a/crates/shared/sqlite/migrations/0007_activity_farm_settings_section.sql b/crates/shared/sqlite/migrations/0007_activity_farm_settings_section.sql @@ -0,0 +1,54 @@ +ALTER TABLE activity_events RENAME TO activity_events_old; + +CREATE TABLE activity_events ( + activity_event_id TEXT PRIMARY KEY, + recorded_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + event_kind TEXT NOT NULL, + settings_section TEXT, + settings_preference TEXT, + preference_enabled INTEGER, + CHECK ( + event_kind IN ( + 'home_opened', + 'settings_opened', + 'settings_section_selected', + 'settings_preference_updated' + ) + ), + CHECK ( + settings_section IS NULL + OR settings_section IN ('account', 'farm', 'settings', 'about') + ), + CHECK ( + settings_preference IS NULL + OR settings_preference IN ( + 'allow_relay_connections', + 'use_media_servers', + 'use_nip05', + 'launch_at_login' + ) + ), + CHECK (preference_enabled IS NULL OR preference_enabled IN (0, 1)) +); + +INSERT INTO activity_events ( + activity_event_id, + recorded_at, + event_kind, + settings_section, + settings_preference, + preference_enabled +) +SELECT + activity_event_id, + recorded_at, + event_kind, + settings_section, + settings_preference, + preference_enabled +FROM activity_events_old; + +DROP TABLE activity_events_old; + +CREATE INDEX activity_events_recorded_at_idx + ON activity_events(recorded_at DESC, activity_event_id DESC); diff --git a/crates/shared/sqlite/src/activity.rs b/crates/shared/sqlite/src/activity.rs @@ -192,6 +192,7 @@ fn decode_settings_section( ) -> Result<SettingsSection, AppSqliteError> { match value.as_deref() { Some("account") => Ok(SettingsSection::Account), + Some("farm") => Ok(SettingsSection::Farm), Some("settings") => Ok(SettingsSection::Settings), Some("about") => Ok(SettingsSection::About), Some(other) => Err(AppSqliteError::DecodeEnum { @@ -238,6 +239,7 @@ fn settings_section_value(kind: &AppActivityKind) -> Option<&'static str> { AppActivityKind::SettingsOpened { section } | AppActivityKind::SettingsSectionSelected { section } => Some(match section { SettingsSection::Account => "account", + SettingsSection::Farm => "farm", SettingsSection::Settings => "settings", SettingsSection::About => "about", }), @@ -280,7 +282,7 @@ mod tests { .expect("record home opened"); repository .record(&AppActivityKind::SettingsOpened { - section: SettingsSection::About, + section: SettingsSection::Farm, }) .expect("record settings opened"); repository @@ -303,7 +305,7 @@ mod tests { assert_eq!( recent[1].kind, AppActivityKind::SettingsOpened { - section: SettingsSection::About, + section: SettingsSection::Farm, } ); assert_eq!(recent[2].kind, AppActivityKind::HomeOpened); diff --git a/crates/shared/sqlite/src/migrations.rs b/crates/shared/sqlite/src/migrations.rs @@ -28,6 +28,10 @@ const MIGRATIONS: &[Migration] = &[ version: 6, sql: include_str!("../migrations/0006_farm_rules_workspace.sql"), }, + Migration { + version: 7, + sql: include_str!("../migrations/0007_activity_farm_settings_section.sql"), + }, ]; pub fn latest_schema_version() -> u32 { diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -132,6 +132,7 @@ "menu.quit": "Quit Radroots", "settings.title": "Radroots Settings", "settings.nav.accounts": "Accounts", + "settings.nav.farm": "Farm", "settings.nav.settings": "Settings", "settings.nav.about": "About", "settings.account.no_selection.title": "No account selected", @@ -157,6 +158,35 @@ "settings.view.account": "account", "settings.view.settings": "settings", "settings.view.about": "about", + "settings.farm.panel.body": "Farm profile and pickup details stay local on this device.", + "settings.farm.field.timezone": "Timezone", + "settings.farm.field.currency": "Currency", + "settings.pickup_locations.section.label": "Pickup locations", + "settings.pickup_locations.field.label": "Label", + "settings.pickup_locations.field.address": "Address", + "settings.pickup_locations.field.directions": "Directions", + "settings.pickup_locations.field.default": "Default pickup location", + "settings.settings.panel.body": "Operating rules, fulfillment windows, blackout periods, and readiness stay local on this device.", + "settings.operating_rules.section.label": "Operating rules", + "settings.operating_rules.field.promise_lead_time": "Promise lead time", + "settings.operating_rules.field.substitution_policy": "Substitution policy", + "settings.operating_rules.field.missed_pickup_policy": "Missed pickup policy", + "settings.fulfillment_windows.section.label": "Fulfillment windows", + "settings.fulfillment_windows.field.label": "Label", + "settings.fulfillment_windows.field.pickup_location": "Pickup location", + "settings.fulfillment_windows.field.starts_at": "Starts at", + "settings.fulfillment_windows.field.ends_at": "Ends at", + "settings.fulfillment_windows.field.order_cutoff": "Order cutoff", + "settings.blackout_periods.section.label": "Blackout periods", + "settings.blackout_periods.field.label": "Label", + "settings.blackout_periods.field.starts_at": "Starts at", + "settings.blackout_periods.field.ends_at": "Ends at", + "settings.readiness.section.label": "Readiness", + "settings.readiness.field.missing_profile_basics": "Missing profile basics", + "settings.readiness.field.missing_pickup_location": "Missing pickup location", + "settings.readiness.field.missing_fulfillment_window": "Missing fulfillment window", + "settings.readiness.field.missing_operating_rules": "Missing operating rules", + "settings.readiness.field.invalid_timing_conflicts": "Invalid timing conflicts", "settings.general.section.label": "General", "settings.general.allow_relay_connections": "Allow relay connections", "settings.general.use_media_servers": "Use Radroots media servers",