Compare commits
No commits in common. "cebac4c32f312050615ad72977f553a1877c3913" and "de44b02d7612fd0d2251e2f304bd5702e63c8809" have entirely different histories.
cebac4c32f
...
de44b02d76
|
|
@ -35,10 +35,6 @@ class _StubProductPublishingRepository implements ProductPublishingRepository {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ProductDraft> updateProductName(String id, String name) => throw UnimplementedError();
|
Future<ProductDraft> updateProductName(String id, String name) => throw UnimplementedError();
|
||||||
|
|
||||||
@override
|
|
||||||
Future<ProductDraft> updateProductDescription(String id, String description) =>
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StubOrdersRepository implements OrdersRepository {
|
class _StubOrdersRepository implements OrdersRepository {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ export 'src/domain/publish_status.dart';
|
||||||
|
|
||||||
// Application
|
// Application
|
||||||
export 'src/application/product_publishing_controller.dart' show ProductSortField;
|
export 'src/application/product_publishing_controller.dart' show ProductSortField;
|
||||||
export 'src/application/update_product_description.dart';
|
|
||||||
export 'src/application/update_product_name.dart';
|
export 'src/application/update_product_name.dart';
|
||||||
export 'src/application/update_product_price.dart';
|
export 'src/application/update_product_price.dart';
|
||||||
export 'src/application/update_product_status.dart';
|
export 'src/application/update_product_status.dart';
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import '../domain/product_draft.dart';
|
||||||
import '../domain/publish_status.dart';
|
import '../domain/publish_status.dart';
|
||||||
import 'get_product_drafts.dart';
|
import 'get_product_drafts.dart';
|
||||||
import 'publish_product.dart';
|
import 'publish_product.dart';
|
||||||
import 'update_product_description.dart';
|
|
||||||
import 'update_product_name.dart';
|
import 'update_product_name.dart';
|
||||||
import 'update_product_price.dart';
|
import 'update_product_price.dart';
|
||||||
import 'update_product_status.dart';
|
import 'update_product_status.dart';
|
||||||
|
|
@ -72,21 +71,6 @@ class NameActionResult {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The outcome of a description-update action.
|
|
||||||
///
|
|
||||||
/// Consumed once by the UI to show a SnackBar, then cleared.
|
|
||||||
class DescriptionActionResult {
|
|
||||||
final bool success;
|
|
||||||
final String productName;
|
|
||||||
final String? errorMessage;
|
|
||||||
|
|
||||||
const DescriptionActionResult({
|
|
||||||
required this.success,
|
|
||||||
required this.productName,
|
|
||||||
this.errorMessage,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 {
|
||||||
|
|
@ -95,7 +79,6 @@ class ProductPublishingController extends ChangeNotifier {
|
||||||
final UpdateProductStatus _updateProductStatus;
|
final UpdateProductStatus _updateProductStatus;
|
||||||
final UpdateProductPrice _updateProductPrice;
|
final UpdateProductPrice _updateProductPrice;
|
||||||
final UpdateProductName _updateProductName;
|
final UpdateProductName _updateProductName;
|
||||||
final UpdateProductDescription _updateProductDescription;
|
|
||||||
|
|
||||||
ProductPublishingController(
|
ProductPublishingController(
|
||||||
this._getProductDrafts,
|
this._getProductDrafts,
|
||||||
|
|
@ -103,7 +86,6 @@ class ProductPublishingController extends ChangeNotifier {
|
||||||
this._updateProductStatus,
|
this._updateProductStatus,
|
||||||
this._updateProductPrice,
|
this._updateProductPrice,
|
||||||
this._updateProductName,
|
this._updateProductName,
|
||||||
this._updateProductDescription,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
bool _disposed = false;
|
bool _disposed = false;
|
||||||
|
|
@ -167,13 +149,6 @@ class ProductPublishingController extends ChangeNotifier {
|
||||||
/// to clear it.
|
/// to clear it.
|
||||||
NameActionResult? lastNameResult;
|
NameActionResult? lastNameResult;
|
||||||
|
|
||||||
/// The result of the last description-update action.
|
|
||||||
///
|
|
||||||
/// Set after [updateDescription] completes. The UI should read this once to
|
|
||||||
/// show feedback (e.g. a SnackBar) and then call [consumeDescriptionResult]
|
|
||||||
/// to clear it.
|
|
||||||
DescriptionActionResult? lastDescriptionResult;
|
|
||||||
|
|
||||||
/// Clears [lastActionResult] so the same result is not shown twice.
|
/// Clears [lastActionResult] so the same result is not shown twice.
|
||||||
void consumeActionResult() {
|
void consumeActionResult() {
|
||||||
lastActionResult = null;
|
lastActionResult = null;
|
||||||
|
|
@ -189,11 +164,6 @@ class ProductPublishingController extends ChangeNotifier {
|
||||||
lastNameResult = null;
|
lastNameResult = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears [lastDescriptionResult] so the same result is not shown twice.
|
|
||||||
void consumeDescriptionResult() {
|
|
||||||
lastDescriptionResult = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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;
|
||||||
|
|
@ -386,35 +356,6 @@ class ProductPublishingController extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates only the description of the product with [id].
|
|
||||||
///
|
|
||||||
/// Follows the same per-row updating pattern as [updateStatus].
|
|
||||||
Future<void> updateDescription(String id, String description) async {
|
|
||||||
if (updatingIds.contains(id)) return;
|
|
||||||
|
|
||||||
final productName = _productNameById(id);
|
|
||||||
|
|
||||||
updatingIds.add(id);
|
|
||||||
_safeNotify();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await _updateProductDescription(id, description);
|
|
||||||
if (_disposed) return;
|
|
||||||
updatingIds.remove(id);
|
|
||||||
lastDescriptionResult = DescriptionActionResult(success: true, productName: productName);
|
|
||||||
await load();
|
|
||||||
} catch (e) {
|
|
||||||
if (_disposed) return;
|
|
||||||
updatingIds.remove(id);
|
|
||||||
lastDescriptionResult = DescriptionActionResult(
|
|
||||||
success: false,
|
|
||||||
productName: productName,
|
|
||||||
errorMessage: e.toString(),
|
|
||||||
);
|
|
||||||
_safeNotify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Lifecycle ───────────────────────────────────────────────────────────
|
// ── Lifecycle ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import '../domain/product_draft.dart';
|
|
||||||
import '../domain/product_publishing_repository.dart';
|
|
||||||
|
|
||||||
/// Use case: update only the description of a single product by its [id].
|
|
||||||
///
|
|
||||||
/// This is a narrow description mutation — not a generic product edit.
|
|
||||||
class UpdateProductDescription {
|
|
||||||
final ProductPublishingRepository repository;
|
|
||||||
|
|
||||||
UpdateProductDescription(this.repository);
|
|
||||||
|
|
||||||
Future<ProductDraft> call(String id, String description) =>
|
|
||||||
repository.updateProductDescription(id, description);
|
|
||||||
}
|
|
||||||
|
|
@ -155,19 +155,4 @@ class FakeProductPublishingRepository implements ProductPublishingRepository {
|
||||||
_drafts[index] = updated;
|
_drafts[index] = updated;
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<ProductDraft> updateProductDescription(String id, String description) async {
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 400));
|
|
||||||
|
|
||||||
final index = _drafts.indexWhere((d) => d.id == id);
|
|
||||||
if (index == -1) {
|
|
||||||
throw StateError('Draft with id $id not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
final original = _drafts[index];
|
|
||||||
final updated = original.copyWith(description: description, lastModified: DateTime.now());
|
|
||||||
_drafts[index] = updated;
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,4 @@ class WordPressProductPublishingRepository implements ProductPublishingRepositor
|
||||||
final json = await _apiClient.updateProduct(id, {'name': name});
|
final json = await _apiClient.updateProduct(id, {'name': name});
|
||||||
return _mapper.fromJson(json);
|
return _mapper.fromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<ProductDraft> updateProductDescription(String id, String description) async {
|
|
||||||
final json = await _apiClient.updateProduct(id, {'description': description});
|
|
||||||
return _mapper.fromJson(json);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,4 @@ abstract class ProductPublishingRepository {
|
||||||
///
|
///
|
||||||
/// Returns the updated [ProductDraft] reflecting the new name.
|
/// Returns the updated [ProductDraft] reflecting the new name.
|
||||||
Future<ProductDraft> updateProductName(String id, String name);
|
Future<ProductDraft> updateProductName(String id, String name);
|
||||||
|
|
||||||
/// Updates only the description of the product identified by [id].
|
|
||||||
///
|
|
||||||
/// Returns the updated [ProductDraft] reflecting the new description.
|
|
||||||
Future<ProductDraft> updateProductDescription(String id, String description);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
||||||
import '../application/get_product_drafts.dart';
|
import '../application/get_product_drafts.dart';
|
||||||
import '../application/product_publishing_controller.dart';
|
import '../application/product_publishing_controller.dart';
|
||||||
import '../application/publish_product.dart';
|
import '../application/publish_product.dart';
|
||||||
import '../application/update_product_description.dart';
|
|
||||||
import '../application/update_product_name.dart';
|
import '../application/update_product_name.dart';
|
||||||
import '../application/update_product_price.dart';
|
import '../application/update_product_price.dart';
|
||||||
import '../application/update_product_status.dart';
|
import '../application/update_product_status.dart';
|
||||||
|
|
@ -60,7 +59,6 @@ class _ProductPublishingPageState extends State<ProductPublishingPage> {
|
||||||
UpdateProductStatus(repo),
|
UpdateProductStatus(repo),
|
||||||
UpdateProductPrice(repo),
|
UpdateProductPrice(repo),
|
||||||
UpdateProductName(repo),
|
UpdateProductName(repo),
|
||||||
UpdateProductDescription(repo),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.addListener(_onControllerChanged);
|
controller.addListener(_onControllerChanged);
|
||||||
|
|
@ -106,12 +104,6 @@ class _ProductPublishingPageState extends State<ProductPublishingPage> {
|
||||||
controller.consumeNameResult();
|
controller.consumeNameResult();
|
||||||
showNameActionSnackBar(context, nameResult);
|
showNameActionSnackBar(context, nameResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
final descriptionResult = controller.lastDescriptionResult;
|
|
||||||
if (descriptionResult != null) {
|
|
||||||
controller.consumeDescriptionResult();
|
|
||||||
showDescriptionActionSnackBar(context, descriptionResult);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -248,7 +240,6 @@ class _ProductPublishingPageState extends State<ProductPublishingPage> {
|
||||||
onMoveToDraft: () => controller.updateStatus(selected.id, PublishStatus.draft),
|
onMoveToDraft: () => controller.updateStatus(selected.id, PublishStatus.draft),
|
||||||
onPriceChanged: (price) => controller.updatePrice(selected.id, price),
|
onPriceChanged: (price) => controller.updatePrice(selected.id, price),
|
||||||
onNameChanged: (name) => controller.updateName(selected.id, name),
|
onNameChanged: (name) => controller.updateName(selected.id, name),
|
||||||
onDescriptionChanged: (desc) => controller.updateDescription(selected.id, desc),
|
|
||||||
onViewPolicy: widget.onViewPolicy,
|
onViewPolicy: widget.onViewPolicy,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,6 @@ class ProductPreviewPanel extends StatefulWidget {
|
||||||
/// Callback to update the product name. Receives the new name value.
|
/// Callback to update the product name. Receives the new name value.
|
||||||
final ValueChanged<String>? onNameChanged;
|
final ValueChanged<String>? onNameChanged;
|
||||||
|
|
||||||
/// Callback to update the product description. Receives the new description value.
|
|
||||||
final ValueChanged<String>? onDescriptionChanged;
|
|
||||||
|
|
||||||
/// Whether this product currently has an in-flight status update.
|
/// Whether this product currently has an in-flight status update.
|
||||||
/// When true, the action button is disabled and a progress indicator is shown.
|
/// When true, the action button is disabled and a progress indicator is shown.
|
||||||
final bool isUpdating;
|
final bool isUpdating;
|
||||||
|
|
@ -43,7 +40,6 @@ class ProductPreviewPanel extends StatefulWidget {
|
||||||
this.onMoveToDraft,
|
this.onMoveToDraft,
|
||||||
this.onPriceChanged,
|
this.onPriceChanged,
|
||||||
this.onNameChanged,
|
this.onNameChanged,
|
||||||
this.onDescriptionChanged,
|
|
||||||
this.isUpdating = false,
|
this.isUpdating = false,
|
||||||
this.onViewPolicy,
|
this.onViewPolicy,
|
||||||
});
|
});
|
||||||
|
|
@ -59,15 +55,11 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
bool _editingName = false;
|
bool _editingName = false;
|
||||||
late TextEditingController _nameController;
|
late TextEditingController _nameController;
|
||||||
|
|
||||||
bool _editingDescription = false;
|
|
||||||
late TextEditingController _descriptionController;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_priceController = TextEditingController(text: widget.draft.price.toStringAsFixed(2));
|
_priceController = TextEditingController(text: widget.draft.price.toStringAsFixed(2));
|
||||||
_nameController = TextEditingController(text: widget.draft.name);
|
_nameController = TextEditingController(text: widget.draft.name);
|
||||||
_descriptionController = TextEditingController(text: widget.draft.description);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -81,18 +73,12 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
_editingName = false;
|
_editingName = false;
|
||||||
_nameController.text = widget.draft.name;
|
_nameController.text = widget.draft.name;
|
||||||
}
|
}
|
||||||
if (oldWidget.draft.id != widget.draft.id ||
|
|
||||||
oldWidget.draft.description != widget.draft.description) {
|
|
||||||
_editingDescription = false;
|
|
||||||
_descriptionController.text = widget.draft.description;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_priceController.dispose();
|
_priceController.dispose();
|
||||||
_nameController.dispose();
|
_nameController.dispose();
|
||||||
_descriptionController.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,19 +108,6 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
setState(() => _editingName = false);
|
setState(() => _editingName = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _submitDescription() {
|
|
||||||
final trimmed = _descriptionController.text.trim();
|
|
||||||
if (trimmed.isNotEmpty) {
|
|
||||||
widget.onDescriptionChanged?.call(trimmed);
|
|
||||||
setState(() => _editingDescription = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _cancelDescriptionEdit() {
|
|
||||||
_descriptionController.text = widget.draft.description;
|
|
||||||
setState(() => _editingDescription = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
@ -166,7 +139,9 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
const SizedBox(height: KcSpacing.md),
|
const SizedBox(height: KcSpacing.md),
|
||||||
|
|
||||||
// ── Description ────────────────────────────────────────────
|
// ── Description ────────────────────────────────────────────
|
||||||
_buildDescriptionSection(context),
|
Text('Description', style: theme.textTheme.titleLarge),
|
||||||
|
const SizedBox(height: KcSpacing.sm),
|
||||||
|
Text(draft.description, style: theme.textTheme.bodyLarge),
|
||||||
const SizedBox(height: KcSpacing.xl),
|
const SizedBox(height: KcSpacing.xl),
|
||||||
|
|
||||||
// ── Policy link ────────────────────────────────────────────
|
// ── Policy link ────────────────────────────────────────────
|
||||||
|
|
@ -360,77 +335,6 @@ class _ProductPreviewPanelState extends State<ProductPreviewPanel> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the Description section — either a static display with an edit
|
|
||||||
/// icon, or a multi-line text field with save/cancel actions.
|
|
||||||
Widget _buildDescriptionSection(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
if (_editingDescription) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text('Description', style: theme.textTheme.titleLarge),
|
|
||||||
const Spacer(),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.check, size: 20),
|
|
||||||
onPressed: _submitDescription,
|
|
||||||
tooltip: 'Save description',
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
),
|
|
||||||
const SizedBox(width: KcSpacing.xs),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.close, size: 20),
|
|
||||||
onPressed: _cancelDescriptionEdit,
|
|
||||||
tooltip: 'Cancel description edit',
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: KcSpacing.sm),
|
|
||||||
TextField(
|
|
||||||
controller: _descriptionController,
|
|
||||||
maxLines: 5,
|
|
||||||
minLines: 3,
|
|
||||||
style: theme.textTheme.bodyLarge,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
isDense: true,
|
|
||||||
contentPadding: EdgeInsets.all(8),
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
autofocus: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text('Description', style: theme.textTheme.titleLarge),
|
|
||||||
if (widget.onDescriptionChanged != null && !widget.isUpdating) ...[
|
|
||||||
const SizedBox(width: KcSpacing.xs),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.edit, size: 18),
|
|
||||||
onPressed: () => setState(() => _editingDescription = true),
|
|
||||||
tooltip: 'Edit description',
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: KcSpacing.sm),
|
|
||||||
Text(widget.draft.description, style: theme.textTheme.bodyLarge),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MetadataRow extends StatelessWidget {
|
class _MetadataRow extends StatelessWidget {
|
||||||
|
|
|
||||||
|
|
@ -45,42 +45,6 @@ void showStatusActionSnackBar(BuildContext context, StatusActionResult result) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows a [SnackBar] for the given [DescriptionActionResult].
|
|
||||||
void showDescriptionActionSnackBar(BuildContext context, DescriptionActionResult result) {
|
|
||||||
final messenger = ScaffoldMessenger.maybeOf(context);
|
|
||||||
if (messenger == null) return;
|
|
||||||
|
|
||||||
final String message;
|
|
||||||
final Color backgroundColor;
|
|
||||||
final IconData icon;
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
message = '${result.productName} description updated.';
|
|
||||||
backgroundColor = KcColors.success;
|
|
||||||
icon = Icons.check_circle_outline;
|
|
||||||
} else {
|
|
||||||
message = 'Failed to update description for ${result.productName}.';
|
|
||||||
backgroundColor = KcColors.danger;
|
|
||||||
icon = Icons.error_outline;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.success ? const Duration(seconds: 3) : const Duration(seconds: 5),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Past-tense verb for success messages (e.g. "X published successfully").
|
/// Past-tense verb for success messages (e.g. "X published successfully").
|
||||||
String _pastVerbForStatus(PublishStatus status) {
|
String _pastVerbForStatus(PublishStatus status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|
|
||||||
|
|
@ -243,61 +243,5 @@ void main() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('updateProductDescription', () {
|
|
||||||
test('updates description of the target product', () async {
|
|
||||||
final updated = await repository.updateProductDescription('4', 'A brand new description.');
|
|
||||||
|
|
||||||
expect(updated.id, '4');
|
|
||||||
expect(updated.description, 'A brand new description.');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('persists the description change in the list', () async {
|
|
||||||
await repository.updateProductDescription('4', 'A brand new description.');
|
|
||||||
|
|
||||||
final drafts = await repository.getProductDrafts();
|
|
||||||
final product4 = drafts.firstWhere((d) => d.id == '4');
|
|
||||||
expect(product4.description, 'A brand new description.');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('preserves all fields except description and lastModified', () async {
|
|
||||||
final draftsBefore = await repository.getProductDrafts();
|
|
||||||
final before = draftsBefore.firstWhere((d) => d.id == '4');
|
|
||||||
|
|
||||||
final updated = await repository.updateProductDescription('4', 'A brand new description.');
|
|
||||||
|
|
||||||
expect(updated.name, before.name);
|
|
||||||
expect(updated.price, before.price);
|
|
||||||
expect(updated.sku, before.sku);
|
|
||||||
expect(updated.category, before.category);
|
|
||||||
expect(updated.imageUrl, before.imageUrl);
|
|
||||||
expect(updated.status, before.status);
|
|
||||||
expect(updated.description, 'A brand new description.');
|
|
||||||
expect(updated.lastModified.isAfter(before.lastModified), isTrue);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('preserves other products unchanged', () async {
|
|
||||||
final draftsBefore = await repository.getProductDrafts();
|
|
||||||
|
|
||||||
await repository.updateProductDescription('4', 'A brand new description.');
|
|
||||||
|
|
||||||
final draftsAfter = await repository.getProductDrafts();
|
|
||||||
for (final before in draftsBefore) {
|
|
||||||
if (before.id == '4') continue;
|
|
||||||
final after = draftsAfter.firstWhere((d) => d.id == before.id);
|
|
||||||
expect(after.description, before.description);
|
|
||||||
expect(after.name, before.name);
|
|
||||||
expect(after.price, before.price);
|
|
||||||
expect(after.status, before.status);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('throws StateError for unknown id', () async {
|
|
||||||
expect(
|
|
||||||
() => repository.updateProductDescription('unknown', 'New desc'),
|
|
||||||
throwsA(isA<StateError>()),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ void main() {
|
||||||
UpdateProductStatus(repository),
|
UpdateProductStatus(repository),
|
||||||
UpdateProductPrice(repository),
|
UpdateProductPrice(repository),
|
||||||
UpdateProductName(repository),
|
UpdateProductName(repository),
|
||||||
UpdateProductDescription(repository),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -429,69 +428,6 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('updateDescription', () {
|
|
||||||
test('updates description and reloads', () async {
|
|
||||||
await controller.load();
|
|
||||||
|
|
||||||
// Product 4 starts with a known description.
|
|
||||||
await controller.updateDescription('4', 'A brand new description.');
|
|
||||||
|
|
||||||
final updated = controller.drafts.firstWhere((d) => d.id == '4');
|
|
||||||
expect(updated.description, 'A brand new description.');
|
|
||||||
expect(controller.error, isNull);
|
|
||||||
expect(controller.updatingIds, isEmpty);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sets lastDescriptionResult on success', () async {
|
|
||||||
await controller.load();
|
|
||||||
|
|
||||||
await controller.updateDescription('4', 'Updated desc.');
|
|
||||||
|
|
||||||
expect(controller.lastDescriptionResult, isNotNull);
|
|
||||||
expect(controller.lastDescriptionResult!.success, isTrue);
|
|
||||||
expect(controller.lastDescriptionResult!.productName, 'Fabric Jar Gripper');
|
|
||||||
expect(controller.lastDescriptionResult!.errorMessage, isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sets lastDescriptionResult on failure', () async {
|
|
||||||
await controller.load();
|
|
||||||
|
|
||||||
await controller.updateDescription('unknown', 'New desc.');
|
|
||||||
|
|
||||||
expect(controller.lastDescriptionResult, isNotNull);
|
|
||||||
expect(controller.lastDescriptionResult!.success, isFalse);
|
|
||||||
expect(controller.lastDescriptionResult!.errorMessage, isNotNull);
|
|
||||||
expect(controller.updatingIds, isEmpty);
|
|
||||||
expect(controller.error, isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('consumeDescriptionResult clears the result', () async {
|
|
||||||
await controller.load();
|
|
||||||
|
|
||||||
await controller.updateDescription('4', 'Updated desc.');
|
|
||||||
expect(controller.lastDescriptionResult, isNotNull);
|
|
||||||
|
|
||||||
controller.consumeDescriptionResult();
|
|
||||||
expect(controller.lastDescriptionResult, isNull);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('prevents duplicate calls while row is already updating', () async {
|
|
||||||
await controller.load();
|
|
||||||
|
|
||||||
final first = controller.updateDescription('4', 'First description');
|
|
||||||
expect(controller.isUpdating('4'), isTrue);
|
|
||||||
|
|
||||||
final second = controller.updateDescription('4', 'Second description');
|
|
||||||
await first;
|
|
||||||
await second;
|
|
||||||
|
|
||||||
// Only the first description should have been applied.
|
|
||||||
final updated = controller.drafts.firstWhere((d) => d.id == '4');
|
|
||||||
expect(updated.description, 'First description');
|
|
||||||
expect(controller.updatingIds, isEmpty);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Search refinements ──────────────────────────────────────────────
|
// ── Search refinements ──────────────────────────────────────────────
|
||||||
|
|
||||||
group('search: category matching', () {
|
group('search: category matching', () {
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,6 @@ void main() {
|
||||||
VoidCallback? onMoveToDraft,
|
VoidCallback? onMoveToDraft,
|
||||||
ValueChanged<double>? onPriceChanged,
|
ValueChanged<double>? onPriceChanged,
|
||||||
ValueChanged<String>? onNameChanged,
|
ValueChanged<String>? onNameChanged,
|
||||||
ValueChanged<String>? onDescriptionChanged,
|
|
||||||
bool isUpdating = false,
|
bool isUpdating = false,
|
||||||
}) {
|
}) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
|
|
@ -85,7 +84,6 @@ void main() {
|
||||||
onMoveToDraft: onMoveToDraft,
|
onMoveToDraft: onMoveToDraft,
|
||||||
onPriceChanged: onPriceChanged,
|
onPriceChanged: onPriceChanged,
|
||||||
onNameChanged: onNameChanged,
|
onNameChanged: onNameChanged,
|
||||||
onDescriptionChanged: onDescriptionChanged,
|
|
||||||
isUpdating: isUpdating,
|
isUpdating: isUpdating,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -463,101 +461,4 @@ void main() {
|
||||||
expect(find.byType(TextField), findsOneWidget);
|
expect(find.byType(TextField), findsOneWidget);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('description editing', () {
|
|
||||||
testWidgets('shows edit description icon when onDescriptionChanged is provided', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.pumpWidget(buildTestWidget(sampleDraft, onDescriptionChanged: (_) {}));
|
|
||||||
expect(find.byTooltip('Edit description'), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('hides edit description icon when onDescriptionChanged is null', (tester) async {
|
|
||||||
await tester.pumpWidget(buildTestWidget(sampleDraft));
|
|
||||||
expect(find.byTooltip('Edit description'), findsNothing);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('hides edit description icon while updating', (tester) async {
|
|
||||||
await tester.pumpWidget(
|
|
||||||
buildTestWidget(sampleDraft, onDescriptionChanged: (_) {}, isUpdating: true),
|
|
||||||
);
|
|
||||||
expect(find.byTooltip('Edit description'), findsNothing);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('tapping edit description icon shows text field with current description', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await tester.pumpWidget(buildTestWidget(sampleDraft, onDescriptionChanged: (_) {}));
|
|
||||||
|
|
||||||
await tester.tap(find.byTooltip('Edit description'));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Should show a multi-line TextField.
|
|
||||||
final textFields = find.byType(TextField);
|
|
||||||
expect(textFields, findsOneWidget);
|
|
||||||
final textField = tester.widget<TextField>(textFields);
|
|
||||||
expect(textField.controller!.text, 'A beautifully crafted test product.');
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('tapping check icon submits the new description', (tester) async {
|
|
||||||
String? receivedDescription;
|
|
||||||
await tester.pumpWidget(
|
|
||||||
buildTestWidget(sampleDraft, onDescriptionChanged: (d) => receivedDescription = d),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Enter edit mode.
|
|
||||||
await tester.tap(find.byTooltip('Edit description'));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Clear and type a new description.
|
|
||||||
await tester.enterText(find.byType(TextField), 'Updated description text.');
|
|
||||||
await tester.tap(find.byTooltip('Save description'));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
expect(receivedDescription, 'Updated description text.');
|
|
||||||
// Should exit edit mode — no more text field.
|
|
||||||
expect(find.byType(TextField), findsNothing);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('tapping close icon cancels description editing', (tester) async {
|
|
||||||
String? receivedDescription;
|
|
||||||
await tester.pumpWidget(
|
|
||||||
buildTestWidget(sampleDraft, onDescriptionChanged: (d) => receivedDescription = d),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Enter edit mode.
|
|
||||||
await tester.tap(find.byTooltip('Edit description'));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Type a new description but cancel.
|
|
||||||
await tester.enterText(find.byType(TextField), 'Cancelled description');
|
|
||||||
await tester.tap(find.byTooltip('Cancel description edit'));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
expect(receivedDescription, isNull);
|
|
||||||
// Should exit edit mode and show original description.
|
|
||||||
expect(find.byType(TextField), findsNothing);
|
|
||||||
expect(find.text('A beautifully crafted test product.'), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('does not submit empty description', (tester) async {
|
|
||||||
String? receivedDescription;
|
|
||||||
await tester.pumpWidget(
|
|
||||||
buildTestWidget(sampleDraft, onDescriptionChanged: (d) => receivedDescription = d),
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.tap(find.byTooltip('Edit description'));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Clear the field and try to submit.
|
|
||||||
await tester.enterText(find.byType(TextField), '');
|
|
||||||
await tester.tap(find.byTooltip('Save description'));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Should not have called the callback.
|
|
||||||
expect(receivedDescription, isNull);
|
|
||||||
// Should still be in edit mode.
|
|
||||||
expect(find.byType(TextField), findsOneWidget);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,6 @@ class _FailingRepository implements ProductPublishingRepository {
|
||||||
@override
|
@override
|
||||||
Future<ProductDraft> updateProductName(String id, String name) async =>
|
Future<ProductDraft> updateProductName(String id, String name) async =>
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
|
|
||||||
@override
|
|
||||||
Future<ProductDraft> updateProductDescription(String id, String description) async =>
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue