Compare commits
No commits in common. "2c3ed3b926160e7d63116f6bb2b50021a5ded809" and "8aec65b46b0a76fc3696e72cb5c776568a552d04" have entirely different histories.
2c3ed3b926
...
8aec65b46b
|
|
@ -2,9 +2,10 @@
|
||||||
|
|
||||||
## Current status
|
## Current status
|
||||||
|
|
||||||
- main baseline updated through: bulk-status-actions (Stage 7A in progress)
|
- main baseline updated through: android-mobile-ux-hardening (Stage 6B complete)
|
||||||
- next branch: feat/bulk-status-actions (Stage 7A)
|
- main baseline commit: merge of `feat/android-mobile-ux-hardening` (2026-05-30)
|
||||||
- current stage: Stage 7A — Bulk Status Actions
|
- next branch: feat/bulk-status-actions (Stage 7)
|
||||||
|
- current stage: Stage 6 complete — Stage 7 next
|
||||||
|
|
||||||
## Slice tracker
|
## Slice tracker
|
||||||
|
|
||||||
|
|
@ -214,7 +215,7 @@
|
||||||
|
|
||||||
### feat/android-mobile-ux-hardening
|
### feat/android-mobile-ux-hardening
|
||||||
|
|
||||||
- status: merged to main
|
- status: ready for merge
|
||||||
- date: 2026-05-30
|
- date: 2026-05-30
|
||||||
- inspection: complete
|
- inspection: complete
|
||||||
- implementation: complete
|
- implementation: complete
|
||||||
|
|
@ -225,18 +226,3 @@
|
||||||
- tests: passed (300/300 feature_wordpress, 14/14 kell_mobile, 24/24 kell_web, 41/41 design_system — 379 total)
|
- tests: passed (300/300 feature_wordpress, 14/14 kell_mobile, 24/24 kell_web, 41/41 design_system — 379 total)
|
||||||
- analyze: passed
|
- analyze: passed
|
||||||
- brief updated: yes
|
- brief updated: yes
|
||||||
|
|
||||||
### feat/bulk-status-actions
|
|
||||||
|
|
||||||
- status: ready for merge
|
|
||||||
- date: 2026-05-30
|
|
||||||
- inspection: complete
|
|
||||||
- implementation: complete
|
|
||||||
- files changed:
|
|
||||||
- `feature_wordpress/lib/src/application/product_publishing_controller.dart` — added `BulkActionResult` value class with `successCount`, `failureCount`, `targetStatus`, `failedProductNames`, `totalCount`, `allSucceeded`; added `lastBulkActionResult`, `consumeBulkActionResult()`, and `bulkUpdateStatus()` method that processes selected products sequentially with per-row updating state, then reloads once and clears multi-selection
|
|
||||||
- `feature_wordpress/lib/src/presentation/widgets/status_action_snack_bar.dart` — added `showBulkActionSnackBar()` helper with success/partial-failure/total-failure variants showing count summaries and up to 3 failed product names
|
|
||||||
- `feature_wordpress/lib/src/presentation/product_publishing_page.dart` — added `_confirmBulkMoveToDraft()` confirmation dialog; updated `_MultiSelectBar` with `onBulkMoveToDraft` callback and "Move to Draft" `OutlinedButton.icon`; added bulk result listener in `_onControllerChanged`
|
|
||||||
- `feature_wordpress/test/product_publishing_controller_test.dart` — added 11 new `bulkUpdateStatus` tests: no-op on empty selection, moves all to draft, sets result on all-success, clears multi-selection, clears updatingIds, handles mixed statuses, consume clears result, starts null, preserves preview selection, persists filter/sort, disposal safety
|
|
||||||
- tests: passed (311/311 feature_wordpress, 24/24 kell_web — 335+ total)
|
|
||||||
- analyze: passed (info-level only — 8 pre-existing unnecessary_import in test files)
|
|
||||||
- brief updated: pending
|
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,6 @@ Rules:
|
||||||
- ✅ Cross-platform shell composition strategy landed (Stage 4B complete — merged `feat/shared-composition-pattern` → `main`, 2026-05-22).
|
- ✅ Cross-platform shell composition strategy landed (Stage 4B complete — merged `feat/shared-composition-pattern` → `main`, 2026-05-22).
|
||||||
- ✅ Flutter CI/CD pipeline landed (Stage 4C complete — merged `feat/flutter-cicd` → `main`, 2026-05-22).
|
- ✅ Flutter CI/CD pipeline landed (Stage 4C complete — merged `feat/flutter-cicd` → `main`, 2026-05-22).
|
||||||
- ✅ Test coverage visibility landed (Stage 4D complete — merged `feat/test-coverage-visibility` → `main`, 2026-05-22).
|
- ✅ Test coverage visibility landed (Stage 4D complete — merged `feat/test-coverage-visibility` → `main`, 2026-05-22).
|
||||||
- ✅ Bulk move-to-draft action landed (Stage 7A complete — merged `feat/bulk-status-actions` → `main`, 2026-05-30).
|
|
||||||
|
|
||||||
### Current narrow edit capabilities on `main`
|
### Current narrow edit capabilities on `main`
|
||||||
|
|
||||||
|
|
@ -130,7 +129,7 @@ Rules:
|
||||||
- `kell_mobile` tests passing
|
- `kell_mobile` tests passing
|
||||||
- latest reported count for `core`: `20/20 passed`
|
- latest reported count for `core`: `20/20 passed`
|
||||||
- latest reported count for `design_system`: `41/41 passed`
|
- latest reported count for `design_system`: `41/41 passed`
|
||||||
- latest reported count for `feature_wordpress`: `311/311 passed`
|
- latest reported count for `feature_wordpress`: `300/300 passed`
|
||||||
- latest reported count for `kell_web`: `24/24 passed`
|
- latest reported count for `kell_web`: `24/24 passed`
|
||||||
- latest reported count for `kell_mobile`: `14/14 passed`
|
- latest reported count for `kell_mobile`: `14/14 passed`
|
||||||
- baseline commit: merge of `feat/android-mobile-ux-hardening` (2026-05-30)
|
- baseline commit: merge of `feat/android-mobile-ux-hardening` (2026-05-30)
|
||||||
|
|
@ -431,24 +430,18 @@ Add carefully scoped bulk operations once single-item workflows are stable acros
|
||||||
- `feat/bulk-status-actions`
|
- `feat/bulk-status-actions`
|
||||||
- `feat/bulk-operator-workflows`
|
- `feat/bulk-operator-workflows`
|
||||||
|
|
||||||
#### Entry criteria ✅ ALL MET
|
#### Entry criteria
|
||||||
|
|
||||||
- ✅ single-item edit/status flows are stable
|
Do not begin until:
|
||||||
- ✅ multi-select groundwork is complete
|
|
||||||
- ✅ post-write consistency is hardened
|
|
||||||
- ✅ Android core publishing surface exists
|
|
||||||
|
|
||||||
#### ~~Stage 7A — Bulk move-to-draft (web)~~ ✅ COMPLETE
|
- single-item edit/status flows are stable
|
||||||
|
- multi-select groundwork is complete
|
||||||
|
- post-write consistency is hardened
|
||||||
|
- Android core publishing surface exists
|
||||||
|
|
||||||
> Merged `feat/bulk-status-actions` → `main` (2026-05-30).
|
#### Candidate first bulk action
|
||||||
> Added `BulkActionResult` value class, `bulkUpdateStatus()` controller method (sequential processing with per-row updating state, single reload after completion, auto-clear multi-selection), `showBulkActionSnackBar()` with success/partial-failure/total-failure messaging. Updated `_MultiSelectBar` with "Move to Draft" button and confirmation dialog. 11 new bulk action tests added (311 total `feature_wordpress` tests passing, 24 `kell_web` tests passing).
|
|
||||||
|
|
||||||
#### Stage 7B — Bulk operator workflows (future)
|
- bulk move to draft
|
||||||
|
|
||||||
Candidate additional bulk actions (implement one per slice):
|
|
||||||
|
|
||||||
- bulk publish
|
|
||||||
- bulk move to pending review
|
|
||||||
|
|
||||||
#### Explicitly defer
|
#### Explicitly defer
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -114,36 +114,6 @@ class CategoryActionResult {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The outcome of a bulk status-change action (e.g. bulk move-to-draft).
|
|
||||||
///
|
|
||||||
/// Consumed once by the UI to show a SnackBar, then cleared.
|
|
||||||
class BulkActionResult {
|
|
||||||
/// The number of products that were successfully updated.
|
|
||||||
final int successCount;
|
|
||||||
|
|
||||||
/// The number of products that failed to update.
|
|
||||||
final int failureCount;
|
|
||||||
|
|
||||||
/// The target status that was applied.
|
|
||||||
final PublishStatus targetStatus;
|
|
||||||
|
|
||||||
/// Names of products that failed, for operator visibility.
|
|
||||||
final List<String> failedProductNames;
|
|
||||||
|
|
||||||
const BulkActionResult({
|
|
||||||
required this.successCount,
|
|
||||||
required this.failureCount,
|
|
||||||
required this.targetStatus,
|
|
||||||
this.failedProductNames = const [],
|
|
||||||
});
|
|
||||||
|
|
||||||
/// The total number of products that were attempted.
|
|
||||||
int get totalCount => successCount + failureCount;
|
|
||||||
|
|
||||||
/// Whether all attempted products were successfully updated.
|
|
||||||
bool get allSucceeded => failureCount == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Controller that manages the product publishing workspace state, including
|
/// Controller that manages the product publishing workspace state, including
|
||||||
/// filtering by publish status, free-text search, and draft selection.
|
/// filtering by publish status, free-text search, and draft selection.
|
||||||
class ProductPublishingController extends ChangeNotifier {
|
class ProductPublishingController extends ChangeNotifier {
|
||||||
|
|
@ -374,68 +344,6 @@ class ProductPublishingController extends ChangeNotifier {
|
||||||
lastCategoryResult = null;
|
lastCategoryResult = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The result of the last bulk status-change action.
|
|
||||||
///
|
|
||||||
/// Set after [bulkUpdateStatus] completes. The UI should read this once
|
|
||||||
/// to show feedback (e.g. a SnackBar) and then call
|
|
||||||
/// [consumeBulkActionResult] to clear it.
|
|
||||||
BulkActionResult? lastBulkActionResult;
|
|
||||||
|
|
||||||
/// Clears [lastBulkActionResult] so the same result is not shown twice.
|
|
||||||
void consumeBulkActionResult() {
|
|
||||||
lastBulkActionResult = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the publishing status of all [multiSelectedIds] to [status].
|
|
||||||
///
|
|
||||||
/// Processes each selected product sequentially to respect API rate limits.
|
|
||||||
/// Tracks per-row updating state via [updatingIds]. After all updates
|
|
||||||
/// complete (whether successful or not), reloads once and clears the
|
|
||||||
/// multi-selection.
|
|
||||||
///
|
|
||||||
/// Does nothing if no items are multi-selected.
|
|
||||||
Future<void> bulkUpdateStatus(PublishStatus status) async {
|
|
||||||
if (multiSelectedIds.isEmpty) return;
|
|
||||||
|
|
||||||
// Snapshot the ids to process — the set may be cleared during iteration.
|
|
||||||
final idsToProcess = List<String>.of(multiSelectedIds);
|
|
||||||
|
|
||||||
// Mark all as updating.
|
|
||||||
updatingIds.addAll(idsToProcess);
|
|
||||||
_safeNotify();
|
|
||||||
|
|
||||||
int successCount = 0;
|
|
||||||
int failureCount = 0;
|
|
||||||
final failedNames = <String>[];
|
|
||||||
|
|
||||||
for (final id in idsToProcess) {
|
|
||||||
if (_disposed) return;
|
|
||||||
try {
|
|
||||||
await _updateProductStatus(id, status);
|
|
||||||
successCount++;
|
|
||||||
} catch (_) {
|
|
||||||
failureCount++;
|
|
||||||
failedNames.add(_productNameById(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_disposed) return;
|
|
||||||
|
|
||||||
// Clean up updating state and multi-selection.
|
|
||||||
updatingIds.removeAll(idsToProcess);
|
|
||||||
multiSelectedIds.clear();
|
|
||||||
|
|
||||||
lastBulkActionResult = BulkActionResult(
|
|
||||||
successCount: successCount,
|
|
||||||
failureCount: failureCount,
|
|
||||||
targetStatus: status,
|
|
||||||
failedProductNames: failedNames,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Single reload after all updates.
|
|
||||||
await load();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads all product drafts and applies any current filter / search.
|
/// Loads all product drafts and applies any current filter / search.
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
|
|
||||||
|
|
@ -125,38 +125,6 @@ class _ProductPublishingPageState extends State<ProductPublishingPage> {
|
||||||
controller.consumeCategoryResult();
|
controller.consumeCategoryResult();
|
||||||
showCategoryActionSnackBar(context, categoryResult);
|
showCategoryActionSnackBar(context, categoryResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
final bulkResult = controller.lastBulkActionResult;
|
|
||||||
if (bulkResult != null) {
|
|
||||||
controller.consumeBulkActionResult();
|
|
||||||
showBulkActionSnackBar(context, bulkResult);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shows a confirmation dialog before executing bulk move-to-draft.
|
|
||||||
Future<void> _confirmBulkMoveToDraft(BuildContext context) async {
|
|
||||||
final count = controller.multiSelectedCount;
|
|
||||||
final confirmed = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
title: const Text('Move to Draft'),
|
|
||||||
content: Text('Move $count ${count == 1 ? 'product' : 'products'} to draft status?'),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
|
||||||
child: const Text('Cancel'),
|
|
||||||
),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
|
||||||
child: const Text('Move to Draft'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (confirmed == true) {
|
|
||||||
await controller.bulkUpdateStatus(PublishStatus.draft);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles keyboard events for list navigation.
|
/// Handles keyboard events for list navigation.
|
||||||
|
|
@ -270,7 +238,6 @@ class _ProductPublishingPageState extends State<ProductPublishingPage> {
|
||||||
totalCount: count,
|
totalCount: count,
|
||||||
onSelectAll: controller.selectAllVisible,
|
onSelectAll: controller.selectAllVisible,
|
||||||
onClearSelection: controller.clearMultiSelection,
|
onClearSelection: controller.clearMultiSelection,
|
||||||
onBulkMoveToDraft: () => _confirmBulkMoveToDraft(context),
|
|
||||||
),
|
),
|
||||||
// ── List header with count and density toggle ───────────────
|
// ── List header with count and density toggle ───────────────
|
||||||
Padding(
|
Padding(
|
||||||
|
|
@ -352,21 +319,21 @@ class _ProductPublishingPageState extends State<ProductPublishingPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A compact action bar shown above the product list when multi-select is
|
/// A compact action bar shown above the product list when multi-select is
|
||||||
/// active. Displays the count of selected items and provides bulk actions,
|
/// active. Displays the count of selected items and provides Select All /
|
||||||
/// Select All, and Clear Selection.
|
/// Clear Selection actions.
|
||||||
|
///
|
||||||
|
/// No bulk-write actions are included — this is groundwork only.
|
||||||
class _MultiSelectBar extends StatelessWidget {
|
class _MultiSelectBar extends StatelessWidget {
|
||||||
final int selectedCount;
|
final int selectedCount;
|
||||||
final int totalCount;
|
final int totalCount;
|
||||||
final VoidCallback onSelectAll;
|
final VoidCallback onSelectAll;
|
||||||
final VoidCallback onClearSelection;
|
final VoidCallback onClearSelection;
|
||||||
final VoidCallback onBulkMoveToDraft;
|
|
||||||
|
|
||||||
const _MultiSelectBar({
|
const _MultiSelectBar({
|
||||||
required this.selectedCount,
|
required this.selectedCount,
|
||||||
required this.totalCount,
|
required this.totalCount,
|
||||||
required this.onSelectAll,
|
required this.onSelectAll,
|
||||||
required this.onClearSelection,
|
required this.onClearSelection,
|
||||||
required this.onBulkMoveToDraft,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -392,12 +359,6 @@ class _MultiSelectBar extends StatelessWidget {
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: KcSpacing.sm),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: onBulkMoveToDraft,
|
|
||||||
icon: const Icon(Icons.drafts_outlined, size: 16),
|
|
||||||
label: const Text('Move to Draft'),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (!allSelected) TextButton(onPressed: onSelectAll, child: const Text('Select All')),
|
if (!allSelected) TextButton(onPressed: onSelectAll, child: const Text('Select All')),
|
||||||
TextButton(onPressed: onClearSelection, child: const Text('Clear')),
|
TextButton(onPressed: onClearSelection, child: const Text('Clear')),
|
||||||
|
|
|
||||||
|
|
@ -192,57 +192,6 @@ void showCategoryActionSnackBar(BuildContext context, CategoryActionResult resul
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows a [SnackBar] for the given [BulkActionResult].
|
|
||||||
///
|
|
||||||
/// Displays success count and, on partial failure, lists the names of
|
|
||||||
/// products that failed to update.
|
|
||||||
void showBulkActionSnackBar(BuildContext context, BulkActionResult result) {
|
|
||||||
final messenger = ScaffoldMessenger.maybeOf(context);
|
|
||||||
if (messenger == null) return;
|
|
||||||
|
|
||||||
final String message;
|
|
||||||
final Color backgroundColor;
|
|
||||||
final IconData icon;
|
|
||||||
|
|
||||||
final verb = _pastVerbForStatus(result.targetStatus);
|
|
||||||
|
|
||||||
if (result.allSucceeded) {
|
|
||||||
message =
|
|
||||||
'${result.successCount} ${result.successCount == 1 ? 'product' : 'products'} $verb successfully.';
|
|
||||||
backgroundColor = KcColors.success;
|
|
||||||
icon = Icons.check_circle_outline;
|
|
||||||
} else if (result.successCount == 0) {
|
|
||||||
message =
|
|
||||||
'Failed to ${_infinitiveVerbForStatus(result.targetStatus)} all ${result.failureCount} products.';
|
|
||||||
backgroundColor = KcColors.danger;
|
|
||||||
icon = Icons.error_outline;
|
|
||||||
} else {
|
|
||||||
final failedList = result.failedProductNames.take(3).join(', ');
|
|
||||||
final extra = result.failedProductNames.length > 3
|
|
||||||
? ' and ${result.failedProductNames.length - 3} more'
|
|
||||||
: '';
|
|
||||||
message = '${result.successCount} $verb, ${result.failureCount} failed ($failedList$extra).';
|
|
||||||
backgroundColor = KcColors.warning;
|
|
||||||
icon = Icons.warning_amber_outlined;
|
|
||||||
}
|
|
||||||
|
|
||||||
messenger.hideCurrentSnackBar();
|
|
||||||
messenger.showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, color: Colors.white, size: 20),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(child: Text(message)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
duration: result.allSucceeded ? const Duration(seconds: 3) : const Duration(seconds: 5),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shows a [SnackBar] for the given [NameActionResult].
|
/// Shows a [SnackBar] for the given [NameActionResult].
|
||||||
void showNameActionSnackBar(BuildContext context, NameActionResult result) {
|
void showNameActionSnackBar(BuildContext context, NameActionResult result) {
|
||||||
final messenger = ScaffoldMessenger.maybeOf(context);
|
final messenger = ScaffoldMessenger.maybeOf(context);
|
||||||
|
|
|
||||||
|
|
@ -1444,156 +1444,6 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Bulk status actions ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
group('bulkUpdateStatus', () {
|
|
||||||
test('does nothing when no items are multi-selected', () async {
|
|
||||||
await controller.load();
|
|
||||||
|
|
||||||
await controller.bulkUpdateStatus(PublishStatus.draft);
|
|
||||||
|
|
||||||
// No result should be set.
|
|
||||||
expect(controller.lastBulkActionResult, isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('moves all selected products to draft', () async {
|
|
||||||
await controller.load();
|
|
||||||
|
|
||||||
// Select two published products (ids 1 and 2).
|
|
||||||
controller.toggleMultiSelect('1');
|
|
||||||
controller.toggleMultiSelect('2');
|
|
||||||
expect(controller.multiSelectedCount, 2);
|
|
||||||
|
|
||||||
await controller.bulkUpdateStatus(PublishStatus.draft);
|
|
||||||
|
|
||||||
// Both should now be draft.
|
|
||||||
final p1 = controller.drafts.firstWhere((d) => d.id == '1');
|
|
||||||
final p2 = controller.drafts.firstWhere((d) => d.id == '2');
|
|
||||||
expect(p1.status, PublishStatus.draft);
|
|
||||||
expect(p2.status, PublishStatus.draft);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sets lastBulkActionResult on all-success', () async {
|
|
||||||
await controller.load();
|
|
||||||
|
|
||||||
controller.toggleMultiSelect('1');
|
|
||||||
controller.toggleMultiSelect('3');
|
|
||||||
await controller.bulkUpdateStatus(PublishStatus.draft);
|
|
||||||
|
|
||||||
expect(controller.lastBulkActionResult, isNotNull);
|
|
||||||
expect(controller.lastBulkActionResult!.successCount, 2);
|
|
||||||
expect(controller.lastBulkActionResult!.failureCount, 0);
|
|
||||||
expect(controller.lastBulkActionResult!.targetStatus, PublishStatus.draft);
|
|
||||||
expect(controller.lastBulkActionResult!.allSucceeded, isTrue);
|
|
||||||
expect(controller.lastBulkActionResult!.totalCount, 2);
|
|
||||||
expect(controller.lastBulkActionResult!.failedProductNames, isEmpty);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('clears multi-selection after bulk action', () async {
|
|
||||||
await controller.load();
|
|
||||||
|
|
||||||
controller.toggleMultiSelect('1');
|
|
||||||
controller.toggleMultiSelect('2');
|
|
||||||
controller.toggleMultiSelect('3');
|
|
||||||
expect(controller.multiSelectedCount, 3);
|
|
||||||
|
|
||||||
await controller.bulkUpdateStatus(PublishStatus.draft);
|
|
||||||
|
|
||||||
expect(controller.multiSelectedIds, isEmpty);
|
|
||||||
expect(controller.multiSelectedCount, 0);
|
|
||||||
expect(controller.isMultiSelectActive, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('clears updatingIds after bulk action', () async {
|
|
||||||
await controller.load();
|
|
||||||
|
|
||||||
controller.toggleMultiSelect('1');
|
|
||||||
controller.toggleMultiSelect('2');
|
|
||||||
|
|
||||||
await controller.bulkUpdateStatus(PublishStatus.draft);
|
|
||||||
|
|
||||||
expect(controller.updatingIds, isEmpty);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('handles mixed statuses — all become draft', () async {
|
|
||||||
await controller.load();
|
|
||||||
|
|
||||||
// Select one of each status type.
|
|
||||||
controller.toggleMultiSelect('1'); // published
|
|
||||||
controller.toggleMultiSelect('3'); // pendingReview
|
|
||||||
controller.toggleMultiSelect('4'); // draft (already)
|
|
||||||
controller.toggleMultiSelect('6'); // unpublished
|
|
||||||
|
|
||||||
await controller.bulkUpdateStatus(PublishStatus.draft);
|
|
||||||
|
|
||||||
for (final id in ['1', '3', '4', '6']) {
|
|
||||||
final product = controller.drafts.firstWhere((d) => d.id == id);
|
|
||||||
expect(product.status, PublishStatus.draft, reason: 'Product $id should be draft');
|
|
||||||
}
|
|
||||||
expect(controller.lastBulkActionResult!.successCount, 4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('consumeBulkActionResult clears the result', () async {
|
|
||||||
await controller.load();
|
|
||||||
|
|
||||||
controller.toggleMultiSelect('1');
|
|
||||||
await controller.bulkUpdateStatus(PublishStatus.draft);
|
|
||||||
expect(controller.lastBulkActionResult, isNotNull);
|
|
||||||
|
|
||||||
controller.consumeBulkActionResult();
|
|
||||||
expect(controller.lastBulkActionResult, isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('lastBulkActionResult starts as null', () {
|
|
||||||
expect(controller.lastBulkActionResult, isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('preserves single-item preview selection after bulk action', () async {
|
|
||||||
await controller.load();
|
|
||||||
|
|
||||||
// Select product 5 for preview.
|
|
||||||
controller.selectBySku('SH-SUN-005');
|
|
||||||
expect(controller.selectedDraft!.id, '5');
|
|
||||||
|
|
||||||
// Multi-select other products.
|
|
||||||
controller.toggleMultiSelect('1');
|
|
||||||
controller.toggleMultiSelect('2');
|
|
||||||
|
|
||||||
await controller.bulkUpdateStatus(PublishStatus.draft);
|
|
||||||
|
|
||||||
// Preview selection should still be on product 5.
|
|
||||||
expect(controller.selectedDraft, isNotNull);
|
|
||||||
expect(controller.selectedDraft!.id, '5');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('filter and sort persist after bulk action', () async {
|
|
||||||
await controller.load();
|
|
||||||
|
|
||||||
controller.setSort(ProductSortField.lastModified, ascending: false);
|
|
||||||
|
|
||||||
controller.toggleMultiSelect('1');
|
|
||||||
controller.toggleMultiSelect('2');
|
|
||||||
|
|
||||||
await controller.bulkUpdateStatus(PublishStatus.draft);
|
|
||||||
|
|
||||||
expect(controller.activeSortField, ProductSortField.lastModified);
|
|
||||||
expect(controller.sortAscending, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('does not notify after disposal during bulk action', () async {
|
|
||||||
await controller.load();
|
|
||||||
|
|
||||||
controller.toggleMultiSelect('1');
|
|
||||||
controller.toggleMultiSelect('2');
|
|
||||||
|
|
||||||
final future = controller.bulkUpdateStatus(PublishStatus.draft);
|
|
||||||
controller.dispose();
|
|
||||||
|
|
||||||
// Should complete without throwing.
|
|
||||||
await future;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('disposed guard', () {
|
group('disposed guard', () {
|
||||||
test('load does not notify after disposal', () async {
|
test('load does not notify after disposal', () async {
|
||||||
// Start load, then immediately dispose.
|
// Start load, then immediately dispose.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue