feat(front-hall): wire quick links to Core API dashboard endpoint
Update quick links integration to use new /dashboard/quick-links API:
- Change API path from /front-hall/quick-links to /dashboard/quick-links
- Update field mappings: name→title, icon_name→icon, sort_order→position, is_active→is_visible
- Change ID type from String to int with conversion in provider
- Parse new response format {"links": [...], "total": N}
- Update reorder endpoint from PUT to POST with link_ids
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
1d68402067
commit
0d15d3dbb5
@@ -8,27 +8,35 @@ part 'quick_links_datasource.g.dart';
|
||||
|
||||
/// Data source for quick link operations.
|
||||
///
|
||||
/// Provides CRUD operations for Front Hall quick links via Core API.
|
||||
/// Provides CRUD operations for Front Hall quick links via Core API
|
||||
/// dashboard/quick-links endpoint.
|
||||
class QuickLinksDatasource {
|
||||
QuickLinksDatasource(this._dio);
|
||||
|
||||
final Dio _dio;
|
||||
|
||||
static const _basePath = '/front-hall/quick-links';
|
||||
static const _basePath = '/dashboard/quick-links';
|
||||
|
||||
/// Gets all quick links.
|
||||
///
|
||||
/// API returns: {"links": [...], "total": N}
|
||||
Future<List<QuickLink>> getQuickLinks() async {
|
||||
final response = await _dio.get<List<dynamic>>(_basePath);
|
||||
final data = response.data ?? [];
|
||||
final response = await _dio.get<Map<String, dynamic>>(_basePath);
|
||||
final data = response.data;
|
||||
|
||||
return data
|
||||
if (data == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final links = data['links'] as List<dynamic>? ?? [];
|
||||
return links
|
||||
.map((json) => QuickLinkModel.fromJson(json as Map<String, dynamic>))
|
||||
.map((model) => model.toEntity())
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Gets a single quick link by ID.
|
||||
Future<QuickLink> getQuickLink(String id) async {
|
||||
Future<QuickLink> getQuickLink(int id) async {
|
||||
final response = await _dio.get<Map<String, dynamic>>('$_basePath/$id');
|
||||
final data = response.data;
|
||||
|
||||
@@ -44,7 +52,7 @@ class QuickLinksDatasource {
|
||||
final model = QuickLinkModel.fromEntity(link);
|
||||
final response = await _dio.post<Map<String, dynamic>>(
|
||||
_basePath,
|
||||
data: model.toJson(),
|
||||
data: model.toCreateJson(),
|
||||
);
|
||||
final data = response.data;
|
||||
|
||||
@@ -58,9 +66,10 @@ class QuickLinksDatasource {
|
||||
/// Updates an existing quick link.
|
||||
Future<QuickLink> updateQuickLink(QuickLink link) async {
|
||||
final model = QuickLinkModel.fromEntity(link);
|
||||
final id = int.tryParse(link.id) ?? 0;
|
||||
final response = await _dio.put<Map<String, dynamic>>(
|
||||
'$_basePath/${link.id}',
|
||||
data: model.toJson(),
|
||||
'$_basePath/$id',
|
||||
data: model.toCreateJson(),
|
||||
);
|
||||
final data = response.data;
|
||||
|
||||
@@ -72,15 +81,17 @@ class QuickLinksDatasource {
|
||||
}
|
||||
|
||||
/// Deletes a quick link.
|
||||
Future<void> deleteQuickLink(String id) async {
|
||||
Future<void> deleteQuickLink(int id) async {
|
||||
await _dio.delete<void>('$_basePath/$id');
|
||||
}
|
||||
|
||||
/// Reorders quick links by updating sort orders.
|
||||
Future<void> reorderQuickLinks(List<String> orderedIds) async {
|
||||
await _dio.put<void>(
|
||||
/// Reorders quick links by updating positions.
|
||||
///
|
||||
/// API expects: {"link_ids": [1, 2, 3]}
|
||||
Future<void> reorderQuickLinks(List<int> orderedIds) async {
|
||||
await _dio.post<void>(
|
||||
'$_basePath/reorder',
|
||||
data: {'ids': orderedIds},
|
||||
data: {'link_ids': orderedIds},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,25 @@ part 'quick_link_model.freezed.dart';
|
||||
part 'quick_link_model.g.dart';
|
||||
|
||||
/// Quick link data model for API serialization.
|
||||
///
|
||||
/// Maps to Core API dashboard/quick-links endpoint:
|
||||
/// - title (API) ↔ name (Flutter entity)
|
||||
/// - icon (API) ↔ iconName (Flutter entity)
|
||||
/// - position (API) ↔ sortOrder (Flutter entity)
|
||||
/// - is_visible (API) ↔ isActive (Flutter entity)
|
||||
@freezed
|
||||
sealed class QuickLinkModel with _$QuickLinkModel {
|
||||
const factory QuickLinkModel({
|
||||
required String id,
|
||||
required String name,
|
||||
required int id,
|
||||
required String title,
|
||||
required String url,
|
||||
@JsonKey(name: 'icon_name') required String iconName,
|
||||
String? icon,
|
||||
String? description,
|
||||
String? category,
|
||||
@Default('iframe') String type,
|
||||
@JsonKey(name: 'sort_order') @Default(0) int sortOrder,
|
||||
@JsonKey(name: 'is_active') @Default(true) bool isActive,
|
||||
@Default(0) int position,
|
||||
@JsonKey(name: 'is_visible') @Default(true) bool isVisible,
|
||||
String? color,
|
||||
@JsonKey(name: 'background_color') String? backgroundColor,
|
||||
}) = _QuickLinkModel;
|
||||
|
||||
const QuickLinkModel._();
|
||||
@@ -26,36 +34,42 @@ sealed class QuickLinkModel with _$QuickLinkModel {
|
||||
/// Converts to domain entity.
|
||||
QuickLink toEntity() {
|
||||
return QuickLink(
|
||||
id: id,
|
||||
name: name,
|
||||
id: id.toString(),
|
||||
name: title,
|
||||
url: url,
|
||||
iconName: iconName,
|
||||
iconName: icon ?? 'link',
|
||||
category: category,
|
||||
type: _parseType(type),
|
||||
sortOrder: sortOrder,
|
||||
isActive: isActive,
|
||||
type: QuickLinkType.iframe,
|
||||
sortOrder: position,
|
||||
isActive: isVisible,
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates model from domain entity.
|
||||
factory QuickLinkModel.fromEntity(QuickLink entity) {
|
||||
return QuickLinkModel(
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
id: int.tryParse(entity.id) ?? 0,
|
||||
title: entity.name,
|
||||
url: entity.url,
|
||||
iconName: entity.iconName,
|
||||
icon: entity.iconName,
|
||||
category: entity.category,
|
||||
type: entity.type == QuickLinkType.iframe ? 'iframe' : 'new_tab',
|
||||
sortOrder: entity.sortOrder,
|
||||
isActive: entity.isActive,
|
||||
position: entity.sortOrder,
|
||||
isVisible: entity.isActive,
|
||||
);
|
||||
}
|
||||
|
||||
QuickLinkType _parseType(String type) {
|
||||
return switch (type.toLowerCase()) {
|
||||
'iframe' => QuickLinkType.iframe,
|
||||
'new_tab' || 'newtab' => QuickLinkType.newTab,
|
||||
_ => QuickLinkType.iframe,
|
||||
/// Creates a JSON map for creating a new quick link (without id).
|
||||
Map<String, dynamic> toCreateJson() {
|
||||
return {
|
||||
'title': title,
|
||||
'url': url,
|
||||
if (icon != null) 'icon': icon,
|
||||
if (description != null) 'description': description,
|
||||
if (category != null) 'category': category,
|
||||
'position': position,
|
||||
'is_visible': isVisible,
|
||||
if (color != null) 'color': color,
|
||||
if (backgroundColor != null) 'background_color': backgroundColor,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,11 @@ Future<QuickLink> quickLink(Ref ref, String id) async {
|
||||
// If not in defaults, try API
|
||||
try {
|
||||
final datasource = ref.watch(quickLinksDatasourceProvider);
|
||||
return await datasource.getQuickLink(id);
|
||||
final intId = int.tryParse(id);
|
||||
if (intId == null) {
|
||||
throw Exception('Invalid link ID: $id');
|
||||
}
|
||||
return await datasource.getQuickLink(intId);
|
||||
} catch (_) {
|
||||
throw Exception('Link not found: $id');
|
||||
}
|
||||
@@ -45,49 +49,82 @@ class QuickLinkActions extends _$QuickLinkActions {
|
||||
@override
|
||||
AsyncValue<void> build() => const AsyncValue.data(null);
|
||||
|
||||
/// Safely sets state, ignoring disposal errors.
|
||||
void _safeSetState(AsyncValue<void> newState) {
|
||||
try {
|
||||
state = newState;
|
||||
} catch (_) {
|
||||
// Provider was disposed - ignore
|
||||
}
|
||||
}
|
||||
|
||||
/// Safely invalidates providers after async operations.
|
||||
void _safeInvalidate(List<ProviderOrFamily> providers) {
|
||||
try {
|
||||
for (final provider in providers) {
|
||||
ref.invalidate(provider);
|
||||
}
|
||||
} catch (_) {
|
||||
// Provider was disposed - ignore
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new quick link.
|
||||
Future<QuickLink?> create(QuickLink link) async {
|
||||
state = const AsyncValue.loading();
|
||||
QuickLink? result;
|
||||
state = await AsyncValue.guard(() async {
|
||||
try {
|
||||
_safeSetState(const AsyncValue.loading());
|
||||
final datasource = ref.read(quickLinksDatasourceProvider);
|
||||
result = await datasource.createQuickLink(link);
|
||||
ref.invalidate(quickLinksProvider);
|
||||
});
|
||||
_safeInvalidate([quickLinksProvider]);
|
||||
_safeSetState(const AsyncValue.data(null));
|
||||
} catch (e, st) {
|
||||
_safeSetState(AsyncValue.error(e, st));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Updates an existing quick link.
|
||||
Future<QuickLink?> update(QuickLink link) async {
|
||||
state = const AsyncValue.loading();
|
||||
QuickLink? result;
|
||||
state = await AsyncValue.guard(() async {
|
||||
try {
|
||||
_safeSetState(const AsyncValue.loading());
|
||||
final datasource = ref.read(quickLinksDatasourceProvider);
|
||||
result = await datasource.updateQuickLink(link);
|
||||
ref.invalidate(quickLinksProvider);
|
||||
ref.invalidate(quickLinkProvider(link.id));
|
||||
});
|
||||
_safeInvalidate([quickLinksProvider, quickLinkProvider(link.id)]);
|
||||
_safeSetState(const AsyncValue.data(null));
|
||||
} catch (e, st) {
|
||||
_safeSetState(AsyncValue.error(e, st));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Deletes a quick link.
|
||||
Future<void> delete(String id) async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
try {
|
||||
_safeSetState(const AsyncValue.loading());
|
||||
final datasource = ref.read(quickLinksDatasourceProvider);
|
||||
await datasource.deleteQuickLink(id);
|
||||
ref.invalidate(quickLinksProvider);
|
||||
});
|
||||
final intId = int.tryParse(id) ?? 0;
|
||||
await datasource.deleteQuickLink(intId);
|
||||
_safeInvalidate([quickLinksProvider]);
|
||||
_safeSetState(const AsyncValue.data(null));
|
||||
} catch (e, st) {
|
||||
_safeSetState(AsyncValue.error(e, st));
|
||||
}
|
||||
}
|
||||
|
||||
/// Reorders quick links.
|
||||
Future<void> reorder(List<String> orderedIds) async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
try {
|
||||
_safeSetState(const AsyncValue.loading());
|
||||
final datasource = ref.read(quickLinksDatasourceProvider);
|
||||
await datasource.reorderQuickLinks(orderedIds);
|
||||
ref.invalidate(quickLinksProvider);
|
||||
});
|
||||
final intIds = orderedIds.map((id) => int.tryParse(id) ?? 0).toList();
|
||||
await datasource.reorderQuickLinks(intIds);
|
||||
_safeInvalidate([quickLinksProvider]);
|
||||
_safeSetState(const AsyncValue.data(null));
|
||||
} catch (e, st) {
|
||||
_safeSetState(AsyncValue.error(e, st));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user