Post page form

This commit is contained in:
2025-12-12 00:11:52 +08:00
parent de20803119
commit 580663dcda
3 changed files with 381 additions and 173 deletions

View File

@@ -95,9 +95,6 @@ class SitePagesNotifier extends AsyncNotifier<List<SnPublicationPage>> {
try { try {
final apiClient = ref.read(apiClientProvider); final apiClient = ref.read(apiClientProvider);
await apiClient.delete('/zone/sites/pages/$pageId'); await apiClient.delete('/zone/sites/pages/$pageId');
// Refresh the pages list
ref.invalidate(sitePagesProvider(arg.pubName, arg.siteSlug));
} catch (error, stackTrace) { } catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace); state = AsyncValue.error(error, stackTrace);
rethrow; rethrow;
@@ -105,8 +102,9 @@ class SitePagesNotifier extends AsyncNotifier<List<SnPublicationPage>> {
} }
} }
final sitePagesNotifierProvider = AsyncNotifierProvider.autoDispose.family< final sitePagesNotifierProvider = AsyncNotifierProvider.autoDispose
.family<
SitePagesNotifier, SitePagesNotifier,
List<SnPublicationPage>, List<SnPublicationPage>,
({String pubName, String siteSlug}) ({String pubName, String siteSlug})
>(SitePagesNotifier.new); >(SitePagesNotifier.new);

View File

@@ -24,6 +24,10 @@ class PageForm extends HookConsumerWidget {
int _getPageType(SnPublicationPage? page) { int _getPageType(SnPublicationPage? page) {
if (page == null) return 0; // Default to HTML if (page == null) return 0; // Default to HTML
// Check config structure to determine type // Check config structure to determine type
if (page.config?.containsKey('filter') == true ||
page.config?.containsKey('layout') == true) {
return 2; // Post Page
}
return page.config?.containsKey('target') == true ? 1 : 0; return page.config?.containsKey('target') == true ? 1 : 0;
} }
@@ -48,6 +52,42 @@ class PageForm extends HookConsumerWidget {
text: pageType.value == 1 ? (page?.config?['target'] ?? '') : '', text: pageType.value == 1 ? (page?.config?['target'] ?? '') : '',
); );
// Post Page Controllers
final filterPubNameController = useTextEditingController(
text: pageType.value == 2
? (page?.config?['filter']?['pub_name'] ?? '')
: '',
);
final filterOrderByController = useTextEditingController(
text: pageType.value == 2
? (page?.config?['filter']?['order_by'] ?? '')
: '',
);
final filterOrderDesc = useState(
pageType.value == 2
? (page?.config?['filter']?['order_desc'] ?? true)
: true,
);
final filterTypes = useState<List<int>>(
(page?.config?['filter']?['types'] as List?)?.cast<int>() ?? [0],
);
final layoutTitleController = useTextEditingController(
text: pageType.value == 2
? (page?.config?['layout']?['title'] ?? '')
: '',
);
final layoutDescriptionController = useTextEditingController(
text: pageType.value == 2
? (page?.config?['layout']?['description'] ?? '')
: '',
);
final layoutShowPub = useState(
pageType.value == 2
? (page?.config?['layout']?['show_pub'] ?? true)
: true,
);
final isLoading = useState(false); final isLoading = useState(false);
// Update controllers when page type changes // Update controllers when page type changes
@@ -59,11 +99,36 @@ class PageForm extends HookConsumerWidget {
page?.config?['html'] ?? page?.config?['content'] ?? ''; page?.config?['html'] ?? page?.config?['content'] ?? '';
titleController.text = page?.config?['title'] ?? ''; titleController.text = page?.config?['title'] ?? '';
targetController.clear(); targetController.clear();
} else { filterPubNameController.clear();
filterOrderByController.clear();
layoutTitleController.clear();
layoutDescriptionController.clear();
} else if (pageType.value == 1) {
// Redirect mode // Redirect mode
htmlController.clear(); htmlController.clear();
titleController.clear(); titleController.clear();
targetController.text = page?.config?['target'] ?? ''; targetController.text = page?.config?['target'] ?? '';
filterPubNameController.clear();
filterOrderByController.clear();
layoutTitleController.clear();
layoutDescriptionController.clear();
} else if (pageType.value == 2) {
// Post Page mode
htmlController.clear();
titleController.clear();
targetController.clear();
filterPubNameController.text =
page?.config?['filter']?['pub_name'] ?? '';
filterOrderByController.text =
page?.config?['filter']?['order_by'] ?? '';
filterOrderDesc.value =
page?.config?['filter']?['order_desc'] ?? true;
filterTypes.value =
(page?.config?['filter']?['types'] as List?)?.cast<int>() ?? [0];
layoutTitleController.text = page?.config?['layout']?['title'] ?? '';
layoutDescriptionController.text =
page?.config?['layout']?['description'] ?? '';
layoutShowPub.value = page?.config?['layout']?['show_pub'] ?? true;
} }
}); });
return null; return null;
@@ -77,14 +142,28 @@ class PageForm extends HookConsumerWidget {
htmlController.text = htmlController.text =
page!.config?['html'] ?? page!.config?['content'] ?? ''; page!.config?['html'] ?? page!.config?['content'] ?? '';
titleController.text = page!.config?['title'] ?? ''; titleController.text = page!.config?['title'] ?? '';
} else { } else if (pageType.value == 1) {
targetController.text = page!.config?['target'] ?? ''; targetController.text = page!.config?['target'] ?? '';
} else if (pageType.value == 2) {
filterPubNameController.text =
page!.config?['filter']?['pub_name'] ?? '';
filterOrderByController.text =
page!.config?['filter']?['order_by'] ?? '';
filterOrderDesc.value =
page!.config?['filter']?['order_desc'] ?? true;
filterTypes.value =
(page!.config?['filter']?['types'] as List?)?.cast<int>() ?? [0];
layoutTitleController.text = page!.config?['layout']?['title'] ?? '';
layoutDescriptionController.text =
page!.config?['layout']?['description'] ?? '';
layoutShowPub.value = page!.config?['layout']?['show_pub'] ?? true;
} }
} }
return null; return null;
}, [page]); }, [page]);
final savePage = useCallback(() async { final savePage = useCallback(
() async {
if (!formKey.currentState!.validate()) return; if (!formKey.currentState!.validate()) return;
isLoading.value = true; isLoading.value = true;
@@ -109,13 +188,36 @@ class PageForm extends HookConsumerWidget {
'html': htmlController.text, 'html': htmlController.text,
}, },
}; };
} else { } else if (pageType.value == 1) {
// Redirect page // Redirect page
pageData = { pageData = {
'type': 1, 'type': 1,
'path': pathController.text, 'path': pathController.text,
'config': {'target': targetController.text}, 'config': {'target': targetController.text},
}; };
} else {
// Post Page
pageData = {
'type': 2,
'path': pathController.text,
'config': {
'filter': {
if (filterPubNameController.text.isNotEmpty)
'pub_name': filterPubNameController.text,
if (filterOrderByController.text.isNotEmpty)
'order_by': filterOrderByController.text,
'order_desc': filterOrderDesc.value,
'types': filterTypes.value,
},
'layout': {
if (layoutTitleController.text.isNotEmpty)
'title': layoutTitleController.text,
if (layoutDescriptionController.text.isNotEmpty)
'description': layoutDescriptionController.text,
'show_pub': layoutShowPub.value,
},
},
};
} }
if (page == null) { if (page == null) {
@@ -139,7 +241,17 @@ class PageForm extends HookConsumerWidget {
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
}, [pageType, pubName, site.slug, page]); },
[
pageType,
pubName,
site.slug,
page,
filterOrderDesc.value,
filterTypes.value,
layoutShowPub.value,
],
);
final deletePage = useCallback(() async { final deletePage = useCallback(() async {
if (page == null) return; // Shouldn't happen for editing if (page == null) return; // Shouldn't happen for editing
@@ -225,6 +337,16 @@ class PageForm extends HookConsumerWidget {
], ],
), ),
), ),
DropdownMenuItem(
value: 2,
child: Row(
children: [
Icon(Symbols.article, size: 20),
Gap(8),
Text('Post Page'),
],
),
),
], ],
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {
@@ -238,14 +360,13 @@ class PageForm extends HookConsumerWidget {
return null; return null;
}, },
).padding(all: 20), ).padding(all: 20),
// Conditional form fields based on page type
if (pageType.value == 0) ...[ // Common "Path" field for all types
// HTML Page fields
TextFormField( TextFormField(
controller: pathController, controller: pathController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Page Path', labelText: 'Page Path',
hintText: '/about, /contact, etc.', hintText: '/about, /posts, etc.',
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)), borderRadius: BorderRadius.all(Radius.circular(12)),
), ),
@@ -269,6 +390,10 @@ class PageForm extends HookConsumerWidget {
FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 20), ).padding(horizontal: 20),
const SizedBox(height: 16), const SizedBox(height: 16),
// Conditional form fields based on page type
if (pageType.value == 0) ...[
// HTML Page fields
TextFormField( TextFormField(
controller: titleController, controller: titleController,
decoration: const InputDecoration( decoration: const InputDecoration(
@@ -309,36 +434,8 @@ class PageForm extends HookConsumerWidget {
return null; return null;
}, },
).padding(horizontal: 20), ).padding(horizontal: 20),
] else ...[ ] else if (pageType.value == 1) ...[
// Redirect Page fields // Redirect Page fields
TextFormField(
controller: pathController,
decoration: const InputDecoration(
labelText: 'Page Path',
hintText: '/old-page, /redirect, etc.',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a page path';
}
if (!RegExp(r'^[a-zA-Z0-9\-/_]+$').hasMatch(value)) {
return 'Page path can only contain letters, numbers, hyphens, underscores, and slashes';
}
if (!value.startsWith('/')) {
return 'Page path must start with /';
}
if (value.contains('//')) {
return 'Page path cannot have consecutive slashes';
}
return null;
},
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 20),
const SizedBox(height: 16),
TextFormField( TextFormField(
controller: targetController, controller: targetController,
decoration: const InputDecoration( decoration: const InputDecoration(
@@ -353,7 +450,9 @@ class PageForm extends HookConsumerWidget {
return 'Please enter a redirect target'; return 'Please enter a redirect target';
} }
if (!value.startsWith('/') && if (!value.startsWith('/') &&
// ignore: use_string_starts_with_pattern
!value.startsWith('http://') && !value.startsWith('http://') &&
// ignore: use_string_starts_with_pattern
!value.startsWith('https://')) { !value.startsWith('https://')) {
return 'Target must be a relative path (/) or absolute URL (http/https)'; return 'Target must be a relative path (/) or absolute URL (http/https)';
} }
@@ -362,7 +461,138 @@ class PageForm extends HookConsumerWidget {
onTapOutside: (_) => onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 20), ).padding(horizontal: 20),
] else if (pageType.value == 2) ...[
// Post Page fields
const Text(
'Filter Settings',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
).alignment(Alignment.centerLeft).padding(horizontal: 24),
const Gap(8),
TextFormField(
controller: filterPubNameController,
decoration: const InputDecoration(
labelText: 'Publication Name (Optional)',
hintText: 'Filter by publication name',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 20),
const Gap(16),
TextFormField(
controller: filterOrderByController,
decoration: const InputDecoration(
labelText: 'Order By (Optional)',
hintText: 'e.g. published_at',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 20),
const Gap(8),
SwitchListTile(
value: filterOrderDesc.value,
onChanged: (value) => filterOrderDesc.value = value,
title: const Text('Order Descending'),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
),
),
const Gap(8),
const Text(
'Content Types',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
).alignment(Alignment.centerLeft).padding(horizontal: 24),
const Gap(4),
Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.start,
runAlignment: WrapAlignment.start,
children: [
FilterChip(
label: const Text('Regular Post'),
selected: filterTypes.value.contains(0),
onSelected: (selected) {
final types = [...filterTypes.value];
if (selected) {
types.add(0);
} else {
types.remove(0);
}
filterTypes.value = types;
},
),
FilterChip(
label: const Text('Article'),
selected: filterTypes.value.contains(1),
onSelected: (selected) {
final types = [...filterTypes.value];
if (selected) {
types.add(1);
} else {
types.remove(1);
}
filterTypes.value = types;
},
),
], ],
).padding(horizontal: 20),
const Gap(24),
const Text(
'Layout Settings',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
).alignment(Alignment.centerLeft).padding(horizontal: 24),
const Gap(8),
TextFormField(
controller: layoutTitleController,
decoration: const InputDecoration(
labelText: 'Title (Optional)',
hintText: 'Page Title',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 20),
const Gap(16),
TextFormField(
controller: layoutDescriptionController,
decoration: const InputDecoration(
labelText: 'Description (Optional)',
hintText: 'Page Description',
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 20),
const Gap(8),
SwitchListTile(
value: layoutShowPub.value,
onChanged: (value) => layoutShowPub.value = value,
title: const Text('Show Publication Info'),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
),
),
],
Row( Row(
children: [ children: [
if (page != null) ...[ if (page != null) ...[

View File

@@ -37,8 +37,7 @@ class PageItem extends HookConsumerWidget {
title: Text(page.path ?? '/'), title: Text(page.path ?? '/'),
subtitle: Text(page.config?['title'] ?? 'Untitled'), subtitle: Text(page.config?['title'] ?? 'Untitled'),
trailing: PopupMenuButton<String>( trailing: PopupMenuButton<String>(
itemBuilder: itemBuilder: (context) => [
(context) => [
PopupMenuItem( PopupMenuItem(
value: 'edit', value: 'edit',
child: Row( child: Row(
@@ -67,8 +66,7 @@ class PageItem extends HookConsumerWidget {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
builder: builder: (context) =>
(context) =>
PageForm(site: site, pubName: pubName, page: page), PageForm(site: site, pubName: pubName, page: page),
).then((_) { ).then((_) {
// Refresh pages after editing // Refresh pages after editing
@@ -76,37 +74,19 @@ class PageItem extends HookConsumerWidget {
}); });
break; break;
case 'delete': case 'delete':
final confirmed = await showDialog<bool>( final confirmed = await showConfirmAlert(
context: context,
builder:
(context) => AlertDialog(
title: const Text('Delete Page'),
content: const Text(
'Are you sure you want to delete this page?', 'Are you sure you want to delete this page?',
), 'Delete the Page',
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Delete'),
),
],
),
); );
if (confirmed == true) { if (confirmed) {
try { try {
await ref final provider = sitePagesNotifierProvider((
.read(
sitePagesNotifierProvider((
pubName: pubName, pubName: pubName,
siteSlug: site.slug, siteSlug: site.slug,
)).notifier, ));
) await ref.read(provider.notifier).deletePage(page.id);
.deletePage(page.id); ref.invalidate(provider);
showSnackBar('Page deleted successfully'); showSnackBar('Page deleted successfully');
} catch (e) { } catch (e) {
showErrorAlert(e); showErrorAlert(e);