Frontend first draft
This commit is contained in:
170
frontend/pshared/lib/provider/template.dart
Normal file
170
frontend/pshared/lib/provider/template.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:pshared/models/permission_bound_storable.dart';
|
||||
import 'package:pshared/provider/exception.dart';
|
||||
import 'package:pshared/provider/resource.dart';
|
||||
import 'package:pshared/service/template.dart';
|
||||
|
||||
|
||||
List<T> mergeLists<T>({
|
||||
required List<T> lhs,
|
||||
required List<T> rhs,
|
||||
required Comparable Function(T) getKey, // Extracts ID dynamically
|
||||
required int Function(T, T) compare,
|
||||
required T Function(T, T) merge,
|
||||
}) {
|
||||
final result = <T>[];
|
||||
final map = {for (var item in lhs) getKey(item): item};
|
||||
|
||||
for (var updated in rhs) {
|
||||
final key = getKey(updated);
|
||||
map[key] = merge(map[key] ?? updated, updated);
|
||||
}
|
||||
|
||||
result.addAll(map.values);
|
||||
result.sort(compare);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// A generic provider that wraps a [BasicService] instance
|
||||
/// to manage state (loading, error, data) without re‑implementing service logic.
|
||||
class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier {
|
||||
final BasicService<T> service;
|
||||
|
||||
Resource<List<T>> _resource = Resource(data: []);
|
||||
Resource<List<T>> get resource => _resource;
|
||||
|
||||
List<T> get items => List.unmodifiable(_resource.data ?? []);
|
||||
bool get isLoading => _resource.isLoading;
|
||||
bool get isEmpty => items.isEmpty;
|
||||
Object? get error => _resource.error;
|
||||
|
||||
String? _currentObjectRef; // Stores the currently selected project ref
|
||||
T? get currentObject => _resource.data?.firstWhereOrNull(
|
||||
(object) => object.id == _currentObjectRef,
|
||||
);
|
||||
|
||||
T? getItemById(String id) => items.firstWhereOrNull((item) => item.id == id);
|
||||
|
||||
GenericProvider({required this.service});
|
||||
|
||||
|
||||
bool Function(T)? _filterPredicate;
|
||||
|
||||
List<T> get filteredItems => _filterPredicate != null ? items.where(_filterPredicate!).toList() : items;
|
||||
|
||||
void setFilterPredicate(bool Function(T)? predicate) {
|
||||
_filterPredicate = predicate;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearFilter() => setFilterPredicate(null);
|
||||
|
||||
void _setResource(Resource<List<T>> newResource) {
|
||||
_resource = newResource;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> loadFuture(Future<List<T>> future) async {
|
||||
_setResource(_resource.copyWith(isLoading: true));
|
||||
try {
|
||||
final list = await future;
|
||||
_setResource(Resource(data: list, isLoading: false));
|
||||
} catch (e) {
|
||||
_setResource(
|
||||
_resource.copyWith(isLoading: false, error: toException(e)),
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> load(String organizationRef, String? parentRef) async {
|
||||
if (parentRef != null) {
|
||||
return loadFuture(service.list(organizationRef, parentRef));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadItem(String itemRef) async {
|
||||
return loadFuture((() async => [await service.get(itemRef)])());
|
||||
}
|
||||
|
||||
|
||||
List<T> merge(List<T> rhs) => mergeLists<T>(
|
||||
lhs: items,
|
||||
rhs: rhs,
|
||||
getKey: (item) => item.id, // Key extractor
|
||||
compare: (a, b) => a.id.compareTo(b.id), // Sorting logic
|
||||
merge: (existing, updated) => updated, // Replace with the updated version
|
||||
);
|
||||
|
||||
Future<T> get(String objectRef) async {
|
||||
_setResource(_resource.copyWith(isLoading: true));
|
||||
try {
|
||||
final item = await service.get(objectRef);
|
||||
_setResource(Resource(data: merge([item]), isLoading: false));
|
||||
return item;
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<T> createObject(String organizationRef, Map<String, dynamic> request) async {
|
||||
_setResource(_resource.copyWith(isLoading: true));
|
||||
try {
|
||||
final newObject = await service.create(organizationRef, request);
|
||||
_setResource(Resource(data: [...items, ...newObject], isLoading: false));
|
||||
return newObject.first;
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> update(Map<String, dynamic> request) async {
|
||||
_setResource(_resource.copyWith(isLoading: true));
|
||||
try {
|
||||
final list = await service.update(request);
|
||||
_setResource(Resource(data: merge(list), isLoading: false));
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> delete(String objectRef) async {
|
||||
_setResource(_resource.copyWith(isLoading: true));
|
||||
|
||||
try {
|
||||
await service.delete(objectRef);
|
||||
if (_currentObjectRef == objectRef) {
|
||||
_currentObjectRef = null;
|
||||
}
|
||||
|
||||
_setResource(Resource(
|
||||
data: _resource.data?.where((p) => p.id != objectRef).toList(),
|
||||
isLoading: false,
|
||||
));
|
||||
} catch (e) {
|
||||
_setResource(Resource(data: _resource.data, isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
bool setCurrentObject(String? objectRef) {
|
||||
if (objectRef == null) {
|
||||
_currentObjectRef = null;
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
if (_resource.data?.any((p) => p.id == objectRef) ?? false) {
|
||||
_currentObjectRef = objectRef;
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // Object not found
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user