@actuate-media/cms-admin 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +21 -21
  2. package/dist/AdminRoot.d.ts.map +1 -1
  3. package/dist/AdminRoot.js +17 -11
  4. package/dist/AdminRoot.js.map +1 -1
  5. package/dist/actuate-admin.css +2 -0
  6. package/dist/components/TipTapEditor.js +78 -78
  7. package/dist/lib/useApiData.d.ts +8 -1
  8. package/dist/lib/useApiData.d.ts.map +1 -1
  9. package/dist/lib/useApiData.js +39 -7
  10. package/dist/lib/useApiData.js.map +1 -1
  11. package/dist/views/Dashboard.d.ts +2 -1
  12. package/dist/views/Dashboard.d.ts.map +1 -1
  13. package/dist/views/Dashboard.js +52 -7
  14. package/dist/views/Dashboard.js.map +1 -1
  15. package/package.json +10 -5
  16. package/src/AdminRoot.tsx +312 -0
  17. package/src/__tests__/lib/search.test.ts +138 -0
  18. package/src/__tests__/lib/utils.test.ts +19 -0
  19. package/src/__tests__/router/match-route.test.ts +47 -0
  20. package/src/__tests__/router/strip-base.test.ts +30 -0
  21. package/src/components/Breadcrumbs.tsx +92 -0
  22. package/src/components/CommandPalette.tsx +384 -0
  23. package/src/components/ErrorBoundary.tsx +52 -0
  24. package/src/components/FocalPointPicker.tsx +54 -0
  25. package/src/components/FolderTree.tsx +427 -0
  26. package/src/components/LivePreview.tsx +136 -0
  27. package/src/components/LocaleProvider.tsx +51 -0
  28. package/src/components/LocaleSwitcher.tsx +51 -0
  29. package/src/components/MediaPickerModal.tsx +183 -0
  30. package/src/components/PresenceIndicator.tsx +71 -0
  31. package/src/components/SEOPanel.tsx +767 -0
  32. package/src/components/ThemeProvider.tsx +98 -0
  33. package/src/components/TipTapEditor.tsx +469 -0
  34. package/src/components/VersionHistory.tsx +167 -0
  35. package/src/components/ui/Avatar.tsx +42 -0
  36. package/src/components/ui/Badge.tsx +25 -0
  37. package/src/components/ui/Button.tsx +52 -0
  38. package/src/components/ui/CommandPalette.tsx +119 -0
  39. package/src/components/ui/ConfirmDialog.tsx +52 -0
  40. package/src/components/ui/DataTable.tsx +194 -0
  41. package/src/components/ui/EmptyState.tsx +29 -0
  42. package/src/components/ui/Modal.tsx +48 -0
  43. package/src/components/ui/Pagination.tsx +79 -0
  44. package/src/components/ui/SearchInput.tsx +44 -0
  45. package/src/components/ui/Skeleton.tsx +48 -0
  46. package/src/components/ui/Toast.tsx +66 -0
  47. package/src/components/ui/index.ts +24 -0
  48. package/src/fields/ArrayField.tsx +92 -0
  49. package/src/fields/BlockBuilderField.tsx +421 -0
  50. package/src/fields/DateField.tsx +41 -0
  51. package/src/fields/FieldRenderer.tsx +84 -0
  52. package/src/fields/GroupField.tsx +41 -0
  53. package/src/fields/MediaField.tsx +48 -0
  54. package/src/fields/NavBuilderField.tsx +78 -0
  55. package/src/fields/NumberField.tsx +45 -0
  56. package/src/fields/RelationshipField.tsx +245 -0
  57. package/src/fields/RichTextField.tsx +26 -0
  58. package/src/fields/SelectField.tsx +117 -0
  59. package/src/fields/SlugField.tsx +65 -0
  60. package/src/fields/TextField.tsx +48 -0
  61. package/src/fields/ToggleField.tsx +36 -0
  62. package/src/fields/block-types.ts +95 -0
  63. package/src/fields/index.ts +17 -0
  64. package/src/hooks/useContentLock.ts +52 -0
  65. package/src/hooks/useDebounce.ts +14 -0
  66. package/src/hooks/useKeyboardShortcuts.ts +32 -0
  67. package/src/index.ts +55 -0
  68. package/src/layout/Header.tsx +135 -0
  69. package/src/layout/Layout.tsx +77 -0
  70. package/src/layout/Sidebar.tsx +216 -0
  71. package/src/lib/api.ts +67 -0
  72. package/src/lib/search.ts +59 -0
  73. package/src/lib/useApiData.ts +95 -0
  74. package/src/lib/utils.ts +6 -0
  75. package/src/router/index.ts +81 -0
  76. package/src/styles/build-input.css +11 -0
  77. package/src/styles/tailwind.css +11 -6
  78. package/src/styles/theme.css +182 -181
  79. package/src/views/CollectionList.tsx +270 -0
  80. package/src/views/Dashboard.tsx +300 -0
  81. package/src/views/DocumentEdit.tsx +377 -0
  82. package/src/views/FormEditor.tsx +533 -0
  83. package/src/views/FormSubmissions.tsx +316 -0
  84. package/src/views/Forms.tsx +106 -0
  85. package/src/views/Login.tsx +322 -0
  86. package/src/views/MediaBrowser.tsx +774 -0
  87. package/src/views/PageEditor.tsx +192 -0
  88. package/src/views/Pages.tsx +354 -0
  89. package/src/views/PostEditor.tsx +251 -0
  90. package/src/views/Posts.tsx +243 -0
  91. package/src/views/Redirects.tsx +293 -0
  92. package/src/views/SEO.tsx +458 -0
  93. package/src/views/Settings.tsx +811 -0
  94. package/src/views/SetupWizard.tsx +207 -0
  95. package/src/views/Users.tsx +282 -0
@@ -1,17 +1,62 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { FileText, Layout, Image, Users, User, Calendar, Loader2, AlertTriangle } from 'lucide-react';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { FileText, File, Layout, Image, Users, User, Calendar, Loader2, AlertTriangle, Database } from 'lucide-react';
4
4
  import { useApiData } from '../lib/useApiData.js';
5
- export function Dashboard({ onNavigate }) {
5
+ function resolveCollections(config) {
6
+ if (!config?.collections)
7
+ return [];
8
+ const raw = config.collections;
9
+ const list = Array.isArray(raw) ? raw : Object.values(raw);
10
+ return list
11
+ .filter((c) => !c.admin?.hidden)
12
+ .map((c) => ({
13
+ slug: c.slug,
14
+ type: c.type,
15
+ labels: c.labels,
16
+ }));
17
+ }
18
+ function collectionLabel(col, plural = true) {
19
+ if (plural)
20
+ return col.labels?.plural ?? col.slug.charAt(0).toUpperCase() + col.slug.slice(1);
21
+ return col.labels?.singular ?? col.slug.charAt(0).toUpperCase() + col.slug.slice(1);
22
+ }
23
+ const STAT_COLORS = [
24
+ { bg: 'bg-blue-100', text: 'text-blue-600' },
25
+ { bg: 'bg-purple-100', text: 'text-purple-600' },
26
+ { bg: 'bg-indigo-100', text: 'text-indigo-600' },
27
+ { bg: 'bg-teal-100', text: 'text-teal-600' },
28
+ ];
29
+ function collectionIcon(col) {
30
+ if (col.type === 'page')
31
+ return File;
32
+ return FileText;
33
+ }
34
+ export function Dashboard({ config, onNavigate }) {
6
35
  const nav = (path) => onNavigate?.(path);
7
- const { data, loading, error, refetch } = useApiData('/stats');
8
- const totalPosts = data?.totalDocuments ?? 0;
36
+ const { data, loading, error, exhausted, refetch } = useApiData('/stats');
37
+ const { data: health } = useApiData('/health');
38
+ const collections = resolveCollections(config);
39
+ const hasCollections = collections.length > 0;
9
40
  const totalMedia = data?.totalMedia ?? 0;
10
41
  const totalUsers = data?.totalUsers ?? 0;
11
- const recentPosts = data?.recentDocuments ?? [];
42
+ const collectionCounts = data?.collectionCounts ?? {};
43
+ const recentDocs = data?.recentDocuments ?? [];
44
+ const primaryCollection = hasCollections ? collections[0] : null;
12
45
  if (loading) {
13
46
  return (_jsx("div", { className: "p-3 pr-6 sm:p-4 sm:pr-8 flex items-center justify-center h-64", children: _jsx(Loader2, { className: "w-6 h-6 animate-spin text-blue-600" }) }));
14
47
  }
15
- return (_jsxs("div", { className: "p-3 pr-6 sm:p-4 sm:pr-8", children: [error && (_jsxs("div", { className: "mb-4 flex items-center gap-3 rounded-lg border border-amber-200 bg-amber-50 p-3", children: [_jsx(AlertTriangle, { className: "w-5 h-5 text-amber-600 shrink-0" }), _jsx("span", { className: "text-sm text-amber-800 flex-1", children: "Some dashboard data may be unavailable. This is normal if your database hasn't been set up yet." }), _jsx("button", { onClick: refetch, className: "px-3 py-1 text-sm text-amber-700 border border-amber-300 rounded-lg hover:bg-amber-100 transition-colors", children: "Retry" })] })), _jsxs("div", { className: "mb-4 sm:mb-6", children: [_jsx("h1", { className: "text-xl sm:text-2xl font-semibold text-gray-900 mb-1", children: "Dashboard" }), _jsx("p", { className: "text-sm text-gray-600", children: "Welcome back! Here's what's happening." })] }), _jsxs("div", { className: "grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 mb-4 sm:mb-6", children: [_jsx("div", { className: "bg-white rounded-lg border border-gray-200 p-3 sm:p-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("p", { className: "text-xs text-gray-600 mb-1", children: "Total Posts" }), _jsx("p", { className: "text-xl sm:text-2xl font-semibold text-gray-900", children: totalPosts }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "\u2014" })] }), _jsx("div", { className: "w-8 h-8 sm:w-10 sm:h-10 bg-blue-100 rounded-lg flex items-center justify-center", children: _jsx(FileText, { className: "w-4 h-4 sm:w-5 sm:h-5 text-blue-600" }) })] }) }), _jsx("div", { className: "bg-white rounded-lg border border-gray-200 p-3 sm:p-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("p", { className: "text-xs text-gray-600 mb-1", children: "Total Pages" }), _jsx("p", { className: "text-xl sm:text-2xl font-semibold text-gray-900", children: "0" }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "\u2014" })] }), _jsx("div", { className: "w-8 h-8 sm:w-10 sm:h-10 bg-purple-100 rounded-lg flex items-center justify-center", children: _jsx(Layout, { className: "w-4 h-4 sm:w-5 sm:h-5 text-purple-600" }) })] }) }), _jsx("div", { className: "bg-white rounded-lg border border-gray-200 p-3 sm:p-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("p", { className: "text-xs text-gray-600 mb-1", children: "Media Files" }), _jsx("p", { className: "text-xl sm:text-2xl font-semibold text-gray-900", children: totalMedia.toLocaleString() }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "\u2014" })] }), _jsx("div", { className: "w-8 h-8 sm:w-10 sm:h-10 bg-green-100 rounded-lg flex items-center justify-center", children: _jsx(Image, { className: "w-4 h-4 sm:w-5 sm:h-5 text-green-600" }) })] }) }), _jsx("div", { className: "bg-white rounded-lg border border-gray-200 p-3 sm:p-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("p", { className: "text-xs text-gray-600 mb-1", children: "Total Users" }), _jsx("p", { className: "text-xl sm:text-2xl font-semibold text-gray-900", children: totalUsers }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "\u2014" })] }), _jsx("div", { className: "w-8 h-8 sm:w-10 sm:h-10 bg-amber-100 rounded-lg flex items-center justify-center", children: _jsx(Users, { className: "w-4 h-4 sm:w-5 sm:h-5 text-amber-600" }) })] }) })] }), _jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-12 gap-3 sm:gap-4", children: [_jsxs("div", { className: "lg:col-span-8 bg-white rounded-lg border border-gray-200", children: [_jsx("div", { className: "p-3 sm:p-4 border-b border-gray-200", children: _jsx("h2", { className: "text-sm sm:text-base font-semibold text-gray-900", children: "Recent Posts" }) }), _jsx("div", { className: "divide-y divide-gray-200", children: recentPosts.length === 0 ? (_jsxs("div", { className: "p-8 text-center", children: [_jsx("p", { className: "text-sm text-gray-500 mb-2", children: "No posts yet" }), _jsx("button", { onClick: () => nav('/posts/new'), className: "px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors", children: "Create your first post" })] })) : recentPosts.map((post) => (_jsx("div", { className: "p-3 sm:p-4 hover:bg-gray-50 transition-colors", children: _jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h3", { className: "text-sm font-medium text-gray-900 mb-1 truncate", children: post.title }), _jsxs("div", { className: "flex flex-wrap items-center gap-2 sm:gap-3 text-xs text-gray-600", children: [_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(User, { className: "w-3 h-3" }), _jsx("span", { className: "hidden sm:inline", children: post.author })] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Calendar, { className: "w-3 h-3" }), post.date] }), _jsx("span", { className: `px-2 py-0.5 rounded-full text-xs font-medium ${post.status === 'Published' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`, children: post.status })] })] }), _jsx("button", { onClick: () => nav(`/posts/${post.id}`), className: "text-xs sm:text-sm text-blue-600 hover:text-blue-700 whitespace-nowrap", children: "Edit" })] }) }, post.id))) })] }), _jsxs("div", { className: "lg:col-span-4 bg-white rounded-lg border border-gray-200", children: [_jsx("div", { className: "p-3 sm:p-4 border-b border-gray-200", children: _jsx("h2", { className: "text-sm sm:text-base font-semibold text-gray-900", children: "Quick Actions" }) }), _jsxs("div", { className: "p-3 sm:p-4 space-y-2", children: [_jsxs("button", { onClick: () => nav('/posts/new'), className: "flex items-center gap-3 p-2.5 sm:p-3 rounded-lg hover:bg-gray-50 transition-colors group w-full text-left", children: [_jsx("div", { className: "w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center group-hover:bg-blue-200 transition-colors", children: _jsx(FileText, { className: "w-4 h-4 text-blue-600" }) }), _jsx("span", { className: "text-sm font-medium text-gray-900", children: "New Post" })] }), _jsxs("button", { onClick: () => nav('/pages/new'), className: "flex items-center gap-3 p-2.5 sm:p-3 rounded-lg hover:bg-gray-50 transition-colors group w-full text-left", children: [_jsx("div", { className: "w-8 h-8 bg-purple-100 rounded-lg flex items-center justify-center group-hover:bg-purple-200 transition-colors", children: _jsx(Layout, { className: "w-4 h-4 text-purple-600" }) }), _jsx("span", { className: "text-sm font-medium text-gray-900", children: "New Page" })] }), _jsxs("button", { onClick: () => nav('/media'), className: "flex items-center gap-3 p-2.5 sm:p-3 rounded-lg hover:bg-gray-50 transition-colors group w-full text-left", children: [_jsx("div", { className: "w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center group-hover:bg-green-200 transition-colors", children: _jsx(Image, { className: "w-4 h-4 text-green-600" }) }), _jsx("span", { className: "text-sm font-medium text-gray-900", children: "Upload Media" })] }), _jsxs("button", { onClick: () => nav('/users'), className: "flex items-center gap-3 p-2.5 sm:p-3 rounded-lg hover:bg-gray-50 transition-colors group w-full text-left", children: [_jsx("div", { className: "w-8 h-8 bg-amber-100 rounded-lg flex items-center justify-center group-hover:bg-amber-200 transition-colors", children: _jsx(Users, { className: "w-4 h-4 text-amber-600" }) }), _jsx("span", { className: "text-sm font-medium text-gray-900", children: "Manage Users" })] })] })] })] })] }));
48
+ return (_jsxs("div", { className: "p-3 pr-6 sm:p-4 sm:pr-8", children: [health && health.status === 'degraded' && (_jsxs("div", { className: "mb-4 flex items-center gap-3 rounded-lg border border-blue-200 bg-blue-50 p-3", children: [_jsx(Database, { className: "w-5 h-5 text-blue-600 shrink-0" }), _jsxs("div", { className: "flex-1", children: [_jsx("span", { className: "text-sm font-medium text-blue-900", children: "Database Setup Required" }), _jsx("p", { className: "text-xs text-blue-700 mt-0.5", children: !health.databaseConnected
49
+ ? 'Cannot connect to the database. Check your DATABASE_URL environment variable.'
50
+ : !health.secretConfigured
51
+ ? 'CMS secret not configured. Set CMS_SECRET or CMS_SESSION_SECRET (min 32 characters).'
52
+ : `Some CMS models are missing: ${Object.entries(health.models).filter(([, v]) => !v).map(([k]) => k).join(', ')}. Run your database migrations to create the required tables.` })] })] })), error && exhausted && (_jsxs("div", { className: "mb-4 flex items-center gap-3 rounded-lg border border-amber-200 bg-amber-50 p-3", children: [_jsx(AlertTriangle, { className: "w-5 h-5 text-amber-600 shrink-0" }), _jsx("span", { className: "text-sm text-amber-800 flex-1", children: "Some dashboard data may be unavailable. This is normal if your database hasn't been set up yet." }), _jsx("button", { onClick: refetch, className: "px-3 py-1 text-sm text-amber-700 border border-amber-300 rounded-lg hover:bg-amber-100 transition-colors", children: "Retry" })] })), _jsxs("div", { className: "mb-4 sm:mb-6", children: [_jsx("h1", { className: "text-xl sm:text-2xl font-semibold text-gray-900 mb-1", children: "Dashboard" }), _jsx("p", { className: "text-sm text-gray-600", children: "Welcome back! Here's what's happening." })] }), _jsxs("div", { className: "grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 mb-4 sm:mb-6", children: [hasCollections ? (collections.slice(0, 2).map((col, i) => {
53
+ const colors = STAT_COLORS[i % STAT_COLORS.length];
54
+ const Icon = collectionIcon(col);
55
+ return (_jsx("div", { className: "bg-white rounded-lg border border-gray-200 p-3 sm:p-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("p", { className: "text-xs text-gray-600 mb-1", children: collectionLabel(col) }), _jsx("p", { className: "text-xl sm:text-2xl font-semibold text-gray-900", children: (collectionCounts[col.slug] ?? 0).toLocaleString() }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "\u2014" })] }), _jsx("div", { className: `w-8 h-8 sm:w-10 sm:h-10 ${colors.bg} rounded-lg flex items-center justify-center`, children: _jsx(Icon, { className: `w-4 h-4 sm:w-5 sm:h-5 ${colors.text}` }) })] }) }, col.slug));
56
+ })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "bg-white rounded-lg border border-gray-200 p-3 sm:p-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("p", { className: "text-xs text-gray-600 mb-1", children: "Documents" }), _jsx("p", { className: "text-xl sm:text-2xl font-semibold text-gray-900", children: (data?.totalDocuments ?? 0).toLocaleString() }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "\u2014" })] }), _jsx("div", { className: "w-8 h-8 sm:w-10 sm:h-10 bg-blue-100 rounded-lg flex items-center justify-center", children: _jsx(FileText, { className: "w-4 h-4 sm:w-5 sm:h-5 text-blue-600" }) })] }) }), _jsx("div", { className: "bg-white rounded-lg border border-gray-200 p-3 sm:p-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("p", { className: "text-xs text-gray-600 mb-1", children: "Pages" }), _jsx("p", { className: "text-xl sm:text-2xl font-semibold text-gray-900", children: (collectionCounts['pages'] ?? 0).toLocaleString() }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "\u2014" })] }), _jsx("div", { className: "w-8 h-8 sm:w-10 sm:h-10 bg-purple-100 rounded-lg flex items-center justify-center", children: _jsx(Layout, { className: "w-4 h-4 sm:w-5 sm:h-5 text-purple-600" }) })] }) })] })), _jsx("div", { className: "bg-white rounded-lg border border-gray-200 p-3 sm:p-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("p", { className: "text-xs text-gray-600 mb-1", children: "Media Files" }), _jsx("p", { className: "text-xl sm:text-2xl font-semibold text-gray-900", children: totalMedia.toLocaleString() }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "\u2014" })] }), _jsx("div", { className: "w-8 h-8 sm:w-10 sm:h-10 bg-green-100 rounded-lg flex items-center justify-center", children: _jsx(Image, { className: "w-4 h-4 sm:w-5 sm:h-5 text-green-600" }) })] }) }), _jsx("div", { className: "bg-white rounded-lg border border-gray-200 p-3 sm:p-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("p", { className: "text-xs text-gray-600 mb-1", children: "Total Users" }), _jsx("p", { className: "text-xl sm:text-2xl font-semibold text-gray-900", children: totalUsers }), _jsx("p", { className: "text-xs text-gray-400 mt-1", children: "\u2014" })] }), _jsx("div", { className: "w-8 h-8 sm:w-10 sm:h-10 bg-amber-100 rounded-lg flex items-center justify-center", children: _jsx(Users, { className: "w-4 h-4 sm:w-5 sm:h-5 text-amber-600" }) })] }) })] }), _jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-12 gap-3 sm:gap-4", children: [_jsxs("div", { className: "lg:col-span-8 bg-white rounded-lg border border-gray-200", children: [_jsx("div", { className: "p-3 sm:p-4 border-b border-gray-200", children: _jsxs("h2", { className: "text-sm sm:text-base font-semibold text-gray-900", children: ["Recent ", primaryCollection ? collectionLabel(primaryCollection) : 'Documents'] }) }), _jsx("div", { className: "divide-y divide-gray-200", children: recentDocs.length === 0 ? (_jsxs("div", { className: "p-8 text-center", children: [_jsxs("p", { className: "text-sm text-gray-500 mb-2", children: ["No ", primaryCollection ? collectionLabel(primaryCollection).toLowerCase() : 'documents', " yet"] }), primaryCollection && (_jsxs("button", { onClick: () => nav(`/${primaryCollection.slug}/new`), className: "px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors", children: ["Create your first ", collectionLabel(primaryCollection, false).toLowerCase()] }))] })) : recentDocs.map((doc) => (_jsx("div", { className: "p-3 sm:p-4 hover:bg-gray-50 transition-colors", children: _jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h3", { className: "text-sm font-medium text-gray-900 mb-1 truncate", children: doc.title ?? 'Untitled' }), _jsxs("div", { className: "flex flex-wrap items-center gap-2 sm:gap-3 text-xs text-gray-600", children: [_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(User, { className: "w-3 h-3" }), _jsx("span", { className: "hidden sm:inline", children: doc.createdById })] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Calendar, { className: "w-3 h-3" }), new Date(doc.updatedAt).toLocaleDateString()] }), _jsx("span", { className: `px-2 py-0.5 rounded-full text-xs font-medium ${doc.status === 'PUBLISHED' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`, children: doc.status })] })] }), _jsx("button", { onClick: () => nav(`/${doc.collection}/${doc.id}`), className: "text-xs sm:text-sm text-blue-600 hover:text-blue-700 whitespace-nowrap", children: "Edit" })] }) }, doc.id))) })] }), _jsxs("div", { className: "lg:col-span-4 bg-white rounded-lg border border-gray-200", children: [_jsx("div", { className: "p-3 sm:p-4 border-b border-gray-200", children: _jsx("h2", { className: "text-sm sm:text-base font-semibold text-gray-900", children: "Quick Actions" }) }), _jsxs("div", { className: "p-3 sm:p-4 space-y-2", children: [hasCollections ? (collections.slice(0, 4).map((col, i) => {
57
+ const colors = STAT_COLORS[i % STAT_COLORS.length];
58
+ const Icon = collectionIcon(col);
59
+ return (_jsxs("button", { onClick: () => nav(`/${col.slug}/new`), className: "flex items-center gap-3 p-2.5 sm:p-3 rounded-lg hover:bg-gray-50 transition-colors group w-full text-left", children: [_jsx("div", { className: `w-8 h-8 ${colors.bg} rounded-lg flex items-center justify-center group-hover:opacity-80 transition-colors`, children: _jsx(Icon, { className: `w-4 h-4 ${colors.text}` }) }), _jsxs("span", { className: "text-sm font-medium text-gray-900", children: ["New ", collectionLabel(col, false)] })] }, col.slug));
60
+ })) : (_jsxs(_Fragment, { children: [_jsxs("button", { onClick: () => nav('/posts/new'), className: "flex items-center gap-3 p-2.5 sm:p-3 rounded-lg hover:bg-gray-50 transition-colors group w-full text-left", children: [_jsx("div", { className: "w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center group-hover:bg-blue-200 transition-colors", children: _jsx(FileText, { className: "w-4 h-4 text-blue-600" }) }), _jsx("span", { className: "text-sm font-medium text-gray-900", children: "New Post" })] }), _jsxs("button", { onClick: () => nav('/pages/new'), className: "flex items-center gap-3 p-2.5 sm:p-3 rounded-lg hover:bg-gray-50 transition-colors group w-full text-left", children: [_jsx("div", { className: "w-8 h-8 bg-purple-100 rounded-lg flex items-center justify-center group-hover:bg-purple-200 transition-colors", children: _jsx(Layout, { className: "w-4 h-4 text-purple-600" }) }), _jsx("span", { className: "text-sm font-medium text-gray-900", children: "New Page" })] })] })), _jsxs("button", { onClick: () => nav('/media'), className: "flex items-center gap-3 p-2.5 sm:p-3 rounded-lg hover:bg-gray-50 transition-colors group w-full text-left", children: [_jsx("div", { className: "w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center group-hover:bg-green-200 transition-colors", children: _jsx(Image, { className: "w-4 h-4 text-green-600" }) }), _jsx("span", { className: "text-sm font-medium text-gray-900", children: "Upload Media" })] }), _jsxs("button", { onClick: () => nav('/users'), className: "flex items-center gap-3 p-2.5 sm:p-3 rounded-lg hover:bg-gray-50 transition-colors group w-full text-left", children: [_jsx("div", { className: "w-8 h-8 bg-amber-100 rounded-lg flex items-center justify-center group-hover:bg-amber-200 transition-colors", children: _jsx(Users, { className: "w-4 h-4 text-amber-600" }) }), _jsx("span", { className: "text-sm font-medium text-gray-900", children: "Manage Users" })] })] })] })] })] }));
16
61
  }
17
62
  //# sourceMappingURL=Dashboard.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Dashboard.js","sourceRoot":"","sources":["../../src/views/Dashboard.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Dashboard.js","sourceRoot":"","sources":["../../src/views/Dashboard.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actuate-media/cms-admin",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/actuate-media/actuatecms.git",
@@ -19,11 +19,14 @@
19
19
  "default": "./dist/index.js"
20
20
  },
21
21
  "./styles": "./src/styles/tailwind.css",
22
- "./styles/theme.css": "./src/styles/theme.css"
22
+ "./styles/theme.css": "./src/styles/theme.css",
23
+ "./styles/precompiled.css": "./dist/actuate-admin.css"
23
24
  },
24
25
  "files": [
25
26
  "dist",
26
- "src/styles"
27
+ "src/styles",
28
+ "src/**/*.tsx",
29
+ "src/**/*.ts"
27
30
  ],
28
31
  "dependencies": {
29
32
  "@dnd-kit/core": "^6.3.1",
@@ -55,9 +58,10 @@
55
58
  "react-dom": "^19.2.0",
56
59
  "sonner": "^2.0.7",
57
60
  "tailwind-merge": "^3.5.0",
58
- "@actuate-media/cms-core": "0.2.3"
61
+ "@actuate-media/cms-core": "0.3.1"
59
62
  },
60
63
  "devDependencies": {
64
+ "@tailwindcss/cli": "^4.0.0",
61
65
  "@types/react": "^19.0.0",
62
66
  "@types/react-dom": "^19.0.0",
63
67
  "tailwindcss": "^4.0.0",
@@ -65,7 +69,8 @@
65
69
  "vitest": "^3.0.0"
66
70
  },
67
71
  "scripts": {
68
- "build": "tsc --project tsconfig.json",
72
+ "build": "tsc --project tsconfig.json && npx @tailwindcss/cli -i src/styles/build-input.css -o dist/actuate-admin.css --minify",
73
+ "build:css": "npx @tailwindcss/cli -i src/styles/build-input.css -o dist/actuate-admin.css --minify",
69
74
  "type-check": "tsc --noEmit",
70
75
  "test": "vitest run",
71
76
  "clean": "rm -rf dist .turbo"
@@ -0,0 +1,312 @@
1
+ 'use client';
2
+
3
+ import { useState, useMemo, useEffect, useRef } from 'react';
4
+ import { Layout } from './layout/Layout.js';
5
+ import { useAdminRouter } from './router/index.js';
6
+ import { Dashboard } from './views/Dashboard.js';
7
+ import { CollectionList } from './views/CollectionList.js';
8
+ import { DocumentEdit } from './views/DocumentEdit.js';
9
+ import { MediaBrowser } from './views/MediaBrowser.js';
10
+ import { Settings } from './views/Settings.js';
11
+ import { Forms } from './views/Forms.js';
12
+ import { FormEditor } from './views/FormEditor.js';
13
+ import { FormSubmissions } from './views/FormSubmissions.js';
14
+ import { Users } from './views/Users.js';
15
+ import { SEO } from './views/SEO.js';
16
+ import { SetupWizard } from './views/SetupWizard.js';
17
+ import { Login } from './views/Login.js';
18
+ import { ErrorBoundary } from './components/ErrorBoundary.js';
19
+ import { ThemeProvider } from './components/ThemeProvider.js';
20
+ import { LocaleProvider } from './components/LocaleProvider.js';
21
+ import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts.js';
22
+
23
+ export interface AdminRootProps {
24
+ config: any;
25
+ session: any;
26
+ basePath?: string;
27
+ initialPath?: string;
28
+ setupRequired?: boolean;
29
+ onSetupComplete?: (data: { name: string; email: string; password: string }) => Promise<{ success: boolean; error?: string }>;
30
+ onLogin?: (email: string, password: string, captchaToken?: string) => Promise<{ success: boolean; error?: string }>;
31
+ captchaConfig?: { provider: 'recaptcha' | 'turnstile' | 'none'; siteKey: string | null };
32
+ }
33
+
34
+ function AdminShell({ config, session, basePath = '/admin', initialPath = '/', setupRequired, onSetupComplete, onLogin, captchaConfig }: AdminRootProps) {
35
+ const { currentPath, navigate, matchRoute } = useAdminRouter(basePath, initialPath);
36
+ const [shortcutHelpOpen, setShortcutHelpOpen] = useState(false);
37
+
38
+ useBranding(config);
39
+ useAdminPageTitle(config, currentPath);
40
+
41
+ const shortcuts = useMemo(() => ({
42
+ 'mod+k': () => {
43
+ const searchInput = document.querySelector<HTMLInputElement>('input[placeholder*="Search"]');
44
+ searchInput?.focus();
45
+ },
46
+ 'mod+s': () => {
47
+ const saveBtn = document.querySelector<HTMLButtonElement>('[data-shortcut="save"]');
48
+ saveBtn?.click();
49
+ },
50
+ 'escape': () => {
51
+ const closeBtn = document.querySelector<HTMLButtonElement>('[data-shortcut="close"]');
52
+ closeBtn?.click();
53
+ },
54
+ 'mod+/': () => {
55
+ setShortcutHelpOpen(prev => !prev);
56
+ },
57
+ }), []);
58
+
59
+ useKeyboardShortcuts(shortcuts);
60
+
61
+ if (setupRequired && onSetupComplete) {
62
+ return <SetupWizard onComplete={onSetupComplete} siteName={config?.admin?.branding?.name ?? 'Actuate CMS'} />;
63
+ }
64
+
65
+ if (!session && !setupRequired) {
66
+ if (onLogin) {
67
+ return <Login onLogin={onLogin} onNavigate={navigate} captchaConfig={captchaConfig} />;
68
+ }
69
+ return (
70
+ <div className="min-h-screen flex items-center justify-center bg-background">
71
+ <div className="text-center">
72
+ <h1 className="text-xl font-semibold text-foreground mb-2">Unauthorized</h1>
73
+ <p className="text-sm text-muted-foreground">Please log in to access the admin panel.</p>
74
+ </div>
75
+ </div>
76
+ );
77
+ }
78
+
79
+ const collectionSlugs = config?.collections
80
+ ? Object.values(config.collections as Record<string, { slug: string }>).map((c) => c.slug)
81
+ : ['pages', 'posts'];
82
+
83
+ function renderView() {
84
+ if (matchRoute('/')) {
85
+ return <Dashboard config={config} onNavigate={navigate} />;
86
+ }
87
+
88
+ for (const slug of collectionSlugs) {
89
+ const newMatch = matchRoute(`/${slug}/new`);
90
+ if (newMatch) {
91
+ return <DocumentEdit collectionSlug={slug} config={config} />;
92
+ }
93
+ const editMatch = matchRoute(`/${slug}/:id`);
94
+ if (editMatch?.id) {
95
+ return <DocumentEdit collectionSlug={slug} documentId={editMatch.id} config={config} />;
96
+ }
97
+ if (matchRoute(`/${slug}`)) {
98
+ return <CollectionList collectionSlug={slug} config={config} onNavigate={navigate} />;
99
+ }
100
+ }
101
+
102
+ const newCollMatch = matchRoute('/collections/:slug/new');
103
+ if (newCollMatch?.slug) {
104
+ return <DocumentEdit collectionSlug={newCollMatch.slug} config={config} />;
105
+ }
106
+
107
+ const editCollMatch = matchRoute('/collections/:slug/:id');
108
+ if (editCollMatch?.slug && editCollMatch.id) {
109
+ return <DocumentEdit collectionSlug={editCollMatch.slug} documentId={editCollMatch.id} config={config} />;
110
+ }
111
+
112
+ const collectionMatch = matchRoute('/collections/:slug');
113
+ if (collectionMatch?.slug) {
114
+ return <CollectionList collectionSlug={collectionMatch.slug} config={config} onNavigate={navigate} />;
115
+ }
116
+
117
+ if (matchRoute('/media')) {
118
+ return <MediaBrowser onNavigate={navigate} />;
119
+ }
120
+
121
+ if (matchRoute('/forms/new')) {
122
+ return <FormEditor onNavigate={navigate} />;
123
+ }
124
+ const formEdit = matchRoute('/forms/:id/edit');
125
+ if (formEdit?.id) {
126
+ return <FormEditor formId={formEdit.id} onNavigate={navigate} />;
127
+ }
128
+ const formSubmissions = matchRoute('/forms/:id/submissions');
129
+ if (formSubmissions?.id) {
130
+ return <FormSubmissions formId={formSubmissions.id} onNavigate={navigate} />;
131
+ }
132
+ if (matchRoute('/forms')) {
133
+ return <Forms onNavigate={navigate} />;
134
+ }
135
+
136
+ if (matchRoute('/seo/redirects')) {
137
+ return <SEO onNavigate={navigate} initialTab="redirects" />;
138
+ }
139
+ if (matchRoute('/seo/canonicals')) {
140
+ return <SEO onNavigate={navigate} initialTab="canonicals" />;
141
+ }
142
+ if (matchRoute('/seo/links')) {
143
+ return <SEO onNavigate={navigate} initialTab="links" />;
144
+ }
145
+ if (matchRoute('/seo')) {
146
+ return <SEO onNavigate={navigate} initialTab="pages" />;
147
+ }
148
+
149
+ if (matchRoute('/users')) {
150
+ return <Users onNavigate={navigate} />;
151
+ }
152
+
153
+ if (matchRoute('/settings')) {
154
+ return <Settings onNavigate={navigate} />;
155
+ }
156
+
157
+ return (
158
+ <div className="flex flex-col items-center justify-center min-h-[400px] text-center p-6">
159
+ <h1 className="text-4xl font-bold text-foreground mb-2">404</h1>
160
+ <p className="text-muted-foreground mb-4">The page you are looking for does not exist.</p>
161
+ <button
162
+ onClick={() => navigate('/')}
163
+ className="px-4 py-2 text-sm font-medium text-primary-foreground bg-primary rounded-md hover:opacity-90"
164
+ >
165
+ Back to Dashboard
166
+ </button>
167
+ </div>
168
+ );
169
+ }
170
+
171
+ return (
172
+ <Layout config={config} session={session} currentPath={currentPath} onNavigate={navigate}>
173
+ <ErrorBoundary>
174
+ {renderView()}
175
+ </ErrorBoundary>
176
+ {shortcutHelpOpen && <ShortcutHelp onClose={() => setShortcutHelpOpen(false)} />}
177
+ </Layout>
178
+ );
179
+ }
180
+
181
+ function ShortcutHelp({ onClose }: { onClose: () => void }) {
182
+ return (
183
+ <div
184
+ className="fixed inset-0 z-100 flex items-center justify-center bg-black/50"
185
+ onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
186
+ >
187
+ <div className="bg-card text-card-foreground rounded-xl shadow-2xl border border-border p-6 max-w-sm w-full mx-4">
188
+ <h3 className="text-lg font-semibold mb-4">Keyboard Shortcuts</h3>
189
+ <div className="space-y-3 text-sm">
190
+ {[
191
+ ['⌘ K', 'Open search'],
192
+ ['⌘ S', 'Save document'],
193
+ ['⌘ /', 'Toggle this help'],
194
+ ['Esc', 'Close panel / modal'],
195
+ ].map(([key, desc]) => (
196
+ <div key={key} className="flex items-center justify-between">
197
+ <span className="text-muted-foreground">{desc}</span>
198
+ <kbd className="px-2 py-1 text-xs font-mono bg-muted rounded">{key}</kbd>
199
+ </div>
200
+ ))}
201
+ </div>
202
+ <button
203
+ onClick={onClose}
204
+ className="mt-4 w-full py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90"
205
+ >
206
+ Close
207
+ </button>
208
+ </div>
209
+ </div>
210
+ );
211
+ }
212
+
213
+ function useBranding(config: any) {
214
+ const originalFaviconRef = useRef<string | null>(null);
215
+ const originalTitleRef = useRef<string | null>(null);
216
+ const injectedLinkRef = useRef<HTMLLinkElement | null>(null);
217
+
218
+ useEffect(() => {
219
+ const branding = config?.admin?.branding;
220
+ if (!branding) return;
221
+
222
+ // --- Favicon ---
223
+ if (branding.favicon) {
224
+ const existing = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
225
+ originalFaviconRef.current = existing?.href ?? null;
226
+
227
+ if (existing) {
228
+ existing.href = branding.favicon;
229
+ } else {
230
+ const link = document.createElement('link');
231
+ link.rel = 'icon';
232
+ link.href = branding.favicon;
233
+ document.head.appendChild(link);
234
+ injectedLinkRef.current = link;
235
+ }
236
+ }
237
+
238
+ // --- Document title ---
239
+ originalTitleRef.current = document.title;
240
+ if (branding.title) {
241
+ document.title = branding.title;
242
+ } else if (branding.name) {
243
+ document.title = `${branding.name} Admin`;
244
+ }
245
+
246
+ // --- Primary color ---
247
+ const adminEl = document.querySelector<HTMLElement>('.actuate-admin');
248
+ if (branding.primaryColor && adminEl) {
249
+ adminEl.style.setProperty('--primary', branding.primaryColor);
250
+ adminEl.style.setProperty('--sidebar-primary', branding.primaryColor);
251
+ }
252
+
253
+ return () => {
254
+ // Restore favicon
255
+ if (branding.favicon) {
256
+ const link = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
257
+ if (link && originalFaviconRef.current) {
258
+ link.href = originalFaviconRef.current;
259
+ }
260
+ if (injectedLinkRef.current) {
261
+ injectedLinkRef.current.remove();
262
+ injectedLinkRef.current = null;
263
+ }
264
+ }
265
+
266
+ // Restore title
267
+ if (originalTitleRef.current !== null) {
268
+ document.title = originalTitleRef.current;
269
+ }
270
+
271
+ // Remove primary color override
272
+ if (branding.primaryColor && adminEl) {
273
+ adminEl.style.removeProperty('--primary');
274
+ adminEl.style.removeProperty('--sidebar-primary');
275
+ }
276
+ };
277
+ }, [config]);
278
+ }
279
+
280
+ function useAdminPageTitle(config: any, currentPath: string) {
281
+ useEffect(() => {
282
+ const branding = config?.admin?.branding;
283
+ const baseName = branding?.title ?? (branding?.name ? `${branding.name} Admin` : null);
284
+ if (!baseName) return;
285
+
286
+ const segment = currentPath === '/' ? 'Dashboard' : currentPath.split('/').filter(Boolean)[0];
287
+ const pageName = segment ? segment.charAt(0).toUpperCase() + segment.slice(1) : 'Dashboard';
288
+ document.title = `${pageName} — ${baseName}`;
289
+ }, [config, currentPath]);
290
+ }
291
+
292
+ const ISOLATION_STYLE: React.CSSProperties = {
293
+ position: 'fixed',
294
+ inset: '0',
295
+ zIndex: 50,
296
+ overflow: 'auto',
297
+ isolation: 'isolate',
298
+ };
299
+
300
+ export function AdminRoot(props: AdminRootProps) {
301
+ const defaultDarkMode = props.config?.admin?.branding?.darkMode;
302
+
303
+ return (
304
+ <div style={ISOLATION_STYLE} className="actuate-admin">
305
+ <ThemeProvider defaultDarkMode={defaultDarkMode}>
306
+ <LocaleProvider config={props.config}>
307
+ <AdminShell {...props} />
308
+ </LocaleProvider>
309
+ </ThemeProvider>
310
+ </div>
311
+ );
312
+ }
@@ -0,0 +1,138 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ scoreRelevance,
4
+ sortByRelevance,
5
+ sortByColumn,
6
+ toggleSort,
7
+ } from '../../lib/search.js';
8
+
9
+ describe('scoreRelevance', () => {
10
+ it('returns 100 for exact match (case-insensitive)', () => {
11
+ expect(scoreRelevance('Hello', 'hello')).toBe(100);
12
+ expect(scoreRelevance('test', 'test')).toBe(100);
13
+ });
14
+
15
+ it('returns 80 when text starts with the query', () => {
16
+ expect(scoreRelevance('hello world', 'hel')).toBe(80);
17
+ expect(scoreRelevance('Alpha', 'al')).toBe(80);
18
+ });
19
+
20
+ it('returns 60 when query matches at a word boundary but not at start', () => {
21
+ expect(scoreRelevance('say hello', 'hello')).toBe(60);
22
+ });
23
+
24
+ it('returns 40 when query appears only as a substring without word-boundary match', () => {
25
+ expect(scoreRelevance('axxhello', 'hello')).toBe(40);
26
+ });
27
+
28
+ it('returns 0 when there is no match', () => {
29
+ expect(scoreRelevance('abc def', 'xyz')).toBe(0);
30
+ });
31
+
32
+ it('returns 0 for an empty query', () => {
33
+ expect(scoreRelevance('anything', '')).toBe(0);
34
+ });
35
+ });
36
+
37
+ describe('sortByRelevance', () => {
38
+ it('sorts items by best field relevance score descending', () => {
39
+ const items = [
40
+ { id: 'a', title: 'zzz' },
41
+ { id: 'b', title: 'hello' },
42
+ { id: 'c', title: 'say hello' },
43
+ ];
44
+ const sorted = sortByRelevance(items, 'hello', item => [item.title]);
45
+ // exact match > word-boundary match > no match
46
+ expect(sorted.map(i => i.id)).toEqual(['b', 'c', 'a']);
47
+ });
48
+
49
+ it('returns the original array reference for an empty query', () => {
50
+ const items = [{ id: '1' }, { id: '2' }];
51
+ const result = sortByRelevance(items, '', item => [item.id]);
52
+ expect(result).toBe(items);
53
+ });
54
+
55
+ it('returns the original array when query is whitespace-only', () => {
56
+ const items = [{ id: '1' }];
57
+ expect(sortByRelevance(items, ' ', item => [item.id])).toBe(items);
58
+ });
59
+
60
+ it('uses the maximum score across multiple search fields', () => {
61
+ const items = [
62
+ { id: 'low', a: 'x', b: 'x' },
63
+ { id: 'high', a: 'x', b: 'exact' },
64
+ ];
65
+ const sorted = sortByRelevance(items, 'exact', item => [item.a, item.b]);
66
+ expect(sorted[0]!.id).toBe('high');
67
+ });
68
+ });
69
+
70
+ describe('sortByColumn', () => {
71
+ it('returns the original array when sortConfig is null', () => {
72
+ const items = [{ n: 2 }, { n: 1 }];
73
+ expect(sortByColumn(items, null, (item, key) => item[key as 'n'])).toBe(items);
74
+ });
75
+
76
+ it('sorts strings ascending', () => {
77
+ const items = [{ name: 'b' }, { name: 'a' }, { name: 'c' }];
78
+ const sorted = sortByColumn(
79
+ items,
80
+ { key: 'name', direction: 'asc' },
81
+ (item, key) => item[key as 'name'],
82
+ );
83
+ expect(sorted.map(i => i.name)).toEqual(['a', 'b', 'c']);
84
+ });
85
+
86
+ it('sorts strings descending', () => {
87
+ const items = [{ name: 'a' }, { name: 'c' }, { name: 'b' }];
88
+ const sorted = sortByColumn(
89
+ items,
90
+ { key: 'name', direction: 'desc' },
91
+ (item, key) => item[key as 'name'],
92
+ );
93
+ expect(sorted.map(i => i.name)).toEqual(['c', 'b', 'a']);
94
+ });
95
+
96
+ it('sorts numbers ascending and descending', () => {
97
+ const items = [{ v: 3 }, { v: 1 }, { v: 2 }];
98
+ const asc = sortByColumn(
99
+ items,
100
+ { key: 'v', direction: 'asc' },
101
+ (item, key) => item[key as 'v'],
102
+ );
103
+ expect(asc.map(i => i.v)).toEqual([1, 2, 3]);
104
+ const desc = sortByColumn(
105
+ items,
106
+ { key: 'v', direction: 'desc' },
107
+ (item, key) => item[key as 'v'],
108
+ );
109
+ expect(desc.map(i => i.v)).toEqual([3, 2, 1]);
110
+ });
111
+ });
112
+
113
+ describe('toggleSort', () => {
114
+ it('returns ascending sort when current is null', () => {
115
+ expect(toggleSort(null, 'title')).toEqual({ key: 'title', direction: 'asc' });
116
+ });
117
+
118
+ it('toggles same key from asc to desc', () => {
119
+ expect(toggleSort({ key: 'title', direction: 'asc' }, 'title')).toEqual({
120
+ key: 'title',
121
+ direction: 'desc',
122
+ });
123
+ });
124
+
125
+ it('toggles same key from desc to asc', () => {
126
+ expect(toggleSort({ key: 'title', direction: 'desc' }, 'title')).toEqual({
127
+ key: 'title',
128
+ direction: 'asc',
129
+ });
130
+ });
131
+
132
+ it('starts ascending when switching to a different key', () => {
133
+ expect(toggleSort({ key: 'foo', direction: 'desc' }, 'bar')).toEqual({
134
+ key: 'bar',
135
+ direction: 'asc',
136
+ });
137
+ });
138
+ });