@orion-studios/payload-admin-components 0.2.0-beta.11 → 0.2.0-beta.12
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/dist/client.d.mts +20 -1
- package/dist/client.d.ts +20 -1
- package/dist/client.js +639 -0
- package/dist/client.mjs +632 -0
- package/dist/index.d.mts +74 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +81 -9
- package/dist/index.mjs +81 -9
- package/package.json +1 -1
- package/src/client.ts +8 -0
- package/src/components/studio/AdminStudioDashboard.tsx +51 -0
- package/src/components/studio/AdminStudioGlobalsView.tsx +61 -0
- package/src/components/studio/AdminStudioMediaView.tsx +50 -0
- package/src/components/studio/AdminStudioNav.tsx +134 -0
- package/src/components/studio/AdminStudioPageEditView.tsx +172 -0
- package/src/components/studio/AdminStudioPagesListView.tsx +208 -0
- package/src/components/studio/AdminStudioToolsView.tsx +80 -0
- package/src/helpers/configureAdmin.ts +110 -7
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { Logout, useAuth } from '@payloadcms/ui'
|
|
6
|
+
|
|
7
|
+
type LinkItem = {
|
|
8
|
+
href: string
|
|
9
|
+
label: string
|
|
10
|
+
matchPrefixes: string[]
|
|
11
|
+
adminOnly?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const isAdmin = (user: unknown): boolean => {
|
|
15
|
+
if (!user || typeof user !== 'object') return false
|
|
16
|
+
const role = (user as any).role
|
|
17
|
+
return typeof role === 'string' && role === 'admin'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const getPropString = (props: unknown, key: string, fallback: string): string => {
|
|
21
|
+
if (!props || typeof props !== 'object') return fallback
|
|
22
|
+
|
|
23
|
+
const direct = (props as any)[key]
|
|
24
|
+
if (typeof direct === 'string' && direct.length > 0) return direct
|
|
25
|
+
|
|
26
|
+
const clientProps = (props as any).clientProps
|
|
27
|
+
if (clientProps && typeof clientProps === 'object') {
|
|
28
|
+
const nested = (clientProps as any)[key]
|
|
29
|
+
if (typeof nested === 'string' && nested.length > 0) return nested
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return fallback
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function AdminStudioNav(props: Record<string, unknown>) {
|
|
36
|
+
const { user } = useAuth()
|
|
37
|
+
const brandName = getPropString(props, 'brandName', 'Orion Studio')
|
|
38
|
+
|
|
39
|
+
const [pathname, setPathname] = useState<string>('')
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const update = () => setPathname(window.location.pathname)
|
|
43
|
+
update()
|
|
44
|
+
|
|
45
|
+
window.addEventListener('popstate', update)
|
|
46
|
+
return () => window.removeEventListener('popstate', update)
|
|
47
|
+
}, [])
|
|
48
|
+
|
|
49
|
+
const links: LinkItem[] = useMemo(
|
|
50
|
+
() => [
|
|
51
|
+
{ href: '/admin', label: 'Dashboard', matchPrefixes: ['/admin'] },
|
|
52
|
+
{
|
|
53
|
+
href: '/admin/pages',
|
|
54
|
+
label: 'Pages',
|
|
55
|
+
matchPrefixes: ['/admin/pages', '/admin/collections/pages'],
|
|
56
|
+
},
|
|
57
|
+
{ href: '/admin/globals', label: 'Globals', matchPrefixes: ['/admin/globals'] },
|
|
58
|
+
{
|
|
59
|
+
href: '/admin/media',
|
|
60
|
+
label: 'Media',
|
|
61
|
+
matchPrefixes: ['/admin/media', '/admin/collections/media'],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
href: '/admin/tools',
|
|
65
|
+
label: 'Admin Tools',
|
|
66
|
+
matchPrefixes: ['/admin/tools'],
|
|
67
|
+
adminOnly: true,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
[],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const linkStyle = (active: boolean): React.CSSProperties => ({
|
|
74
|
+
background: active ? 'var(--theme-elevation-100)' : 'transparent',
|
|
75
|
+
borderRadius: 10,
|
|
76
|
+
color: 'var(--theme-elevation-900)',
|
|
77
|
+
display: 'block',
|
|
78
|
+
fontSize: '0.95rem',
|
|
79
|
+
fontWeight: active ? 800 : 650,
|
|
80
|
+
padding: '0.6rem 0.75rem',
|
|
81
|
+
textDecoration: 'none',
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
style={{
|
|
87
|
+
display: 'flex',
|
|
88
|
+
flexDirection: 'column',
|
|
89
|
+
gap: '0.85rem',
|
|
90
|
+
height: '100%',
|
|
91
|
+
padding: '1rem 0.85rem',
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<div style={{ padding: '0 0.35rem' }}>
|
|
95
|
+
<div style={{ fontSize: '1.05rem', fontWeight: 900, letterSpacing: '-0.01em' }}>
|
|
96
|
+
{brandName}
|
|
97
|
+
</div>
|
|
98
|
+
<div style={{ color: 'var(--theme-elevation-600)', fontSize: '0.85rem' }}>Studio</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<nav style={{ display: 'grid', gap: '0.25rem' }}>
|
|
102
|
+
{links
|
|
103
|
+
.filter((link) => !link.adminOnly || isAdmin(user))
|
|
104
|
+
.map((link) => {
|
|
105
|
+
const active =
|
|
106
|
+
link.href === '/admin'
|
|
107
|
+
? pathname === '/admin'
|
|
108
|
+
: link.matchPrefixes.some((prefix) => pathname.startsWith(prefix))
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<a href={link.href} key={link.href} style={linkStyle(active)}>
|
|
112
|
+
{link.label}
|
|
113
|
+
</a>
|
|
114
|
+
)
|
|
115
|
+
})}
|
|
116
|
+
</nav>
|
|
117
|
+
|
|
118
|
+
<div style={{ flex: 1 }} />
|
|
119
|
+
|
|
120
|
+
<div
|
|
121
|
+
style={{
|
|
122
|
+
borderTop: '1px solid var(--theme-elevation-150)',
|
|
123
|
+
paddingTop: '0.85rem',
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
<div style={{ color: 'var(--theme-elevation-700)', fontSize: '0.85rem' }}>Signed in as</div>
|
|
127
|
+
<div style={{ fontWeight: 800, marginBottom: '0.55rem' }}>
|
|
128
|
+
{typeof (user as any)?.email === 'string' ? (user as any).email : 'User'}
|
|
129
|
+
</div>
|
|
130
|
+
<Logout />
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { toast, useAuth } from '@payloadcms/ui'
|
|
6
|
+
|
|
7
|
+
type AnyRecord = Record<string, unknown>
|
|
8
|
+
|
|
9
|
+
const isAdmin = (user: unknown): boolean => {
|
|
10
|
+
if (!user || typeof user !== 'object') return false
|
|
11
|
+
const role = (user as any).role
|
|
12
|
+
return typeof role === 'string' && role === 'admin'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const isEditor = (user: unknown): boolean => {
|
|
16
|
+
if (!user || typeof user !== 'object') return false
|
|
17
|
+
const role = (user as any).role
|
|
18
|
+
return typeof role === 'string' && role === 'editor'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const getPropString = (props: unknown, key: string, fallback: string): string => {
|
|
22
|
+
if (!props || typeof props !== 'object') return fallback
|
|
23
|
+
|
|
24
|
+
const direct = (props as any)[key]
|
|
25
|
+
if (typeof direct === 'string' && direct.length > 0) return direct
|
|
26
|
+
|
|
27
|
+
const clientProps = (props as any).clientProps
|
|
28
|
+
if (clientProps && typeof clientProps === 'object') {
|
|
29
|
+
const nested = (clientProps as any)[key]
|
|
30
|
+
if (typeof nested === 'string' && nested.length > 0) return nested
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return fallback
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const getParam = (params: unknown, key: string): string | null => {
|
|
37
|
+
if (!params || typeof params !== 'object') return null
|
|
38
|
+
if (!(key in (params as Record<string, unknown>))) return null
|
|
39
|
+
const value = (params as Record<string, unknown>)[key]
|
|
40
|
+
if (typeof value === 'string') return value
|
|
41
|
+
if (Array.isArray(value) && typeof value[0] === 'string') return value[0]
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function AdminStudioPageEditView(props: AnyRecord) {
|
|
46
|
+
const { user } = useAuth()
|
|
47
|
+
const iframeRef = useRef<HTMLIFrameElement | null>(null)
|
|
48
|
+
const [saving, setSaving] = useState<null | 'draft' | 'published'>(null)
|
|
49
|
+
|
|
50
|
+
const builderBasePath = getPropString(props, 'builderBasePath', '/builder')
|
|
51
|
+
const pageID = useMemo(() => getParam(props.params, 'id'), [props.params])
|
|
52
|
+
|
|
53
|
+
const canPublish = isAdmin(user) || isEditor(user)
|
|
54
|
+
|
|
55
|
+
const requestSave = (status: 'draft' | 'published') => {
|
|
56
|
+
const iframe = iframeRef.current
|
|
57
|
+
if (!iframe?.contentWindow) {
|
|
58
|
+
toast.error('Editor is not ready yet. Please try again.')
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setSaving(status)
|
|
63
|
+
iframe.contentWindow.postMessage({ source: 'payload-visual-builder-parent', type: 'save', status }, '*')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const onMessage = (event: MessageEvent) => {
|
|
68
|
+
const data = event.data as
|
|
69
|
+
| { source?: string; type?: string; ok?: boolean; message?: string }
|
|
70
|
+
| undefined
|
|
71
|
+
|
|
72
|
+
if (!data || data.source !== 'payload-visual-builder-child' || data.type !== 'save-result') {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setSaving(null)
|
|
77
|
+
|
|
78
|
+
if (data.ok) {
|
|
79
|
+
toast.success(typeof data.message === 'string' ? data.message : 'Saved.')
|
|
80
|
+
} else {
|
|
81
|
+
toast.error(typeof data.message === 'string' ? data.message : 'Save failed.')
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
window.addEventListener('message', onMessage)
|
|
86
|
+
return () => window.removeEventListener('message', onMessage)
|
|
87
|
+
}, [])
|
|
88
|
+
|
|
89
|
+
if (!pageID) {
|
|
90
|
+
return (
|
|
91
|
+
<div style={{ padding: '1.2rem' }}>
|
|
92
|
+
<h1 style={{ margin: 0 }}>Page Editor</h1>
|
|
93
|
+
<p style={{ color: 'var(--theme-elevation-600)' }}>Missing page ID.</p>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div style={{ display: 'grid', gridTemplateRows: 'auto 1fr', height: 'calc(100vh - 0px)' }}>
|
|
100
|
+
<div
|
|
101
|
+
style={{
|
|
102
|
+
alignItems: 'center',
|
|
103
|
+
background: 'var(--theme-elevation-0)',
|
|
104
|
+
borderBottom: '1px solid var(--theme-elevation-150)',
|
|
105
|
+
display: 'flex',
|
|
106
|
+
gap: '0.6rem',
|
|
107
|
+
justifyContent: 'space-between',
|
|
108
|
+
padding: '0.65rem 0.9rem',
|
|
109
|
+
position: 'sticky',
|
|
110
|
+
top: 0,
|
|
111
|
+
zIndex: 20,
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<div style={{ minWidth: 0 }}>
|
|
115
|
+
<div style={{ fontWeight: 900 }}>Page Editor</div>
|
|
116
|
+
<div
|
|
117
|
+
style={{
|
|
118
|
+
color: 'var(--theme-elevation-600)',
|
|
119
|
+
fontSize: '0.85rem',
|
|
120
|
+
overflow: 'hidden',
|
|
121
|
+
textOverflow: 'ellipsis',
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
Editing: {pageID}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div style={{ alignItems: 'center', display: 'flex', gap: '0.5rem' }}>
|
|
129
|
+
<button
|
|
130
|
+
disabled={saving !== null}
|
|
131
|
+
onClick={() => requestSave('draft')}
|
|
132
|
+
style={{
|
|
133
|
+
border: '1px solid var(--theme-elevation-300)',
|
|
134
|
+
borderRadius: 12,
|
|
135
|
+
cursor: saving ? 'not-allowed' : 'pointer',
|
|
136
|
+
fontWeight: 800,
|
|
137
|
+
padding: '0.5rem 0.75rem',
|
|
138
|
+
}}
|
|
139
|
+
type="button"
|
|
140
|
+
>
|
|
141
|
+
{saving === 'draft' ? 'Saving…' : 'Save Draft'}
|
|
142
|
+
</button>
|
|
143
|
+
|
|
144
|
+
<button
|
|
145
|
+
disabled={!canPublish || saving !== null}
|
|
146
|
+
onClick={() => requestSave('published')}
|
|
147
|
+
style={{
|
|
148
|
+
background: canPublish ? 'var(--theme-elevation-900)' : 'var(--theme-elevation-300)',
|
|
149
|
+
border: 'none',
|
|
150
|
+
borderRadius: 12,
|
|
151
|
+
color: canPublish ? 'var(--theme-elevation-0)' : 'var(--theme-elevation-700)',
|
|
152
|
+
cursor: !canPublish || saving ? 'not-allowed' : 'pointer',
|
|
153
|
+
fontWeight: 900,
|
|
154
|
+
padding: '0.5rem 0.75rem',
|
|
155
|
+
}}
|
|
156
|
+
type="button"
|
|
157
|
+
title={!canPublish ? 'You do not have publish permissions.' : undefined}
|
|
158
|
+
>
|
|
159
|
+
{saving === 'published' ? 'Publishing…' : 'Publish'}
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<iframe
|
|
165
|
+
ref={iframeRef}
|
|
166
|
+
src={`${builderBasePath.replace(/\/$/, '')}/${pageID}`}
|
|
167
|
+
style={{ border: 'none', height: '100%', width: '100%' }}
|
|
168
|
+
title="Page Builder"
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { useAuth } from '@payloadcms/ui'
|
|
6
|
+
|
|
7
|
+
type AnyRecord = Record<string, unknown>
|
|
8
|
+
|
|
9
|
+
type PageDoc = {
|
|
10
|
+
id?: number | string
|
|
11
|
+
title?: unknown
|
|
12
|
+
path?: unknown
|
|
13
|
+
_status?: unknown
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const isAdmin = (user: unknown): boolean => {
|
|
17
|
+
if (!user || typeof user !== 'object') return false
|
|
18
|
+
const role = (user as any).role
|
|
19
|
+
return typeof role === 'string' && role === 'admin'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const getPropString = (props: unknown, key: string, fallback: string): string => {
|
|
23
|
+
if (!props || typeof props !== 'object') return fallback
|
|
24
|
+
|
|
25
|
+
const direct = (props as any)[key]
|
|
26
|
+
if (typeof direct === 'string' && direct.length > 0) return direct
|
|
27
|
+
|
|
28
|
+
const clientProps = (props as any).clientProps
|
|
29
|
+
if (clientProps && typeof clientProps === 'object') {
|
|
30
|
+
const nested = (clientProps as any)[key]
|
|
31
|
+
if (typeof nested === 'string' && nested.length > 0) return nested
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return fallback
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function AdminStudioPagesListView(props: AnyRecord) {
|
|
38
|
+
const { user } = useAuth()
|
|
39
|
+
const pagesCollectionSlug = getPropString(props, 'pagesCollectionSlug', 'pages')
|
|
40
|
+
|
|
41
|
+
const [loading, setLoading] = useState(true)
|
|
42
|
+
const [error, setError] = useState<string | null>(null)
|
|
43
|
+
const [docs, setDocs] = useState<PageDoc[]>([])
|
|
44
|
+
|
|
45
|
+
const apiURL = useMemo(() => {
|
|
46
|
+
const params = new URLSearchParams({
|
|
47
|
+
depth: '0',
|
|
48
|
+
limit: '100',
|
|
49
|
+
sort: '-updatedAt',
|
|
50
|
+
draft: 'true',
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return `/api/${pagesCollectionSlug}?${params.toString()}`
|
|
54
|
+
}, [pagesCollectionSlug])
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
let cancelled = false
|
|
58
|
+
|
|
59
|
+
const run = async () => {
|
|
60
|
+
setLoading(true)
|
|
61
|
+
setError(null)
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(apiURL, { credentials: 'include' })
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
const body = await res.text()
|
|
67
|
+
throw new Error(body || 'Failed to fetch pages')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const data = (await res.json()) as { docs?: PageDoc[] }
|
|
71
|
+
const nextDocs = Array.isArray(data.docs) ? data.docs : []
|
|
72
|
+
if (!cancelled) setDocs(nextDocs)
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (!cancelled) setError(err instanceof Error ? err.message : 'Failed to fetch pages')
|
|
75
|
+
} finally {
|
|
76
|
+
if (!cancelled) setLoading(false)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
void run()
|
|
81
|
+
|
|
82
|
+
return () => {
|
|
83
|
+
cancelled = true
|
|
84
|
+
}
|
|
85
|
+
}, [apiURL])
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div style={{ padding: '1.2rem 1.2rem 2.5rem' }}>
|
|
89
|
+
<div style={{ alignItems: 'flex-end', display: 'flex', gap: '0.75rem' }}>
|
|
90
|
+
<div style={{ flex: 1 }}>
|
|
91
|
+
<h1 style={{ margin: 0 }}>Pages</h1>
|
|
92
|
+
<p style={{ color: 'var(--theme-elevation-600)', marginTop: '0.35rem' }}>
|
|
93
|
+
Open a page to edit it in the custom editor.
|
|
94
|
+
</p>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{isAdmin(user) ? (
|
|
98
|
+
<a
|
|
99
|
+
href={`/admin/collections/${pagesCollectionSlug}/create`}
|
|
100
|
+
style={{
|
|
101
|
+
background: 'var(--theme-elevation-900)',
|
|
102
|
+
borderRadius: 12,
|
|
103
|
+
color: 'var(--theme-elevation-0)',
|
|
104
|
+
fontWeight: 800,
|
|
105
|
+
padding: '0.55rem 0.85rem',
|
|
106
|
+
textDecoration: 'none',
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
New Page
|
|
110
|
+
</a>
|
|
111
|
+
) : null}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{loading ? (
|
|
115
|
+
<div style={{ color: 'var(--theme-elevation-600)', marginTop: '1rem' }}>Loading…</div>
|
|
116
|
+
) : null}
|
|
117
|
+
|
|
118
|
+
{error ? (
|
|
119
|
+
<div style={{ color: 'crimson', marginTop: '1rem' }}>{error}</div>
|
|
120
|
+
) : null}
|
|
121
|
+
|
|
122
|
+
<div style={{ display: 'grid', gap: '0.6rem', marginTop: '1rem' }}>
|
|
123
|
+
{!loading && !error && docs.length === 0 ? (
|
|
124
|
+
<div
|
|
125
|
+
style={{
|
|
126
|
+
border: '1px dashed var(--theme-elevation-300)',
|
|
127
|
+
borderRadius: 16,
|
|
128
|
+
color: 'var(--theme-elevation-700)',
|
|
129
|
+
padding: '1rem',
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
No pages found.
|
|
133
|
+
</div>
|
|
134
|
+
) : null}
|
|
135
|
+
|
|
136
|
+
{docs.map((doc) => {
|
|
137
|
+
const id = typeof doc.id === 'string' || typeof doc.id === 'number' ? String(doc.id) : ''
|
|
138
|
+
const title = typeof doc.title === 'string' ? doc.title : 'Untitled Page'
|
|
139
|
+
const path = typeof doc.path === 'string' ? doc.path : '/'
|
|
140
|
+
const status = typeof doc._status === 'string' ? doc._status : ''
|
|
141
|
+
|
|
142
|
+
if (!id) return null
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<a
|
|
146
|
+
href={`/admin/pages/${id}`}
|
|
147
|
+
key={id}
|
|
148
|
+
style={{
|
|
149
|
+
background: 'var(--theme-elevation-0)',
|
|
150
|
+
border: '1px solid var(--theme-elevation-150)',
|
|
151
|
+
borderRadius: 16,
|
|
152
|
+
color: 'inherit',
|
|
153
|
+
display: 'flex',
|
|
154
|
+
gap: '0.85rem',
|
|
155
|
+
justifyContent: 'space-between',
|
|
156
|
+
padding: '0.85rem 1rem',
|
|
157
|
+
textDecoration: 'none',
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
<div style={{ minWidth: 0 }}>
|
|
161
|
+
<div style={{ fontWeight: 900, overflow: 'hidden', textOverflow: 'ellipsis' }}>{title}</div>
|
|
162
|
+
<div
|
|
163
|
+
style={{
|
|
164
|
+
color: 'var(--theme-elevation-600)',
|
|
165
|
+
fontSize: '0.9rem',
|
|
166
|
+
overflow: 'hidden',
|
|
167
|
+
textOverflow: 'ellipsis',
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
{path}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
<div
|
|
174
|
+
style={{
|
|
175
|
+
alignItems: 'center',
|
|
176
|
+
display: 'flex',
|
|
177
|
+
flexShrink: 0,
|
|
178
|
+
gap: '0.5rem',
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
{status ? (
|
|
182
|
+
<span
|
|
183
|
+
style={{
|
|
184
|
+
background:
|
|
185
|
+
status === 'published'
|
|
186
|
+
? 'rgba(16, 185, 129, 0.12)'
|
|
187
|
+
: 'rgba(245, 158, 11, 0.14)',
|
|
188
|
+
borderRadius: 999,
|
|
189
|
+
color:
|
|
190
|
+
status === 'published' ? 'rgb(5, 122, 85)' : 'rgb(180, 83, 9)',
|
|
191
|
+
fontSize: '0.82rem',
|
|
192
|
+
fontWeight: 900,
|
|
193
|
+
padding: '0.25rem 0.55rem',
|
|
194
|
+
textTransform: 'capitalize',
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
{status}
|
|
198
|
+
</span>
|
|
199
|
+
) : null}
|
|
200
|
+
<span style={{ color: 'var(--theme-elevation-600)', fontWeight: 800 }}>Open</span>
|
|
201
|
+
</div>
|
|
202
|
+
</a>
|
|
203
|
+
)
|
|
204
|
+
})}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
)
|
|
208
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useAuth } from '@payloadcms/ui'
|
|
4
|
+
|
|
5
|
+
type AnyRecord = Record<string, unknown>
|
|
6
|
+
|
|
7
|
+
const isAdmin = (user: unknown): boolean => {
|
|
8
|
+
if (!user || typeof user !== 'object') return false
|
|
9
|
+
const role = (user as any).role
|
|
10
|
+
return typeof role === 'string' && role === 'admin'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const getPropString = (props: unknown, key: string, fallback: string): string => {
|
|
14
|
+
if (!props || typeof props !== 'object') return fallback
|
|
15
|
+
|
|
16
|
+
const direct = (props as any)[key]
|
|
17
|
+
if (typeof direct === 'string' && direct.length > 0) return direct
|
|
18
|
+
|
|
19
|
+
const clientProps = (props as any).clientProps
|
|
20
|
+
if (clientProps && typeof clientProps === 'object') {
|
|
21
|
+
const nested = (clientProps as any)[key]
|
|
22
|
+
if (typeof nested === 'string' && nested.length > 0) return nested
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return fallback
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function AdminStudioToolsView(props: AnyRecord) {
|
|
29
|
+
const { user } = useAuth()
|
|
30
|
+
|
|
31
|
+
if (!isAdmin(user)) {
|
|
32
|
+
return (
|
|
33
|
+
<div style={{ padding: '1.2rem' }}>
|
|
34
|
+
<h1 style={{ margin: 0 }}>Admin Tools</h1>
|
|
35
|
+
<p style={{ color: 'var(--theme-elevation-600)' }}>You do not have access to this page.</p>
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const pagesCollectionSlug = getPropString(props, 'pagesCollectionSlug', 'pages')
|
|
41
|
+
const mediaCollectionSlug = getPropString(props, 'mediaCollectionSlug', 'media')
|
|
42
|
+
|
|
43
|
+
const links = [
|
|
44
|
+
{ href: `/admin/collections/${pagesCollectionSlug}`, label: 'Raw Pages Collection' },
|
|
45
|
+
{ href: `/admin/collections/${mediaCollectionSlug}`, label: 'Raw Media Collection' },
|
|
46
|
+
{ href: '/admin/globals/site-settings', label: 'Raw Site Settings Global' },
|
|
47
|
+
{ href: '/admin/globals/header', label: 'Raw Header Global' },
|
|
48
|
+
{ href: '/admin/globals/footer', label: 'Raw Footer Global' },
|
|
49
|
+
{ href: '/admin/collections/users', label: 'Users / Roles' },
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div style={{ padding: '1.2rem 1.2rem 2.5rem' }}>
|
|
54
|
+
<h1 style={{ margin: 0 }}>Admin Tools</h1>
|
|
55
|
+
<p style={{ color: 'var(--theme-elevation-600)', marginTop: '0.35rem' }}>
|
|
56
|
+
Hidden fallback links for administrators.
|
|
57
|
+
</p>
|
|
58
|
+
|
|
59
|
+
<div style={{ display: 'grid', gap: '0.6rem', marginTop: '1rem' }}>
|
|
60
|
+
{links.map((link) => (
|
|
61
|
+
<a
|
|
62
|
+
href={link.href}
|
|
63
|
+
key={link.href}
|
|
64
|
+
style={{
|
|
65
|
+
background: 'var(--theme-elevation-0)',
|
|
66
|
+
border: '1px solid var(--theme-elevation-150)',
|
|
67
|
+
borderRadius: 16,
|
|
68
|
+
color: 'inherit',
|
|
69
|
+
padding: '0.85rem 1rem',
|
|
70
|
+
textDecoration: 'none',
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
<div style={{ fontWeight: 900 }}>{link.label}</div>
|
|
74
|
+
<div style={{ color: 'var(--theme-elevation-600)', fontSize: '0.9rem' }}>{link.href}</div>
|
|
75
|
+
</a>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|