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:
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",