feat: add updateCategoryMine endpoint to manage product category updates
Test Suite / test (24.15.0) (push) Has been cancelled
Test Suite / test (24.15.0) (push) Has been cancelled
- Implemented a new PATCH endpoint in ProductsController to update the category of a user's product. - Added corresponding service method in ProductsService to handle business logic and validation. - Created UpdateCategoryMineDto for request validation. - Enhanced error handling for forbidden actions and not found resources. - Updated API error mapping in Flutter to handle specific forbidden messages. - Modified ProductPickerField to allow product creation directly from the picker. - Added tests for the new endpoint and service method to ensure proper functionality and error handling.
This commit is contained in:
@@ -4,6 +4,11 @@ import 'package:flutter/services.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import 'api_exception.dart';
|
||||
|
||||
const _safeForbiddenMessages = {
|
||||
'Du kan inte omkategorisera, ta bort eller ändra produkter som är globala.',
|
||||
'Du kan bara omkategorisera dina egna produkter',
|
||||
};
|
||||
|
||||
String mapErrorToUserMessage(Object error, BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
if (error is ApiException) {
|
||||
@@ -11,7 +16,9 @@ String mapErrorToUserMessage(Object error, BuildContext context) {
|
||||
case ApiErrorType.unauthorized:
|
||||
return l10n.sessionExpiredError;
|
||||
case ApiErrorType.forbidden:
|
||||
return l10n.forbiddenError;
|
||||
return _safeForbiddenMessages.contains(error.message)
|
||||
? error.message
|
||||
: l10n.forbiddenError;
|
||||
case ApiErrorType.server:
|
||||
return l10n.serverError;
|
||||
case ApiErrorType.network:
|
||||
|
||||
@@ -13,6 +13,7 @@ class ProductApiPaths {
|
||||
static const deleted = '/products/deleted';
|
||||
static const merge = '/products/merge';
|
||||
static const mergePrivate = '/products/private/merge';
|
||||
static String updateMineCategory(int id) => '/products/mine/$id/category';
|
||||
static String mergePreview(int sourceProductId, int targetProductId) =>
|
||||
'/products/merge-preview?sourceProductId=$sourceProductId&targetProductId=$targetProductId';
|
||||
static String setStatus(int id) => '/products/$id/status';
|
||||
|
||||
@@ -36,6 +36,9 @@ class ProductPickerField extends StatelessWidget {
|
||||
/// If set, the picker bottom sheet opens with this text pre-filled in the search field.
|
||||
final String? initialQuery;
|
||||
|
||||
/// Optional callback to create a new product directly from the picker.
|
||||
final Future<ProductOption?> Function(String name)? onCreate;
|
||||
|
||||
const ProductPickerField({
|
||||
super.key,
|
||||
required this.products,
|
||||
@@ -46,6 +49,7 @@ class ProductPickerField extends StatelessWidget {
|
||||
this.label = 'Produkt',
|
||||
this.errorText,
|
||||
this.initialQuery,
|
||||
this.onCreate,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -57,7 +61,7 @@ class ProductPickerField extends StatelessWidget {
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
final interactive = enabled && !isLoading && products.isNotEmpty;
|
||||
final interactive = enabled && !isLoading && (products.isNotEmpty || onCreate != null);
|
||||
|
||||
return MouseRegion(
|
||||
cursor: interactive ? SystemMouseCursors.click : MouseCursor.defer,
|
||||
@@ -81,7 +85,9 @@ class ProductPickerField extends StatelessWidget {
|
||||
: selected == null
|
||||
? Text(
|
||||
products.isEmpty
|
||||
? 'Inga produkter tillgängliga'
|
||||
? (onCreate != null
|
||||
? 'Inga produkter tillgängliga. Tryck för att välja eller skapa.'
|
||||
: 'Inga produkter tillgängliga')
|
||||
: 'Tryck för att välja produkt',
|
||||
)
|
||||
: Text(selected.name),
|
||||
@@ -98,6 +104,7 @@ class ProductPickerField extends StatelessWidget {
|
||||
value: value,
|
||||
label: label,
|
||||
initialQuery: initialQuery,
|
||||
onCreate: onCreate,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
if (result == null) return;
|
||||
|
||||
Reference in New Issue
Block a user