fix(feature_wordpress): allow pending review products to move to draft
Publish Docs / publish-docs (push) Successful in 1m12s Details

This commit is contained in:
Mike Kell 2026-04-11 08:16:49 -04:00
parent f9c5ef36da
commit f8f373b018
7 changed files with 134 additions and 21 deletions

0
draft Normal file
View File

View File

@ -103,14 +103,7 @@ class FakeProductPublishingRepository implements ProductPublishingRepository {
}
final original = _drafts[index];
final updated = ProductDraft(
id: original.id,
name: original.name,
description: original.description,
price: original.price,
sku: original.sku,
category: original.category,
imageUrl: original.imageUrl,
final updated = original.copyWith(
status: PublishStatus.published,
lastModified: DateTime.now(),
);
@ -128,14 +121,7 @@ class FakeProductPublishingRepository implements ProductPublishingRepository {
}
final original = _drafts[index];
final updated = ProductDraft(
id: original.id,
name: original.name,
description: original.description,
price: original.price,
sku: original.sku,
category: original.category,
imageUrl: original.imageUrl,
final updated = original.copyWith(
status: status,
lastModified: DateTime.now(),
);

View File

@ -23,4 +23,29 @@ class ProductDraft {
required this.status,
required this.lastModified,
});
/// Returns a copy of this [ProductDraft] with the given fields replaced.
ProductDraft copyWith({
String? id,
String? name,
String? description,
double? price,
String? sku,
String? category,
String? imageUrl,
PublishStatus? status,
DateTime? lastModified,
}) {
return ProductDraft(
id: id ?? this.id,
name: name ?? this.name,
description: description ?? this.description,
price: price ?? this.price,
sku: sku ?? this.sku,
category: category ?? this.category,
imageUrl: imageUrl ?? this.imageUrl,
status: status ?? this.status,
lastModified: lastModified ?? this.lastModified,
);
}
}

View File

@ -10,8 +10,8 @@ import 'publish_status_chip.dart';
/// Includes product image placeholder, description, metadata, and
/// status-aware action buttons:
/// - **Publish to Store** when the draft status is [PublishStatus.draft].
/// - **Move to Draft** when the draft status is [PublishStatus.published].
/// - No action button for other statuses.
/// - **Move to Draft** when the draft status is [PublishStatus.published],
/// [PublishStatus.unpublished], or [PublishStatus.pendingReview].
class ProductPreviewPanel extends StatelessWidget {
final ProductDraft draft;
final VoidCallback? onPublish;
@ -118,7 +118,9 @@ class ProductPreviewPanel extends StatelessWidget {
label: const Text('Publish to Store'),
),
),
if (draft.status == PublishStatus.published)
if (draft.status == PublishStatus.published ||
draft.status == PublishStatus.unpublished ||
draft.status == PublishStatus.pendingReview)
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(

View File

@ -75,6 +75,20 @@ void main() {
expect(product1.status, PublishStatus.draft);
});
test('unpublished to draft updates only the target product', () async {
// Product 6 starts as unpublished.
final updated = await repository.updateProductStatus('6', PublishStatus.draft);
expect(updated.id, '6');
expect(updated.status, PublishStatus.draft);
expect(updated.name, 'Sublimated Slate Coaster');
// Verify the change is persisted in the list.
final drafts = await repository.getProductDrafts();
final product6 = drafts.firstWhere((d) => d.id == '6');
expect(product6.status, PublishStatus.draft);
});
test('preserves other products unchanged', () async {
final draftsBefore = await repository.getProductDrafts();

View File

@ -157,6 +157,30 @@ void main() {
expect(controller.updatingIds, isEmpty);
});
test('pendingReview to draft updates status and reloads', () async {
await controller.load();
// Product 3 starts as pendingReview.
await controller.updateStatus('3', PublishStatus.draft);
final updated = controller.drafts.firstWhere((d) => d.id == '3');
expect(updated.status, PublishStatus.draft);
expect(controller.error, isNull);
expect(controller.updatingIds, isEmpty);
});
test('unpublished to draft updates status and reloads', () async {
await controller.load();
// Product 6 starts as unpublished.
await controller.updateStatus('6', PublishStatus.draft);
final updated = controller.drafts.firstWhere((d) => d.id == '6');
expect(updated.status, PublishStatus.draft);
expect(controller.error, isNull);
expect(controller.updatingIds, isEmpty);
});
test('sets error on failed update', () async {
await controller.load();

View File

@ -42,6 +42,18 @@ void main() {
lastModified: DateTime(2026, 4, 1),
);
final unpublishedDraft = ProductDraft(
id: '4',
name: 'Unpublished Product',
description: 'Previously published but taken down.',
price: 9.99,
sku: 'UP-001',
category: 'Coasters',
imageUrl: '',
status: PublishStatus.unpublished,
lastModified: DateTime(2026, 3, 20),
);
Widget buildTestWidget(
ProductDraft draft, {
VoidCallback? onPublish,
@ -110,10 +122,21 @@ void main() {
expect(find.text('Publish to Store'), findsNothing);
});
testWidgets('pending review row shows no action button', (tester) async {
testWidgets('pending review row shows Move to Draft button', (tester) async {
await tester.pumpWidget(buildTestWidget(pendingDraft));
expect(find.text('Move to Draft'), findsOneWidget);
expect(find.text('Publish to Store'), findsNothing);
expect(find.text('Move to Draft'), findsNothing);
});
testWidgets('calls onMoveToDraft when Move to Draft is tapped for pending review', (
tester,
) async {
var movedToDraft = false;
await tester.pumpWidget(
buildTestWidget(pendingDraft, onMoveToDraft: () => movedToDraft = true),
);
await tester.tap(find.text('Move to Draft'));
expect(movedToDraft, true);
});
testWidgets('calls onMoveToDraft when Move to Draft is tapped', (tester) async {
@ -124,6 +147,21 @@ void main() {
await tester.tap(find.text('Move to Draft'));
expect(movedToDraft, true);
});
testWidgets('unpublished row shows Move to Draft button', (tester) async {
await tester.pumpWidget(buildTestWidget(unpublishedDraft));
expect(find.text('Move to Draft'), findsOneWidget);
expect(find.text('Publish to Store'), findsNothing);
});
testWidgets('calls onMoveToDraft when Move to Draft is tapped for unpublished', (tester) async {
var movedToDraft = false;
await tester.pumpWidget(
buildTestWidget(unpublishedDraft, onMoveToDraft: () => movedToDraft = true),
);
await tester.tap(find.text('Move to Draft'));
expect(movedToDraft, true);
});
});
group('updating state', () {
@ -151,6 +189,30 @@ void main() {
expect(tapped, false);
});
testWidgets('Move to Draft button is disabled while updating for pending review', (
tester,
) async {
var tapped = false;
await tester.pumpWidget(
buildTestWidget(pendingDraft, isUpdating: true, onMoveToDraft: () => tapped = true),
);
expect(find.text('Move to Draft'), findsOneWidget);
await tester.tap(find.text('Move to Draft'));
expect(tapped, false);
});
testWidgets('Move to Draft button is disabled while updating for unpublished', (tester) async {
var tapped = false;
await tester.pumpWidget(
buildTestWidget(unpublishedDraft, isUpdating: true, onMoveToDraft: () => tapped = true),
);
expect(find.text('Move to Draft'), findsOneWidget);
await tester.tap(find.text('Move to Draft'));
expect(tapped, false);
});
testWidgets('shows progress indicator while updating', (tester) async {
await tester.pumpWidget(buildTestWidget(sampleDraft, isUpdating: true));
expect(find.byType(CircularProgressIndicator), findsOneWidget);