@payload-onpage/plugin 0.2.1 → 0.3.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.
- package/README.md +6 -0
- package/dist/access.d.ts +7 -4
- package/dist/access.d.ts.map +1 -1
- package/dist/access.js +23 -20
- package/dist/access.js.map +1 -1
- package/dist/admin/OnPageAdminControls.d.ts +3 -2
- package/dist/admin/OnPageAdminControls.d.ts.map +1 -1
- package/dist/admin/OnPageAdminControls.js +12 -8
- package/dist/admin/OnPageAdminControls.js.map +1 -1
- package/dist/endpoints.d.ts +22 -6
- package/dist/endpoints.d.ts.map +1 -1
- package/dist/endpoints.js +87 -80
- package/dist/endpoints.js.map +1 -1
- package/dist/hooks.d.ts +11 -5
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +2 -2
- package/dist/hooks.js.map +1 -1
- package/dist/http.d.ts +18 -4
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +30 -16
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +61 -50
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +22 -11
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +11 -3
- package/dist/options.js.map +1 -1
- package/dist/paths.d.ts +2 -2
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +11 -24
- package/dist/paths.js.map +1 -1
- package/dist/schema.d.ts +6 -4
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +115 -62
- package/dist/schema.js.map +1 -1
- package/dist/translations.d.ts +12 -1
- package/dist/translations.d.ts.map +1 -1
- package/dist/translations.js +10 -9
- package/dist/translations.js.map +1 -1
- package/package.json +15 -4
- package/src/access.ts +26 -19
- package/src/admin/OnPageAdminControls.tsx +16 -16
- package/src/endpoints.ts +105 -98
- package/src/hooks.ts +14 -5
- package/src/http.ts +51 -15
- package/src/index.ts +77 -64
- package/src/options.ts +29 -17
- package/src/paths.ts +14 -27
- package/src/schema.ts +137 -79
- package/src/translations.ts +17 -11
package/src/index.ts
CHANGED
|
@@ -9,8 +9,15 @@
|
|
|
9
9
|
* Payload's REST API, so access control, hooks, validation, drafts and versions
|
|
10
10
|
* apply unchanged.
|
|
11
11
|
*/
|
|
12
|
-
import { DEFAULTS, type SchemaField } from '@payload-onpage/core'
|
|
13
|
-
import
|
|
12
|
+
import { DEFAULTS, type DocRef, type SchemaField } from '@payload-onpage/core'
|
|
13
|
+
import {
|
|
14
|
+
InvalidConfiguration,
|
|
15
|
+
type CollectionConfig,
|
|
16
|
+
type Config,
|
|
17
|
+
type GlobalConfig,
|
|
18
|
+
type Plugin,
|
|
19
|
+
} from 'payload'
|
|
20
|
+
import { deepMergeSimple } from 'payload/shared'
|
|
14
21
|
import { createEndpoints } from './endpoints.ts'
|
|
15
22
|
import { encodeOnRead, guardStaleWrite, stripMarkers } from './hooks.ts'
|
|
16
23
|
import { normalizeEntries, type Entry, type OnPageOptions } from './options.ts'
|
|
@@ -19,56 +26,73 @@ import { translations } from './translations.ts'
|
|
|
19
26
|
|
|
20
27
|
const APPLIED = 'payload-onpage'
|
|
21
28
|
|
|
22
|
-
type Translations = Record<string, Record<string, unknown>>
|
|
23
|
-
/** Ours first; the site's own translations (any language, any key) win. */
|
|
24
|
-
function mergeTranslations(
|
|
25
|
-
ours: Translations,
|
|
26
|
-
theirs: unknown,
|
|
27
|
-
): NonNullable<Config['i18n']>['translations'] {
|
|
28
|
-
const out: Translations = { ...ours }
|
|
29
|
-
for (const [lang, table] of Object.entries((theirs ?? {}) as Translations)) {
|
|
30
|
-
const mine = out[lang] ?? {}
|
|
31
|
-
out[lang] = {
|
|
32
|
-
...mine,
|
|
33
|
-
...table,
|
|
34
|
-
onpage: { ...(mine.onpage as object), ...(table.onpage ?? {}) },
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return out
|
|
38
|
-
}
|
|
39
|
-
|
|
40
29
|
export function onpage(options: OnPageOptions): Plugin {
|
|
41
30
|
return (incoming: Config): Config => {
|
|
42
31
|
if (options.enabled === false || incoming.custom?.[APPLIED]) return incoming
|
|
32
|
+
const { session } = options
|
|
33
|
+
if (!session)
|
|
34
|
+
throw new InvalidConfiguration(
|
|
35
|
+
'payload-onpage: pass `session` (e.g. nextSession() from @payload-onpage/next/session). The enter, mode, exit and preview endpoints need it.',
|
|
36
|
+
)
|
|
43
37
|
const prefix = options.routePrefix ?? DEFAULTS.routePrefix
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
...normalizeEntries(options.globals, 'global'),
|
|
48
|
-
]
|
|
49
|
-
const byKey = new Map(entries.map((e) => [`${e.kind}:${e.slug}`, e]))
|
|
38
|
+
const entries = normalizeEntries(options)
|
|
39
|
+
const keyOf = (kind: DocRef['kind'], slug: string) => `${kind}:${slug}`
|
|
40
|
+
const byKey = new Map(entries.map((e) => [keyOf(e.kind, e.slug), e]))
|
|
50
41
|
|
|
51
|
-
const
|
|
42
|
+
const missing = entries.filter((e) =>
|
|
43
|
+
e.kind === 'global'
|
|
44
|
+
? !incoming.globals?.some((g) => g.slug === e.slug)
|
|
45
|
+
: !incoming.collections?.some((c) => c.slug === e.slug),
|
|
46
|
+
)
|
|
47
|
+
if (missing.length) {
|
|
48
|
+
throw new InvalidConfiguration(
|
|
49
|
+
`payload-onpage: unknown ${missing.map((e) => `${e.kind} "${e.slug}"`).join(', ')}`,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The hooks every enabled document gets (see ./hooks). Encoding serializes the fields once, lazily. */
|
|
54
|
+
const hooksFor = (cfg: CollectionConfig | GlobalConfig, entry: Entry) => {
|
|
52
55
|
let fields: SchemaField[] | undefined
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
return {
|
|
57
|
+
beforeValidate: [stripMarkers],
|
|
58
|
+
beforeChange: [stripMarkers, guardStaleWrite],
|
|
59
|
+
afterRead: [
|
|
60
|
+
encodeOnRead({
|
|
61
|
+
kind: entry.kind,
|
|
62
|
+
slug: cfg.slug,
|
|
63
|
+
fields: () => (fields ??= serializeFields(cfg.fields, { blocks: incoming.blocks })),
|
|
64
|
+
...(entry.options.fields ? { only: entry.options.fields } : {}),
|
|
65
|
+
...(options.encode ? { filter: options.encode } : {}),
|
|
66
|
+
}),
|
|
67
|
+
],
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const withCollectionHooks = (c: CollectionConfig, entry: Entry): CollectionConfig => {
|
|
71
|
+
const add = hooksFor(c, entry)
|
|
72
|
+
return {
|
|
73
|
+
...c,
|
|
74
|
+
hooks: {
|
|
75
|
+
...c.hooks,
|
|
76
|
+
beforeValidate: [...(c.hooks?.beforeValidate ?? []), ...add.beforeValidate],
|
|
77
|
+
beforeChange: [...(c.hooks?.beforeChange ?? []), ...add.beforeChange],
|
|
78
|
+
afterRead: [...(c.hooks?.afterRead ?? []), ...add.afterRead],
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const withGlobalHooks = (g: GlobalConfig, entry: Entry): GlobalConfig => {
|
|
83
|
+
const add = hooksFor(g, entry)
|
|
84
|
+
return {
|
|
85
|
+
...g,
|
|
86
|
+
hooks: {
|
|
87
|
+
...g.hooks,
|
|
88
|
+
beforeValidate: [...(g.hooks?.beforeValidate ?? []), ...add.beforeValidate],
|
|
89
|
+
beforeChange: [...(g.hooks?.beforeChange ?? []), ...add.beforeChange],
|
|
90
|
+
afterRead: [...(g.hooks?.afterRead ?? []), ...add.afterRead],
|
|
91
|
+
},
|
|
92
|
+
}
|
|
69
93
|
}
|
|
70
94
|
|
|
71
|
-
const control = (kind:
|
|
95
|
+
const control = (kind: DocRef['kind'], slug: string) => ({
|
|
72
96
|
path: '@payload-onpage/plugin/admin#OnPageAdminControls',
|
|
73
97
|
clientProps: { kind, slug, prefix },
|
|
74
98
|
})
|
|
@@ -112,39 +136,28 @@ export function onpage(options: OnPageOptions): Plugin {
|
|
|
112
136
|
}
|
|
113
137
|
}
|
|
114
138
|
|
|
115
|
-
const missing = entries.filter((e) =>
|
|
116
|
-
e.kind === 'global'
|
|
117
|
-
? !incoming.globals?.some((g) => g.slug === e.slug)
|
|
118
|
-
: !incoming.collections?.some((c) => c.slug === e.slug),
|
|
119
|
-
)
|
|
120
|
-
if (missing.length) {
|
|
121
|
-
throw new Error(
|
|
122
|
-
`payload-onpage: unknown ${missing.map((e) => `${e.kind} "${e.slug}"`).join(', ')}`,
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
139
|
return {
|
|
127
140
|
...incoming,
|
|
128
141
|
custom: { ...incoming.custom, [APPLIED]: true },
|
|
129
142
|
i18n: {
|
|
130
143
|
...incoming.i18n,
|
|
131
|
-
translations
|
|
144
|
+
// Ours first; the site's own translations (any language, any key) win.
|
|
145
|
+
translations: deepMergeSimple(translations, incoming.i18n?.translations ?? {}),
|
|
132
146
|
},
|
|
133
147
|
collections: (incoming.collections ?? []).map((c) => {
|
|
134
|
-
const entry = byKey.get(
|
|
135
|
-
return entry ? withCollectionAdmin(
|
|
148
|
+
const entry = byKey.get(keyOf('collection', c.slug))
|
|
149
|
+
return entry ? withCollectionAdmin(withCollectionHooks(c, entry)) : c
|
|
136
150
|
}),
|
|
137
151
|
globals: (incoming.globals ?? []).map((g) => {
|
|
138
|
-
const entry = byKey.get(
|
|
139
|
-
return entry ? withGlobalAdmin(
|
|
152
|
+
const entry = byKey.get(keyOf('global', g.slug))
|
|
153
|
+
return entry ? withGlobalAdmin(withGlobalHooks(g, entry)) : g
|
|
140
154
|
}),
|
|
141
155
|
endpoints: [
|
|
142
156
|
...(incoming.endpoints ?? []),
|
|
143
157
|
...createEndpoints({
|
|
144
|
-
options,
|
|
158
|
+
options: { ...options, session },
|
|
145
159
|
prefix,
|
|
146
|
-
|
|
147
|
-
entry: (kind, slug) => byKey.get(`${kind}:${slug}`),
|
|
160
|
+
entry: (kind, slug) => byKey.get(keyOf(kind, slug)),
|
|
148
161
|
}),
|
|
149
162
|
],
|
|
150
163
|
onInit: async (payload) => {
|
|
@@ -159,7 +172,7 @@ export function onpage(options: OnPageOptions): Plugin {
|
|
|
159
172
|
}
|
|
160
173
|
}
|
|
161
174
|
|
|
162
|
-
export type { OnPageDocOptions, OnPageOptions } from './options.ts'
|
|
175
|
+
export type { OnPageDocOptions, OnPageOptions, OnPageTargets } from './options.ts'
|
|
163
176
|
export { canEditDocument, canUseOnPage } from './access.ts'
|
|
164
177
|
export { createPreviewToken, verifyPreviewToken, type PreviewTokenPayload } from './tokens.ts'
|
|
165
178
|
export { serializeFields, type SerializeContext } from './schema.ts'
|
package/src/options.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DocRef, EncodeFilter, SessionAdapter } from '@payload-onpage/core'
|
|
2
|
-
import type { PayloadRequest } from 'payload'
|
|
2
|
+
import type { CollectionSlug, GlobalSlug, PayloadRequest } from 'payload'
|
|
3
3
|
|
|
4
4
|
export interface OnPageDocOptions {
|
|
5
5
|
/** Top-level fields editable on the page. Default: every top-level field the user may update. */
|
|
@@ -10,21 +10,29 @@ export interface OnPageDocOptions {
|
|
|
10
10
|
*/
|
|
11
11
|
url?: (args: {
|
|
12
12
|
doc: Record<string, unknown>
|
|
13
|
-
locale?: string
|
|
13
|
+
locale?: string | undefined
|
|
14
14
|
req: PayloadRequest
|
|
15
15
|
}) => string | null | Promise<string | null>
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Collections or globals to enable: a list of slugs, or slugs mapped to options.
|
|
20
|
+
* Slugs are checked against your generated Payload types (`CollectionSlug` /
|
|
21
|
+
* `GlobalSlug`), so a typo is a compile error.
|
|
22
|
+
*/
|
|
23
|
+
export type OnPageTargets<Slug extends string> =
|
|
24
|
+
Slug[] | Partial<Record<Slug, OnPageDocOptions | true>>
|
|
25
|
+
|
|
18
26
|
export interface OnPageOptions {
|
|
19
|
-
collections?:
|
|
20
|
-
globals?:
|
|
27
|
+
collections?: OnPageTargets<CollectionSlug>
|
|
28
|
+
globals?: OnPageTargets<GlobalSlug>
|
|
21
29
|
/**
|
|
22
30
|
* Who may use on-page editing (`ref` undefined) and edit a given document.
|
|
23
31
|
* Default: users of the admin user collection, with the document's own
|
|
24
32
|
* `update` access (evaluated by Payload, including row-level rules).
|
|
25
33
|
*/
|
|
26
34
|
access?: (args: { req: PayloadRequest; ref?: DocRef }) => boolean | Promise<boolean>
|
|
27
|
-
/** Framework adapter that switches edit/preview mode (e.g. `nextSession()`). Required for the enter/exit/preview endpoints. */
|
|
35
|
+
/** Framework adapter that switches edit/preview mode (e.g. `nextSession()`). Required for the enter/mode/exit/preview endpoints. */
|
|
28
36
|
session?: SessionAdapter
|
|
29
37
|
/** Narrow which strings get auto-mapping markers. */
|
|
30
38
|
encode?: EncodeFilter
|
|
@@ -46,20 +54,24 @@ export interface OnPageOptions {
|
|
|
46
54
|
/** Auth cookie an editor's session is bound to. Default `${cookiePrefix}-token` ("payload-token"). */
|
|
47
55
|
authCookie?: string
|
|
48
56
|
/** Endpoint prefix under the API route. Default "/onpage". */
|
|
49
|
-
routePrefix?: string
|
|
57
|
+
routePrefix?: `/${string}`
|
|
50
58
|
enabled?: boolean
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
slug:
|
|
56
|
-
options: OnPageDocOptions
|
|
61
|
+
/** One enabled collection or global, with its slug typed by kind. */
|
|
62
|
+
export type Entry =
|
|
63
|
+
| { kind: 'collection'; slug: CollectionSlug; options: OnPageDocOptions }
|
|
64
|
+
| { kind: 'global'; slug: GlobalSlug; options: OnPageDocOptions }
|
|
65
|
+
|
|
66
|
+
function entriesOf<Slug extends string>(input: OnPageTargets<Slug> | undefined) {
|
|
67
|
+
return Array.isArray(input)
|
|
68
|
+
? input.map((slug) => ({ slug, options: {} }))
|
|
69
|
+
: (Object.entries(input ?? {}) as Array<[Slug, OnPageDocOptions | true | undefined]>)
|
|
70
|
+
.filter(([, o]) => o !== undefined)
|
|
71
|
+
.map(([slug, o]) => ({ slug, options: o === true || !o ? {} : o }))
|
|
57
72
|
}
|
|
58
73
|
|
|
59
|
-
export const normalizeEntries = (
|
|
60
|
-
|
|
61
|
-
kind:
|
|
62
|
-
|
|
63
|
-
Array.isArray(input)
|
|
64
|
-
? input.map((slug) => ({ kind, slug, options: {} }))
|
|
65
|
-
: Object.entries(input ?? {}).map(([slug, o]) => ({ kind, slug, options: o === true ? {} : o }))
|
|
74
|
+
export const normalizeEntries = (options: OnPageOptions): Entry[] => [
|
|
75
|
+
...entriesOf(options.collections).map((e) => ({ kind: 'collection' as const, ...e })),
|
|
76
|
+
...entriesOf(options.globals).map((e) => ({ kind: 'global' as const, ...e })),
|
|
77
|
+
]
|
package/src/paths.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Where a document lives on the site. */
|
|
2
2
|
import { safePath, type DocRef } from '@payload-onpage/core'
|
|
3
|
-
import type { PayloadRequest } from 'payload'
|
|
3
|
+
import type { PayloadRequest, TypedLocale } from 'payload'
|
|
4
4
|
import { originOf } from './http.ts'
|
|
5
5
|
import type { Entry } from './options.ts'
|
|
6
6
|
|
|
@@ -13,36 +13,23 @@ export async function resolvePath(
|
|
|
13
13
|
req: PayloadRequest,
|
|
14
14
|
ref: DocRef,
|
|
15
15
|
entry: Entry,
|
|
16
|
-
locale?:
|
|
16
|
+
locale?: TypedLocale,
|
|
17
17
|
): Promise<string | null> {
|
|
18
|
-
const
|
|
19
|
-
const doc =
|
|
20
|
-
|
|
21
|
-
? await req.payload
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
...localeArg,
|
|
28
|
-
})
|
|
29
|
-
.catch(() => null)
|
|
30
|
-
: await req.payload
|
|
31
|
-
.findByID({
|
|
32
|
-
collection: ref.slug as never,
|
|
33
|
-
id: ref.id,
|
|
34
|
-
draft: true,
|
|
35
|
-
depth: 0,
|
|
36
|
-
overrideAccess: true,
|
|
37
|
-
...localeArg,
|
|
38
|
-
})
|
|
39
|
-
.catch(() => null)
|
|
18
|
+
const read = { draft: true, depth: 0, overrideAccess: true, ...(locale ? { locale } : {}) }
|
|
19
|
+
const doc: Record<string, unknown> | null =
|
|
20
|
+
entry.kind === 'global'
|
|
21
|
+
? await req.payload.findGlobal({ slug: entry.slug, ...read }).catch(() => null)
|
|
22
|
+
: ref.kind === 'collection'
|
|
23
|
+
? await req.payload
|
|
24
|
+
.findByID({ collection: entry.slug, id: ref.id, ...read })
|
|
25
|
+
.catch(() => null)
|
|
26
|
+
: null
|
|
40
27
|
if (!doc) return null
|
|
41
28
|
|
|
42
29
|
let url: string | null | undefined
|
|
43
|
-
if (entry.options.url) url = await entry.options.url({ doc, locale, req })
|
|
44
|
-
else if (
|
|
45
|
-
const preview = req.payload.collections[
|
|
30
|
+
if (entry.options.url) url = await entry.options.url({ doc, locale: locale ?? undefined, req })
|
|
31
|
+
else if (entry.kind === 'collection') {
|
|
32
|
+
const preview = req.payload.collections[entry.slug]?.config.admin?.preview
|
|
46
33
|
if (preview) url = await preview(doc, { locale: locale ?? '', req, token: null })
|
|
47
34
|
if (!url && typeof doc.slug === 'string') url = `/${doc.slug.replace(/^\/+/, '')}`
|
|
48
35
|
} else url = '/'
|
package/src/schema.ts
CHANGED
|
@@ -21,44 +21,45 @@ const SYSTEM_FIELDS = new Set(['id', 'createdAt', 'updatedAt', '_status', 'delet
|
|
|
21
21
|
/** Data fields with no meaningful on-page value. */
|
|
22
22
|
const SKIP_TYPES = new Set(['join', 'ui'])
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The properties every data field may have. Payload's `Field` union declares
|
|
26
|
+
* them per member, so they're read through this one view; everything specific
|
|
27
|
+
* to a field type is read from Payload's narrowed type in `serializeFields`.
|
|
28
|
+
*/
|
|
29
|
+
interface CommonField {
|
|
26
30
|
label?: unknown
|
|
27
|
-
labels?: { singular?: unknown; plural?: unknown }
|
|
28
31
|
localized?: boolean
|
|
29
32
|
required?: boolean
|
|
30
|
-
hasMany?: boolean
|
|
31
33
|
defaultValue?: unknown
|
|
32
|
-
options?: unknown[]
|
|
33
|
-
relationTo?: string | string[]
|
|
34
|
-
minRows?: number
|
|
35
|
-
maxRows?: number
|
|
36
|
-
min?: number
|
|
37
|
-
max?: number
|
|
38
34
|
custom?: { onpage?: { editor?: unknown } }
|
|
39
|
-
admin?: {
|
|
40
|
-
description?: unknown
|
|
41
|
-
custom?: { onpage?: { editor?: unknown } }
|
|
42
|
-
date?: { pickerAppearance?: unknown }
|
|
43
|
-
}
|
|
35
|
+
admin?: { description?: unknown; custom?: { onpage?: { editor?: unknown } } }
|
|
44
36
|
}
|
|
45
37
|
|
|
38
|
+
/** `{ key: n }` when n is a number, else nothing (for spreading optional limits). */
|
|
39
|
+
const num = <K extends string>(key: K, n: unknown): Partial<Record<K, number>> =>
|
|
40
|
+
typeof n === 'number' ? ({ [key]: n } as Record<K, number>) : {}
|
|
41
|
+
|
|
46
42
|
type I18nLike = Parameters<typeof getTranslation>[1]
|
|
47
43
|
type AnyLabel = Parameters<typeof getTranslation>[0]
|
|
48
44
|
type Permissions = SanitizedFieldsPermissions | undefined
|
|
49
45
|
|
|
50
46
|
export interface SerializeContext {
|
|
51
47
|
/** Translates labels into the admin language. Without it, plain-string labels or the field name are used. */
|
|
52
|
-
i18n?: I18nLike
|
|
48
|
+
i18n?: I18nLike | undefined
|
|
53
49
|
/** Global blocks (`config.blocks`), to resolve `blockReferences`. */
|
|
54
|
-
blocks?: Block[]
|
|
50
|
+
blocks?: Block[] | undefined
|
|
55
51
|
/** Field-level permissions: fields the user can't update are left out. */
|
|
56
|
-
permissions?: Permissions
|
|
52
|
+
permissions?: Permissions | undefined
|
|
57
53
|
/** Collects function defaults to evaluate for this request. */
|
|
58
|
-
defaults?: { req: PayloadRequest; pending: Promise<void>[] }
|
|
54
|
+
defaults?: { req: PayloadRequest; pending: Promise<void>[] } | undefined
|
|
59
55
|
}
|
|
60
56
|
|
|
61
|
-
|
|
57
|
+
/** A config label (string, translations or function) in the request's language, else `fallback`. */
|
|
58
|
+
export const label = (
|
|
59
|
+
value: unknown,
|
|
60
|
+
fallback: string,
|
|
61
|
+
ctx: Pick<SerializeContext, 'i18n'>,
|
|
62
|
+
): string => {
|
|
62
63
|
if (value == null || value === false) return fallback
|
|
63
64
|
if (typeof value === 'string') return value || fallback
|
|
64
65
|
if (!ctx.i18n) return fallback
|
|
@@ -160,76 +161,134 @@ export function serializeFields(
|
|
|
160
161
|
const perms = childPermissions(ctx.permissions, f.name)
|
|
161
162
|
if (perms === false) continue
|
|
162
163
|
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
const field: SchemaField = {
|
|
164
|
+
const common = f as CommonField
|
|
165
|
+
const base = {
|
|
166
166
|
name: f.name,
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
label: label(common.label, toWords(f.name), ctx),
|
|
168
|
+
...(common.localized ? { localized: true } : {}),
|
|
169
|
+
...(common.required ? { required: true } : {}),
|
|
170
|
+
}
|
|
171
|
+
const description = label(common.admin?.description, '', ctx)
|
|
172
|
+
const editor = common.admin?.custom?.onpage?.editor ?? common.custom?.onpage?.editor
|
|
173
|
+
const childCtx = { ...ctx, permissions: perms }
|
|
174
|
+
const rows = (
|
|
175
|
+
labels: { singular?: unknown; plural?: unknown } | undefined,
|
|
176
|
+
min: unknown,
|
|
177
|
+
max: unknown,
|
|
178
|
+
) => {
|
|
179
|
+
const fallback = formatLabels(f.name)
|
|
180
|
+
return {
|
|
181
|
+
labels: {
|
|
182
|
+
singular: label(labels?.singular, fallback.singular, ctx),
|
|
183
|
+
plural: label(labels?.plural, fallback.plural, ctx),
|
|
184
|
+
},
|
|
185
|
+
...num('minRows', min),
|
|
186
|
+
...num('maxRows', max),
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let field: SchemaField
|
|
191
|
+
switch (f.type) {
|
|
192
|
+
case 'text':
|
|
193
|
+
case 'textarea':
|
|
194
|
+
case 'email':
|
|
195
|
+
case 'code':
|
|
196
|
+
case 'json':
|
|
197
|
+
case 'checkbox':
|
|
198
|
+
case 'point':
|
|
199
|
+
case 'richText':
|
|
200
|
+
field = { ...base, type: f.type, ...('hasMany' in f && f.hasMany ? { hasMany: true } : {}) }
|
|
201
|
+
break
|
|
202
|
+
case 'number':
|
|
203
|
+
field = {
|
|
204
|
+
...base,
|
|
205
|
+
type: 'number',
|
|
206
|
+
...(f.hasMany ? { hasMany: true } : {}),
|
|
207
|
+
...num('min', f.min),
|
|
208
|
+
...num('max', f.max),
|
|
209
|
+
}
|
|
210
|
+
break
|
|
211
|
+
case 'date': {
|
|
212
|
+
const picker = f.admin?.date?.pickerAppearance
|
|
213
|
+
field = {
|
|
214
|
+
...base,
|
|
215
|
+
type: 'date',
|
|
216
|
+
...(typeof picker === 'string' ? { pickerAppearance: picker } : {}),
|
|
217
|
+
}
|
|
218
|
+
break
|
|
219
|
+
}
|
|
220
|
+
case 'select':
|
|
221
|
+
case 'radio':
|
|
222
|
+
field = {
|
|
223
|
+
...base,
|
|
224
|
+
type: f.type,
|
|
225
|
+
options: f.options.map((o) =>
|
|
226
|
+
typeof o === 'string'
|
|
227
|
+
? { label: o, value: o }
|
|
228
|
+
: { label: label(o.label, String(o.value), ctx), value: String(o.value) },
|
|
229
|
+
),
|
|
230
|
+
...('hasMany' in f && f.hasMany ? { hasMany: true } : {}),
|
|
231
|
+
}
|
|
232
|
+
break
|
|
233
|
+
case 'relationship':
|
|
234
|
+
case 'upload':
|
|
235
|
+
field = {
|
|
236
|
+
...base,
|
|
237
|
+
type: f.type,
|
|
238
|
+
relationTo: f.relationTo,
|
|
239
|
+
...(f.hasMany ? { hasMany: true } : {}),
|
|
240
|
+
}
|
|
241
|
+
break
|
|
242
|
+
case 'group':
|
|
243
|
+
field = { ...base, type: 'group', fields: serializeFields(f.fields, childCtx) }
|
|
244
|
+
break
|
|
245
|
+
case 'array':
|
|
246
|
+
field = {
|
|
247
|
+
...base,
|
|
248
|
+
type: 'array',
|
|
249
|
+
fields: serializeFields(f.fields, childCtx),
|
|
250
|
+
...rows(f.labels, f.minRows, f.maxRows),
|
|
251
|
+
}
|
|
252
|
+
break
|
|
253
|
+
case 'blocks': {
|
|
254
|
+
const refs = (f.blockReferences ?? [])
|
|
255
|
+
.map((b) => (typeof b === 'string' ? ctx.blocks?.find((x) => x.slug === b) : b))
|
|
256
|
+
.filter((b): b is Block => !!b)
|
|
257
|
+
field = {
|
|
258
|
+
...base,
|
|
259
|
+
type: 'blocks',
|
|
260
|
+
blocks: [...f.blocks, ...refs].map((b) =>
|
|
261
|
+
serializeBlock(b, blockPermissions(ctx.permissions, f.name, b.slug), ctx),
|
|
262
|
+
),
|
|
263
|
+
...rows(f.labels, f.minRows, f.maxRows),
|
|
264
|
+
}
|
|
265
|
+
break
|
|
266
|
+
}
|
|
267
|
+
default:
|
|
268
|
+
// join/ui are skipped above; anything newer than this plugin isn't editable on the page.
|
|
269
|
+
continue
|
|
169
270
|
}
|
|
170
|
-
if (
|
|
171
|
-
if (
|
|
172
|
-
if (
|
|
173
|
-
if (typeof anyField.defaultValue === 'function') {
|
|
271
|
+
if (description) field.description = description
|
|
272
|
+
if (typeof editor === 'string') field.editor = editor
|
|
273
|
+
if (typeof common.defaultValue === 'function') {
|
|
174
274
|
if (ctx.defaults) {
|
|
175
275
|
const { req, pending } = ctx.defaults
|
|
276
|
+
const target = field
|
|
176
277
|
pending.push(
|
|
177
278
|
getDefaultValue({
|
|
178
|
-
defaultValue:
|
|
279
|
+
defaultValue: common.defaultValue,
|
|
179
280
|
locale: req.locale ?? undefined,
|
|
180
281
|
req,
|
|
181
282
|
user: req.user,
|
|
182
283
|
})
|
|
183
284
|
.then((v) => {
|
|
184
|
-
if (isJsonSafe(v))
|
|
285
|
+
if (isJsonSafe(v)) target.defaultValue = v
|
|
185
286
|
})
|
|
186
287
|
.catch(() => undefined),
|
|
187
288
|
)
|
|
188
289
|
}
|
|
189
|
-
} else if (isJsonSafe(
|
|
190
|
-
field.defaultValue =
|
|
191
|
-
}
|
|
192
|
-
const description = label(anyField.admin?.description, '', ctx)
|
|
193
|
-
if (description) field.description = description
|
|
194
|
-
if (Array.isArray(anyField.options)) {
|
|
195
|
-
field.options = anyField.options.map((o: unknown) =>
|
|
196
|
-
typeof o === 'string'
|
|
197
|
-
? { label: o, value: o }
|
|
198
|
-
: {
|
|
199
|
-
label: label(
|
|
200
|
-
(o as { label?: unknown }).label,
|
|
201
|
-
String((o as { value: unknown }).value),
|
|
202
|
-
ctx,
|
|
203
|
-
),
|
|
204
|
-
value: String((o as { value: unknown }).value),
|
|
205
|
-
},
|
|
206
|
-
)
|
|
207
|
-
}
|
|
208
|
-
if (anyField.relationTo) field.relationTo = anyField.relationTo
|
|
209
|
-
for (const k of ['minRows', 'maxRows', 'min', 'max'] as const) {
|
|
210
|
-
const n = anyField[k]
|
|
211
|
-
if (typeof n === 'number') field[k] = n
|
|
212
|
-
}
|
|
213
|
-
const editor = anyField.admin?.custom?.onpage?.editor ?? anyField.custom?.onpage?.editor
|
|
214
|
-
if (typeof editor === 'string') field.editor = editor
|
|
215
|
-
const picker = anyField.admin?.date?.pickerAppearance
|
|
216
|
-
if (typeof picker === 'string') field.pickerAppearance = picker
|
|
217
|
-
const childCtx = { ...ctx, permissions: perms }
|
|
218
|
-
if (f.type === 'group' || f.type === 'array') field.fields = serializeFields(f.fields, childCtx)
|
|
219
|
-
if (f.type === 'array') {
|
|
220
|
-
const fallback = formatLabels(f.name)
|
|
221
|
-
field.labels = {
|
|
222
|
-
singular: label(anyField.labels?.singular, fallback.singular, ctx),
|
|
223
|
-
plural: label(anyField.labels?.plural, fallback.plural, ctx),
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
if (f.type === 'blocks') {
|
|
227
|
-
const refs = (f.blockReferences ?? [])
|
|
228
|
-
.map((b) => (typeof b === 'string' ? ctx.blocks?.find((x) => x.slug === b) : b))
|
|
229
|
-
.filter((b): b is Block => !!b)
|
|
230
|
-
field.blocks = [...(f.blocks ?? []), ...refs].map((b) =>
|
|
231
|
-
serializeBlock(b, blockPermissions(ctx.permissions, f.name, b.slug), ctx),
|
|
232
|
-
)
|
|
290
|
+
} else if (isJsonSafe(common.defaultValue)) {
|
|
291
|
+
field.defaultValue = common.defaultValue
|
|
233
292
|
}
|
|
234
293
|
out.push(field)
|
|
235
294
|
}
|
|
@@ -239,11 +298,10 @@ export function serializeFields(
|
|
|
239
298
|
/** Every collection a relationship or upload field in `fields` points to. */
|
|
240
299
|
export function relationTargets(fields: SchemaField[], into = new Set<string>()): Set<string> {
|
|
241
300
|
for (const f of fields) {
|
|
242
|
-
if (
|
|
301
|
+
if (f.type === 'relationship' || f.type === 'upload')
|
|
243
302
|
for (const slug of Array.isArray(f.relationTo) ? f.relationTo : [f.relationTo]) into.add(slug)
|
|
244
|
-
|
|
245
|
-
if (f.
|
|
246
|
-
for (const b of f.blocks ?? []) relationTargets(b.fields, into)
|
|
303
|
+
if (f.type === 'group' || f.type === 'array') relationTargets(f.fields, into)
|
|
304
|
+
if (f.type === 'blocks') for (const b of f.blocks) relationTargets(b.fields, into)
|
|
247
305
|
}
|
|
248
306
|
return into
|
|
249
307
|
}
|
package/src/translations.ts
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
|
+
import type { NestedKeysStripped } from '@payloadcms/translations'
|
|
1
2
|
/**
|
|
2
3
|
* Admin UI strings, registered with Payload's i18n under the `onpage` namespace
|
|
3
4
|
* (so the admin language applies). Add or override languages with
|
|
4
5
|
* `i18n.translations` in your Payload config.
|
|
5
6
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
linkFailed: "Couldn't create a preview link: {{message}}",
|
|
14
|
-
},
|
|
7
|
+
const en = {
|
|
8
|
+
onpage: {
|
|
9
|
+
editOnPage: 'Edit on page',
|
|
10
|
+
copyPreviewLink: 'Copy preview link',
|
|
11
|
+
copyPreviewLinkHint: 'Copy a read-only link to this draft',
|
|
12
|
+
linkCopied: 'Preview link copied — valid until {{date}}',
|
|
13
|
+
linkFailed: "Couldn't create a preview link: {{message}}",
|
|
15
14
|
},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Every language must translate every key: a missing or extra one is a compile error. */
|
|
18
|
+
type Table = { onpage: Record<keyof (typeof en)['onpage'], string> }
|
|
19
|
+
|
|
20
|
+
export const translations = {
|
|
21
|
+
en,
|
|
16
22
|
es: {
|
|
17
23
|
onpage: {
|
|
18
24
|
editOnPage: 'Editar en la página',
|
|
@@ -40,6 +46,6 @@ export const translations = {
|
|
|
40
46
|
linkFailed: 'Vorschaulink konnte nicht erstellt werden: {{message}}',
|
|
41
47
|
},
|
|
42
48
|
},
|
|
43
|
-
}
|
|
49
|
+
} satisfies Record<string, Table>
|
|
44
50
|
|
|
45
|
-
export type OnPageTranslationKey =
|
|
51
|
+
export type OnPageTranslationKey = NestedKeysStripped<typeof en>
|