feat: add canonical name endpoint and update product renaming functionality in admin panel
Test Suite / test (24.15.0) (push) Has been cancelled
Test Suite / test (24.15.0) (push) Has been cancelled
This commit is contained in:
@@ -42,6 +42,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
||||
final Set<int> _selectedIds = <int>{};
|
||||
final Map<int, String> _rowCategoryValue = <int, String>{};
|
||||
final Set<int> _rowCategorySaving = <int>{};
|
||||
List<({String value, String label})> _cachedCategoryOptions = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -65,6 +66,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
||||
setState(() {
|
||||
_products = results[0] as List<AdminProduct>;
|
||||
_categories = results[1] as List<AdminCategoryNode>;
|
||||
_cachedCategoryOptions = _flattenCategories(_categories);
|
||||
_rowCategoryValue.clear();
|
||||
_rowCategorySaving.clear();
|
||||
});
|
||||
@@ -76,22 +78,14 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
||||
}
|
||||
}
|
||||
|
||||
String _sortLabel(_ProductSort sort) {
|
||||
switch (sort) {
|
||||
case _ProductSort.newest:
|
||||
return context.l10n.adminSortNewest;
|
||||
case _ProductSort.oldest:
|
||||
return context.l10n.adminSortOldest;
|
||||
case _ProductSort.nameAsc:
|
||||
return context.l10n.adminSortNameAsc;
|
||||
case _ProductSort.nameDesc:
|
||||
return context.l10n.adminSortNameDesc;
|
||||
case _ProductSort.categoryAsc:
|
||||
return context.l10n.adminSortCategoryAsc;
|
||||
case _ProductSort.categoryDesc:
|
||||
return context.l10n.adminSortCategoryDesc;
|
||||
}
|
||||
}
|
||||
String _sortLabel(_ProductSort sort) => switch (sort) {
|
||||
_ProductSort.newest => context.l10n.adminSortNewest,
|
||||
_ProductSort.oldest => context.l10n.adminSortOldest,
|
||||
_ProductSort.nameAsc => context.l10n.adminSortNameAsc,
|
||||
_ProductSort.nameDesc => context.l10n.adminSortNameDesc,
|
||||
_ProductSort.categoryAsc => context.l10n.adminSortCategoryAsc,
|
||||
_ProductSort.categoryDesc => context.l10n.adminSortCategoryDesc,
|
||||
};
|
||||
|
||||
List<({String value, String label})> _flattenCategories(
|
||||
List<AdminCategoryNode> nodes, [
|
||||
@@ -132,7 +126,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)),
|
||||
_showError(e);
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isApplying = false);
|
||||
@@ -181,7 +175,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)),
|
||||
_showError(e);
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isAiRunning = false);
|
||||
@@ -321,7 +315,70 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)),
|
||||
_showError(e);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _renameProduct(AdminProduct product) async {
|
||||
final controller = TextEditingController(
|
||||
text: product.canonicalName?.isNotEmpty == true
|
||||
? product.canonicalName
|
||||
: product.name,
|
||||
);
|
||||
final newName = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Byt namn på produkt'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Produkt-ID: ${product.id}'),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Kanoniskt namn',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
if (value.trim().isNotEmpty) Navigator.pop(dialogContext, value.trim());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(context.l10n.cancelAction),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final value = controller.text.trim();
|
||||
if (value.isNotEmpty) Navigator.pop(dialogContext, value);
|
||||
},
|
||||
child: const Text('Spara'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
controller.dispose();
|
||||
if (newName == null || !mounted) return;
|
||||
|
||||
try {
|
||||
await ref.read(adminRepositoryProvider).updateCanonicalName(product.id, newName);
|
||||
if (!mounted) return;
|
||||
await _load();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Namn uppdaterat till "$newName"')),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
_showError(e);
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -358,7 +415,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)),
|
||||
_showError(e);
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -367,9 +424,8 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
||||
if (_selectedIds.isEmpty || !_showDeletedOnly || _isApplying) return;
|
||||
setState(() => _isApplying = true);
|
||||
try {
|
||||
for (final id in _selectedIds.toList()) {
|
||||
await ref.read(adminRepositoryProvider).restoreProduct(id);
|
||||
}
|
||||
final repo = ref.read(adminRepositoryProvider);
|
||||
await Future.wait(_selectedIds.map(repo.restoreProduct));
|
||||
if (!mounted) return;
|
||||
setState(() => _selectedIds.clear());
|
||||
await _load();
|
||||
@@ -380,7 +436,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)),
|
||||
_showError(e);
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isApplying = false);
|
||||
@@ -399,7 +455,7 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)),
|
||||
_showError(e);
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -435,22 +491,30 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)),
|
||||
_showError(e);
|
||||
);
|
||||
setState(() => _rowCategorySaving.remove(product.id));
|
||||
}
|
||||
}
|
||||
|
||||
String _nameForId(int id) {
|
||||
final match = _products.where((p) => p.id == id).toList();
|
||||
if (match.isEmpty) return '#$id';
|
||||
return match.first.displayName;
|
||||
for (final p in _products) {
|
||||
if (p.id == id) return p.displayName;
|
||||
}
|
||||
return '#$id';
|
||||
}
|
||||
|
||||
void _showError(Object e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
_showError(e);
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final categoryOptions = _flattenCategories(_categories);
|
||||
final categoryOptions = _cachedCategoryOptions;
|
||||
final filtered = _products.where((product) {
|
||||
if (!_showDeletedOnly && _showUncategorizedOnly && product.categoryId != null) {
|
||||
return false;
|
||||
@@ -709,6 +773,13 @@ class _AdminProductsPanelState extends ConsumerState<AdminProductsPanel> {
|
||||
else
|
||||
const SizedBox.shrink(),
|
||||
const SizedBox(width: 8),
|
||||
if (!_showDeletedOnly)
|
||||
TextButton.icon(
|
||||
onPressed: () => _renameProduct(product),
|
||||
icon: const Icon(Icons.drive_file_rename_outline),
|
||||
label: const Text('Byt namn'),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
if (_showDeletedOnly)
|
||||
TextButton.icon(
|
||||
onPressed: () => _restoreProduct(product),
|
||||
|
||||
Reference in New Issue
Block a user