@payload-onpage/react 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/OnPageClient.d.ts.map +1 -1
- package/dist/OnPageClient.js +9 -5
- package/dist/OnPageClient.js.map +1 -1
- package/dist/annotations.d.ts +1 -1
- package/dist/annotations.d.ts.map +1 -1
- package/dist/context.d.ts +19 -5
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +31 -7
- package/dist/context.js.map +1 -1
- package/dist/dom.d.ts +16 -12
- package/dist/dom.d.ts.map +1 -1
- package/dist/dom.js +16 -16
- package/dist/dom.js.map +1 -1
- package/dist/editor/FieldHost.d.ts.map +1 -1
- package/dist/editor/FieldHost.js +18 -11
- package/dist/editor/FieldHost.js.map +1 -1
- package/dist/editor/OnPageEditor.d.ts +15 -8
- package/dist/editor/OnPageEditor.d.ts.map +1 -1
- package/dist/editor/OnPageEditor.js +73 -103
- package/dist/editor/OnPageEditor.js.map +1 -1
- package/dist/editor/editTarget.d.ts +28 -0
- package/dist/editor/editTarget.d.ts.map +1 -0
- package/dist/editor/editTarget.js +20 -0
- package/dist/editor/editTarget.js.map +1 -0
- package/dist/editor/useClientDocs.d.ts +4 -0
- package/dist/editor/useClientDocs.d.ts.map +1 -0
- package/dist/editor/useClientDocs.js +16 -0
- package/dist/editor/useClientDocs.js.map +1 -0
- package/dist/editor/useEditorSession.d.ts +13 -0
- package/dist/editor/useEditorSession.d.ts.map +1 -0
- package/dist/editor/useEditorSession.js +30 -0
- package/dist/editor/useEditorSession.js.map +1 -0
- package/dist/editor/useFlash.d.ts +4 -0
- package/dist/editor/useFlash.d.ts.map +1 -0
- package/dist/editor/useFlash.js +16 -0
- package/dist/editor/useFlash.js.map +1 -0
- package/dist/editor/useGhosts.d.ts +5 -7
- package/dist/editor/useGhosts.d.ts.map +1 -1
- package/dist/editor/useGhosts.js.map +1 -1
- package/dist/editor/usePageEvents.d.ts.map +1 -1
- package/dist/editor/usePageEvents.js +16 -16
- package/dist/editor/usePageEvents.js.map +1 -1
- package/dist/editor-kit.d.ts +10 -6
- package/dist/editor-kit.d.ts.map +1 -1
- package/dist/editor-kit.js +8 -5
- package/dist/editor-kit.js.map +1 -1
- package/dist/editors/CheckboxEditor.d.ts +6 -1
- package/dist/editors/CheckboxEditor.d.ts.map +1 -1
- package/dist/editors/CheckboxEditor.js +6 -3
- package/dist/editors/CheckboxEditor.js.map +1 -1
- package/dist/editors/ColorEditor.d.ts +3 -1
- package/dist/editors/ColorEditor.d.ts.map +1 -1
- package/dist/editors/ColorEditor.js +9 -15
- package/dist/editors/ColorEditor.js.map +1 -1
- package/dist/editors/DateEditor.d.ts +3 -1
- package/dist/editors/DateEditor.d.ts.map +1 -1
- package/dist/editors/DateEditor.js +10 -12
- package/dist/editors/DateEditor.js.map +1 -1
- package/dist/editors/InlineText.d.ts +2 -2
- package/dist/editors/InlineText.d.ts.map +1 -1
- package/dist/editors/InlineText.js +2 -3
- package/dist/editors/InlineText.js.map +1 -1
- package/dist/editors/NumberEditor.d.ts +3 -1
- package/dist/editors/NumberEditor.d.ts.map +1 -1
- package/dist/editors/NumberEditor.js +9 -17
- package/dist/editors/NumberEditor.js.map +1 -1
- package/dist/editors/RelationshipEditor.d.ts +7 -1
- package/dist/editors/RelationshipEditor.d.ts.map +1 -1
- package/dist/editors/RelationshipEditor.js +10 -18
- package/dist/editors/RelationshipEditor.js.map +1 -1
- package/dist/editors/SelectEditor.d.ts +4 -1
- package/dist/editors/SelectEditor.d.ts.map +1 -1
- package/dist/editors/SelectEditor.js +8 -13
- package/dist/editors/SelectEditor.js.map +1 -1
- package/dist/editors/UploadEditor.d.ts +7 -1
- package/dist/editors/UploadEditor.d.ts.map +1 -1
- package/dist/editors/UploadEditor.js +5 -5
- package/dist/editors/UploadEditor.js.map +1 -1
- package/dist/editors/index.d.ts +0 -1
- package/dist/editors/index.d.ts.map +1 -1
- package/dist/editors/index.js +56 -16
- package/dist/editors/index.js.map +1 -1
- package/dist/editors/kit.d.ts +39 -0
- package/dist/editors/kit.d.ts.map +1 -0
- package/dist/editors/kit.js +44 -0
- package/dist/editors/kit.js.map +1 -0
- package/dist/editors/useDebouncedSearch.d.ts +10 -6
- package/dist/editors/useDebouncedSearch.d.ts.map +1 -1
- package/dist/editors/useDebouncedSearch.js +11 -7
- package/dist/editors/useDebouncedSearch.js.map +1 -1
- package/dist/geometry.d.ts +49 -0
- package/dist/geometry.d.ts.map +1 -0
- package/dist/geometry.js +48 -0
- package/dist/geometry.js.map +1 -0
- package/dist/hub.d.ts +4 -4
- package/dist/hub.d.ts.map +1 -1
- package/dist/hub.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +4 -2
- package/dist/layout.js.map +1 -1
- package/dist/locators.d.ts +0 -2
- package/dist/locators.d.ts.map +1 -1
- package/dist/locators.js +10 -16
- package/dist/locators.js.map +1 -1
- package/dist/messages.d.ts +15 -2
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +5 -0
- package/dist/messages.js.map +1 -1
- package/dist/registry.d.ts +21 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +31 -6
- package/dist/registry.js.map +1 -1
- package/dist/rowOps.d.ts +20 -0
- package/dist/rowOps.d.ts.map +1 -0
- package/dist/rowOps.js +48 -0
- package/dist/rowOps.js.map +1 -0
- package/dist/sanitize.d.ts +2 -0
- package/dist/sanitize.d.ts.map +1 -1
- package/dist/sanitize.js +3 -0
- package/dist/sanitize.js.map +1 -1
- package/dist/settings.d.ts +10 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +30 -0
- package/dist/settings.js.map +1 -0
- package/dist/styles.d.ts +1 -5
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +5 -4
- package/dist/styles.js.map +1 -1
- package/dist/targets.d.ts +1 -1
- package/dist/targets.d.ts.map +1 -1
- package/dist/types.d.ts +71 -46
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/BlockPicker.d.ts.map +1 -1
- package/dist/ui/BlockPicker.js +6 -5
- package/dist/ui/BlockPicker.js.map +1 -1
- package/dist/ui/Dialog.d.ts +0 -5
- package/dist/ui/Dialog.d.ts.map +1 -1
- package/dist/ui/Dialog.js +10 -6
- package/dist/ui/Dialog.js.map +1 -1
- package/dist/ui/Dock.d.ts +1 -6
- package/dist/ui/Dock.d.ts.map +1 -1
- package/dist/ui/Dock.js +46 -28
- package/dist/ui/Dock.js.map +1 -1
- package/dist/ui/Overlay.d.ts.map +1 -1
- package/dist/ui/Overlay.js +39 -37
- package/dist/ui/Overlay.js.map +1 -1
- package/dist/ui/PreviewToolbar.d.ts +3 -3
- package/dist/ui/PreviewToolbar.d.ts.map +1 -1
- package/dist/ui/PreviewToolbar.js +4 -6
- package/dist/ui/PreviewToolbar.js.map +1 -1
- package/dist/ui/SettingsPanel.d.ts +0 -9
- package/dist/ui/SettingsPanel.d.ts.map +1 -1
- package/dist/ui/SettingsPanel.js +36 -41
- package/dist/ui/SettingsPanel.js.map +1 -1
- package/dist/ui/ToolbarButton.d.ts +12 -0
- package/dist/ui/ToolbarButton.d.ts.map +1 -0
- package/dist/ui/ToolbarButton.js +7 -0
- package/dist/ui/ToolbarButton.js.map +1 -0
- package/dist/useEvent.d.ts +1 -1
- package/dist/useEvent.d.ts.map +1 -1
- package/dist/useEvent.js +3 -1
- package/dist/useEvent.js.map +1 -1
- package/dist/useIsClient.d.ts +5 -0
- package/dist/useIsClient.d.ts.map +1 -0
- package/dist/useIsClient.js +8 -0
- package/dist/useIsClient.js.map +1 -0
- package/package.json +15 -4
- package/src/OnPageClient.tsx +9 -5
- package/src/annotations.ts +1 -1
- package/src/context.tsx +53 -10
- package/src/dom.ts +17 -18
- package/src/editor/FieldHost.tsx +26 -11
- package/src/editor/OnPageEditor.tsx +103 -160
- package/src/editor/editTarget.ts +41 -0
- package/src/editor/useClientDocs.ts +19 -0
- package/src/editor/useEditorSession.ts +36 -0
- package/src/editor/useFlash.ts +18 -0
- package/src/editor/useGhosts.ts +5 -7
- package/src/editor/usePageEvents.ts +13 -14
- package/src/editor-kit.ts +10 -6
- package/src/editors/CheckboxEditor.tsx +11 -12
- package/src/editors/ColorEditor.tsx +15 -35
- package/src/editors/DateEditor.tsx +18 -37
- package/src/editors/InlineText.tsx +4 -5
- package/src/editors/NumberEditor.tsx +15 -36
- package/src/editors/RelationshipEditor.tsx +18 -36
- package/src/editors/SelectEditor.tsx +14 -22
- package/src/editors/UploadEditor.tsx +6 -6
- package/src/editors/index.ts +56 -16
- package/src/editors/kit.tsx +97 -0
- package/src/editors/useDebouncedSearch.ts +18 -10
- package/src/geometry.ts +62 -0
- package/src/hub.ts +5 -5
- package/src/index.ts +5 -2
- package/src/layout.ts +5 -2
- package/src/locators.ts +11 -14
- package/src/messages.ts +26 -5
- package/src/registry.ts +42 -8
- package/src/rowOps.ts +64 -0
- package/src/sanitize.ts +6 -0
- package/src/settings.ts +49 -0
- package/src/styles.ts +5 -4
- package/src/targets.ts +1 -1
- package/src/types.ts +78 -45
- package/src/ui/BlockPicker.tsx +6 -7
- package/src/ui/Dialog.tsx +9 -5
- package/src/ui/Dock.tsx +67 -50
- package/src/ui/Overlay.tsx +75 -102
- package/src/ui/PreviewToolbar.tsx +7 -15
- package/src/ui/SettingsPanel.tsx +84 -81
- package/src/ui/ToolbarButton.tsx +36 -0
- package/src/useEvent.ts +4 -2
- package/src/useIsClient.ts +15 -0
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import type { ScalarField } from '@payload-onpage/core'
|
|
3
4
|
import type { FieldEditorProps } from '../types.ts'
|
|
4
|
-
import {
|
|
5
|
+
import { EditorPopover } from './kit.tsx'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
/** A checkbox value (unset falls back to the field's default). */
|
|
8
|
+
export const checkedOf = (raw: unknown, field: { defaultValue?: unknown }): boolean =>
|
|
9
|
+
Boolean(raw ?? field.defaultValue)
|
|
10
|
+
|
|
11
|
+
export function CheckboxEditor(props: FieldEditorProps<ScalarField, boolean>) {
|
|
12
|
+
const { field, value, commit, close } = props
|
|
7
13
|
return (
|
|
8
|
-
<
|
|
9
|
-
variant="popover"
|
|
10
|
-
anchor={element}
|
|
11
|
-
title={field.label}
|
|
12
|
-
onClose={close}
|
|
13
|
-
t={t}
|
|
14
|
-
testId="onpage-checkbox"
|
|
15
|
-
>
|
|
14
|
+
<EditorPopover props={props} testId="onpage-checkbox">
|
|
16
15
|
<label className="po-check">
|
|
17
16
|
<input
|
|
18
17
|
type="checkbox"
|
|
19
18
|
data-autofocus
|
|
20
|
-
checked={
|
|
19
|
+
checked={value}
|
|
21
20
|
onChange={(e) => {
|
|
22
21
|
commit(e.target.checked)
|
|
23
22
|
close()
|
|
@@ -26,6 +25,6 @@ export function CheckboxEditor({ field, value, element, commit, close, t }: Fiel
|
|
|
26
25
|
/>
|
|
27
26
|
{field.label}
|
|
28
27
|
</label>
|
|
29
|
-
</
|
|
28
|
+
</EditorPopover>
|
|
30
29
|
)
|
|
31
30
|
}
|
|
@@ -1,41 +1,23 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { ScalarField } from '@payload-onpage/core'
|
|
4
4
|
import type { FieldEditorProps } from '../types.ts'
|
|
5
|
-
import {
|
|
5
|
+
import { ApplyBar, EditorPopover, useDraft } from './kit.tsx'
|
|
6
6
|
|
|
7
7
|
const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i
|
|
8
8
|
|
|
9
|
+
export const toColor = (raw: unknown): string | null =>
|
|
10
|
+
typeof raw === 'string' && raw ? raw : null
|
|
11
|
+
|
|
9
12
|
/** Hex color with a native picker. Use it from a text field: `admin.custom.onpage.editor: 'color'`. */
|
|
10
|
-
export function ColorEditor({
|
|
11
|
-
field,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
close,
|
|
17
|
-
live,
|
|
18
|
-
t,
|
|
19
|
-
}: FieldEditorProps) {
|
|
20
|
-
const [draft, setDraft] = useState(typeof value === 'string' ? value : '')
|
|
21
|
-
const change = (v: string) => {
|
|
22
|
-
setDraft(v)
|
|
23
|
-
if (live && HEX.test(v)) preview(v)
|
|
24
|
-
}
|
|
25
|
-
const save = () => {
|
|
26
|
-
if (draft !== (value ?? '') && (draft === '' || HEX.test(draft))) commit(draft || null)
|
|
27
|
-
close()
|
|
28
|
-
}
|
|
13
|
+
export function ColorEditor(props: FieldEditorProps<ScalarField, string | null>) {
|
|
14
|
+
const { field, t } = props
|
|
15
|
+
const { draft, change, save, onKeyDown } = useDraft(props, {
|
|
16
|
+
format: (v) => v ?? '',
|
|
17
|
+
parse: (d) => (d === '' ? null : HEX.test(d) ? d : undefined),
|
|
18
|
+
})
|
|
29
19
|
return (
|
|
30
|
-
<
|
|
31
|
-
variant="popover"
|
|
32
|
-
anchor={element}
|
|
33
|
-
title={field.label}
|
|
34
|
-
onClose={close}
|
|
35
|
-
t={t}
|
|
36
|
-
width={260}
|
|
37
|
-
testId="onpage-color"
|
|
38
|
-
>
|
|
20
|
+
<EditorPopover props={props} testId="onpage-color" width={260}>
|
|
39
21
|
<div className="po-row po-inline-row">
|
|
40
22
|
<input
|
|
41
23
|
type="color"
|
|
@@ -52,13 +34,11 @@ export function ColorEditor({
|
|
|
52
34
|
placeholder="#1e40af"
|
|
53
35
|
aria-invalid={draft !== '' && !HEX.test(draft)}
|
|
54
36
|
onChange={(e) => change(e.target.value)}
|
|
55
|
-
onKeyDown={
|
|
37
|
+
onKeyDown={onKeyDown}
|
|
56
38
|
data-testid="onpage-color-input"
|
|
57
39
|
/>
|
|
58
40
|
</div>
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
</button>
|
|
62
|
-
</Dialog>
|
|
41
|
+
<ApplyBar onApply={save} t={t} />
|
|
42
|
+
</EditorPopover>
|
|
63
43
|
)
|
|
64
44
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { DateField } from '@payload-onpage/core'
|
|
4
4
|
import type { FieldEditorProps } from '../types.ts'
|
|
5
|
-
import {
|
|
5
|
+
import { ApplyBar, EditorPopover, useDraft } from './kit.tsx'
|
|
6
6
|
|
|
7
7
|
/** Payload stores dates as ISO strings; the input works in local time. */
|
|
8
|
-
const toLocalInput = (iso:
|
|
9
|
-
if (
|
|
8
|
+
const toLocalInput = (iso: string | null, withTime: boolean): string => {
|
|
9
|
+
if (!iso) return ''
|
|
10
10
|
const d = new Date(iso)
|
|
11
11
|
if (Number.isNaN(d.getTime())) return ''
|
|
12
12
|
const pad = (n: number) => String(n).padStart(2, '0')
|
|
@@ -14,24 +14,19 @@ const toLocalInput = (iso: unknown, withTime: boolean): string => {
|
|
|
14
14
|
return withTime ? `${date}T${pad(d.getHours())}:${pad(d.getMinutes())}` : date
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export
|
|
17
|
+
export const toIsoDate = (raw: unknown): string | null =>
|
|
18
|
+
typeof raw === 'string' && raw ? raw : null
|
|
19
|
+
|
|
20
|
+
export function DateEditor(props: FieldEditorProps<DateField, string | null>) {
|
|
21
|
+
const { field, t } = props
|
|
18
22
|
const withTime = field.pickerAppearance === 'dayAndTime' || field.pickerAppearance === 'timeOnly'
|
|
19
|
-
const
|
|
20
|
-
|
|
23
|
+
const { draft, change, save, onKeyDown, clear } = useDraft(props, {
|
|
24
|
+
format: (v) => toLocalInput(v, withTime),
|
|
21
25
|
// Date-only values are stored at local noon, which survives any timezone shift (as Payload's picker does).
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
close()
|
|
25
|
-
}
|
|
26
|
+
parse: (d) => (d ? new Date(withTime ? d : `${d}T12:00:00`).toISOString() : null),
|
|
27
|
+
})
|
|
26
28
|
return (
|
|
27
|
-
<
|
|
28
|
-
variant="popover"
|
|
29
|
-
anchor={element}
|
|
30
|
-
title={field.label}
|
|
31
|
-
onClose={close}
|
|
32
|
-
t={t}
|
|
33
|
-
testId="onpage-date"
|
|
34
|
-
>
|
|
29
|
+
<EditorPopover props={props} testId="onpage-date">
|
|
35
30
|
<div className="po-row">
|
|
36
31
|
<input
|
|
37
32
|
className="po-input"
|
|
@@ -39,26 +34,12 @@ export function DateEditor({ field, value, element, commit, close, t }: FieldEdi
|
|
|
39
34
|
aria-label={field.label}
|
|
40
35
|
data-autofocus
|
|
41
36
|
value={draft}
|
|
42
|
-
onChange={(e) =>
|
|
43
|
-
onKeyDown={
|
|
37
|
+
onChange={(e) => change(e.target.value)}
|
|
38
|
+
onKeyDown={onKeyDown}
|
|
44
39
|
data-testid="onpage-date-input"
|
|
45
40
|
/>
|
|
46
41
|
</div>
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
type="button"
|
|
50
|
-
className="po-btn po-primary"
|
|
51
|
-
onClick={save}
|
|
52
|
-
data-testid="onpage-apply"
|
|
53
|
-
>
|
|
54
|
-
{t('common.apply')}
|
|
55
|
-
</button>
|
|
56
|
-
{!field.required && (
|
|
57
|
-
<button type="button" className="po-btn" onClick={() => setDraft('')}>
|
|
58
|
-
{t('common.clear')}
|
|
59
|
-
</button>
|
|
60
|
-
)}
|
|
61
|
-
</div>
|
|
62
|
-
</Dialog>
|
|
42
|
+
<ApplyBar onApply={save} onClear={field.required ? undefined : clear} t={t} />
|
|
43
|
+
</EditorPopover>
|
|
63
44
|
)
|
|
64
45
|
}
|
|
@@ -11,13 +11,12 @@
|
|
|
11
11
|
* cancels.
|
|
12
12
|
*/
|
|
13
13
|
import { useLayoutEffect, useRef } from 'react'
|
|
14
|
-
import {
|
|
14
|
+
import type { ScalarField } from '@payload-onpage/core'
|
|
15
15
|
import { copyTypography, HIDDEN, placeCaret, placeOver, typographyOf } from '../dom.ts'
|
|
16
16
|
import { useTrackElement } from '../layout.ts'
|
|
17
17
|
import type { FieldEditorProps } from '../types.ts'
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
s.replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]!)
|
|
19
|
+
import { escapeHtml } from '../sanitize.ts'
|
|
21
20
|
|
|
22
21
|
export function InlineText({
|
|
23
22
|
field,
|
|
@@ -30,10 +29,10 @@ export function InlineText({
|
|
|
30
29
|
live,
|
|
31
30
|
point,
|
|
32
31
|
t,
|
|
33
|
-
}: FieldEditorProps) {
|
|
32
|
+
}: FieldEditorProps<ScalarField, string>) {
|
|
34
33
|
const multiline = field.type === 'textarea'
|
|
35
34
|
const ref = useRef<HTMLDivElement>(null)
|
|
36
|
-
const initial = useRef(
|
|
35
|
+
const initial = useRef(value)
|
|
37
36
|
const done = useRef(false)
|
|
38
37
|
|
|
39
38
|
useLayoutEffect(() => {
|
|
@@ -1,41 +1,22 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { useState } from 'react'
|
|
3
|
+
import type { NumberField } from '@payload-onpage/core'
|
|
5
4
|
import type { FieldEditorProps } from '../types.ts'
|
|
6
|
-
import {
|
|
5
|
+
import { ApplyBar, EditorPopover, useDraft } from './kit.tsx'
|
|
6
|
+
|
|
7
|
+
export const toNumber = (raw: unknown): number | null =>
|
|
8
|
+
typeof raw === 'number' && Number.isFinite(raw) ? raw : null
|
|
7
9
|
|
|
8
10
|
/** Number input, with a slider when the field has min and max. */
|
|
9
|
-
export function NumberEditor({
|
|
10
|
-
field,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
close,
|
|
16
|
-
live,
|
|
17
|
-
t,
|
|
18
|
-
}: FieldEditorProps) {
|
|
19
|
-
const [draft, setDraft] = useState(toText(value))
|
|
11
|
+
export function NumberEditor(props: FieldEditorProps<NumberField, number | null>) {
|
|
12
|
+
const { field, t } = props
|
|
13
|
+
const { draft, change, save, onKeyDown } = useDraft(props, {
|
|
14
|
+
format: (v) => (v === null ? '' : String(v)),
|
|
15
|
+
parse: (d) => (d === '' ? null : Number.isNaN(Number(d)) ? undefined : Number(d)),
|
|
16
|
+
})
|
|
20
17
|
const slider = field.min !== undefined && field.max !== undefined
|
|
21
|
-
const save = () => {
|
|
22
|
-
const next = draft === '' ? null : Number(draft)
|
|
23
|
-
if (next !== value && (next === null || !Number.isNaN(next))) commit(next)
|
|
24
|
-
close()
|
|
25
|
-
}
|
|
26
|
-
const change = (v: string) => {
|
|
27
|
-
setDraft(v)
|
|
28
|
-
if (live && v !== '' && !Number.isNaN(Number(v))) preview(Number(v))
|
|
29
|
-
}
|
|
30
18
|
return (
|
|
31
|
-
<
|
|
32
|
-
variant="popover"
|
|
33
|
-
anchor={element}
|
|
34
|
-
title={field.label}
|
|
35
|
-
onClose={close}
|
|
36
|
-
t={t}
|
|
37
|
-
testId="onpage-number"
|
|
38
|
-
>
|
|
19
|
+
<EditorPopover props={props} testId="onpage-number">
|
|
39
20
|
<div className="po-row">
|
|
40
21
|
{slider && (
|
|
41
22
|
<input
|
|
@@ -57,13 +38,11 @@ export function NumberEditor({
|
|
|
57
38
|
max={field.max}
|
|
58
39
|
value={draft}
|
|
59
40
|
onChange={(e) => change(e.target.value)}
|
|
60
|
-
onKeyDown={
|
|
41
|
+
onKeyDown={onKeyDown}
|
|
61
42
|
data-testid="onpage-number-input"
|
|
62
43
|
/>
|
|
63
44
|
</div>
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
</button>
|
|
67
|
-
</Dialog>
|
|
45
|
+
<ApplyBar onApply={save} t={t} />
|
|
46
|
+
</EditorPopover>
|
|
68
47
|
)
|
|
69
48
|
}
|
|
@@ -5,43 +5,32 @@
|
|
|
5
5
|
* (Payload REST, so access control applies) and pick one, or several for
|
|
6
6
|
* hasMany. Polymorphic fields get a collection switch and store `{ relationTo, value }`.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
asList,
|
|
10
|
+
relationId,
|
|
11
|
+
relationTitle,
|
|
12
|
+
type RelationField,
|
|
13
|
+
type SearchHit,
|
|
14
|
+
} from '@payload-onpage/core'
|
|
9
15
|
import { useCallback, useState } from 'react'
|
|
10
16
|
import { useOnPage } from '../context.tsx'
|
|
11
17
|
import type { FieldEditorProps } from '../types.ts'
|
|
12
|
-
import {
|
|
18
|
+
import { EditorPopover } from './kit.tsx'
|
|
13
19
|
import { useDebouncedSearch } from './useDebouncedSearch.ts'
|
|
14
20
|
|
|
15
|
-
export function RelationshipEditor({
|
|
16
|
-
docKey,
|
|
17
|
-
field,
|
|
18
|
-
value,
|
|
19
|
-
element,
|
|
20
|
-
commit,
|
|
21
|
-
close,
|
|
22
|
-
t,
|
|
23
|
-
}: FieldEditorProps) {
|
|
21
|
+
export function RelationshipEditor(props: FieldEditorProps<RelationField>) {
|
|
22
|
+
const { docKey, field, value, commit, close, t } = props
|
|
24
23
|
const { session, snap } = useOnPage()
|
|
25
|
-
const targets = Array.isArray(field.relationTo)
|
|
26
|
-
? field.relationTo
|
|
27
|
-
: field.relationTo
|
|
28
|
-
? [field.relationTo]
|
|
29
|
-
: []
|
|
24
|
+
const targets = Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo]
|
|
30
25
|
const poly = Array.isArray(field.relationTo)
|
|
31
26
|
const [collection, setCollection] = useState(targets[0] ?? '')
|
|
32
27
|
const relations = snap.docs[docKey]?.schema?.relations ?? {}
|
|
33
28
|
const titleField = relations[collection]?.titleField
|
|
34
29
|
const many = !!field.hasMany
|
|
35
|
-
const current
|
|
36
|
-
? Array.isArray(value)
|
|
37
|
-
? value
|
|
38
|
-
: []
|
|
39
|
-
: value == null
|
|
40
|
-
? []
|
|
41
|
-
: [value]
|
|
30
|
+
const current = asList(value, many)
|
|
42
31
|
const ds = session.dataSource
|
|
43
32
|
const search = useCallback(
|
|
44
|
-
async (query: string) =>
|
|
33
|
+
async (query: string, _page: number, signal: AbortSignal) =>
|
|
45
34
|
ds.search
|
|
46
35
|
? {
|
|
47
36
|
items: await ds.search(collection, {
|
|
@@ -49,13 +38,14 @@ export function RelationshipEditor({
|
|
|
49
38
|
titleField,
|
|
50
39
|
locale: session.locale,
|
|
51
40
|
limit: 12,
|
|
41
|
+
signal,
|
|
52
42
|
}),
|
|
53
43
|
hasMore: false,
|
|
54
44
|
}
|
|
55
45
|
: { items: [] as SearchHit[], hasMore: false },
|
|
56
46
|
[ds, collection, titleField, session.locale],
|
|
57
47
|
)
|
|
58
|
-
const results = useDebouncedSearch<SearchHit>(ds.search ? search : null
|
|
48
|
+
const results = useDebouncedSearch<SearchHit>(ds.search ? search : null)
|
|
59
49
|
|
|
60
50
|
const wrap = (hit: SearchHit): unknown =>
|
|
61
51
|
poly ? { relationTo: collection, value: hit.doc } : hit.doc
|
|
@@ -75,15 +65,7 @@ export function RelationshipEditor({
|
|
|
75
65
|
const remove = (i: number) => commit(many ? current.filter((_, j) => j !== i) : null)
|
|
76
66
|
|
|
77
67
|
return (
|
|
78
|
-
<
|
|
79
|
-
variant="popover"
|
|
80
|
-
anchor={element}
|
|
81
|
-
title={field.label}
|
|
82
|
-
onClose={close}
|
|
83
|
-
t={t}
|
|
84
|
-
width={320}
|
|
85
|
-
testId="onpage-relationship"
|
|
86
|
-
>
|
|
68
|
+
<EditorPopover props={props} testId="onpage-relationship" width={320}>
|
|
87
69
|
{current.map((v, i) => (
|
|
88
70
|
<div className="po-rel-item" key={`${String(relationId(v))}-${i}`}>
|
|
89
71
|
{relationTitle(v, titleField)}
|
|
@@ -91,7 +73,7 @@ export function RelationshipEditor({
|
|
|
91
73
|
<button
|
|
92
74
|
type="button"
|
|
93
75
|
onClick={() => remove(i)}
|
|
94
|
-
aria-label={
|
|
76
|
+
aria-label={t('common.removeItem', { item: relationTitle(v, titleField) })}
|
|
95
77
|
>
|
|
96
78
|
✕
|
|
97
79
|
</button>
|
|
@@ -148,6 +130,6 @@ export function RelationshipEditor({
|
|
|
148
130
|
</div>
|
|
149
131
|
</>
|
|
150
132
|
)}
|
|
151
|
-
</
|
|
133
|
+
</EditorPopover>
|
|
152
134
|
)
|
|
153
135
|
}
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { asList, type ChoiceField } from '@payload-onpage/core'
|
|
4
4
|
import type { FieldEditorProps } from '../types.ts'
|
|
5
|
-
import {
|
|
5
|
+
import { EditorPopover } from './kit.tsx'
|
|
6
|
+
|
|
7
|
+
/** A select/radio value as the selected option values. */
|
|
8
|
+
export const selectedValues = (raw: unknown, field: ChoiceField): string[] =>
|
|
9
|
+
asList(raw, field.hasMany, (v) =>
|
|
10
|
+
typeof v === 'string' || typeof v === 'number' ? String(v) : undefined,
|
|
11
|
+
)
|
|
6
12
|
|
|
7
13
|
/** Select / radio: pick one (closes) or toggle several (hasMany). */
|
|
8
|
-
export function SelectEditor(
|
|
9
|
-
const
|
|
14
|
+
export function SelectEditor(props: FieldEditorProps<ChoiceField, string[]>) {
|
|
15
|
+
const { field, value: current, commit, close, t } = props
|
|
10
16
|
const many = !!field.hasMany
|
|
11
|
-
const current: string[] = many
|
|
12
|
-
? Array.isArray(value)
|
|
13
|
-
? (value as string[])
|
|
14
|
-
: []
|
|
15
|
-
: value == null
|
|
16
|
-
? []
|
|
17
|
-
: [toText(value)]
|
|
18
17
|
const pick = (v: string | null) => {
|
|
19
18
|
if (!many) {
|
|
20
19
|
commit(v)
|
|
@@ -25,17 +24,10 @@ export function SelectEditor({ field, value, element, commit, close, t }: FieldE
|
|
|
25
24
|
if (v === null) set.clear()
|
|
26
25
|
else if (set.has(v)) set.delete(v)
|
|
27
26
|
else set.add(v)
|
|
28
|
-
commit(options.map((o) => o.value).filter((o) => set.has(o)))
|
|
27
|
+
commit(field.options.map((o) => o.value).filter((o) => set.has(o)))
|
|
29
28
|
}
|
|
30
29
|
return (
|
|
31
|
-
<
|
|
32
|
-
variant="popover"
|
|
33
|
-
anchor={element}
|
|
34
|
-
title={field.label}
|
|
35
|
-
onClose={close}
|
|
36
|
-
t={t}
|
|
37
|
-
testId="onpage-select"
|
|
38
|
-
>
|
|
30
|
+
<EditorPopover props={props} testId="onpage-select">
|
|
39
31
|
<div
|
|
40
32
|
className="po-pop-list"
|
|
41
33
|
role="listbox"
|
|
@@ -52,7 +44,7 @@ export function SelectEditor({ field, value, element, commit, close, t }: FieldE
|
|
|
52
44
|
{t('common.none')}
|
|
53
45
|
</button>
|
|
54
46
|
)}
|
|
55
|
-
{options.map((o) => {
|
|
47
|
+
{field.options.map((o) => {
|
|
56
48
|
const selected = current.includes(o.value)
|
|
57
49
|
return (
|
|
58
50
|
<button
|
|
@@ -69,6 +61,6 @@ export function SelectEditor({ field, value, element, commit, close, t }: FieldE
|
|
|
69
61
|
)
|
|
70
62
|
})}
|
|
71
63
|
</div>
|
|
72
|
-
</
|
|
64
|
+
</EditorPopover>
|
|
73
65
|
)
|
|
74
66
|
}
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
* new file, through the MediaLibrary port. The chosen document goes into state
|
|
6
6
|
* populated, so a client-rendered page shows it at once; saving sends its id.
|
|
7
7
|
*/
|
|
8
|
-
import { relationId, type MediaItem } from '@payload-onpage/core'
|
|
8
|
+
import { errorText, relationId, type MediaItem, type RelationField } from '@payload-onpage/core'
|
|
9
9
|
import { useCallback, useRef, useState } from 'react'
|
|
10
10
|
import { useOnPage } from '../context.tsx'
|
|
11
11
|
import type { FieldEditorProps } from '../types.ts'
|
|
12
12
|
import { Dialog } from '../ui/Dialog.tsx'
|
|
13
13
|
import { useDebouncedSearch } from './useDebouncedSearch.ts'
|
|
14
14
|
|
|
15
|
-
export function UploadEditor({ field, value, commit, close, t }: FieldEditorProps) {
|
|
15
|
+
export function UploadEditor({ field, value, commit, close, t }: FieldEditorProps<RelationField>) {
|
|
16
16
|
const { media } = useOnPage()
|
|
17
17
|
const relationTo =
|
|
18
18
|
(Array.isArray(field.relationTo) ? field.relationTo[0] : field.relationTo) ?? 'media'
|
|
@@ -20,13 +20,13 @@ export function UploadEditor({ field, value, commit, close, t }: FieldEditorProp
|
|
|
20
20
|
const [uploadError, setUploadError] = useState<string | null>(null)
|
|
21
21
|
const fileRef = useRef<HTMLInputElement>(null)
|
|
22
22
|
const search = useCallback(
|
|
23
|
-
(query: string, page: number) =>
|
|
23
|
+
(query: string, page: number, signal: AbortSignal) =>
|
|
24
24
|
media
|
|
25
|
-
? media.search({ query, page, relationTo })
|
|
25
|
+
? media.search({ query, page, relationTo, signal })
|
|
26
26
|
: Promise.resolve({ items: [] as MediaItem[], hasMore: false }),
|
|
27
27
|
[media, relationTo],
|
|
28
28
|
)
|
|
29
|
-
const results = useDebouncedSearch<MediaItem>(media ? search : null
|
|
29
|
+
const results = useDebouncedSearch<MediaItem>(media ? search : null)
|
|
30
30
|
const currentId = relationId(value)
|
|
31
31
|
|
|
32
32
|
const choose = (m: MediaItem) => {
|
|
@@ -39,7 +39,7 @@ export function UploadEditor({ field, value, commit, close, t }: FieldEditorProp
|
|
|
39
39
|
try {
|
|
40
40
|
choose(await media.upload(file, { relationTo }))
|
|
41
41
|
} catch (e) {
|
|
42
|
-
setUploadError((e
|
|
42
|
+
setUploadError(errorText(e))
|
|
43
43
|
} finally {
|
|
44
44
|
setUploading(false)
|
|
45
45
|
}
|
package/src/editors/index.ts
CHANGED
|
@@ -1,26 +1,66 @@
|
|
|
1
1
|
/** The default field editors. Spread, override or remove (`null`) entries via the `editors` option. */
|
|
2
|
+
import { toText } from '@payload-onpage/core'
|
|
3
|
+
import { defineEditor } from '../registry.ts'
|
|
2
4
|
import type { EditorRegistry } from '../types.ts'
|
|
3
|
-
import { CheckboxEditor } from './CheckboxEditor.tsx'
|
|
4
|
-
import { ColorEditor } from './ColorEditor.tsx'
|
|
5
|
-
import { DateEditor } from './DateEditor.tsx'
|
|
5
|
+
import { checkedOf, CheckboxEditor } from './CheckboxEditor.tsx'
|
|
6
|
+
import { ColorEditor, toColor } from './ColorEditor.tsx'
|
|
7
|
+
import { DateEditor, toIsoDate } from './DateEditor.tsx'
|
|
6
8
|
import { InlineText } from './InlineText.tsx'
|
|
7
|
-
import { NumberEditor } from './NumberEditor.tsx'
|
|
9
|
+
import { NumberEditor, toNumber } from './NumberEditor.tsx'
|
|
8
10
|
import { RelationshipEditor } from './RelationshipEditor.tsx'
|
|
9
|
-
import { SelectEditor } from './SelectEditor.tsx'
|
|
11
|
+
import { SelectEditor, selectedValues } from './SelectEditor.tsx'
|
|
10
12
|
import { UploadEditor } from './UploadEditor.tsx'
|
|
11
13
|
|
|
14
|
+
const text = defineEditor({
|
|
15
|
+
mode: 'inline',
|
|
16
|
+
accepts: ['text', 'textarea', 'email'],
|
|
17
|
+
normalize: (raw) => toText(raw),
|
|
18
|
+
component: InlineText,
|
|
19
|
+
})
|
|
20
|
+
const select = defineEditor({
|
|
21
|
+
mode: 'popover',
|
|
22
|
+
accepts: ['select', 'radio'],
|
|
23
|
+
normalize: selectedValues,
|
|
24
|
+
component: SelectEditor,
|
|
25
|
+
})
|
|
26
|
+
|
|
12
27
|
export const defaultEditors: EditorRegistry = {
|
|
13
|
-
text
|
|
14
|
-
textarea:
|
|
15
|
-
email:
|
|
16
|
-
select
|
|
17
|
-
radio:
|
|
18
|
-
checkbox: {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
text,
|
|
29
|
+
textarea: text,
|
|
30
|
+
email: text,
|
|
31
|
+
select,
|
|
32
|
+
radio: select,
|
|
33
|
+
checkbox: defineEditor({
|
|
34
|
+
mode: 'popover',
|
|
35
|
+
accepts: ['checkbox'],
|
|
36
|
+
normalize: checkedOf,
|
|
37
|
+
component: CheckboxEditor,
|
|
38
|
+
}),
|
|
39
|
+
number: defineEditor({
|
|
40
|
+
mode: 'popover',
|
|
41
|
+
accepts: ['number'],
|
|
42
|
+
normalize: toNumber,
|
|
43
|
+
component: NumberEditor,
|
|
44
|
+
}),
|
|
45
|
+
date: defineEditor({
|
|
46
|
+
mode: 'popover',
|
|
47
|
+
accepts: ['date'],
|
|
48
|
+
normalize: toIsoDate,
|
|
49
|
+
component: DateEditor,
|
|
50
|
+
}),
|
|
51
|
+
relationship: defineEditor({
|
|
52
|
+
mode: 'popover',
|
|
53
|
+
accepts: ['relationship'],
|
|
54
|
+
component: RelationshipEditor,
|
|
55
|
+
}),
|
|
56
|
+
upload: defineEditor({ mode: 'popover', accepts: ['upload'], component: UploadEditor }),
|
|
57
|
+
// A text field holding a hex color: `admin.custom.onpage.editor: 'color'`.
|
|
58
|
+
color: defineEditor({
|
|
59
|
+
mode: 'popover',
|
|
60
|
+
accepts: ['text'],
|
|
61
|
+
normalize: toColor,
|
|
62
|
+
component: ColorEditor,
|
|
63
|
+
}),
|
|
24
64
|
}
|
|
25
65
|
|
|
26
66
|
export {
|