Features: - Health check endpoint for Portainer monitoring - Local search filtering in DataGrid - Container status badges reflect health (green/orange/blue) Improvements: - Standardized 56px header heights across panels - Container grid parses Docker API format correctly - Search bar styling improvements - Status badges have consistent width Fixes: - Quick links persistence (link type, form refresh) - Iframe switching closes existing content first - ContainerState type conflict resolved Branding: - Updated favicon and icons with Tatlock bucket logo 🤖 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
0d15d3dbb5
commit
bdced8739c
@@ -52,7 +52,7 @@ class QuickLinksDatasource {
|
||||
final model = QuickLinkModel.fromEntity(link);
|
||||
final response = await _dio.post<Map<String, dynamic>>(
|
||||
_basePath,
|
||||
data: model.toCreateJson(),
|
||||
data: model.toJson(),
|
||||
);
|
||||
final data = response.data;
|
||||
|
||||
@@ -69,7 +69,7 @@ class QuickLinksDatasource {
|
||||
final id = int.tryParse(link.id) ?? 0;
|
||||
final response = await _dio.put<Map<String, dynamic>>(
|
||||
'$_basePath/$id',
|
||||
data: model.toCreateJson(),
|
||||
data: model.toJson(),
|
||||
);
|
||||
final data = response.data;
|
||||
|
||||
|
||||
@@ -4,17 +4,18 @@ import 'package:tatlock_ui/features/front_hall/domain/entities/quick_link.dart';
|
||||
part 'quick_link_model.freezed.dart';
|
||||
part 'quick_link_model.g.dart';
|
||||
|
||||
/// Quick link data model for API serialization.
|
||||
/// Quick link data model matching Core API dashboard/quick-links endpoint.
|
||||
///
|
||||
/// Maps to Core API dashboard/quick-links endpoint:
|
||||
/// Field mapping:
|
||||
/// - title (API) ↔ name (Flutter entity)
|
||||
/// - icon (API) ↔ iconName (Flutter entity)
|
||||
/// - position (API) ↔ sortOrder (Flutter entity)
|
||||
/// - is_visible (API) ↔ isActive (Flutter entity)
|
||||
/// - link_type (API) ↔ type (Flutter entity)
|
||||
@freezed
|
||||
sealed class QuickLinkModel with _$QuickLinkModel {
|
||||
const factory QuickLinkModel({
|
||||
required int id,
|
||||
@Default(0) int id,
|
||||
required String title,
|
||||
required String url,
|
||||
String? icon,
|
||||
@@ -22,6 +23,7 @@ sealed class QuickLinkModel with _$QuickLinkModel {
|
||||
String? category,
|
||||
@Default(0) int position,
|
||||
@JsonKey(name: 'is_visible') @Default(true) bool isVisible,
|
||||
@JsonKey(name: 'link_type') @Default('iframe') String linkType,
|
||||
String? color,
|
||||
@JsonKey(name: 'background_color') String? backgroundColor,
|
||||
}) = _QuickLinkModel;
|
||||
@@ -39,7 +41,7 @@ sealed class QuickLinkModel with _$QuickLinkModel {
|
||||
url: url,
|
||||
iconName: icon ?? 'link',
|
||||
category: category,
|
||||
type: QuickLinkType.iframe,
|
||||
type: _parseQuickLinkType(linkType),
|
||||
sortOrder: position,
|
||||
isActive: isVisible,
|
||||
);
|
||||
@@ -55,21 +57,23 @@ sealed class QuickLinkModel with _$QuickLinkModel {
|
||||
category: entity.category,
|
||||
position: entity.sortOrder,
|
||||
isVisible: entity.isActive,
|
||||
linkType: _formatQuickLinkType(entity.type),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
};
|
||||
}
|
||||
/// Parses API link_type string to QuickLinkType enum.
|
||||
QuickLinkType _parseQuickLinkType(String linkType) {
|
||||
return switch (linkType) {
|
||||
'new_tab' => QuickLinkType.newTab,
|
||||
_ => QuickLinkType.iframe,
|
||||
};
|
||||
}
|
||||
|
||||
/// Formats QuickLinkType enum to API link_type string.
|
||||
String _formatQuickLinkType(QuickLinkType type) {
|
||||
return switch (type) {
|
||||
QuickLinkType.newTab => 'new_tab',
|
||||
QuickLinkType.iframe => 'iframe',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ class FrontHallPage extends ConsumerWidget {
|
||||
|
||||
case FrontHallMode.iframe:
|
||||
return IframeView(
|
||||
key: ValueKey(state.activeIframeUrl),
|
||||
url: state.activeIframeUrl!,
|
||||
title: state.activeIframeTitle ?? 'External Content',
|
||||
onClose: () =>
|
||||
|
||||
@@ -83,7 +83,7 @@ class _IframeViewState extends State<IframeView> {
|
||||
children: [
|
||||
// Header bar
|
||||
Container(
|
||||
height: 48,
|
||||
height: 56,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
|
||||
@@ -67,24 +67,21 @@ class _QuickLinkFormState extends ConsumerState<QuickLinkForm> {
|
||||
|
||||
try {
|
||||
final actions = ref.read(quickLinkActionsProvider.notifier);
|
||||
final category = _categoryController.text.trim();
|
||||
|
||||
// Generate ID for new links
|
||||
final id = widget.quickLink?.id ??
|
||||
_nameController.text.toLowerCase().replaceAll(RegExp(r'\s+'), '-');
|
||||
|
||||
final link = QuickLink(
|
||||
id: id,
|
||||
name: _nameController.text.trim(),
|
||||
url: _urlController.text.trim(),
|
||||
iconName: _iconName,
|
||||
category:
|
||||
_categoryController.text.trim().isEmpty ? null : _categoryController.text.trim(),
|
||||
type: _type,
|
||||
isActive: _isActive,
|
||||
sortOrder: widget.quickLink?.sortOrder ?? 0,
|
||||
);
|
||||
|
||||
final QuickLink link;
|
||||
if (isCreate) {
|
||||
// Create new entity
|
||||
link = QuickLink(
|
||||
id: _nameController.text.toLowerCase().replaceAll(RegExp(r'\s+'), '-'),
|
||||
name: _nameController.text.trim(),
|
||||
url: _urlController.text.trim(),
|
||||
iconName: _iconName,
|
||||
category: category.isEmpty ? null : category,
|
||||
type: _type,
|
||||
isActive: _isActive,
|
||||
sortOrder: 0,
|
||||
);
|
||||
await actions.create(link);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -95,6 +92,15 @@ class _QuickLinkFormState extends ConsumerState<QuickLinkForm> {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Update: use copyWith to preserve all existing data
|
||||
link = widget.quickLink!.copyWith(
|
||||
name: _nameController.text.trim(),
|
||||
url: _urlController.text.trim(),
|
||||
iconName: _iconName,
|
||||
category: category.isEmpty ? null : category,
|
||||
type: _type,
|
||||
isActive: _isActive,
|
||||
);
|
||||
await actions.update(link);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -106,6 +112,9 @@ class _QuickLinkFormState extends ConsumerState<QuickLinkForm> {
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh the list before exiting
|
||||
ref.invalidate(quickLinksProvider);
|
||||
|
||||
widget.onSaved();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
|
||||
@@ -53,7 +53,8 @@ class _QuickLinkSettingsContentState
|
||||
body: QuickLinkForm(
|
||||
mode: EntityPageMode.create,
|
||||
onCancel: _stopCreating,
|
||||
onSaved: _stopCreating,
|
||||
onSaved: () =>
|
||||
ref.read(frontHallStateProvider.notifier).exitSettings(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -131,14 +132,14 @@ class _QuickLinkSettingsContentState
|
||||
title: Text('Edit: ${link.name}'),
|
||||
),
|
||||
body: QuickLinkForm(
|
||||
key: ValueKey(link.id), // Force recreation when link changes
|
||||
mode: EntityPageMode.edit,
|
||||
quickLink: link,
|
||||
onCancel: () => ref
|
||||
.read(frontHallStateProvider.notifier)
|
||||
.clearSelectedLink(),
|
||||
onSaved: () => ref
|
||||
.read(frontHallStateProvider.notifier)
|
||||
.clearSelectedLink(),
|
||||
onSaved: () =>
|
||||
ref.read(frontHallStateProvider.notifier).exitSettings(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -297,7 +297,10 @@ class _QuickLinksList extends ConsumerWidget {
|
||||
// In settings mode, select link for editing
|
||||
ref.read(frontHallStateProvider.notifier).selectLink(link.id);
|
||||
} else {
|
||||
// In normal mode, open link
|
||||
// Close any open content first to ensure clean state
|
||||
ref.read(frontHallStateProvider.notifier).showDashboard();
|
||||
|
||||
// Then open the new link
|
||||
if (link.isIframe) {
|
||||
ref
|
||||
.read(frontHallStateProvider.notifier)
|
||||
|
||||
Reference in New Issue
Block a user