Merge feat/multi-select-groundwork into main (Stage 3A complete)
Publish Docs / publish-docs (push) Successful in 1m26s
Details
Publish Docs / publish-docs (push) Successful in 1m26s
Details
This commit is contained in:
commit
f3fbbca06d
|
|
@ -81,6 +81,7 @@ Rules:
|
||||||
- ✅ Category-only product edit landed (Stage 1B complete — merged `feat/category-only-edit` → `main` at `8e7e4cb`, 2026-04-11). Stage 1 complete.
|
- ✅ Category-only product edit landed (Stage 1B complete — merged `feat/category-only-edit` → `main` at `8e7e4cb`, 2026-04-11). Stage 1 complete.
|
||||||
- ✅ Post-write consistency hardening landed (Stage 2A complete — merged `feat/post-write-consistency` → `main` at `7acff83`, 2026-04-11).
|
- ✅ Post-write consistency hardening landed (Stage 2A complete — merged `feat/post-write-consistency` → `main` at `7acff83`, 2026-04-11).
|
||||||
- ✅ Publishing workflow UX hardening landed (Stage 2B complete — merged `feat/publishing-ux-hardening` → `main` at `b81016d`, 2026-04-11). Stage 2 complete.
|
- ✅ Publishing workflow UX hardening landed (Stage 2B complete — merged `feat/publishing-ux-hardening` → `main` at `b81016d`, 2026-04-11). Stage 2 complete.
|
||||||
|
- ✅ Multi-select groundwork landed (Stage 3A complete — merged `feat/multi-select-groundwork` → `main`, 2026-05-22).
|
||||||
|
|
||||||
### Current narrow edit capabilities on `main`
|
### Current narrow edit capabilities on `main`
|
||||||
|
|
||||||
|
|
@ -95,14 +96,14 @@ Rules:
|
||||||
- `dart analyze` clean
|
- `dart analyze` clean
|
||||||
- `feature_wordpress` tests passing
|
- `feature_wordpress` tests passing
|
||||||
- `kell_web` dashboard tests passing
|
- `kell_web` dashboard tests passing
|
||||||
- latest reported count for `feature_wordpress`: `247/247 passed`
|
- latest reported count for `feature_wordpress`: `262/262 passed`
|
||||||
- latest reported count for `kell_web` dashboard tests: `5/5 passed`
|
- latest reported count for `kell_web` dashboard tests: `5/5 passed`
|
||||||
- baseline commit: `b81016d` (2026-04-11)
|
- baseline commit: merge of `feat/multi-select-groundwork` (2026-05-22)
|
||||||
|
|
||||||
### Next recommended branch
|
### Next recommended branch
|
||||||
|
|
||||||
**`feat/multi-select-groundwork`** — Stage 3A: Multi-select groundwork (read/state only first).
|
**`feat/list-efficiency-improvements`** — Stage 3B: List efficiency improvements.
|
||||||
Branch from `main` at `b81016d`. Stage 2 (operational hardening) is complete.
|
Branch from latest `main`. Stage 3A (multi-select groundwork) is complete.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -184,24 +185,10 @@ Increase throughput for product triage and management using existing data.
|
||||||
- `feat/multi-select-groundwork`
|
- `feat/multi-select-groundwork`
|
||||||
- `feat/list-efficiency-improvements`
|
- `feat/list-efficiency-improvements`
|
||||||
|
|
||||||
#### Stage 3A — Multi-select groundwork (read/state only first)
|
#### ~~Stage 3A — Multi-select groundwork (read/state only first)~~ ✅ COMPLETE
|
||||||
|
|
||||||
##### Goal
|
> Merged `feat/multi-select-groundwork` → `main` (2026-05-22).
|
||||||
|
> Added multi-selection state to `ProductPublishingController` (`toggleMultiSelect`, `clearMultiSelection`, `selectAllVisible`, `isMultiSelected`, `multiSelectedIds`, `multiSelectedCount`). Updated `ProductDraftCard` with optional leading checkbox for multi-select mode. Added `_MultiSelectBar` to `ProductPublishingPage` with selected-count display, Select All, and Clear actions. Single-item preview selection preserved independently. 15 new multi-select tests added (262 total `feature_wordpress` tests passing). No bulk writes introduced.
|
||||||
Prepare for future bulk actions without implementing bulk writes yet.
|
|
||||||
|
|
||||||
##### Requirements
|
|
||||||
|
|
||||||
- add selection model for multiple items
|
|
||||||
- show selected-count UI
|
|
||||||
- allow clear selection
|
|
||||||
- preserve current single-item preview behavior where appropriate
|
|
||||||
- do not add bulk publish/edit/delete actions yet
|
|
||||||
|
|
||||||
##### Definition of done
|
|
||||||
|
|
||||||
- multi-select state exists and is tested
|
|
||||||
- no bulk writes introduced
|
|
||||||
|
|
||||||
#### Stage 3B — List efficiency improvements
|
#### Stage 3B — List efficiency improvements
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,46 @@ class ProductPublishingController extends ChangeNotifier {
|
||||||
/// Whether the current sort is ascending (`true`) or descending (`false`).
|
/// Whether the current sort is ascending (`true`) or descending (`false`).
|
||||||
bool sortAscending = true;
|
bool sortAscending = true;
|
||||||
|
|
||||||
|
/// Product IDs that the operator has selected for potential bulk actions.
|
||||||
|
///
|
||||||
|
/// Multi-select is independent of the single-item [selectedDraft] preview.
|
||||||
|
/// Adding or removing items here does not change which product is shown
|
||||||
|
/// in the detail panel.
|
||||||
|
final Set<String> multiSelectedIds = {};
|
||||||
|
|
||||||
|
/// The number of products currently multi-selected.
|
||||||
|
int get multiSelectedCount => multiSelectedIds.length;
|
||||||
|
|
||||||
|
/// Whether multi-select mode is active (at least one item selected).
|
||||||
|
bool get isMultiSelectActive => multiSelectedIds.isNotEmpty;
|
||||||
|
|
||||||
|
/// Whether the product with [id] is part of the multi-selection.
|
||||||
|
bool isMultiSelected(String id) => multiSelectedIds.contains(id);
|
||||||
|
|
||||||
|
/// Toggles the multi-select state of the product with [id].
|
||||||
|
///
|
||||||
|
/// If the product is already selected it is removed; otherwise it is added.
|
||||||
|
void toggleMultiSelect(String id) {
|
||||||
|
if (multiSelectedIds.contains(id)) {
|
||||||
|
multiSelectedIds.remove(id);
|
||||||
|
} else {
|
||||||
|
multiSelectedIds.add(id);
|
||||||
|
}
|
||||||
|
_safeNotify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects all currently visible products (those in [drafts]).
|
||||||
|
void selectAllVisible() {
|
||||||
|
multiSelectedIds.addAll(drafts.map((d) => d.id));
|
||||||
|
_safeNotify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the entire multi-selection.
|
||||||
|
void clearMultiSelection() {
|
||||||
|
multiSelectedIds.clear();
|
||||||
|
_safeNotify();
|
||||||
|
}
|
||||||
|
|
||||||
/// Product IDs that currently have an in-flight status update.
|
/// Product IDs that currently have an in-flight status update.
|
||||||
///
|
///
|
||||||
/// Used to prevent duplicate clicks and to let the UI show per-row
|
/// Used to prevent duplicate clicks and to let the UI show per-row
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,14 @@ class _ProductPublishingPageState extends State<ProductPublishingPage> {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// ── Multi-select action bar ──────────────────────────────────
|
||||||
|
if (controller.isMultiSelectActive)
|
||||||
|
_MultiSelectBar(
|
||||||
|
selectedCount: controller.multiSelectedCount,
|
||||||
|
totalCount: count,
|
||||||
|
onSelectAll: controller.selectAllVisible,
|
||||||
|
onClearSelection: controller.clearMultiSelection,
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: KcSpacing.xs, bottom: KcSpacing.sm),
|
padding: const EdgeInsets.only(left: KcSpacing.xs, bottom: KcSpacing.sm),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -223,6 +231,8 @@ class _ProductPublishingPageState extends State<ProductPublishingPage> {
|
||||||
draft: draft,
|
draft: draft,
|
||||||
isSelected: draft.id == controller.selectedDraft?.id,
|
isSelected: draft.id == controller.selectedDraft?.id,
|
||||||
onTap: () => controller.selectDraft(draft),
|
onTap: () => controller.selectDraft(draft),
|
||||||
|
isMultiSelected: controller.isMultiSelected(draft.id),
|
||||||
|
onMultiSelectToggle: () => controller.toggleMultiSelect(draft.id),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -262,3 +272,53 @@ class _ProductPublishingPageState extends State<ProductPublishingPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A compact action bar shown above the product list when multi-select is
|
||||||
|
/// active. Displays the count of selected items and provides Select All /
|
||||||
|
/// Clear Selection actions.
|
||||||
|
///
|
||||||
|
/// No bulk-write actions are included — this is groundwork only.
|
||||||
|
class _MultiSelectBar extends StatelessWidget {
|
||||||
|
final int selectedCount;
|
||||||
|
final int totalCount;
|
||||||
|
final VoidCallback onSelectAll;
|
||||||
|
final VoidCallback onClearSelection;
|
||||||
|
|
||||||
|
const _MultiSelectBar({
|
||||||
|
required this.selectedCount,
|
||||||
|
required this.totalCount,
|
||||||
|
required this.onSelectAll,
|
||||||
|
required this.onClearSelection,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final allSelected = selectedCount == totalCount;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: KcSpacing.sm, vertical: KcSpacing.xs),
|
||||||
|
margin: const EdgeInsets.only(bottom: KcSpacing.sm),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: KcColors.denimBlue.withValues(alpha: 0.08),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: KcColors.denimBlue.withValues(alpha: 0.3)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.checklist, size: 18, color: KcColors.denimBlue),
|
||||||
|
const SizedBox(width: KcSpacing.xs),
|
||||||
|
Text(
|
||||||
|
'$selectedCount selected',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: KcColors.denimBlue,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (!allSelected) TextButton(onPressed: onSelectAll, child: const Text('Select All')),
|
||||||
|
TextButton(onPressed: onClearSelection, child: const Text('Clear')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,36 @@ import 'publish_status_chip.dart';
|
||||||
/// A card displaying a summary of a [ProductDraft].
|
/// A card displaying a summary of a [ProductDraft].
|
||||||
///
|
///
|
||||||
/// Shows the product name, SKU, price, category, and publish status.
|
/// Shows the product name, SKU, price, category, and publish status.
|
||||||
/// Highlights when [isSelected] is true.
|
/// Highlights when [isSelected] is true (single-item preview selection).
|
||||||
|
///
|
||||||
|
/// When [isMultiSelected] is non-null, a leading checkbox is shown to
|
||||||
|
/// support multi-select mode. Tapping the checkbox fires [onMultiSelectToggle];
|
||||||
|
/// tapping the rest of the card still fires [onTap] for single-item preview.
|
||||||
class ProductDraftCard extends StatelessWidget {
|
class ProductDraftCard extends StatelessWidget {
|
||||||
final ProductDraft draft;
|
final ProductDraft draft;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
const ProductDraftCard({super.key, required this.draft, this.isSelected = false, this.onTap});
|
/// Whether this card is part of the multi-selection.
|
||||||
|
/// When `null`, the checkbox is hidden (multi-select not active).
|
||||||
|
final bool? isMultiSelected;
|
||||||
|
|
||||||
|
/// Called when the multi-select checkbox is toggled.
|
||||||
|
final VoidCallback? onMultiSelectToggle;
|
||||||
|
|
||||||
|
const ProductDraftCard({
|
||||||
|
super.key,
|
||||||
|
required this.draft,
|
||||||
|
this.isSelected = false,
|
||||||
|
this.onTap,
|
||||||
|
this.isMultiSelected,
|
||||||
|
this.onMultiSelectToggle,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final showCheckbox = isMultiSelected != null;
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
|
|
@ -33,6 +53,17 @@ class ProductDraftCard extends StatelessWidget {
|
||||||
BoxShadow(blurRadius: 8, offset: Offset(0, 2), color: Color(0x11000000)),
|
BoxShadow(blurRadius: 8, offset: Offset(0, 2), color: Color(0x11000000)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (showCheckbox) ...[
|
||||||
|
Checkbox(
|
||||||
|
value: isMultiSelected ?? false,
|
||||||
|
onChanged: (_) => onMultiSelectToggle?.call(),
|
||||||
|
),
|
||||||
|
const SizedBox(width: KcSpacing.xs),
|
||||||
|
],
|
||||||
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -56,7 +87,9 @@ class ProductDraftCard extends StatelessWidget {
|
||||||
const SizedBox(width: KcSpacing.sm),
|
const SizedBox(width: KcSpacing.sm),
|
||||||
Text(
|
Text(
|
||||||
draft.category,
|
draft.category,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: KcColors.neutral),
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyMedium?.copyWith(color: KcColors.neutral),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -67,13 +100,18 @@ class ProductDraftCard extends StatelessWidget {
|
||||||
PublishStatusChip(status: draft.status),
|
PublishStatusChip(status: draft.status),
|
||||||
Text(
|
Text(
|
||||||
'${draft.lastModified.year}-${draft.lastModified.month.toString().padLeft(2, '0')}-${draft.lastModified.day.toString().padLeft(2, '0')}',
|
'${draft.lastModified.year}-${draft.lastModified.month.toString().padLeft(2, '0')}-${draft.lastModified.day.toString().padLeft(2, '0')}',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: KcColors.neutral),
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall?.copyWith(color: KcColors.neutral),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1017,6 +1017,203 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Multi-select groundwork ─────────────────────────────────────────
|
||||||
|
|
||||||
|
group('multiSelect', () {
|
||||||
|
test('starts with empty multi-selection', () {
|
||||||
|
expect(controller.multiSelectedIds, isEmpty);
|
||||||
|
expect(controller.multiSelectedCount, 0);
|
||||||
|
expect(controller.isMultiSelectActive, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toggleMultiSelect adds an id', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
controller.toggleMultiSelect('1');
|
||||||
|
|
||||||
|
expect(controller.isMultiSelected('1'), true);
|
||||||
|
expect(controller.multiSelectedCount, 1);
|
||||||
|
expect(controller.isMultiSelectActive, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toggleMultiSelect removes an already-selected id', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
controller.toggleMultiSelect('1');
|
||||||
|
expect(controller.isMultiSelected('1'), true);
|
||||||
|
|
||||||
|
controller.toggleMultiSelect('1');
|
||||||
|
expect(controller.isMultiSelected('1'), false);
|
||||||
|
expect(controller.multiSelectedCount, 0);
|
||||||
|
expect(controller.isMultiSelectActive, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multiple ids can be multi-selected', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
controller.toggleMultiSelect('1');
|
||||||
|
controller.toggleMultiSelect('3');
|
||||||
|
controller.toggleMultiSelect('5');
|
||||||
|
|
||||||
|
expect(controller.multiSelectedCount, 3);
|
||||||
|
expect(controller.isMultiSelected('1'), true);
|
||||||
|
expect(controller.isMultiSelected('3'), true);
|
||||||
|
expect(controller.isMultiSelected('5'), true);
|
||||||
|
expect(controller.isMultiSelected('2'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clearMultiSelection removes all multi-selected ids', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
controller.toggleMultiSelect('1');
|
||||||
|
controller.toggleMultiSelect('2');
|
||||||
|
controller.toggleMultiSelect('3');
|
||||||
|
expect(controller.multiSelectedCount, 3);
|
||||||
|
|
||||||
|
controller.clearMultiSelection();
|
||||||
|
|
||||||
|
expect(controller.multiSelectedIds, isEmpty);
|
||||||
|
expect(controller.multiSelectedCount, 0);
|
||||||
|
expect(controller.isMultiSelectActive, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('selectAllVisible selects all visible draft ids', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
controller.selectAllVisible();
|
||||||
|
|
||||||
|
expect(controller.multiSelectedCount, 6);
|
||||||
|
for (final draft in controller.drafts) {
|
||||||
|
expect(controller.isMultiSelected(draft.id), true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('selectAllVisible respects active filter', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
controller.setFilter('draft');
|
||||||
|
expect(controller.drafts.length, 2);
|
||||||
|
|
||||||
|
controller.selectAllVisible();
|
||||||
|
|
||||||
|
expect(controller.multiSelectedCount, 2);
|
||||||
|
expect(controller.isMultiSelected('4'), true);
|
||||||
|
expect(controller.isMultiSelected('5'), true);
|
||||||
|
// Non-visible ids should not be selected.
|
||||||
|
expect(controller.isMultiSelected('1'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multi-select is independent of single-item preview selection', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
// Multi-select two items.
|
||||||
|
controller.toggleMultiSelect('1');
|
||||||
|
controller.toggleMultiSelect('3');
|
||||||
|
|
||||||
|
// Single-select a different item for preview.
|
||||||
|
controller.selectDraft(controller.drafts.firstWhere((d) => d.id == '5'));
|
||||||
|
|
||||||
|
// Preview selection should be independent.
|
||||||
|
expect(controller.selectedDraft!.id, '5');
|
||||||
|
expect(controller.isMultiSelected('5'), false);
|
||||||
|
expect(controller.multiSelectedCount, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multi-selection persists across load cycles', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
controller.toggleMultiSelect('2');
|
||||||
|
controller.toggleMultiSelect('4');
|
||||||
|
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
expect(controller.isMultiSelected('2'), true);
|
||||||
|
expect(controller.isMultiSelected('4'), true);
|
||||||
|
expect(controller.multiSelectedCount, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multi-selection persists after write operations', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
controller.toggleMultiSelect('1');
|
||||||
|
controller.toggleMultiSelect('4');
|
||||||
|
|
||||||
|
// Update price on product 4.
|
||||||
|
await controller.updatePrice('4', 20.00);
|
||||||
|
|
||||||
|
// Multi-selection should still be intact.
|
||||||
|
expect(controller.isMultiSelected('1'), true);
|
||||||
|
expect(controller.isMultiSelected('4'), true);
|
||||||
|
expect(controller.multiSelectedCount, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toggleMultiSelect notifies listeners', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
int notifyCount = 0;
|
||||||
|
controller.addListener(() => notifyCount++);
|
||||||
|
|
||||||
|
controller.toggleMultiSelect('1');
|
||||||
|
expect(notifyCount, 1);
|
||||||
|
|
||||||
|
controller.toggleMultiSelect('1');
|
||||||
|
expect(notifyCount, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clearMultiSelection notifies listeners', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
controller.toggleMultiSelect('1');
|
||||||
|
|
||||||
|
int notifyCount = 0;
|
||||||
|
controller.addListener(() => notifyCount++);
|
||||||
|
|
||||||
|
controller.clearMultiSelection();
|
||||||
|
expect(notifyCount, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('selectAllVisible notifies listeners', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
int notifyCount = 0;
|
||||||
|
controller.addListener(() => notifyCount++);
|
||||||
|
|
||||||
|
controller.selectAllVisible();
|
||||||
|
expect(notifyCount, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('selectAllVisible with search selects only matching products', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
controller.setSearchQuery('coaster');
|
||||||
|
expect(controller.drafts.length, 2);
|
||||||
|
|
||||||
|
controller.selectAllVisible();
|
||||||
|
|
||||||
|
expect(controller.multiSelectedCount, 2);
|
||||||
|
expect(controller.isMultiSelected('2'), true); // Citrus Coaster Set
|
||||||
|
expect(controller.isMultiSelected('6'), true); // Sublimated Slate Coaster
|
||||||
|
expect(controller.isMultiSelected('1'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('selectAllVisible is additive to existing selection', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
// Select one product manually.
|
||||||
|
controller.toggleMultiSelect('1');
|
||||||
|
|
||||||
|
// Filter to drafts only and select all visible.
|
||||||
|
controller.setFilter('draft');
|
||||||
|
controller.selectAllVisible();
|
||||||
|
|
||||||
|
// Should have the manually selected + the two drafts.
|
||||||
|
expect(controller.isMultiSelected('1'), true);
|
||||||
|
expect(controller.isMultiSelected('4'), true);
|
||||||
|
expect(controller.isMultiSelected('5'), true);
|
||||||
|
expect(controller.multiSelectedCount, 3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
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