feat(mobile): add Android publishing surface (Stage 5B)
- Add MobilePublishingPage with search, filter chips, sort, product count, compact card list, pull-to-refresh, and push navigation to detail - Add MobileProductDetailPage wrapping shared ProductPreviewPanel with all narrow edit callbacks (status, price, name, description, category) - Switch Products tab in MobileShell from ProductPublishingPage to MobilePublishingPage - Expand feature_wordpress barrel exports for mobile consumption - Add 4 new widget tests (10 total kell_mobile tests passing) - Zero business logic forked — all shared layers reused - dart analyze clean, all tests passing Stage 5 (Android application foundation) complete.
This commit is contained in:
parent
65466ba513
commit
591de0c5c4
|
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
## Current status
|
## Current status
|
||||||
|
|
||||||
- main baseline updated through: android-app-shell (Stage 5A complete)
|
- main baseline updated through: android-publishing-surface (Stage 5B complete)
|
||||||
- main baseline commit: merge of `feat/android-app-shell` (2026-05-28)
|
- main baseline commit: merge of `feat/android-publishing-surface` (2026-05-29)
|
||||||
- next branch: feat/android-publishing-surface (Stage 5B)
|
- next branch: feat/android-feedback-polish (Stage 6A)
|
||||||
- current stage: Stage 5 — Android application foundation
|
- current stage: Stage 5 complete — Stage 6 next
|
||||||
|
|
||||||
## Slice tracker
|
## Slice tracker
|
||||||
|
|
||||||
|
|
@ -182,3 +182,19 @@
|
||||||
- tests: passed (6/6 kell_mobile, 24/24 kell_web, 294/294 feature_wordpress — all passing)
|
- tests: passed (6/6 kell_mobile, 24/24 kell_web, 294/294 feature_wordpress — all passing)
|
||||||
- analyze: not yet run (SDK constraint fix was prerequisite)
|
- analyze: not yet run (SDK constraint fix was prerequisite)
|
||||||
- brief updated: yes
|
- brief updated: yes
|
||||||
|
|
||||||
|
### feat/android-publishing-surface
|
||||||
|
|
||||||
|
- status: merged to main
|
||||||
|
- date: 2026-05-29
|
||||||
|
- inspection: complete
|
||||||
|
- implementation: complete
|
||||||
|
- files changed:
|
||||||
|
- `feature_wordpress/lib/feature_wordpress.dart` — expanded barrel exports with `ProductPublishingController`, `ProductSortField`, `ProductDraftCard`, `ProductPreviewPanel`, and all 5 use cases + snack bar helpers for mobile consumption
|
||||||
|
- `kell_mobile/lib/pages/mobile_publishing_page.dart` — new mobile-optimized publishing workspace with search bar, horizontal filter chips, sort dropdown, product count, compact card list, pull-to-refresh, and push navigation to detail page
|
||||||
|
- `kell_mobile/lib/pages/mobile_product_detail_page.dart` — new full-screen product detail page wrapping shared `ProductPreviewPanel` with all narrow edit callbacks
|
||||||
|
- `kell_mobile/lib/shell/mobile_shell.dart` — Products tab (case 2) switched from `ProductPublishingPage` to `MobilePublishingPage`; removed unused `feature_wordpress` import
|
||||||
|
- `kell_mobile/test/widget_test.dart` — added 4 new mobile publishing surface tests (search bar, filter chips, product count, sort button) — 10 total kell_mobile tests
|
||||||
|
- tests: passed (10/10 kell_mobile)
|
||||||
|
- analyze: passed (dart analyze — no issues found)
|
||||||
|
- brief updated: yes
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,8 @@ No minimum thresholds are enforced — this is visibility-only tracking. Coverag
|
||||||
|
|
||||||
### Next recommended branch
|
### Next recommended branch
|
||||||
|
|
||||||
**`feat/android-publishing-surface`** — Stage 5B: Android publishing surface.
|
**`feat/android-feedback-polish`** — Stage 6A: Android feedback and action polish.
|
||||||
Branch from latest `main`. Stage 5A (Android app shell and bootstrap) is complete.
|
Branch from latest `main`. Stage 5 (Android application foundation) is complete.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -285,23 +285,10 @@ Business logic, domain logic, repositories, and feature application logic should
|
||||||
> Merged `feat/android-app-shell` → `main` (2026-05-28).
|
> Merged `feat/android-app-shell` → `main` (2026-05-28).
|
||||||
> Replaced the default Flutter counter template with a fully integrated mobile app shell. Created `MobileAppServices` extending `KcAppServices` with shared composition pattern, `KellMobileApp` with `KcAppScope<MobileAppServices>` and `KcBootstrap`, `MobileShell` with 5-tab `NavigationBar` (Dashboard, Inventory, Orders, Publishing, More). Dashboard reuses shared `DashboardSummary`/`DashboardController` with mobile-optimized `GridView` layout and design system widgets. Placeholder pages for Finance, Integrations, and feature tab content. `pubspec.yaml` references all shared packages (`core`, `design_system`, `feature_inventory`, `feature_orders`, `feature_policy`, `feature_wordpress`). Environment badge shows runtime mode. SDK constraint corrected to `^3.11.0` across all 14 pubspec files. 6 new `kell_mobile` widget tests added (6/6 kell_mobile, 24/24 kell_web, 294/294 feature_wordpress — all passing).
|
> Replaced the default Flutter counter template with a fully integrated mobile app shell. Created `MobileAppServices` extending `KcAppServices` with shared composition pattern, `KellMobileApp` with `KcAppScope<MobileAppServices>` and `KcBootstrap`, `MobileShell` with 5-tab `NavigationBar` (Dashboard, Inventory, Orders, Publishing, More). Dashboard reuses shared `DashboardSummary`/`DashboardController` with mobile-optimized `GridView` layout and design system widgets. Placeholder pages for Finance, Integrations, and feature tab content. `pubspec.yaml` references all shared packages (`core`, `design_system`, `feature_inventory`, `feature_orders`, `feature_policy`, `feature_wordpress`). Environment badge shows runtime mode. SDK constraint corrected to `^3.11.0` across all 14 pubspec files. 6 new `kell_mobile` widget tests added (6/6 kell_mobile, 24/24 kell_web, 294/294 feature_wordpress — all passing).
|
||||||
|
|
||||||
#### Stage 5B — Android publishing surface
|
#### ~~Stage 5B — Android publishing surface~~ ✅ COMPLETE
|
||||||
|
|
||||||
##### Goal
|
> Merged `feat/android-publishing-surface` → `main` (2026-05-29).
|
||||||
|
> Created mobile-optimized publishing workspace: `MobilePublishingPage` with search bar, horizontal filter chips, sort dropdown with ascending/descending toggle, product count display, compact card list with pull-to-refresh, and push navigation to detail page. `MobileProductDetailPage` wraps the shared `ProductPreviewPanel` with all narrow edit callbacks (status, price, name, description, category). Products tab in `MobileShell` switched from web `ProductPublishingPage` to `MobilePublishingPage`. Expanded `feature_wordpress` barrel exports for mobile consumption (`ProductPublishingController`, `ProductSortField`, `ProductDraftCard`, `ProductPreviewPanel`, all 5 use cases, snack bar helpers). Zero business logic forked — all shared layers reused. 4 new mobile publishing surface tests added (10 total `kell_mobile` tests passing). Analyze clean. Stage 5 complete.
|
||||||
Adapt the publishing workflow for mobile form factor.
|
|
||||||
|
|
||||||
##### Requirements
|
|
||||||
|
|
||||||
- reuse shared controller/use case/repository layers
|
|
||||||
- optimize preview/edit interactions for smaller screens
|
|
||||||
- keep feature parity for current publishing workflow where feasible
|
|
||||||
- do not fork business rules for Android
|
|
||||||
|
|
||||||
##### Definition of done
|
|
||||||
|
|
||||||
- Android supports browsing, filtering, status changes, and existing narrow edits
|
|
||||||
- mobile presentation is usable and tested where practical
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import 'package:feature_wordpress/feature_wordpress.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Full-screen product detail page for mobile.
|
||||||
|
///
|
||||||
|
/// Receives the shared [ProductPublishingController] so that edits
|
||||||
|
/// (name, price, description, category, status) are immediately
|
||||||
|
/// reflected in the list when the user pops back.
|
||||||
|
///
|
||||||
|
/// Wraps the shared [ProductPreviewPanel] from [feature_wordpress]
|
||||||
|
/// inside a [Scaffold] with an [AppBar] showing the product name.
|
||||||
|
class MobileProductDetailPage extends StatelessWidget {
|
||||||
|
final ProductPublishingController controller;
|
||||||
|
|
||||||
|
const MobileProductDetailPage({super.key, required this.controller});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: controller,
|
||||||
|
builder: (context, _) {
|
||||||
|
final draft = controller.selectedDraft;
|
||||||
|
|
||||||
|
// If the product was removed or deselected, pop back.
|
||||||
|
if (draft == null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (context.mounted) Navigator.of(context).pop();
|
||||||
|
});
|
||||||
|
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text(draft.name)),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: ProductPreviewPanel(
|
||||||
|
draft: draft,
|
||||||
|
isUpdating: controller.isUpdating(draft.id),
|
||||||
|
onPublish: () => controller.updateStatus(draft.id, PublishStatus.published),
|
||||||
|
onMoveToDraft: () => controller.updateStatus(draft.id, PublishStatus.draft),
|
||||||
|
onPriceChanged: (price) => controller.updatePrice(draft.id, price),
|
||||||
|
onNameChanged: (name) => controller.updateName(draft.id, name),
|
||||||
|
onDescriptionChanged: (desc) => controller.updateDescription(draft.id, desc),
|
||||||
|
onCategoryChanged: (cat) => controller.updateCategory(draft.id, cat),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,272 @@
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:feature_wordpress/feature_wordpress.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'mobile_product_detail_page.dart';
|
||||||
|
|
||||||
|
/// Mobile-optimized publishing workspace page.
|
||||||
|
///
|
||||||
|
/// Provides browsing, filtering, searching, sorting, and status changes
|
||||||
|
/// for the product publishing workflow on smaller screens. Tapping a
|
||||||
|
/// product pushes a full-screen detail page for viewing and editing.
|
||||||
|
///
|
||||||
|
/// Reuses the shared [ProductPublishingController] and all use cases
|
||||||
|
/// from [feature_wordpress] — no business logic is forked for mobile.
|
||||||
|
class MobilePublishingPage extends StatefulWidget {
|
||||||
|
final ProductPublishingRepository repository;
|
||||||
|
|
||||||
|
const MobilePublishingPage({super.key, required this.repository});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MobilePublishingPage> createState() => _MobilePublishingPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobilePublishingPageState extends State<MobilePublishingPage> {
|
||||||
|
late final ProductPublishingController _controller;
|
||||||
|
late final TextEditingController _searchController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_searchController = TextEditingController();
|
||||||
|
|
||||||
|
final repo = widget.repository;
|
||||||
|
_controller = ProductPublishingController(
|
||||||
|
GetProductDrafts(repo),
|
||||||
|
PublishProduct(repo),
|
||||||
|
UpdateProductStatus(repo),
|
||||||
|
UpdateProductPrice(repo),
|
||||||
|
UpdateProductName(repo),
|
||||||
|
UpdateProductDescription(repo),
|
||||||
|
UpdateProductCategory(repo),
|
||||||
|
);
|
||||||
|
|
||||||
|
_controller.addListener(_onControllerChanged);
|
||||||
|
_controller.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.removeListener(_onControllerChanged);
|
||||||
|
_controller.dispose();
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles action result feedback via SnackBars.
|
||||||
|
void _onControllerChanged() {
|
||||||
|
final result = _controller.lastActionResult;
|
||||||
|
if (result != null) {
|
||||||
|
_controller.consumeActionResult();
|
||||||
|
showStatusActionSnackBar(context, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
final priceResult = _controller.lastPriceResult;
|
||||||
|
if (priceResult != null) {
|
||||||
|
_controller.consumePriceResult();
|
||||||
|
showPriceActionSnackBar(context, priceResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
final nameResult = _controller.lastNameResult;
|
||||||
|
if (nameResult != null) {
|
||||||
|
_controller.consumeNameResult();
|
||||||
|
showNameActionSnackBar(context, nameResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
final descriptionResult = _controller.lastDescriptionResult;
|
||||||
|
if (descriptionResult != null) {
|
||||||
|
_controller.consumeDescriptionResult();
|
||||||
|
showDescriptionActionSnackBar(context, descriptionResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
final categoryResult = _controller.lastCategoryResult;
|
||||||
|
if (categoryResult != null) {
|
||||||
|
_controller.consumeCategoryResult();
|
||||||
|
showCategoryActionSnackBar(context, categoryResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, _) {
|
||||||
|
if (_controller.isLoading) {
|
||||||
|
return const KcLoadingState(message: 'Loading products…');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_controller.error != null) {
|
||||||
|
return KcErrorState(message: 'Failed to load product drafts.', onRetry: _controller.load);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildSearchBar(),
|
||||||
|
_buildFilterChips(),
|
||||||
|
_buildSortAndCount(),
|
||||||
|
Expanded(child: _buildProductList()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search bar with real-time filtering.
|
||||||
|
Widget _buildSearchBar() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(KcSpacing.md, KcSpacing.sm, KcSpacing.md, KcSpacing.xs),
|
||||||
|
child: TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Search products…',
|
||||||
|
prefixIcon: const Icon(Icons.search, size: 20),
|
||||||
|
suffixIcon: _searchController.text.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.clear, size: 20),
|
||||||
|
onPressed: () {
|
||||||
|
_searchController.clear();
|
||||||
|
_controller.setSearchQuery('');
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
isDense: true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: KcSpacing.sm,
|
||||||
|
vertical: KcSpacing.sm,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
|
),
|
||||||
|
onChanged: (query) {
|
||||||
|
_controller.setSearchQuery(query);
|
||||||
|
setState(() {}); // refresh clear button visibility
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Horizontal scrollable filter chips for status filtering.
|
||||||
|
Widget _buildFilterChips() {
|
||||||
|
const filters = [
|
||||||
|
(null, 'All'),
|
||||||
|
('draft', 'Draft'),
|
||||||
|
('pendingReview', 'Pending'),
|
||||||
|
('published', 'Published'),
|
||||||
|
('unpublished', 'Unpublished'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
height: 48,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: KcSpacing.md),
|
||||||
|
itemCount: filters.length,
|
||||||
|
separatorBuilder: (_, _) => const SizedBox(width: KcSpacing.xs),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final (value, label) = filters[index];
|
||||||
|
final isActive = _controller.activeFilter == value;
|
||||||
|
return FilterChip(
|
||||||
|
label: Text(label),
|
||||||
|
selected: isActive,
|
||||||
|
onSelected: (_) => _controller.setFilter(isActive ? null : value),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sort dropdown and product count row.
|
||||||
|
Widget _buildSortAndCount() {
|
||||||
|
final count = _controller.drafts.length;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: KcSpacing.md, vertical: KcSpacing.xs),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'$count ${count == 1 ? 'product' : 'products'}',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: KcColors.neutral),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
PopupMenuButton<ProductSortField>(
|
||||||
|
icon: const Icon(Icons.sort, size: 20),
|
||||||
|
tooltip: 'Sort',
|
||||||
|
onSelected: (field) {
|
||||||
|
if (field == _controller.activeSortField) {
|
||||||
|
_controller.setSort(field, ascending: !_controller.sortAscending);
|
||||||
|
} else {
|
||||||
|
_controller.setSort(field, ascending: true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (_) => [
|
||||||
|
_sortMenuItem(ProductSortField.name, 'Name'),
|
||||||
|
_sortMenuItem(ProductSortField.lastModified, 'Last Modified'),
|
||||||
|
_sortMenuItem(ProductSortField.status, 'Status'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PopupMenuEntry<ProductSortField> _sortMenuItem(ProductSortField field, String label) {
|
||||||
|
final isActive = _controller.activeSortField == field;
|
||||||
|
return PopupMenuItem(
|
||||||
|
value: field,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: isActive ? const TextStyle(fontWeight: FontWeight.bold) : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isActive)
|
||||||
|
Icon(_controller.sortAscending ? Icons.arrow_upward : Icons.arrow_downward, size: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scrollable product list using compact cards.
|
||||||
|
Widget _buildProductList() {
|
||||||
|
final drafts = _controller.drafts;
|
||||||
|
|
||||||
|
if (drafts.isEmpty) {
|
||||||
|
return KcEmptyState(
|
||||||
|
icon: Icons.search_off,
|
||||||
|
message: _controller.searchQuery.isNotEmpty || _controller.activeFilter != null
|
||||||
|
? 'No products match your criteria.'
|
||||||
|
: 'No product drafts available.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: _controller.load,
|
||||||
|
child: ListView.separated(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: KcSpacing.md),
|
||||||
|
itemCount: drafts.length,
|
||||||
|
separatorBuilder: (_, _) => const SizedBox(height: KcSpacing.xs),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final draft = drafts[index];
|
||||||
|
return SizedBox(
|
||||||
|
height: 72,
|
||||||
|
child: ProductDraftCard(
|
||||||
|
draft: draft,
|
||||||
|
isSelected: draft.id == _controller.selectedDraft?.id,
|
||||||
|
isCompact: true,
|
||||||
|
isStale: _controller.isStale(draft),
|
||||||
|
onTap: () => _navigateToDetail(draft),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Navigates to the full-screen product detail page.
|
||||||
|
void _navigateToDetail(ProductDraft draft) {
|
||||||
|
_controller.selectDraft(draft);
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute<void>(builder: (_) => MobileProductDetailPage(controller: _controller)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:core/core.dart';
|
||||||
import 'package:feature_inventory/feature_inventory.dart';
|
import 'package:feature_inventory/feature_inventory.dart';
|
||||||
import 'package:feature_orders/feature_orders.dart';
|
import 'package:feature_orders/feature_orders.dart';
|
||||||
import 'package:feature_policy/feature_policy.dart';
|
import 'package:feature_policy/feature_policy.dart';
|
||||||
import 'package:feature_wordpress/feature_wordpress.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../composition/mobile_app_services.dart';
|
import '../composition/mobile_app_services.dart';
|
||||||
|
|
@ -11,6 +10,7 @@ import '../dashboard/application/get_dashboard_summary.dart';
|
||||||
import '../pages/dashboard_page.dart';
|
import '../pages/dashboard_page.dart';
|
||||||
import '../pages/finance_placeholder_page.dart';
|
import '../pages/finance_placeholder_page.dart';
|
||||||
import '../pages/integrations_placeholder_page.dart';
|
import '../pages/integrations_placeholder_page.dart';
|
||||||
|
import '../pages/mobile_publishing_page.dart';
|
||||||
|
|
||||||
/// The main shell for the mobile app.
|
/// The main shell for the mobile app.
|
||||||
///
|
///
|
||||||
|
|
@ -104,13 +104,7 @@ class _MobileShellState extends State<MobileShell> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
case 2:
|
case 2:
|
||||||
return ProductPublishingPage(
|
return MobilePublishingPage(repository: services.productPublishingRepository);
|
||||||
repository: services.productPublishingRepository,
|
|
||||||
onViewPolicy: () {
|
|
||||||
// Cross-feature nav: not directly reachable from bottom nav,
|
|
||||||
// but we can show it as a placeholder for now.
|
|
||||||
},
|
|
||||||
);
|
|
||||||
case 3:
|
case 3:
|
||||||
return OrdersPage(
|
return OrdersPage(
|
||||||
repository: services.ordersRepository,
|
repository: services.ordersRepository,
|
||||||
|
|
|
||||||
|
|
@ -74,4 +74,56 @@ void main() {
|
||||||
expect(find.text('Policy'), findsOneWidget);
|
expect(find.text('Policy'), findsOneWidget);
|
||||||
expect(find.text('Integrations'), findsOneWidget);
|
expect(find.text('Integrations'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Mobile Publishing Surface tests ──────────────────────────────
|
||||||
|
|
||||||
|
testWidgets('Products tab shows mobile publishing page with search bar', (
|
||||||
|
WidgetTester tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(_buildTestApp());
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Tap the Products destination
|
||||||
|
await tester.tap(find.text('Products').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Should show the search bar
|
||||||
|
expect(find.byType(TextField), findsOneWidget);
|
||||||
|
expect(find.text('Search products…'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Products tab shows filter chips', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(_buildTestApp());
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Products').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Should show filter chips for status categories
|
||||||
|
expect(find.widgetWithText(FilterChip, 'All'), findsOneWidget);
|
||||||
|
expect(find.widgetWithText(FilterChip, 'Draft'), findsOneWidget);
|
||||||
|
expect(find.widgetWithText(FilterChip, 'Published'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Products tab shows product count', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(_buildTestApp());
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Products').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Fake data should produce a product count label
|
||||||
|
expect(find.textContaining('products'), findsWidgets);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Products tab shows sort button', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(_buildTestApp());
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Products').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Should have a sort icon/button
|
||||||
|
expect(find.byIcon(Icons.sort), findsOneWidget);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ export 'src/domain/product_publishing_repository.dart';
|
||||||
export 'src/domain/publish_status.dart';
|
export 'src/domain/publish_status.dart';
|
||||||
|
|
||||||
// Application
|
// Application
|
||||||
export 'src/application/product_publishing_controller.dart' show ListDensity, ProductSortField;
|
export 'src/application/get_product_drafts.dart';
|
||||||
|
export 'src/application/product_publishing_controller.dart';
|
||||||
|
export 'src/application/publish_product.dart';
|
||||||
export 'src/application/update_product_category.dart';
|
export 'src/application/update_product_category.dart';
|
||||||
export 'src/application/update_product_description.dart';
|
export 'src/application/update_product_description.dart';
|
||||||
export 'src/application/update_product_name.dart';
|
export 'src/application/update_product_name.dart';
|
||||||
|
|
@ -21,3 +23,7 @@ export 'src/application/update_product_status.dart';
|
||||||
|
|
||||||
// Presentation
|
// Presentation
|
||||||
export 'src/presentation/product_publishing_page.dart';
|
export 'src/presentation/product_publishing_page.dart';
|
||||||
|
export 'src/presentation/widgets/product_draft_card.dart';
|
||||||
|
export 'src/presentation/widgets/product_preview_panel.dart';
|
||||||
|
export 'src/presentation/widgets/publish_status_chip.dart';
|
||||||
|
export 'src/presentation/widgets/status_action_snack_bar.dart';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue