feat: Refactor inventory screens with category selection and product handling improvements
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-11 21:09:40 +02:00
parent 2281df3716
commit 3e0af925d5
7 changed files with 319 additions and 145 deletions
@@ -0,0 +1,9 @@
String? normalizedOptionalText(String? value) {
final normalized = value?.trim();
if (normalized == null || normalized.isEmpty) return null;
return normalized;
}
String l1CategoryChipLabel(String prefix, String l1Category) => '$prefix$l1Category';
String locationLabel(String prefix, String location) => '$prefix$location';
@@ -9,13 +9,15 @@ import '../../../core/api/api_providers.dart';
import '../../../core/forms/form_options.dart'; import '../../../core/forms/form_options.dart';
import '../../../core/l10n/l10n.dart'; import '../../../core/l10n/l10n.dart';
import '../../../core/ui/category_then_product_picker.dart'; import '../../../core/ui/category_then_product_picker.dart';
import '../../../core/ui/product_picker_field.dart'; import '../../../core/ui/product_picker_field.dart' show ProductOption;
import '../../../core/ui/searchable_category_field.dart'; import '../../../core/ui/searchable_category_field.dart' show CategorySelectOption;
import '../../auth/data/auth_providers.dart'; import '../../auth/data/auth_providers.dart';
import '../../admin/domain/admin_category_node.dart'; import '../../admin/domain/admin_category_node.dart';
import '../../pantry/data/pantry_providers.dart'; import '../../pantry/data/pantry_providers.dart';
import '../data/inventory_providers.dart'; import '../data/inventory_providers.dart';
import '../../import/data/receipt_import_session.dart' show ImportDestination; import '../../import/data/receipt_import_session.dart' show ImportDestination;
import 'inventory_category_helpers.dart';
import 'inventory_category_product_section.dart';
class CreateInventoryScreen extends ConsumerStatefulWidget { class CreateInventoryScreen extends ConsumerStatefulWidget {
final String? initialDestination; final String? initialDestination;
@@ -41,6 +43,8 @@ class _CreateInventoryScreenState
List<Map<String, dynamic>> _products = []; List<Map<String, dynamic>> _products = [];
List<AdminCategoryNode> _categoryTree = []; List<AdminCategoryNode> _categoryTree = [];
List<CategorySelectOption> _categoryOptions = []; List<CategorySelectOption> _categoryOptions = [];
InventoryCategoryBranchIndex? _categoryBranchIndex;
final _sortedProductsCache = InventorySortedProductsCache();
bool _loadingProducts = false; bool _loadingProducts = false;
DateTime? _purchaseDate; DateTime? _purchaseDate;
DateTime? _bestBeforeDate; DateTime? _bestBeforeDate;
@@ -95,6 +99,7 @@ class _CreateInventoryScreenState
.map((e) => AdminCategoryNode.fromJson(Map<String, dynamic>.from(e as Map))) .map((e) => AdminCategoryNode.fromJson(Map<String, dynamic>.from(e as Map)))
.toList(); .toList();
_categoryOptions = _flattenCategoryOptions(_categoryTree); _categoryOptions = _flattenCategoryOptions(_categoryTree);
_categoryBranchIndex = InventoryCategoryBranchIndex.fromTree(_categoryTree);
_loadingProducts = false; _loadingProducts = false;
}); });
} }
@@ -132,11 +137,23 @@ class _CreateInventoryScreenState
return null; return null;
} }
Set<int>? _selectedCategoryBranchIds() {
final selectedId = _selectedCategoryId;
if (selectedId == null) return null;
return _categoryBranchIndex?.branchIdsFor(selectedId) ?? {selectedId};
}
List<ProductOption> _productOptions() { List<ProductOption> _productOptions() {
final selectedCategoryIds = _selectedCategoryBranchIds();
final source = _selectedCategoryId == null final source = _selectedCategoryId == null
? _products ? _products
: _products : _products
.where((p) => (p['categoryId'] as num?)?.toInt() == _selectedCategoryId) .where(
(p) => productInSelectedBranch(
productCategoryId: (p['categoryId'] as num?)?.toInt(),
selectedCategoryIds: selectedCategoryIds,
),
)
.toList(); .toList();
final selected = _selectedProduct(); final selected = _selectedProduct();
@@ -146,13 +163,8 @@ class _CreateInventoryScreenState
withSelected.add(selected); withSelected.add(selected);
} }
withSelected.sort((a, b) { final sorted = _sortedProductsCache.sort(withSelected);
final aName = (a['canonicalName'] ?? a['name'] ?? '').toString().toLowerCase(); return sorted
final bName = (b['canonicalName'] ?? b['name'] ?? '').toString().toLowerCase();
return aName.compareTo(bName);
});
return withSelected
.map( .map(
(p) => ( (p) => (
id: (p['id'] as num).toInt(), id: (p['id'] as num).toInt(),
@@ -172,9 +184,13 @@ class _CreateInventoryScreenState
if (selected == null || !mounted) return; if (selected == null || !mounted) return;
setState(() { setState(() {
_selectedCategoryId = selected.id; _selectedCategoryId = selected.id;
final selectedCategoryIds = _selectedCategoryBranchIds();
final current = _selectedProduct(); final current = _selectedProduct();
final currentCategoryId = (current?['categoryId'] as num?)?.toInt(); final currentCategoryId = (current?['categoryId'] as num?)?.toInt();
if (currentCategoryId != _selectedCategoryId) { if (!productInSelectedBranch(
productCategoryId: currentCategoryId,
selectedCategoryIds: selectedCategoryIds,
)) {
_selectedProductId = null; _selectedProductId = null;
} }
}); });
@@ -302,54 +318,41 @@ class _CreateInventoryScreenState
: (selected) => setState(() => _destination = selected.first), : (selected) => setState(() => _destination = selected.first),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
SearchableCategoryField( InventoryCategoryProductSection(
options: _categoryOptions, categoryOptions: _categoryOptions,
value: _selectedCategoryId?.toString(), selectedCategoryId: _selectedCategoryId,
label: 'Kategori (sökbar)', categorySearchLabel: 'Kategori (sökbar)',
onChanged: (value) { onCategoryChanged: (value) {
if (value == null) return; if (value == null) return;
setState(() { setState(() {
_selectedCategoryId = int.tryParse(value); _selectedCategoryId = int.tryParse(value);
final selectedCategoryIds = _selectedCategoryBranchIds();
final current = _selectedProduct(); final current = _selectedProduct();
final currentCategoryId = (current?['categoryId'] as num?)?.toInt(); final currentCategoryId = (current?['categoryId'] as num?)?.toInt();
if (currentCategoryId != _selectedCategoryId) { if (!productInSelectedBranch(
productCategoryId: currentCategoryId,
selectedCategoryIds: selectedCategoryIds,
)) {
_selectedProductId = null; _selectedProductId = null;
} }
}); });
}, },
), onPickCategory: _pickCategory,
const SizedBox(height: 12), pickCategoryLabel: 'Välj kategori',
Row( onClearCategory: () {
children: [ setState(() {
OutlinedButton.icon( _selectedCategoryId = null;
onPressed: _loadingProducts || _saving || _categoryTree.isEmpty });
? null },
: _pickCategory, clearCategoryLabel: 'Rensa kategori',
icon: const Icon(Icons.category_outlined), canPickCategory: !_loadingProducts && !_saving && _categoryTree.isNotEmpty,
label: const Text('Välj kategori'), canClearCategory: !_saving,
), productOptions: productOptions,
const SizedBox(width: 8), selectedProductId: _selectedProductId,
OutlinedButton.icon( onProductChanged: (value) => setState(() => _selectedProductId = value),
onPressed: _saving isLoadingProducts: _loadingProducts,
? null productEnabled: !_saving,
: () { productLabel: context.l10n.inventoryProductLabel,
setState(() {
_selectedCategoryId = null;
});
},
icon: const Icon(Icons.clear),
label: const Text('Rensa kategori'),
),
],
),
const SizedBox(height: 12),
ProductPickerField(
products: productOptions,
value: _selectedProductId,
isLoading: _loadingProducts,
enabled: !_saving,
label: context.l10n.inventoryProductLabel,
onChanged: (value) => setState(() => _selectedProductId = value),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
@@ -0,0 +1,78 @@
import '../../admin/domain/admin_category_node.dart';
class InventoryCategoryBranchIndex {
final Map<int, Set<int>> _descendantsById;
InventoryCategoryBranchIndex._(this._descendantsById);
factory InventoryCategoryBranchIndex.fromTree(List<AdminCategoryNode> roots) {
final descendantsById = <int, Set<int>>{};
Set<int> collect(AdminCategoryNode node) {
final ids = <int>{node.id};
for (final child in node.children) {
ids.addAll(collect(child));
}
descendantsById[node.id] = ids;
return ids;
}
for (final root in roots) {
collect(root);
}
return InventoryCategoryBranchIndex._(descendantsById);
}
Set<int>? branchIdsFor(int? categoryId) {
if (categoryId == null) return null;
return _descendantsById[categoryId] ?? {categoryId};
}
}
bool productInSelectedBranch({
required int? productCategoryId,
required Set<int>? selectedCategoryIds,
}) {
if (selectedCategoryIds == null) return true;
if (productCategoryId == null) return false;
return selectedCategoryIds.contains(productCategoryId);
}
class InventorySortedProductsCache {
String? _lastKey;
List<Map<String, dynamic>> _lastSorted = const [];
List<Map<String, dynamic>> sort(List<Map<String, dynamic>> products) {
final key = _buildKey(products);
if (_lastKey == key) return _lastSorted;
final sorted = [...products]
..sort((a, b) {
final aName = (a['canonicalName'] ?? a['name'] ?? '').toString().toLowerCase();
final bName = (b['canonicalName'] ?? b['name'] ?? '').toString().toLowerCase();
return aName.compareTo(bName);
});
_lastKey = key;
_lastSorted = sorted;
return _lastSorted;
}
String _buildKey(List<Map<String, dynamic>> products) {
final b = StringBuffer('${products.length}|');
for (final p in products) {
final id = (p['id'] as num?)?.toInt() ?? -1;
final categoryId = (p['categoryId'] as num?)?.toInt() ?? -1;
final name = (p['canonicalName'] ?? p['name'] ?? '').toString();
b
..write(id)
..write(':')
..write(categoryId)
..write(':')
..write(name)
..write('|');
}
return b.toString();
}
}
@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import '../../../core/ui/product_picker_field.dart' as product_picker;
import '../../../core/ui/searchable_category_field.dart';
class InventoryCategoryProductSection extends StatelessWidget {
final List<CategorySelectOption> categoryOptions;
final int? selectedCategoryId;
final ValueChanged<String?> onCategoryChanged;
final String categorySearchLabel;
final VoidCallback onPickCategory;
final String pickCategoryLabel;
final VoidCallback onClearCategory;
final String clearCategoryLabel;
final bool canPickCategory;
final bool canClearCategory;
final List<product_picker.ProductOption> productOptions;
final int? selectedProductId;
final ValueChanged<int?> onProductChanged;
final bool isLoadingProducts;
final bool productEnabled;
final String productLabel;
const InventoryCategoryProductSection({
super.key,
required this.categoryOptions,
required this.selectedCategoryId,
required this.onCategoryChanged,
required this.categorySearchLabel,
required this.onPickCategory,
required this.pickCategoryLabel,
required this.onClearCategory,
required this.clearCategoryLabel,
required this.canPickCategory,
required this.canClearCategory,
required this.productOptions,
required this.selectedProductId,
required this.onProductChanged,
required this.isLoadingProducts,
required this.productEnabled,
required this.productLabel,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
SearchableCategoryField(
options: categoryOptions,
value: selectedCategoryId?.toString(),
label: categorySearchLabel,
onChanged: onCategoryChanged,
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
OutlinedButton.icon(
onPressed: canPickCategory ? onPickCategory : null,
icon: const Icon(Icons.category_outlined),
label: Text(pickCategoryLabel),
),
OutlinedButton.icon(
onPressed: canClearCategory ? onClearCategory : null,
icon: const Icon(Icons.clear),
label: Text(clearCategoryLabel),
),
],
),
const SizedBox(height: 12),
product_picker.ProductPickerField(
products: productOptions,
value: selectedProductId,
isLoading: isLoadingProducts,
enabled: productEnabled,
label: productLabel,
onChanged: onProductChanged,
),
],
);
}
}
@@ -10,12 +10,14 @@ import '../../../core/forms/form_options.dart';
import '../../../core/l10n/l10n.dart'; import '../../../core/l10n/l10n.dart';
import '../../../core/ui/async_state_views.dart'; import '../../../core/ui/async_state_views.dart';
import '../../../core/ui/category_then_product_picker.dart'; import '../../../core/ui/category_then_product_picker.dart';
import '../../../core/ui/product_picker_field.dart'; import '../../../core/ui/product_picker_field.dart' show ProductOption;
import '../../../core/ui/searchable_category_field.dart'; import '../../../core/ui/searchable_category_field.dart' show CategorySelectOption;
import '../../auth/data/auth_providers.dart'; import '../../auth/data/auth_providers.dart';
import '../../admin/domain/admin_category_node.dart'; import '../../admin/domain/admin_category_node.dart';
import '../data/inventory_providers.dart'; import '../data/inventory_providers.dart';
import '../domain/inventory_item.dart'; import '../domain/inventory_item.dart';
import 'inventory_category_helpers.dart';
import 'inventory_category_product_section.dart';
class InventoryEditScreen extends ConsumerStatefulWidget { class InventoryEditScreen extends ConsumerStatefulWidget {
final int itemId; final int itemId;
@@ -46,6 +48,8 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
List<Map<String, dynamic>> _products = []; List<Map<String, dynamic>> _products = [];
List<AdminCategoryNode> _categoryTree = []; List<AdminCategoryNode> _categoryTree = [];
List<CategorySelectOption> _categoryOptions = []; List<CategorySelectOption> _categoryOptions = [];
InventoryCategoryBranchIndex? _categoryBranchIndex;
final _sortedProductsCache = InventorySortedProductsCache();
@override @override
void dispose() { void dispose() {
@@ -113,6 +117,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
_products = products; _products = products;
_categoryTree = categoryTree; _categoryTree = categoryTree;
_categoryOptions = categoryOptions; _categoryOptions = categoryOptions;
_categoryBranchIndex = InventoryCategoryBranchIndex.fromTree(categoryTree);
final selected = _selectedProduct(); final selected = _selectedProduct();
if (selected != null) { if (selected != null) {
@@ -154,10 +159,24 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
return null; return null;
} }
Set<int>? _selectedCategoryBranchIds() {
final selectedId = _selectedCategoryId;
if (selectedId == null) return null;
return _categoryBranchIndex?.branchIdsFor(selectedId) ?? {selectedId};
}
List<ProductOption> _productOptions() { List<ProductOption> _productOptions() {
final selectedCategoryIds = _selectedCategoryBranchIds();
final source = _selectedCategoryId == null final source = _selectedCategoryId == null
? _products ? _products
: _products.where((p) => (p['categoryId'] as num?)?.toInt() == _selectedCategoryId).toList(); : _products
.where(
(p) => productInSelectedBranch(
productCategoryId: (p['categoryId'] as num?)?.toInt(),
selectedCategoryIds: selectedCategoryIds,
),
)
.toList();
final selected = _selectedProduct(); final selected = _selectedProduct();
final withSelected = [...source]; final withSelected = [...source];
@@ -165,13 +184,8 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
withSelected.add(selected); withSelected.add(selected);
} }
withSelected.sort((a, b) { final sorted = _sortedProductsCache.sort(withSelected);
final aName = (a['canonicalName'] ?? a['name'] ?? '').toString().toLowerCase(); return sorted
final bName = (b['canonicalName'] ?? b['name'] ?? '').toString().toLowerCase();
return aName.compareTo(bName);
});
return withSelected
.map( .map(
(p) => ( (p) => (
id: (p['id'] as num).toInt(), id: (p['id'] as num).toInt(),
@@ -191,9 +205,13 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
if (selected == null || !mounted) return; if (selected == null || !mounted) return;
setState(() { setState(() {
_selectedCategoryId = selected.id; _selectedCategoryId = selected.id;
final selectedCategoryIds = _selectedCategoryBranchIds();
final current = _selectedProduct(); final current = _selectedProduct();
final currentCategoryId = (current?['categoryId'] as num?)?.toInt(); final currentCategoryId = (current?['categoryId'] as num?)?.toInt();
if (currentCategoryId != _selectedCategoryId) { if (!productInSelectedBranch(
productCategoryId: currentCategoryId,
selectedCategoryIds: selectedCategoryIds,
)) {
_selectedProductId = null; _selectedProductId = null;
} }
}); });
@@ -292,54 +310,41 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
Text(item.categoryPath!, style: Theme.of(context).textTheme.bodySmall), Text(item.categoryPath!, style: Theme.of(context).textTheme.bodySmall),
], ],
const SizedBox(height: 16), const SizedBox(height: 16),
SearchableCategoryField( InventoryCategoryProductSection(
options: _categoryOptions, categoryOptions: _categoryOptions,
value: _selectedCategoryId?.toString(), selectedCategoryId: _selectedCategoryId,
label: 'Kategori (sökbar)', categorySearchLabel: 'Kategori (sökbar)',
onChanged: (value) { onCategoryChanged: (value) {
if (value == null) return; if (value == null) return;
setState(() { setState(() {
_selectedCategoryId = int.tryParse(value); _selectedCategoryId = int.tryParse(value);
final selectedCategoryIds = _selectedCategoryBranchIds();
final current = _selectedProduct(); final current = _selectedProduct();
final currentCategoryId = (current?['categoryId'] as num?)?.toInt(); final currentCategoryId = (current?['categoryId'] as num?)?.toInt();
if (currentCategoryId != _selectedCategoryId) { if (!productInSelectedBranch(
productCategoryId: currentCategoryId,
selectedCategoryIds: selectedCategoryIds,
)) {
_selectedProductId = null; _selectedProductId = null;
} }
}); });
}, },
), onPickCategory: _pickCategory,
const SizedBox(height: 12), pickCategoryLabel: 'Välj kategori',
Row( onClearCategory: () {
children: [ setState(() {
OutlinedButton.icon( _selectedCategoryId = null;
onPressed: _loadingProducts || _saving || _categoryTree.isEmpty });
? null },
: _pickCategory, clearCategoryLabel: 'Rensa kategori',
icon: const Icon(Icons.category_outlined), canPickCategory: !_loadingProducts && !_saving && _categoryTree.isNotEmpty,
label: const Text('Välj kategori'), canClearCategory: !_saving,
), productOptions: _productOptions(),
const SizedBox(width: 8), selectedProductId: _selectedProductId,
OutlinedButton.icon( onProductChanged: (value) => setState(() => _selectedProductId = value),
onPressed: _saving isLoadingProducts: _loadingProducts,
? null productEnabled: !_saving,
: () { productLabel: context.l10n.inventoryProductLabel,
setState(() {
_selectedCategoryId = null;
});
},
icon: const Icon(Icons.clear),
label: const Text('Rensa kategori'),
),
],
),
const SizedBox(height: 12),
ProductPickerField(
products: _productOptions(),
value: _selectedProductId,
isLoading: _loadingProducts,
enabled: !_saving,
label: context.l10n.inventoryProductLabel,
onChanged: (value) => setState(() => _selectedProductId = value),
), ),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../../core/api/api_error_mapper.dart'; import '../../../core/api/api_error_mapper.dart';
import '../../../core/utils/display_labels.dart';
import '../../../core/utils/formatters.dart'; import '../../../core/utils/formatters.dart';
import '../../auth/data/auth_providers.dart'; import '../../auth/data/auth_providers.dart';
import '../data/inventory_providers.dart'; import '../data/inventory_providers.dart';
@@ -286,12 +287,11 @@ class _ForegroundTile extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final location = item.location?.trim(); final location = normalizedOptionalText(item.location);
final hasLocation = location != null && location.isNotEmpty;
final subtitleText = [ final subtitleText = [
'${_fmtQty(item.quantity)} ${item.unit}', '${_fmtQty(item.quantity)} ${item.unit}',
if (hasLocation) location, if (location != null) location,
if (item.bestBeforeDate != null) if (item.bestBeforeDate != null)
'Bäst före: ${_formatDate(item.bestBeforeDate!)}', 'Bäst före: ${_formatDate(item.bestBeforeDate!)}',
].join(' · '); ].join(' · ');
@@ -331,26 +331,23 @@ class _ForegroundTile extends ConsumerWidget {
], ],
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Row( Align(
children: [ alignment: Alignment.centerLeft,
Flexible( child: Chip(
child: Chip( label: Text(
label: Text( l1CategoryChipLabel('L1: ', item.l1Category),
'L1: ${item.l1Category}', maxLines: 1,
maxLines: 1, overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis,
),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: BorderSide(color: theme.colorScheme.outlineVariant),
backgroundColor: theme.colorScheme.surface,
labelStyle: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
), ),
], padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: BorderSide(color: theme.colorScheme.outlineVariant),
backgroundColor: theme.colorScheme.surface,
labelStyle: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
), ),
], ],
), ),
@@ -7,6 +7,7 @@ import '../../../core/api/api_error_mapper.dart';
import '../../../core/forms/form_options.dart'; import '../../../core/forms/form_options.dart';
import '../../../core/l10n/l10n.dart'; import '../../../core/l10n/l10n.dart';
import '../../../core/ui/async_state_views.dart'; import '../../../core/ui/async_state_views.dart';
import '../../../core/utils/display_labels.dart';
import '../../auth/data/auth_providers.dart'; import '../../auth/data/auth_providers.dart';
import '../../inventory/data/inventory_providers.dart'; import '../../inventory/data/inventory_providers.dart';
import '../data/pantry_providers.dart'; import '../data/pantry_providers.dart';
@@ -373,8 +374,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
if (index == 1) return headerSection; if (index == 1) return headerSection;
final item = filteredItems[index - 2]; final item = filteredItems[index - 2];
final l1Category = _resolveL1Category(item); final l1Category = _resolveL1Category(item);
final location = item.location?.trim(); final location = normalizedOptionalText(item.location);
final hasLocation = location != null && location.isNotEmpty;
return ListTile( return ListTile(
title: Text(item.displayName), title: Text(item.displayName),
@@ -382,32 +382,29 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (hasLocation) if (location != null)
Text( Text(
'Plats: $location', locationLabel('${context.l10n.locationLabel}: ', location),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Row( Align(
children: [ alignment: Alignment.centerLeft,
Flexible( child: Chip(
child: Chip( label: Text(
label: Text( l1CategoryChipLabel('L1: ', l1Category),
'L1: $l1Category', maxLines: 1,
maxLines: 1, overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis,
),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: BorderSide(color: colorScheme.outlineVariant),
backgroundColor: colorScheme.surface,
labelStyle: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
), ),
], padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: BorderSide(color: colorScheme.outlineVariant),
backgroundColor: colorScheme.surface,
labelStyle: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
), ),
], ],
), ),