Compare commits
No commits in common. "f3fbbca06d1945e1d7c1855cbece7e7b2ae1dccc" and "49a3702cecac8cd5e35f8821a72c4403e58b5316" have entirely different histories.
f3fbbca06d
...
49a3702cec
|
|
@ -81,7 +81,6 @@ 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`
|
||||||
|
|
||||||
|
|
@ -96,14 +95,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`: `262/262 passed`
|
- latest reported count for `feature_wordpress`: `247/247 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: merge of `feat/multi-select-groundwork` (2026-05-22)
|
- baseline commit: `b81016d` (2026-04-11)
|
||||||
|
|
||||||
### Next recommended branch
|
### Next recommended branch
|
||||||
|
|
||||||
**`feat/list-efficiency-improvements`** — Stage 3B: List efficiency improvements.
|
**`feat/multi-select-groundwork`** — Stage 3A: Multi-select groundwork (read/state only first).
|
||||||
Branch from latest `main`. Stage 3A (multi-select groundwork) is complete.
|
Branch from `main` at `b81016d`. Stage 2 (operational hardening) is complete.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -185,10 +184,24 @@ 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)~~ ✅ COMPLETE
|
#### Stage 3A — Multi-select groundwork (read/state only first)
|
||||||
|
|
||||||
> Merged `feat/multi-select-groundwork` → `main` (2026-05-22).
|
##### Goal
|
||||||
> 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,46 +157,6 @@ 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,14 +204,6 @@ 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(
|
||||||
|
|
@ -231,8 +223,6 @@ 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),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -272,53 +262,3 @@ 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,36 +7,16 @@ 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 (single-item preview selection).
|
/// Highlights when [isSelected] is true.
|
||||||
///
|
|
||||||
/// 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;
|
||||||
|
|
||||||
/// Whether this card is part of the multi-selection.
|
const ProductDraftCard({super.key, required this.draft, this.isSelected = false, this.onTap});
|
||||||
/// 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(
|
||||||
|
|
@ -53,17 +33,6 @@ 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: [
|
||||||
|
|
@ -87,9 +56,7 @@ class ProductDraftCard extends StatelessWidget {
|
||||||
const SizedBox(width: KcSpacing.sm),
|
const SizedBox(width: KcSpacing.sm),
|
||||||
Text(
|
Text(
|
||||||
draft.category,
|
draft.category,
|
||||||
style: Theme.of(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: KcColors.neutral),
|
||||||
context,
|
|
||||||
).textTheme.bodyMedium?.copyWith(color: KcColors.neutral),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -100,18 +67,13 @@ 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(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: KcColors.neutral),
|
||||||
context,
|
|
||||||
).textTheme.bodySmall?.copyWith(color: KcColors.neutral),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1017,203 +1017,6 @@ 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