app

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

commit 2a3c0de2c0d1ae222d5e5457bef89b660145637b
parent cd707bd59c2874add5e27788b1ec954969cd4462
Author: triesap <tyson@radroots.org>
Date:   Tue, 21 Apr 2026 21:19:03 +0000

app: wire pack day artifact actions

Diffstat:
Mcrates/launchers/desktop/src/window.rs | 256++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Mcrates/shared/models/src/lib.rs | 11+++++++++--
2 files changed, 237 insertions(+), 30 deletions(-)

diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs @@ -65,7 +65,12 @@ use radroots_app_ui::{ runtime_metadata_rows, utility_title_row, }; use radroots_nostr::prelude::RadrootsNostrClient; -use std::{collections::BTreeSet, path::PathBuf, sync::Arc, time::Duration}; +use std::{ + collections::BTreeSet, + path::{Component, Path, PathBuf}, + sync::Arc, + time::Duration, +}; use tracing::error; use crate::pack_day_host_handoff::{ @@ -3450,6 +3455,20 @@ impl HomeView { cx, ) }), + cx.listener(|this, _, window, cx| { + this.start_pack_day_host_handoff( + PackDayHostHandoffKind::OpenPickupRoster, + window, + cx, + ) + }), + cx.listener(|this, _, window, cx| { + this.start_pack_day_host_handoff( + PackDayHostHandoffKind::OpenCustomerLabels, + window, + cx, + ) + }), cx, )) .when(!projection.reminders.is_empty(), |this| { @@ -9915,6 +9934,8 @@ fn pack_day_export_card( on_export: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, on_reveal_bundle: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, on_open_pack_sheet: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + on_open_pickup_roster: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + on_open_customer_labels: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, cx: &App, ) -> impl IntoElement { let export = &runtime.pack_day_projection.export; @@ -9974,6 +9995,8 @@ fn pack_day_export_card( .when(!host_handoff_actions.is_empty(), |this| { let on_reveal_bundle = Arc::new(on_reveal_bundle); let on_open_pack_sheet = Arc::new(on_open_pack_sheet); + let on_open_pickup_roster = Arc::new(on_open_pickup_roster); + let on_open_customer_labels = Arc::new(on_open_customer_labels); this.child( app_stack_v(APP_UI_THEME.foundation.spacing.small_px) .w_full() @@ -10012,6 +10035,40 @@ fn pack_day_export_card( ) .into_any_element() } + PackDayHostHandoffKind::OpenPickupRoster + if action.enabled => + { + action_button( + "pack-day-open-pickup-roster", + app_shared_text(action.label_key), + { + let on_open_pickup_roster = + Arc::clone(&on_open_pickup_roster); + move |event, window, cx| { + (on_open_pickup_roster)(event, window, cx) + } + }, + cx, + ) + .into_any_element() + } + PackDayHostHandoffKind::OpenCustomerLabels + if action.enabled => + { + action_button( + "pack-day-open-customer-labels", + app_shared_text(action.label_key), + { + let on_open_customer_labels = + Arc::clone(&on_open_customer_labels); + move |event, window, cx| { + (on_open_customer_labels)(event, window, cx) + } + }, + cx, + ) + .into_any_element() + } PackDayHostHandoffKind::RevealBundle => { action_button_disabled( "pack-day-reveal-bundle", @@ -10062,9 +10119,9 @@ fn pack_day_export_card( fn pack_day_host_handoff_action_presentations( runtime: &DesktopAppRuntimeSummary, ) -> Vec<PackDayHostHandoffActionPresentation> { - if pack_day_export_succeeded_bundle(runtime).is_none() { + let Some(bundle) = pack_day_export_succeeded_bundle(runtime) else { return Vec::new(); - } + }; let host_handoff = &runtime.pack_day_projection.host_handoff; let running_kind = (host_handoff.status == PackDayHostHandoffStatus::Running) @@ -10076,11 +10133,46 @@ fn pack_day_host_handoff_action_presentations( .map(|kind| PackDayHostHandoffActionPresentation { kind, label_key: pack_day_host_handoff_action_label_key(kind, running_kind), - enabled: running_kind.is_none(), + enabled: running_kind.is_none() + && pack_day_host_handoff_action_is_available(bundle, kind), }) .collect() } +fn pack_day_host_handoff_action_is_available( + bundle: &PackDayExportBundle, + kind: PackDayHostHandoffKind, +) -> bool { + match kind.artifact_kind() { + None => Path::new(&bundle.bundle_directory).is_dir(), + Some(artifact_kind) => bundle + .artifacts + .iter() + .find(|artifact| artifact.kind == artifact_kind) + .and_then(|artifact| pack_day_host_handoff_target_path(bundle, &artifact.relative_path)) + .is_some_and(|path| path.is_file()), + } +} + +fn pack_day_host_handoff_target_path( + bundle: &PackDayExportBundle, + relative_path: &str, +) -> Option<PathBuf> { + let relative_path = Path::new(relative_path); + if relative_path.is_absolute() + || relative_path.components().any(|component| { + matches!( + component, + Component::ParentDir | Component::RootDir | Component::Prefix(_) + ) + }) + { + return None; + } + + Some(PathBuf::from(&bundle.bundle_directory).join(relative_path)) +} + fn pack_day_host_handoff_action_label_key( kind: PackDayHostHandoffKind, running_kind: Option<PackDayHostHandoffKind>, @@ -12218,7 +12310,57 @@ mod tests { SyncConflictKind, SyncConflictResolutionStatus, SyncConflictSeverity, SyncConflictStatus, }; use radroots_identity::RadrootsIdentity; - use std::path::PathBuf; + use std::{fs, path::PathBuf}; + + struct TestDirectory { + path: PathBuf, + } + + impl TestDirectory { + fn new() -> Self { + let path = std::env::temp_dir().join(FulfillmentWindowId::new().to_string()); + fs::create_dir_all(&path).unwrap(); + Self { path } + } + + fn path(&self) -> &PathBuf { + &self.path + } + } + + impl Drop for TestDirectory { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.path); + } + } + + fn write_artifact(bundle_directory: &PathBuf, file_name: &str) -> PathBuf { + let path = bundle_directory.join(file_name); + fs::write(&path, file_name).unwrap(); + path + } + + fn sample_pack_day_bundle(bundle_directory: &PathBuf) -> PackDayExportBundle { + PackDayExportBundle { + fulfillment_window_id: FulfillmentWindowId::new(), + generated_at_utc: "2026-04-23T15:00:00Z".to_owned(), + bundle_directory: bundle_directory.to_string_lossy().into_owned(), + artifacts: vec![ + PackDayExportArtifact { + kind: PackDayExportArtifactKind::PackSheet, + relative_path: "pack_sheet.txt".to_owned(), + }, + PackDayExportArtifact { + kind: PackDayExportArtifactKind::PickupRoster, + relative_path: "pickup_roster.txt".to_owned(), + }, + PackDayExportArtifact { + kind: PackDayExportArtifactKind::CustomerLabels, + relative_path: "customer_labels.txt".to_owned(), + }, + ], + } + } #[test] fn farm_setup_onboarding_uses_frozen_copy_and_primary_action() { @@ -12923,16 +13065,12 @@ mod tests { #[test] fn pack_day_host_handoff_actions_only_surface_after_a_successful_export() { - let fulfillment_window_id = FulfillmentWindowId::new(); - let bundle = PackDayExportBundle { - fulfillment_window_id, - generated_at_utc: "2026-04-23T15:00:00Z".to_owned(), - bundle_directory: "exports/pack_day/window-1/20260423T150000Z".to_owned(), - artifacts: vec![PackDayExportArtifact { - kind: PackDayExportArtifactKind::PackSheet, - relative_path: "pack_sheet.txt".to_owned(), - }], - }; + let temp_dir = TestDirectory::new(); + write_artifact(temp_dir.path(), "pack_sheet.txt"); + write_artifact(temp_dir.path(), "pickup_roster.txt"); + write_artifact(temp_dir.path(), "customer_labels.txt"); + let bundle = sample_pack_day_bundle(temp_dir.path()); + let fulfillment_window_id = bundle.fulfillment_window_id; let mut runtime = summary( HomeRoute::Today, TodayAgendaProjection::default(), @@ -12959,28 +13097,36 @@ mod tests { label_key: AppTextKey::PackDayHostHandoffOpenPackSheetAction, enabled: true, }, + PackDayHostHandoffActionPresentation { + kind: PackDayHostHandoffKind::OpenPickupRoster, + label_key: AppTextKey::PackDayHostHandoffOpenPickupRosterAction, + enabled: true, + }, + PackDayHostHandoffActionPresentation { + kind: PackDayHostHandoffKind::OpenCustomerLabels, + label_key: AppTextKey::PackDayHostHandoffOpenCustomerLabelsAction, + enabled: true, + }, ] ); } #[test] fn pack_day_host_handoff_running_and_failure_postures_track_the_active_request() { - let fulfillment_window_id = FulfillmentWindowId::new(); - let bundle = PackDayExportBundle { - fulfillment_window_id, - generated_at_utc: "2026-04-23T15:00:00Z".to_owned(), - bundle_directory: "exports/pack_day/window-1/20260423T150000Z".to_owned(), - artifacts: vec![PackDayExportArtifact { - kind: PackDayExportArtifactKind::PackSheet, - relative_path: "pack_sheet.txt".to_owned(), - }], - }; + let temp_dir = TestDirectory::new(); + write_artifact(temp_dir.path(), "pack_sheet.txt"); + write_artifact(temp_dir.path(), "pickup_roster.txt"); + write_artifact(temp_dir.path(), "customer_labels.txt"); + let bundle = sample_pack_day_bundle(temp_dir.path()); + let fulfillment_window_id = bundle.fulfillment_window_id; let export_request = radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id); let reveal_request = PackDayHostHandoffRequest::for_bundle(PackDayHostHandoffKind::RevealBundle, &bundle); - let open_request = - PackDayHostHandoffRequest::for_bundle(PackDayHostHandoffKind::OpenPackSheet, &bundle); + let open_request = PackDayHostHandoffRequest::for_bundle( + PackDayHostHandoffKind::OpenCustomerLabels, + &bundle, + ); let mut runtime = summary( HomeRoute::Today, TodayAgendaProjection::default(), @@ -13004,6 +13150,16 @@ mod tests { label_key: AppTextKey::PackDayHostHandoffOpenPackSheetAction, enabled: false, }, + PackDayHostHandoffActionPresentation { + kind: PackDayHostHandoffKind::OpenPickupRoster, + label_key: AppTextKey::PackDayHostHandoffOpenPickupRosterAction, + enabled: false, + }, + PackDayHostHandoffActionPresentation { + kind: PackDayHostHandoffKind::OpenCustomerLabels, + label_key: AppTextKey::PackDayHostHandoffOpenCustomerLabelsAction, + enabled: false, + }, ] ); assert_eq!( @@ -13024,12 +13180,56 @@ mod tests { pack_day_host_handoff_status_presentation(&runtime), Some(PackDayHostHandoffStatusPresentation { indicator_color: APP_UI_THEME.components.app_status_indicator.attention, - title_key: AppTextKey::PackDayHostHandoffOpenPackSheetFailedTitle, + title_key: AppTextKey::PackDayHostHandoffOpenCustomerLabelsFailedTitle, }) ); } #[test] + fn pack_day_host_handoff_actions_disable_missing_artifacts_even_after_export_success() { + let temp_dir = TestDirectory::new(); + write_artifact(temp_dir.path(), "pack_sheet.txt"); + let bundle = sample_pack_day_bundle(temp_dir.path()); + let fulfillment_window_id = bundle.fulfillment_window_id; + let mut runtime = summary( + HomeRoute::Today, + TodayAgendaProjection::default(), + FarmSetupProjection::default(), + ); + + runtime.pack_day_projection.export = PackDayExportProjection::succeeded( + radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id), + bundle, + ); + + assert_eq!( + pack_day_host_handoff_action_presentations(&runtime), + vec![ + PackDayHostHandoffActionPresentation { + kind: PackDayHostHandoffKind::RevealBundle, + label_key: AppTextKey::PackDayHostHandoffRevealAction, + enabled: true, + }, + PackDayHostHandoffActionPresentation { + kind: PackDayHostHandoffKind::OpenPackSheet, + label_key: AppTextKey::PackDayHostHandoffOpenPackSheetAction, + enabled: true, + }, + PackDayHostHandoffActionPresentation { + kind: PackDayHostHandoffKind::OpenPickupRoster, + label_key: AppTextKey::PackDayHostHandoffOpenPickupRosterAction, + enabled: false, + }, + PackDayHostHandoffActionPresentation { + kind: PackDayHostHandoffKind::OpenCustomerLabels, + label_key: AppTextKey::PackDayHostHandoffOpenCustomerLabelsAction, + enabled: false, + }, + ] + ); + } + + #[test] fn sidebar_navigation_keeps_the_active_destination_first() { assert_eq!( home_sidebar_navigation_sections(FarmerSection::Today, true, false), diff --git a/crates/shared/models/src/lib.rs b/crates/shared/models/src/lib.rs @@ -1619,8 +1619,13 @@ pub enum PackDayHostHandoffKind { } impl PackDayHostHandoffKind { - pub const fn all_v1() -> [Self; 2] { - [Self::RevealBundle, Self::OpenPackSheet] + pub const fn all_v1() -> [Self; 4] { + [ + Self::RevealBundle, + Self::OpenPackSheet, + Self::OpenPickupRoster, + Self::OpenCustomerLabels, + ] } pub const fn storage_key(self) -> &'static str { @@ -3030,6 +3035,8 @@ mod tests { [ PackDayHostHandoffKind::RevealBundle, PackDayHostHandoffKind::OpenPackSheet, + PackDayHostHandoffKind::OpenPickupRoster, + PackDayHostHandoffKind::OpenCustomerLabels, ] ); assert_eq!(