Compare commits
2 Commits
8e7e4cbc69
...
7acff83bf4
| Author | SHA1 | Date |
|---|---|---|
|
|
7acff83bf4 | |
|
|
cf0889d4a9 |
|
|
@ -2,16 +2,17 @@
|
||||||
|
|
||||||
## Current status
|
## Current status
|
||||||
|
|
||||||
- main baseline updated through: description-only-edit
|
- main baseline updated through: category-only-edit
|
||||||
- current branch: feat/category-only-edit (implementation complete, pending merge)
|
- main baseline commit: `8e7e4cb` (2026-04-11)
|
||||||
- next branch: feat/post-write-consistency
|
- next branch: feat/post-write-consistency
|
||||||
- current stage: Stage 1 — Web application completion (Stage 1B complete)
|
- current stage: Stage 2 — Web application operational hardening
|
||||||
|
|
||||||
## Slice tracker
|
## Slice tracker
|
||||||
|
|
||||||
### feat/description-only-edit
|
### feat/description-only-edit
|
||||||
|
|
||||||
- status: merged to main
|
- status: merged to main
|
||||||
|
- commit: `cebac4c`
|
||||||
- inspection: complete
|
- inspection: complete
|
||||||
- implementation: complete
|
- implementation: complete
|
||||||
- tests: passed (212/212)
|
- tests: passed (212/212)
|
||||||
|
|
@ -20,34 +21,22 @@
|
||||||
|
|
||||||
### feat/category-only-edit
|
### feat/category-only-edit
|
||||||
|
|
||||||
- status: implementation complete, pending merge to main
|
- status: merged to main
|
||||||
|
- commit: `8e7e4cb`
|
||||||
- inspection: complete
|
- inspection: complete
|
||||||
- implementation: complete
|
- implementation: complete
|
||||||
- tests: passed (223/223 feature_wordpress, 5/5 kell_web dashboard)
|
- tests: passed (223/223 feature_wordpress, 5/5 kell_web dashboard)
|
||||||
- analyze: passed (dart analyze — no issues found)
|
- analyze: passed (dart analyze — no issues found)
|
||||||
- brief updated: yes
|
- brief updated: yes
|
||||||
- changed files:
|
|
||||||
- `lib/src/domain/product_publishing_repository.dart` — added `updateProductCategory` contract
|
|
||||||
- `lib/src/application/update_product_category.dart` — new use case (created)
|
|
||||||
- `lib/src/application/product_publishing_controller.dart` — added `CategoryActionResult`, `updateCategory`, `lastCategoryResult`, `consumeCategoryResult`
|
|
||||||
- `lib/src/data/fake_product_publishing_repository.dart` — implemented `updateProductCategory`
|
|
||||||
- `lib/src/data/wordpress_product_publishing_repository.dart` — implemented `updateProductCategory` (WP API mapping)
|
|
||||||
- `lib/src/presentation/widgets/product_preview_panel.dart` — added inline category edit UI (`onCategoryChanged`, `_buildCategoryRow`)
|
|
||||||
- `lib/src/presentation/product_publishing_page.dart` — wired `UpdateProductCategory` use case and `onCategoryChanged` callback
|
|
||||||
- `lib/src/presentation/widgets/status_action_snack_bar.dart` — added `showCategoryActionSnackBar`
|
|
||||||
- `lib/feature_wordpress.dart` — barrel export for `update_product_category.dart`
|
|
||||||
- `test/product_publishing_controller_test.dart` — added `updateCategory` test group (5 tests)
|
|
||||||
- `test/fake_product_publishing_repository_test.dart` — added `updateProductCategory` test group (6 tests)
|
|
||||||
- `test/widgets/product_publishing_page_test.dart` — updated mock to include `updateProductCategory`
|
|
||||||
- `apps/kell_web/test/dashboard/application/dashboard_controller_test.dart` — updated mock to include `updateProductCategory`
|
|
||||||
- `docs/development/master_development_brief.md` — updated baseline, validation state, next branch
|
|
||||||
- `docs/development/build_execution_tracker.md` — this file
|
|
||||||
|
|
||||||
### feat/post-write-consistency
|
### feat/post-write-consistency
|
||||||
|
|
||||||
- status: queued (Stage 2A)
|
- status: implemented — ready for review / merge
|
||||||
- inspection: pending
|
- inspection: complete
|
||||||
- implementation: pending
|
- implementation: complete
|
||||||
- tests: pending
|
- files changed:
|
||||||
- analyze: pending
|
- `feature_wordpress/lib/src/application/product_publishing_controller.dart` — added `_refreshSelection()` helper; wired into `load()` and all write methods to preserve/refresh `selectedDraft` by id after reload
|
||||||
- brief updated: no
|
- `feature_wordpress/test/product_publishing_controller_test.dart` — added 11 post-write consistency tests covering selection preservation, field refresh (status/price/name/description/category), lastModified, filter/search/sort persistence after writes, item repositioning under active sort, and filter-exit auto-reselection
|
||||||
|
- tests: passed (234/234 feature_wordpress)
|
||||||
|
- analyze: passed (dart analyze — no issues found)
|
||||||
|
- brief updated: yes
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ Rules:
|
||||||
- Search/filter/sort refinement landed.
|
- Search/filter/sort refinement landed.
|
||||||
- Name-only product edit landed.
|
- Name-only product edit landed.
|
||||||
- ✅ Description-only product edit landed (Stage 1A complete — merged `feat/description-only-edit` → `main` at `cebac4c`, 2026-04-11).
|
- ✅ Description-only product edit landed (Stage 1A complete — merged `feat/description-only-edit` → `main` at `cebac4c`, 2026-04-11).
|
||||||
- ✅ Category-only product edit implemented (Stage 1B — `feat/category-only-edit`, pending merge to `main`, 2026-04-11).
|
- ✅ Category-only product edit landed (Stage 1B complete — merged `feat/category-only-edit` → `main` at `8e7e4cb`, 2026-04-11). Stage 1 complete.
|
||||||
|
|
||||||
### Current narrow edit capabilities on `main`
|
### Current narrow edit capabilities on `main`
|
||||||
|
|
||||||
|
|
@ -86,21 +86,28 @@ Rules:
|
||||||
- update product price only
|
- update product price only
|
||||||
- update product name only
|
- update product name only
|
||||||
- update product description only
|
- update product description only
|
||||||
- update product category only _(on `feat/category-only-edit`, pending merge)_
|
- update product category only
|
||||||
|
|
||||||
### Latest known validation state on `feat/category-only-edit`
|
### Latest known validation state on `main`
|
||||||
|
|
||||||
- `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`: `223/223 passed`
|
- latest reported count for `feature_wordpress`: `223/223 passed`
|
||||||
- latest reported count for `kell_web` dashboard tests: `5/5 passed`
|
- latest reported count for `kell_web` dashboard tests: `5/5 passed`
|
||||||
- baseline: branched from `main` at `cebac4c` (2026-04-11)
|
- baseline commit: `8e7e4cb` (2026-04-11)
|
||||||
|
|
||||||
|
### Post-write consistency (Stage 2A) — on branch `feat/post-write-consistency`
|
||||||
|
|
||||||
|
- `dart analyze` clean
|
||||||
|
- `feature_wordpress` tests: `234/234 passed`
|
||||||
|
- controller `_refreshSelection()` preserves/refreshes selection after all writes
|
||||||
|
- 11 new post-write consistency tests added
|
||||||
|
|
||||||
### Next recommended branch
|
### Next recommended branch
|
||||||
|
|
||||||
**`feat/post-write-consistency`** — Stage 2A: Post-write consistency hardening.
|
**`feat/publishing-ux-hardening`** — Stage 2B: Publishing workflow UX hardening.
|
||||||
Branch from `main` after merging `feat/category-only-edit`. This completes Stage 1 (controlled product editing) and moves to Stage 2 (operational hardening).
|
Branch from `main` after merging `feat/post-write-consistency`. Stage 2A is complete on branch.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -141,25 +148,10 @@ Use one branch per edit slice:
|
||||||
> Merged `feat/description-only-edit` → `main` at `cebac4c` (2026-04-11).
|
> Merged `feat/description-only-edit` → `main` at `cebac4c` (2026-04-11).
|
||||||
> All artifacts delivered: repository contract, fake/WP repo implementations, use case, controller action/result handling, preview panel inline description edit UI, and targeted tests (212 total `feature_wordpress` tests passing).
|
> All artifacts delivered: repository contract, fake/WP repo implementations, use case, controller action/result handling, preview panel inline description edit UI, and targeted tests (212 total `feature_wordpress` tests passing).
|
||||||
|
|
||||||
#### Stage 1B — Category-only product edit ← **NEXT**
|
#### ~~Stage 1B — Category-only product edit~~ ✅ COMPLETE
|
||||||
|
|
||||||
##### Goal
|
> Merged `feat/category-only-edit` → `main` at `8e7e4cb` (2026-04-11).
|
||||||
|
> All artifacts delivered: repository contract, fake/WP repo implementations, use case, controller action/result handling, preview panel inline category edit UI, snack bar feedback, and targeted tests (223 total `feature_wordpress` tests passing). Stage 1 complete.
|
||||||
Allow updating product category only.
|
|
||||||
|
|
||||||
##### Requirements
|
|
||||||
|
|
||||||
- Use existing category representation only.
|
|
||||||
- Do not create full taxonomy management.
|
|
||||||
- Keep write mapping narrow inside WP repository.
|
|
||||||
- Use a simple constrained UI.
|
|
||||||
- Reuse the established narrow single-field edit pattern.
|
|
||||||
|
|
||||||
##### Definition of done
|
|
||||||
|
|
||||||
- category can be updated through a controlled workflow
|
|
||||||
- no taxonomy subsystem added
|
|
||||||
- tests/analyze clean
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -534,9 +534,10 @@ class ProductPublishingController extends ChangeNotifier {
|
||||||
|
|
||||||
drafts = result;
|
drafts = result;
|
||||||
|
|
||||||
// Keep selection valid; clear if the selected draft is no longer visible.
|
// Keep selection valid and refresh to latest values.
|
||||||
if (selectedDraft != null && !drafts.any((d) => d.id == selectedDraft!.id)) {
|
if (selectedDraft != null) {
|
||||||
selectedDraft = null;
|
final refreshed = drafts.where((d) => d.id == selectedDraft!.id).firstOrNull;
|
||||||
|
selectedDraft = refreshed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -829,6 +829,194 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Post-write consistency ───────────────────────────────────────────
|
||||||
|
|
||||||
|
group('post-write consistency', () {
|
||||||
|
test('selection preserved after status update', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
// Select product 4 (draft).
|
||||||
|
controller.selectBySku('JG-BLU-004');
|
||||||
|
expect(controller.selectedDraft!.id, '4');
|
||||||
|
|
||||||
|
// Publish it.
|
||||||
|
await controller.updateStatus('4', PublishStatus.published);
|
||||||
|
|
||||||
|
// Selection should still point to the same product.
|
||||||
|
expect(controller.selectedDraft, isNotNull);
|
||||||
|
expect(controller.selectedDraft!.id, '4');
|
||||||
|
// And reflect the updated status.
|
||||||
|
expect(controller.selectedDraft!.status, PublishStatus.published);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('selected draft reflects latest price after update', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
controller.selectBySku('JG-BLU-004');
|
||||||
|
expect(controller.selectedDraft!.price, 8.50);
|
||||||
|
|
||||||
|
await controller.updatePrice('4', 12.00);
|
||||||
|
|
||||||
|
expect(controller.selectedDraft, isNotNull);
|
||||||
|
expect(controller.selectedDraft!.id, '4');
|
||||||
|
expect(controller.selectedDraft!.price, 12.00);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('selected draft reflects latest name after update', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
controller.selectBySku('JG-BLU-004');
|
||||||
|
expect(controller.selectedDraft!.name, 'Fabric Jar Gripper');
|
||||||
|
|
||||||
|
await controller.updateName('4', 'Premium Jar Gripper');
|
||||||
|
|
||||||
|
expect(controller.selectedDraft, isNotNull);
|
||||||
|
expect(controller.selectedDraft!.id, '4');
|
||||||
|
expect(controller.selectedDraft!.name, 'Premium Jar Gripper');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('selected draft reflects latest description after update', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
controller.selectBySku('JG-BLU-004');
|
||||||
|
final originalDesc = controller.selectedDraft!.description;
|
||||||
|
|
||||||
|
await controller.updateDescription('4', 'Completely new description.');
|
||||||
|
|
||||||
|
expect(controller.selectedDraft, isNotNull);
|
||||||
|
expect(controller.selectedDraft!.id, '4');
|
||||||
|
expect(controller.selectedDraft!.description, 'Completely new description.');
|
||||||
|
expect(controller.selectedDraft!.description, isNot(originalDesc));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('selected draft reflects latest category after update', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
controller.selectBySku('JG-BLU-004');
|
||||||
|
expect(controller.selectedDraft!.category, 'Kitchen Accessories');
|
||||||
|
|
||||||
|
await controller.updateCategory('4', 'Grippers');
|
||||||
|
|
||||||
|
expect(controller.selectedDraft, isNotNull);
|
||||||
|
expect(controller.selectedDraft!.id, '4');
|
||||||
|
expect(controller.selectedDraft!.category, 'Grippers');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lastModified updates after write', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
controller.selectBySku('JG-BLU-004');
|
||||||
|
final originalModified = controller.selectedDraft!.lastModified;
|
||||||
|
|
||||||
|
await controller.updatePrice('4', 15.00);
|
||||||
|
|
||||||
|
expect(controller.selectedDraft, isNotNull);
|
||||||
|
expect(controller.selectedDraft!.lastModified.isAfter(originalModified), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('selection preserved with active filter after write within filter', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
// Filter to drafts only.
|
||||||
|
controller.setFilter('draft');
|
||||||
|
expect(controller.drafts.length, 2);
|
||||||
|
|
||||||
|
// Select product 4 (a draft).
|
||||||
|
controller.selectBySku('JG-BLU-004');
|
||||||
|
expect(controller.selectedDraft!.id, '4');
|
||||||
|
|
||||||
|
// Update its price — it stays a draft, so it stays in the filter.
|
||||||
|
await controller.updatePrice('4', 20.00);
|
||||||
|
|
||||||
|
expect(controller.selectedDraft, isNotNull);
|
||||||
|
expect(controller.selectedDraft!.id, '4');
|
||||||
|
expect(controller.selectedDraft!.price, 20.00);
|
||||||
|
expect(controller.activeFilter, 'draft');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('selection moves when write causes item to leave active filter', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
// Filter to drafts only.
|
||||||
|
controller.setFilter('draft');
|
||||||
|
expect(controller.drafts.length, 2);
|
||||||
|
|
||||||
|
// Select product 4 (a draft).
|
||||||
|
controller.selectBySku('JG-BLU-004');
|
||||||
|
expect(controller.selectedDraft!.id, '4');
|
||||||
|
|
||||||
|
// Publish it — it leaves the 'draft' filter.
|
||||||
|
await controller.updateStatus('4', PublishStatus.published);
|
||||||
|
|
||||||
|
// The original selection left the filter; load() auto-selects the
|
||||||
|
// first remaining visible draft.
|
||||||
|
expect(controller.activeFilter, 'draft');
|
||||||
|
expect(controller.drafts.length, 1);
|
||||||
|
expect(controller.selectedDraft, isNotNull);
|
||||||
|
expect(controller.selectedDraft!.id, isNot('4'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('search persists after write', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
controller.setSearchQuery('fabric');
|
||||||
|
expect(controller.drafts.length, 1);
|
||||||
|
expect(controller.drafts.first.id, '4');
|
||||||
|
|
||||||
|
controller.selectDraft(controller.drafts.first);
|
||||||
|
|
||||||
|
await controller.updatePrice('4', 25.00);
|
||||||
|
|
||||||
|
// Search should still be active.
|
||||||
|
expect(controller.searchQuery, 'fabric');
|
||||||
|
expect(controller.drafts.length, 1);
|
||||||
|
expect(controller.selectedDraft, isNotNull);
|
||||||
|
expect(controller.selectedDraft!.price, 25.00);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sort persists after write', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
controller.setSort(ProductSortField.lastModified, ascending: false);
|
||||||
|
|
||||||
|
// Select the first item (most recently modified).
|
||||||
|
controller.selectDraft(controller.drafts.first);
|
||||||
|
|
||||||
|
// Update a different product's price — this changes its lastModified.
|
||||||
|
await controller.updatePrice('1', 99.00);
|
||||||
|
|
||||||
|
// Sort should still be lastModified descending.
|
||||||
|
expect(controller.activeSortField, ProductSortField.lastModified);
|
||||||
|
expect(controller.sortAscending, false);
|
||||||
|
|
||||||
|
// Product 1 should now be first (most recently modified).
|
||||||
|
expect(controller.drafts.first.id, '1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('item repositions under active sort after name change', () async {
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
// Default sort: name ascending.
|
||||||
|
// Order: Citrus, Fabric, Floral, Ocean, Skillet, Sublimated
|
||||||
|
expect(controller.drafts.first.name, 'Citrus Coaster Set');
|
||||||
|
|
||||||
|
// Select Fabric Jar Gripper (id 4, currently 2nd).
|
||||||
|
controller.selectBySku('JG-BLU-004');
|
||||||
|
expect(controller.selectedDraft!.id, '4');
|
||||||
|
|
||||||
|
// Rename to 'Zzz Gripper' — should move to end of list.
|
||||||
|
await controller.updateName('4', 'Zzz Gripper');
|
||||||
|
|
||||||
|
// Selection should still be on the same product.
|
||||||
|
expect(controller.selectedDraft, isNotNull);
|
||||||
|
expect(controller.selectedDraft!.id, '4');
|
||||||
|
expect(controller.selectedDraft!.name, 'Zzz Gripper');
|
||||||
|
|
||||||
|
// It should now be last in the sorted list.
|
||||||
|
expect(controller.drafts.last.id, '4');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
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