@actuate-media/cms-admin 0.67.0 → 0.69.0
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.
- package/CHANGELOG.md +28 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +59 -15
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/hooks/useIdleLogout.test.d.ts +2 -0
- package/dist/__tests__/hooks/useIdleLogout.test.d.ts.map +1 -0
- package/dist/__tests__/hooks/useIdleLogout.test.js +119 -0
- package/dist/__tests__/hooks/useIdleLogout.test.js.map +1 -0
- package/dist/__tests__/lib/post-types-service.test.d.ts +2 -0
- package/dist/__tests__/lib/post-types-service.test.d.ts.map +1 -0
- package/dist/__tests__/lib/post-types-service.test.js +88 -0
- package/dist/__tests__/lib/post-types-service.test.js.map +1 -0
- package/dist/__tests__/router/query-string.test.d.ts +2 -0
- package/dist/__tests__/router/query-string.test.d.ts.map +1 -0
- package/dist/__tests__/router/query-string.test.js +44 -0
- package/dist/__tests__/router/query-string.test.js.map +1 -0
- package/dist/__tests__/views/collection-list-nav.render.test.js +43 -5
- package/dist/__tests__/views/collection-list-nav.render.test.js.map +1 -1
- package/dist/__tests__/views/forms-list.render.test.js +25 -6
- package/dist/__tests__/views/forms-list.render.test.js.map +1 -1
- package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-builder-sections-panel.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-builder-sections-panel.render.test.js +121 -0
- package/dist/__tests__/views/page-builder-sections-panel.render.test.js.map +1 -0
- package/dist/__tests__/views/page-templates.render.test.d.ts +2 -0
- package/dist/__tests__/views/page-templates.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/page-templates.render.test.js +105 -0
- package/dist/__tests__/views/page-templates.render.test.js.map +1 -0
- package/dist/__tests__/views/post-type-editor.render.test.d.ts +2 -0
- package/dist/__tests__/views/post-type-editor.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/post-type-editor.render.test.js +102 -0
- package/dist/__tests__/views/post-type-editor.render.test.js.map +1 -0
- package/dist/__tests__/views/security-settings.render.test.js +85 -4
- package/dist/__tests__/views/security-settings.render.test.js.map +1 -1
- package/dist/__tests__/views/seo-content-pane.render.test.d.ts +2 -0
- package/dist/__tests__/views/seo-content-pane.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/seo-content-pane.render.test.js +117 -0
- package/dist/__tests__/views/seo-content-pane.render.test.js.map +1 -0
- package/dist/__tests__/views/users-selected-deeplink.render.test.d.ts +2 -0
- package/dist/__tests__/views/users-selected-deeplink.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/users-selected-deeplink.render.test.js +91 -0
- package/dist/__tests__/views/users-selected-deeplink.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/collections/EntryDetailPane.d.ts +10 -0
- package/dist/components/collections/EntryDetailPane.d.ts.map +1 -0
- package/dist/components/collections/EntryDetailPane.js +92 -0
- package/dist/components/collections/EntryDetailPane.js.map +1 -0
- package/dist/components/forms/FieldsPanel.js +1 -1
- package/dist/components/forms/FieldsPanel.js.map +1 -1
- package/dist/components/forms/FormSchemaPane.d.ts +8 -0
- package/dist/components/forms/FormSchemaPane.d.ts.map +1 -0
- package/dist/components/forms/FormSchemaPane.js +95 -0
- package/dist/components/forms/FormSchemaPane.js.map +1 -0
- package/dist/components/seo/SeoEditorPane.d.ts +9 -0
- package/dist/components/seo/SeoEditorPane.d.ts.map +1 -0
- package/dist/components/seo/SeoEditorPane.js +175 -0
- package/dist/components/seo/SeoEditorPane.js.map +1 -0
- package/dist/components/ui/Badge.d.ts +1 -1
- package/dist/components/ui/Button.d.ts +1 -1
- package/dist/components/ui/StatusBadge.d.ts +1 -1
- package/dist/hooks/useIdleLogout.d.ts +33 -0
- package/dist/hooks/useIdleLogout.d.ts.map +1 -0
- package/dist/hooks/useIdleLogout.js +99 -0
- package/dist/hooks/useIdleLogout.js.map +1 -0
- package/dist/lib/api.d.ts +1 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +7 -1
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/post-types-service.d.ts +44 -0
- package/dist/lib/post-types-service.d.ts.map +1 -0
- package/dist/lib/post-types-service.js +83 -0
- package/dist/lib/post-types-service.js.map +1 -0
- package/dist/router/index.d.ts +7 -0
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +23 -4
- package/dist/router/index.js.map +1 -1
- package/dist/views/CollectionList.d.ts.map +1 -1
- package/dist/views/CollectionList.js +23 -4
- package/dist/views/CollectionList.js.map +1 -1
- package/dist/views/Forms.d.ts.map +1 -1
- package/dist/views/Forms.js +25 -21
- package/dist/views/Forms.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts +4 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +27 -5
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/Posts/PostTypeEditor.d.ts +16 -0
- package/dist/views/Posts/PostTypeEditor.d.ts.map +1 -0
- package/dist/views/Posts/PostTypeEditor.js +159 -0
- package/dist/views/Posts/PostTypeEditor.js.map +1 -0
- package/dist/views/Posts/PostTypesView.d.ts +3 -2
- package/dist/views/Posts/PostTypesView.d.ts.map +1 -1
- package/dist/views/Posts/PostTypesView.js +1 -1
- package/dist/views/Posts/PostTypesView.js.map +1 -1
- package/dist/views/SEO.d.ts.map +1 -1
- package/dist/views/SEO.js +2 -1
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/Settings.d.ts +4 -1
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +4 -3
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/Users.d.ts +4 -1
- package/dist/views/Users.d.ts.map +1 -1
- package/dist/views/Users.js +22 -2
- package/dist/views/Users.js.map +1 -1
- package/dist/views/page-builder/BuilderSectionsPanel.d.ts +20 -0
- package/dist/views/page-builder/BuilderSectionsPanel.d.ts.map +1 -0
- package/dist/views/page-builder/BuilderSectionsPanel.js +105 -0
- package/dist/views/page-builder/BuilderSectionsPanel.js.map +1 -0
- package/dist/views/page-builder/PageBuilder.d.ts.map +1 -1
- package/dist/views/page-builder/PageBuilder.js +51 -2
- package/dist/views/page-builder/PageBuilder.js.map +1 -1
- package/dist/views/page-builder/PageTemplates.d.ts.map +1 -1
- package/dist/views/page-builder/PageTemplates.js +113 -6
- package/dist/views/page-builder/PageTemplates.js.map +1 -1
- package/dist/views/seo/ContentTab.d.ts.map +1 -1
- package/dist/views/seo/ContentTab.js +22 -6
- package/dist/views/seo/ContentTab.js.map +1 -1
- package/dist/views/settings/AccessProtectionCard.d.ts +5 -1
- package/dist/views/settings/AccessProtectionCard.d.ts.map +1 -1
- package/dist/views/settings/AccessProtectionCard.js +13 -10
- package/dist/views/settings/AccessProtectionCard.js.map +1 -1
- package/dist/views/settings/SecurityTab.d.ts.map +1 -1
- package/dist/views/settings/SecurityTab.js +5 -4
- package/dist/views/settings/SecurityTab.js.map +1 -1
- package/dist/views/settings/SessionPolicyCard.d.ts +1 -2
- package/dist/views/settings/SessionPolicyCard.d.ts.map +1 -1
- package/dist/views/settings/SessionPolicyCard.js +12 -5
- package/dist/views/settings/SessionPolicyCard.js.map +1 -1
- package/dist/views/settings/useSecuritySettings.d.ts +13 -1
- package/dist/views/settings/useSecuritySettings.d.ts.map +1 -1
- package/dist/views/settings/useSecuritySettings.js +47 -4
- package/dist/views/settings/useSecuritySettings.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +88 -18
- package/src/__tests__/hooks/useIdleLogout.test.tsx +188 -0
- package/src/__tests__/lib/post-types-service.test.ts +112 -0
- package/src/__tests__/router/query-string.test.ts +54 -0
- package/src/__tests__/views/collection-list-nav.render.test.tsx +51 -5
- package/src/__tests__/views/forms-list.render.test.tsx +34 -6
- package/src/__tests__/views/page-builder-sections-panel.render.test.tsx +138 -0
- package/src/__tests__/views/page-templates.render.test.tsx +142 -0
- package/src/__tests__/views/post-type-editor.render.test.tsx +116 -0
- package/src/__tests__/views/security-settings.render.test.tsx +111 -4
- package/src/__tests__/views/seo-content-pane.render.test.tsx +142 -0
- package/src/__tests__/views/users-selected-deeplink.render.test.tsx +102 -0
- package/src/components/collections/EntryDetailPane.tsx +182 -0
- package/src/components/forms/FieldsPanel.tsx +3 -1
- package/src/components/forms/FormSchemaPane.tsx +192 -0
- package/src/components/seo/SeoEditorPane.tsx +533 -0
- package/src/hooks/useIdleLogout.ts +129 -0
- package/src/lib/api.ts +8 -2
- package/src/lib/post-types-service.ts +129 -0
- package/src/router/index.ts +25 -4
- package/src/views/CollectionList.tsx +158 -110
- package/src/views/Forms.tsx +154 -126
- package/src/views/MediaBrowser.tsx +28 -5
- package/src/views/Posts/PostTypeEditor.tsx +568 -0
- package/src/views/Posts/PostTypesView.tsx +5 -4
- package/src/views/SEO.tsx +2 -1
- package/src/views/Settings.tsx +7 -2
- package/src/views/Users.tsx +23 -2
- package/src/views/page-builder/BuilderSectionsPanel.tsx +304 -0
- package/src/views/page-builder/PageBuilder.tsx +69 -2
- package/src/views/page-builder/PageTemplates.tsx +340 -57
- package/src/views/seo/ContentTab.tsx +113 -80
- package/src/views/settings/AccessProtectionCard.tsx +83 -53
- package/src/views/settings/SecurityTab.tsx +25 -3
- package/src/views/settings/SessionPolicyCard.tsx +45 -9
- package/src/views/settings/useSecuritySettings.ts +78 -22
- package/dist/components/forms/FormSchemaDrawer.d.ts +0 -9
- package/dist/components/forms/FormSchemaDrawer.d.ts.map +0 -1
- package/dist/components/forms/FormSchemaDrawer.js +0 -99
- package/dist/components/forms/FormSchemaDrawer.js.map +0 -1
- package/dist/components/seo/SeoEditorDrawer.d.ts +0 -8
- package/dist/components/seo/SeoEditorDrawer.d.ts.map +0 -1
- package/dist/components/seo/SeoEditorDrawer.js +0 -172
- package/dist/components/seo/SeoEditorDrawer.js.map +0 -1
- package/src/components/forms/FormSchemaDrawer.tsx +0 -192
- package/src/components/seo/SeoEditorDrawer.tsx +0 -529
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { cmsApi } from './api.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detail metadata for one post type (collection), sourced from the public
|
|
5
|
+
* `GET /collections` introspection endpoint. Read-only by design: collection
|
|
6
|
+
* schemas are file-defined in `actuate.config.ts`, so the Post Type editor
|
|
7
|
+
* inspects the live schema and generates config snippets for changes.
|
|
8
|
+
*/
|
|
9
|
+
export interface PostTypeFieldSummary {
|
|
10
|
+
name: string
|
|
11
|
+
type: string
|
|
12
|
+
required: boolean
|
|
13
|
+
relationTo?: string | string[]
|
|
14
|
+
options?: unknown[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PostTypeDetail {
|
|
18
|
+
slug: string
|
|
19
|
+
singular: string
|
|
20
|
+
plural: string
|
|
21
|
+
description: string | null
|
|
22
|
+
urlPrefix: string
|
|
23
|
+
icon: string | null
|
|
24
|
+
color: string | null
|
|
25
|
+
fields: PostTypeFieldSummary[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface CollectionDetailRow {
|
|
29
|
+
slug: string
|
|
30
|
+
labels: { singular: string; plural: string }
|
|
31
|
+
type: string
|
|
32
|
+
urlPrefix: string
|
|
33
|
+
description: string | null
|
|
34
|
+
icon: string | null
|
|
35
|
+
color: string | null
|
|
36
|
+
hidden: boolean
|
|
37
|
+
fields: PostTypeFieldSummary[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function fetchPostTypeDetail(slug: string): Promise<PostTypeDetail | null> {
|
|
41
|
+
const res = await cmsApi<CollectionDetailRow[]>('/collections')
|
|
42
|
+
if (res.error || !res.data) {
|
|
43
|
+
throw new Error(res.error ?? 'Failed to load collections')
|
|
44
|
+
}
|
|
45
|
+
const row = res.data.find((c) => c.slug === slug)
|
|
46
|
+
if (!row) return null
|
|
47
|
+
return {
|
|
48
|
+
slug: row.slug,
|
|
49
|
+
singular: row.labels?.singular ?? slug,
|
|
50
|
+
plural: row.labels?.plural ?? slug,
|
|
51
|
+
description: row.description ?? null,
|
|
52
|
+
urlPrefix: row.urlPrefix || slug,
|
|
53
|
+
icon: row.icon ?? null,
|
|
54
|
+
color: row.color ?? null,
|
|
55
|
+
fields: Array.isArray(row.fields) ? row.fields : [],
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Friendly display names for cms-core field types (design's type badges). */
|
|
60
|
+
export const FIELD_TYPE_LABELS: Record<string, string> = {
|
|
61
|
+
text: 'Short Text',
|
|
62
|
+
richText: 'Rich Text',
|
|
63
|
+
number: 'Number',
|
|
64
|
+
boolean: 'Boolean',
|
|
65
|
+
date: 'Date',
|
|
66
|
+
slug: 'Slug',
|
|
67
|
+
select: 'Select',
|
|
68
|
+
relationship: 'Relation',
|
|
69
|
+
media: 'Media',
|
|
70
|
+
json: 'JSON',
|
|
71
|
+
color: 'Color',
|
|
72
|
+
url: 'URL',
|
|
73
|
+
email: 'Email',
|
|
74
|
+
array: 'List',
|
|
75
|
+
blocks: 'Blocks',
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function fieldTypeLabel(type: string): string {
|
|
79
|
+
return FIELD_TYPE_LABELS[type] ?? type
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** "clientName" / "client_name" → "Client Name" for display. */
|
|
83
|
+
export function humanizeFieldName(name: string): string {
|
|
84
|
+
return name
|
|
85
|
+
.replace(/[_-]+/g, ' ')
|
|
86
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
87
|
+
.replace(/\s+/g, ' ')
|
|
88
|
+
.trim()
|
|
89
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** "Client Name" → "clientName" for generated field keys. */
|
|
93
|
+
export function fieldKeyFromName(name: string): string {
|
|
94
|
+
const words = name
|
|
95
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
96
|
+
.trim()
|
|
97
|
+
.split(/\s+/)
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
if (words.length === 0) return ''
|
|
100
|
+
return words
|
|
101
|
+
.map((w, i) => (i === 0 ? w.toLowerCase() : w[0]!.toUpperCase() + w.slice(1).toLowerCase()))
|
|
102
|
+
.join('')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface FieldSnippetInput {
|
|
106
|
+
key: string
|
|
107
|
+
label: string
|
|
108
|
+
type: string
|
|
109
|
+
required: boolean
|
|
110
|
+
helpText?: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Paste-ready field entry for the collection's `fields` map in
|
|
115
|
+
* `actuate.config.ts` — same escape rules as the New Post Type snippet.
|
|
116
|
+
*/
|
|
117
|
+
export function renderFieldSnippet(input: FieldSnippetInput): string {
|
|
118
|
+
const esc = (s: string) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
119
|
+
const parts = [`type: '${input.type}'`, `label: '${esc(input.label)}'`]
|
|
120
|
+
if (input.required) parts.push('required: true')
|
|
121
|
+
if (input.helpText?.trim()) parts.push(`admin: { description: '${esc(input.helpText.trim())}' }`)
|
|
122
|
+
if (input.type === 'select') {
|
|
123
|
+
parts.push(`options: [{ label: 'Option A', value: 'option-a' }]`)
|
|
124
|
+
}
|
|
125
|
+
if (input.type === 'relationship') {
|
|
126
|
+
parts.push(`relationTo: 'posts'`)
|
|
127
|
+
}
|
|
128
|
+
return `${input.key}: { ${parts.join(', ')} },`
|
|
129
|
+
}
|
package/src/router/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState, useCallback, useEffect, useRef } from 'react'
|
|
3
|
+
import { useState, useCallback, useEffect, useMemo, useRef } from 'react'
|
|
4
4
|
|
|
5
5
|
interface RouteParams {
|
|
6
6
|
[key: string]: string
|
|
@@ -14,6 +14,15 @@ function stripBase(pathname: string, basePath: string): string {
|
|
|
14
14
|
return '/'
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Parse the query string out of an admin path (`/pages/new?parent=abc`) or a
|
|
19
|
+
* raw search string (`?parent=abc`) into `URLSearchParams`. Pure helper for
|
|
20
|
+
* view components that only receive `currentPath` / `onNavigate` props.
|
|
21
|
+
*/
|
|
22
|
+
export function parseRouteQuery(path: string): URLSearchParams {
|
|
23
|
+
return new URLSearchParams(path.split('#')[0]!.split('?')[1] ?? '')
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
export function useAdminRouter(basePath = '/admin', serverPath = '/') {
|
|
18
27
|
const [currentPath, setCurrentPath] = useState(serverPath)
|
|
19
28
|
const baseRef = useRef(basePath)
|
|
@@ -21,7 +30,10 @@ export function useAdminRouter(basePath = '/admin', serverPath = '/') {
|
|
|
21
30
|
|
|
22
31
|
useEffect(() => {
|
|
23
32
|
if (typeof window !== 'undefined') {
|
|
24
|
-
|
|
33
|
+
// Include the query string so state-bearing URLs (`/pages/new?parent=x`,
|
|
34
|
+
// `/settings?tab=users`) survive hard reloads. matchRoute strips the
|
|
35
|
+
// query before matching, so `/?foo=1` still resolves to `/`.
|
|
36
|
+
const browserPath = stripBase(window.location.pathname, basePath) + window.location.search
|
|
25
37
|
if (browserPath !== currentPath) {
|
|
26
38
|
setCurrentPath(browserPath)
|
|
27
39
|
}
|
|
@@ -30,7 +42,9 @@ export function useAdminRouter(basePath = '/admin', serverPath = '/') {
|
|
|
30
42
|
|
|
31
43
|
useEffect(() => {
|
|
32
44
|
function onPopState() {
|
|
33
|
-
|
|
45
|
+
// Query string included for the same reason as the mount sync above —
|
|
46
|
+
// back/forward must restore deep-link state, not just the pathname.
|
|
47
|
+
setCurrentPath(stripBase(window.location.pathname, baseRef.current) + window.location.search)
|
|
34
48
|
}
|
|
35
49
|
window.addEventListener('popstate', onPopState)
|
|
36
50
|
return () => window.removeEventListener('popstate', onPopState)
|
|
@@ -92,5 +106,12 @@ export function useAdminRouter(basePath = '/admin', serverPath = '/') {
|
|
|
92
106
|
[currentPath],
|
|
93
107
|
)
|
|
94
108
|
|
|
95
|
-
|
|
109
|
+
// Raw query string of the current route (`?tab=users`), or '' when absent.
|
|
110
|
+
// Views wanting parsed params can pass it (or currentPath) to parseRouteQuery.
|
|
111
|
+
const search = useMemo(() => {
|
|
112
|
+
const queryIndex = currentPath.indexOf('?')
|
|
113
|
+
return queryIndex === -1 ? '' : currentPath.slice(queryIndex).split('#')[0]!
|
|
114
|
+
}, [currentPath])
|
|
115
|
+
|
|
116
|
+
return { currentPath, search, navigate, matchRoute, buildHref }
|
|
96
117
|
}
|
|
@@ -15,6 +15,8 @@ import { toast } from 'sonner'
|
|
|
15
15
|
import { cmsApi } from '../lib/api.js'
|
|
16
16
|
import { useApiData } from '../lib/useApiData.js'
|
|
17
17
|
import { editPathForDoc, newPathForCollection } from '../lib/collection-routes.js'
|
|
18
|
+
import { InspectorPane, SplitPaneLayout, SplitPaneList } from '../components/ui/InspectorPane.js'
|
|
19
|
+
import { EntryDetailPane } from '../components/collections/EntryDetailPane.js'
|
|
18
20
|
|
|
19
21
|
export interface CollectionListProps {
|
|
20
22
|
collectionSlug: string
|
|
@@ -164,6 +166,8 @@ export function CollectionList({ collectionSlug, config, onNavigate }: Collectio
|
|
|
164
166
|
const [sort, setSort] = useState<SortField | null>(null)
|
|
165
167
|
const [order, setOrder] = useState<SortOrder>('asc')
|
|
166
168
|
const [selected, setSelected] = useState<Set<string>>(new Set())
|
|
169
|
+
// Row selected into the inline detail pane (design Entries split-pane).
|
|
170
|
+
const [detailId, setDetailId] = useState<string | null>(null)
|
|
167
171
|
const [filters, setFilters] = useState<Record<string, string>>({})
|
|
168
172
|
// In-use values per `values`-sourced facet, fetched once per collection.
|
|
169
173
|
const [facetValues, setFacetValues] = useState<Record<string, string[]>>({})
|
|
@@ -196,6 +200,7 @@ export function CollectionList({ collectionSlug, config, onNavigate }: Collectio
|
|
|
196
200
|
useEffect(() => {
|
|
197
201
|
setPage(1)
|
|
198
202
|
setSelected(new Set())
|
|
203
|
+
setDetailId(null)
|
|
199
204
|
setSearch('')
|
|
200
205
|
setFilters({})
|
|
201
206
|
setFacetValues({})
|
|
@@ -242,6 +247,12 @@ export function CollectionList({ collectionSlug, config, onNavigate }: Collectio
|
|
|
242
247
|
})
|
|
243
248
|
}, [])
|
|
244
249
|
|
|
250
|
+
const detailDoc = useMemo(
|
|
251
|
+
() => (detailId ? (docs.find((d: any) => String(d.id) === detailId) ?? null) : null),
|
|
252
|
+
[docs, detailId],
|
|
253
|
+
)
|
|
254
|
+
const toggleDetail = (id: string) => setDetailId((prev) => (prev === id ? null : id))
|
|
255
|
+
|
|
245
256
|
const toggleSelect = (id: string) =>
|
|
246
257
|
setSelected((prev) => {
|
|
247
258
|
const s = new Set(prev)
|
|
@@ -454,120 +465,157 @@ export function CollectionList({ collectionSlug, config, onNavigate }: Collectio
|
|
|
454
465
|
</button>
|
|
455
466
|
</div>
|
|
456
467
|
) : (
|
|
457
|
-
<
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
<thead
|
|
463
|
-
className="sticky top-0"
|
|
464
|
-
style={{
|
|
465
|
-
background: 'var(--actuate-surface-alt, #f9fafb)',
|
|
466
|
-
borderBottom: '1px solid var(--actuate-border, #d1d5db)',
|
|
467
|
-
}}
|
|
468
|
+
<SplitPaneLayout>
|
|
469
|
+
<SplitPaneList>
|
|
470
|
+
<div
|
|
471
|
+
className="h-full overflow-auto rounded-lg border"
|
|
472
|
+
style={{ borderColor: 'var(--actuate-border, #d1d5db)' }}
|
|
468
473
|
>
|
|
469
|
-
<
|
|
470
|
-
<
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
/>
|
|
477
|
-
</th>
|
|
478
|
-
<th className="px-3 py-2 text-left">
|
|
479
|
-
<SortCol field="title">Title</SortCol>
|
|
480
|
-
</th>
|
|
481
|
-
{facets.map((facet) => (
|
|
482
|
-
<th
|
|
483
|
-
key={facet.name}
|
|
484
|
-
className="px-3 py-2 text-left text-xs font-medium"
|
|
485
|
-
style={{ color: 'var(--actuate-text-secondary, #6b7280)' }}
|
|
486
|
-
>
|
|
487
|
-
{facet.label}
|
|
488
|
-
</th>
|
|
489
|
-
))}
|
|
490
|
-
<th className="px-3 py-2 text-left">
|
|
491
|
-
<SortCol field="status">Status</SortCol>
|
|
492
|
-
</th>
|
|
493
|
-
<th className="px-3 py-2 text-left">
|
|
494
|
-
<SortCol field="updatedAt">Updated</SortCol>
|
|
495
|
-
</th>
|
|
496
|
-
<th
|
|
497
|
-
className="px-3 py-2 text-left text-xs font-medium"
|
|
498
|
-
style={{ color: 'var(--actuate-text-secondary, #6b7280)' }}
|
|
474
|
+
<table className="w-full text-sm">
|
|
475
|
+
<thead
|
|
476
|
+
className="sticky top-0"
|
|
477
|
+
style={{
|
|
478
|
+
background: 'var(--actuate-surface-alt, #f9fafb)',
|
|
479
|
+
borderBottom: '1px solid var(--actuate-border, #d1d5db)',
|
|
480
|
+
}}
|
|
499
481
|
>
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
{facets.map((facet) => (
|
|
530
|
-
<td
|
|
531
|
-
key={facet.name}
|
|
532
|
-
className="max-w-[200px] truncate px-3 py-2"
|
|
482
|
+
<tr>
|
|
483
|
+
<th className="w-10 px-3 py-2 text-left">
|
|
484
|
+
<input
|
|
485
|
+
type="checkbox"
|
|
486
|
+
checked={selected.size === docs.length && docs.length > 0}
|
|
487
|
+
onChange={(e) => toggleAll(e.target.checked)}
|
|
488
|
+
className="rounded"
|
|
489
|
+
/>
|
|
490
|
+
</th>
|
|
491
|
+
<th className="px-3 py-2 text-left">
|
|
492
|
+
<SortCol field="title">Title</SortCol>
|
|
493
|
+
</th>
|
|
494
|
+
{facets.map((facet) => (
|
|
495
|
+
<th
|
|
496
|
+
key={facet.name}
|
|
497
|
+
className="px-3 py-2 text-left text-xs font-medium"
|
|
498
|
+
style={{ color: 'var(--actuate-text-secondary, #6b7280)' }}
|
|
499
|
+
>
|
|
500
|
+
{facet.label}
|
|
501
|
+
</th>
|
|
502
|
+
))}
|
|
503
|
+
<th className="px-3 py-2 text-left">
|
|
504
|
+
<SortCol field="status">Status</SortCol>
|
|
505
|
+
</th>
|
|
506
|
+
<th className="px-3 py-2 text-left">
|
|
507
|
+
<SortCol field="updatedAt">Updated</SortCol>
|
|
508
|
+
</th>
|
|
509
|
+
<th
|
|
510
|
+
className="px-3 py-2 text-left text-xs font-medium"
|
|
533
511
|
style={{ color: 'var(--actuate-text-secondary, #6b7280)' }}
|
|
534
|
-
title={facetCellValue(doc, facet) || undefined}
|
|
535
|
-
>
|
|
536
|
-
{facetCellValue(doc, facet) || '—'}
|
|
537
|
-
</td>
|
|
538
|
-
))}
|
|
539
|
-
<td className="px-3 py-2">
|
|
540
|
-
<span
|
|
541
|
-
className="inline-block rounded-full px-2 py-0.5 text-xs font-medium"
|
|
542
|
-
style={{ background: statusColor(doc.status), color: statusText(doc.status) }}
|
|
543
512
|
>
|
|
544
|
-
|
|
545
|
-
</
|
|
546
|
-
</
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
513
|
+
Actions
|
|
514
|
+
</th>
|
|
515
|
+
</tr>
|
|
516
|
+
</thead>
|
|
517
|
+
<tbody>
|
|
518
|
+
{docs.map((doc: any) => {
|
|
519
|
+
const isDetail = detailId === String(doc.id)
|
|
520
|
+
return (
|
|
521
|
+
<tr
|
|
522
|
+
key={doc.id}
|
|
523
|
+
className={`cursor-pointer transition-colors ${isDetail ? 'bg-[var(--acc-l)]' : 'hover:opacity-95'}`}
|
|
524
|
+
style={{ borderBottom: '1px solid var(--actuate-border, #e5e7eb)' }}
|
|
525
|
+
onClick={() => toggleDetail(String(doc.id))}
|
|
526
|
+
onDoubleClick={() => onNavigate(editPath(doc.id))}
|
|
527
|
+
>
|
|
528
|
+
<td className="px-3 py-2" onClick={(e) => e.stopPropagation()}>
|
|
529
|
+
<input
|
|
530
|
+
type="checkbox"
|
|
531
|
+
checked={selected.has(String(doc.id))}
|
|
532
|
+
onChange={() => toggleSelect(String(doc.id))}
|
|
533
|
+
className="rounded"
|
|
534
|
+
/>
|
|
535
|
+
</td>
|
|
536
|
+
<td className="px-3 py-2">
|
|
537
|
+
<button
|
|
538
|
+
type="button"
|
|
539
|
+
onClick={(e) => {
|
|
540
|
+
e.stopPropagation()
|
|
541
|
+
toggleDetail(String(doc.id))
|
|
542
|
+
}}
|
|
543
|
+
aria-expanded={isDetail}
|
|
544
|
+
className="text-left font-medium hover:underline"
|
|
545
|
+
style={{ color: 'var(--actuate-text, #111827)' }}
|
|
546
|
+
>
|
|
547
|
+
{doc.title || doc.name || `#${doc.id}`}
|
|
548
|
+
</button>
|
|
549
|
+
</td>
|
|
550
|
+
{facets.map((facet) => (
|
|
551
|
+
<td
|
|
552
|
+
key={facet.name}
|
|
553
|
+
className="max-w-[200px] truncate px-3 py-2"
|
|
554
|
+
style={{ color: 'var(--actuate-text-secondary, #6b7280)' }}
|
|
555
|
+
title={facetCellValue(doc, facet) || undefined}
|
|
556
|
+
>
|
|
557
|
+
{facetCellValue(doc, facet) || '—'}
|
|
558
|
+
</td>
|
|
559
|
+
))}
|
|
560
|
+
<td className="px-3 py-2">
|
|
561
|
+
<span
|
|
562
|
+
className="inline-block rounded-full px-2 py-0.5 text-xs font-medium"
|
|
563
|
+
style={{
|
|
564
|
+
background: statusColor(doc.status),
|
|
565
|
+
color: statusText(doc.status),
|
|
566
|
+
}}
|
|
567
|
+
>
|
|
568
|
+
{doc.status ?? 'DRAFT'}
|
|
569
|
+
</span>
|
|
570
|
+
</td>
|
|
571
|
+
<td
|
|
572
|
+
className="px-3 py-2"
|
|
573
|
+
style={{ color: 'var(--actuate-text-secondary, #6b7280)' }}
|
|
574
|
+
>
|
|
575
|
+
{formatDate(doc.updatedAt)}
|
|
576
|
+
</td>
|
|
577
|
+
<td className="px-3 py-2">
|
|
578
|
+
<button
|
|
579
|
+
type="button"
|
|
580
|
+
onClick={(e) => {
|
|
581
|
+
e.stopPropagation()
|
|
582
|
+
onNavigate(editPath(doc.id))
|
|
583
|
+
}}
|
|
584
|
+
className="rounded p-1.5 transition-opacity hover:opacity-75"
|
|
585
|
+
title="Edit"
|
|
586
|
+
aria-label={`Edit ${doc.title || doc.name || `#${doc.id}`}`}
|
|
587
|
+
>
|
|
588
|
+
<MoreHorizontal
|
|
589
|
+
className="h-4 w-4"
|
|
590
|
+
style={{ color: 'var(--actuate-text-secondary, #6b7280)' }}
|
|
591
|
+
/>
|
|
592
|
+
</button>
|
|
593
|
+
</td>
|
|
594
|
+
</tr>
|
|
595
|
+
)
|
|
596
|
+
})}
|
|
597
|
+
</tbody>
|
|
598
|
+
</table>
|
|
599
|
+
</div>
|
|
600
|
+
</SplitPaneList>
|
|
601
|
+
|
|
602
|
+
<InspectorPane
|
|
603
|
+
open={detailDoc !== null}
|
|
604
|
+
width={400}
|
|
605
|
+
aria-label={`${labels.singular} details`}
|
|
606
|
+
>
|
|
607
|
+
{detailDoc && (
|
|
608
|
+
<EntryDetailPane
|
|
609
|
+
key={String(detailDoc.id)}
|
|
610
|
+
doc={detailDoc}
|
|
611
|
+
collection={collectionDef}
|
|
612
|
+
singularLabel={labels.singular}
|
|
613
|
+
onClose={() => setDetailId(null)}
|
|
614
|
+
onOpenEditor={() => onNavigate(editPath(detailDoc.id))}
|
|
615
|
+
/>
|
|
616
|
+
)}
|
|
617
|
+
</InspectorPane>
|
|
618
|
+
</SplitPaneLayout>
|
|
571
619
|
)}
|
|
572
620
|
|
|
573
621
|
{/* Pagination */}
|