chore(next): wires server action into document edit view (#4873)

This commit is contained in:
Jacob Fletcher
2024-01-19 14:51:56 -05:00
committed by GitHub
parent 7bc43b4fe8
commit 75c12e8966
50 changed files with 583 additions and 250 deletions

View File

@@ -6,6 +6,7 @@ const nextConfig = {
},
serverComponentsExternalPackages: ['drizzle-kit', 'drizzle-kit/utils', 'pino', 'pino-pretty'],
},
reactStrictMode: false,
// transpilePackages: ['@payloadcms/db-mongodb', 'mongoose'],
webpack: (config) => {
return {

View File

@@ -47,11 +47,13 @@ export const Pages: CollectionConfig = {
name: 'number',
label: 'Number',
type: 'number',
required: true,
},
{
name: 'select',
label: 'Select',
type: 'select',
required: true,
options: [
{
label: 'Option 1',
@@ -67,6 +69,7 @@ export const Pages: CollectionConfig = {
type: 'textarea',
name: 'textarea',
label: 'Textarea',
required: true,
admin: {
rows: 10,
},
@@ -80,6 +83,7 @@ export const Pages: CollectionConfig = {
name: 'groupText',
label: 'Group Text',
type: 'text',
required: true,
},
],
},

View File

@@ -42,6 +42,7 @@
"@faceless-ui/modal": "2.0.1",
"@payloadcms/translations": "workspace:^",
"@payloadcms/ui": "workspace:*",
"deep-equal": "2.2.2",
"path-to-regexp": "^6.2.1",
"react-diff-viewer-continued": "3.2.6",
"react-toastify": "8.2.0"

View File

@@ -0,0 +1,8 @@
.login__form {
&__inputWrap {
display: flex;
flex-direction: column;
gap: var(--base);
margin-bottom: calc(var(--base) / 4);
}
}

View File

@@ -1,33 +1,34 @@
'use client'
import {
Email,
Form,
FormLoadingOverlayToggle,
FormSubmit,
Password,
useConfig,
useTranslation,
} from '@payloadcms/ui'
import React from 'react'
import { FormLoadingOverlayToggle } from '../../elements/Loading'
import Form from '../../forms/Form'
import FormSubmit from '../../forms/Submit'
import { Email } from '../../forms/field-types/Email'
import { Password } from '../../forms/field-types/Password'
import Link from 'next/link'
import { useTranslation } from '../../providers/Translation'
import { useConfig } from '../../providers/Config'
const baseClass = 'login__form'
import './index.scss'
const baseClass = 'login-form'
import Link from 'next/link'
export const LoginForm: React.FC<{
action?: (formData: FormData) => Promise<void> | string
searchParams: { [key: string]: string | string[] | undefined }
}> = async ({ searchParams }) => {
}> = ({ searchParams }) => {
const config = useConfig()
const {
admin: { autoLogin, user: userSlug },
routes: { admin, api },
} = config
const { t } = useTranslation()
const prefillForm = autoLogin && autoLogin.prefillOnly
const { t } = useTranslation()
return (
<Form
action={`${api}/${userSlug}/login`}
@@ -45,9 +46,9 @@ export const LoginForm: React.FC<{
valid: true,
},
}}
method="POST"
redirect={`${admin}${searchParams?.redirect || ''}`}
waitForAutocomplete
method="POST"
>
<FormLoadingOverlayToggle action="loading" name="login-form" />
<div className={`${baseClass}__inputWrap`}>

View File

@@ -6,6 +6,15 @@
margin-bottom: calc(var(--base) * 2);
}
&__form {
&__inputWrap {
display: flex;
flex-direction: column;
gap: var(--base);
margin-bottom: calc(var(--base) / 4);
}
}
&__wrap {
& > *:first-child {
margin-top: 0;

View File

@@ -1,7 +1,7 @@
import React, { Fragment } from 'react'
import { Logo } from '@payloadcms/ui/graphics'
import { MinimalTemplate, LoginForm } from '@payloadcms/ui'
import { MinimalTemplate } from '@payloadcms/ui'
import type { SanitizedConfig } from 'payload/types'
import { meta } from '../../utilities/meta'
import { Metadata } from 'next'
@@ -9,6 +9,7 @@ import { initPage } from '../../utilities/initPage'
import { redirect } from 'next/navigation'
import { getNextT } from '../../utilities/getNextT'
import './index.scss'
import { LoginForm } from './LoginForm'
const baseClass = 'login'
@@ -36,7 +37,7 @@ export const Login: React.FC<{
const { config, user } = await initPage({ configPromise })
const {
admin: { components: { afterLogin, beforeLogin } = {}, logoutRoute, user: userSlug },
admin: { components: { afterLogin, beforeLogin } = {}, user: userSlug },
routes: { admin },
collections,
} = config

View File

@@ -31,7 +31,8 @@
"src/**/*.tsx",
"src/**/*.d.ts",
"src/**/*.json",
"../ui/src/createClientConfig.ts"
"../ui/src/createClientConfig.ts",
"../dev/src/app/(payload)/admin/login/action.ts"
],
"references": [{ "path": "../payload" }, { "path": "../ui" }, { "path": "../translations" }]
}

View File

@@ -1,5 +1,6 @@
export type ErrorProps = {
alignCaret?: 'center' | 'left' | 'right'
message: string
message?: string
path: string
showError?: boolean
}

View File

@@ -1,5 +1,8 @@
import type { I18n } from '@payloadcms/translations'
export type LabelProps = {
htmlFor?: string
i18n: I18n
label?: JSX.Element | Record<string, string> | false | string
required?: boolean
}

View File

@@ -0,0 +1,3 @@
import { SanitizedConfig } from 'payload/config'
export default {} as Promise<SanitizedConfig>

View File

@@ -1,10 +0,0 @@
@import '../../scss/styles';
.login-form {
&__inputWrap {
display: flex;
flex-direction: column;
gap: base(1);
margin-bottom: base(0.25);
}
}

View File

@@ -1,7 +1,6 @@
export { Card } from '../elements/Card'
export { Button } from '../elements/Button'
export { RenderCustomComponent } from '../elements/RenderCustomComponent'
export { LoginForm } from '../elements/LoginForm'
export { TableColumnsProvider } from '../elements/TableColumns'
export { HydrateClientUser } from '../elements/HydrateClientUser'
export { DocumentHeader } from '../elements/DocumentHeader'

View File

@@ -1,15 +1,30 @@
'use client'
import React from 'react'
import { Tooltip } from '../../elements/Tooltip'
import './index.scss'
import type { ErrorProps } from 'payload/types'
import { useFormFields } from '../Form/context'
import './index.scss'
const baseClass = 'field-error'
const Error: React.FC<ErrorProps> = (props) => {
const { alignCaret = 'right', message, showError = false } = props
const {
alignCaret = 'right',
message: messageFromProps,
path,
showError: showErrorFromProps,
} = props
if (showError) {
const field = useFormFields(([fields]) => fields[path])
const { valid, errorMessage } = field || {}
const message = messageFromProps || errorMessage
const showMessage = showErrorFromProps || !valid
if (showMessage) {
return (
<Tooltip alignCaret={alignCaret} className={baseClass} delay={0}>
{message}

View File

@@ -2,14 +2,14 @@ import React from 'react'
import type { Props } from './types'
// import { getTranslation } from '@payloadcms/translations'
import { getTranslation } from '@payloadcms/translations'
import { isComponent } from './types'
import './index.scss'
const baseClass = 'field-description'
const FieldDescription: React.FC<Props> = (props) => {
const { className, description, marginPlacement, path, value } = props
const { className, description, marginPlacement, path, value, i18n } = props
if (isComponent(description)) {
const Description = description
@@ -27,14 +27,12 @@ const FieldDescription: React.FC<Props> = (props) => {
.filter(Boolean)
.join(' ')}
>
{
typeof description === 'function'
? description({
path,
value,
})
: '' // : getTranslation(description, i18n)
}
{typeof description === 'function'
? description({
path,
value,
})
: getTranslation(description, i18n)}
</div>
)
}

View File

@@ -1,3 +1,4 @@
import { I18n } from '@payloadcms/translations'
import { Description, DescriptionComponent } from 'payload/types'
import React from 'react'
@@ -7,6 +8,7 @@ export type Props = {
marginPlacement?: 'bottom' | 'top'
path?: string
value?: unknown
i18n: I18n
}
export function isComponent(description: Description): description is DescriptionComponent {

View File

@@ -43,6 +43,7 @@ import getDataByPathFunc from './getDataByPath'
import getSiblingDataFunc from './getSiblingData'
import initContextState from './initContextState'
import reduceFieldsToValues from './reduceFieldsToValues'
import useDebounce from '../../hooks/useDebounce'
const baseClass = 'form'
@@ -59,14 +60,16 @@ const Form: React.FC<Props> = (props) => {
// fields: fieldsFromProps = collection?.fields || global?.fields,
handleResponse,
initialState, // fully formed initial field state
method,
onSubmit,
onSuccess,
redirect,
submitted: submittedFromProps,
waitForAutocomplete,
onChange,
} = props
const method = 'method' in props ? props.method : undefined
const { push } = useRouter()
const { code: locale } = useLocale()
@@ -89,6 +92,8 @@ const Form: React.FC<Props> = (props) => {
*/
const [fields, dispatchFields] = fieldsReducer
const debouncedFields = useDebounce(fields, 150)
contextRef.current.fields = fields
contextRef.current.dispatchFields = dispatchFields
@@ -652,6 +657,30 @@ const Form: React.FC<Props> = (props) => {
const classes = [className, baseClass].filter(Boolean).join(' ')
useEffect(() => {
const executeOnChange = async () => {
if (Array.isArray(onChange)) {
let newFormState
await onChange.reduce(async (priorOnChange, onChangeFn) => {
await priorOnChange
const result = await onChangeFn({
formState: debouncedFields,
})
newFormState = result
}, Promise.resolve())
if (!isDeepEqual(debouncedFields, newFormState)) {
dispatchFields({ state: newFormState, type: 'REPLACE_STATE' })
}
}
}
executeOnChange()
}, [debouncedFields])
return (
<form
action={action}

View File

@@ -31,8 +31,15 @@ export type Preferences = {
[key: string]: unknown
}
export type Props = {
action?: string | ((formData: FormData) => Promise<void>)
export type Props = (
| {
action?: string
method?: 'DELETE' | 'GET' | 'PATCH' | 'POST'
}
| {
action: (formData: FormData) => Promise<void>
}
) & {
children?: React.ReactNode
className?: string
disableSuccessStatus?: boolean
@@ -46,13 +53,13 @@ export type Props = {
handleResponse?: (res: Response) => void
initialState?: FormState
log?: boolean
method?: 'DELETE' | 'GET' | 'PATCH' | 'POST'
onSubmit?: (fields: FormState, data: Data) => void
onSuccess?: (json: unknown) => void
redirect?: string
submitted?: boolean
validationOperation?: 'create' | 'update'
waitForAutocomplete?: boolean
onChange?: ((args: { formState: FormState }) => Promise<FormState | void>)[]
}
export type SubmitOptions = {

View File

@@ -1,18 +1,17 @@
import React from 'react'
// import { getTranslation } from '@payloadcms/translations'
import './index.scss'
import { getTranslation } from '@payloadcms/translations'
import { LabelProps } from 'payload/types'
import './index.scss'
const Label: React.FC<LabelProps> = (props) => {
const { htmlFor, label, required = false } = props
// const { i18n } = useTranslation()
const { htmlFor, label, required = false, i18n } = props
if (label) {
return (
<label className="field-label" htmlFor={htmlFor}>
{typeof label === 'string' ? label : ''}
{/* {getTranslation(label, i18n)} */}
{getTranslation(label, i18n)}
{required && <span className="required">*</span>}
</label>
)

View File

@@ -1,10 +1,10 @@
'use client'
import React, { Fragment, useCallback } from 'react'
import useField from '../../useField'
import useField from '../../../useField'
import { Validate } from 'payload/types'
import { Check } from '../../../icons/Check'
import { Line } from '../../../icons/Line'
import { Check } from '../../../../icons/Check'
import { Line } from '../../../../icons/Line'
type CheckboxInputProps = {
'aria-label'?: string

View File

@@ -1,8 +1,6 @@
'use client'
import React from 'react'
import { useFormFields } from '../../Form/context'
import './index.scss'
import { useFormFields } from '../../../Form/context'
export const CheckboxWrapper: React.FC<{
path: string
@@ -12,7 +10,9 @@ export const CheckboxWrapper: React.FC<{
}> = (props) => {
const { path, children, readOnly, baseClass } = props
const { value: checked } = useFormFields(([fields]) => fields[path])
const field = useFormFields(([fields]) => fields[path])
const { value: checked } = field || {}
return (
<div

View File

@@ -13,7 +13,8 @@ import { CheckboxWrapper } from './Wrapper'
import './index.scss'
const baseClass = 'checkbox'
const inputBaseClass = 'checkbox-input'
export const inputBaseClass = 'checkbox-input'
const Checkbox: React.FC<Props> = (props) => {
const {
@@ -61,7 +62,7 @@ const Checkbox: React.FC<Props> = (props) => {
}}
>
<div className={`${baseClass}__error-wrap`}>
<ErrorComp alignCaret="left" message={errorMessage} showError={!valid} />
<ErrorComp alignCaret="left" path={path} />
</div>
<CheckboxWrapper path={path} readOnly={readOnly} baseClass={inputBaseClass}>
<div className={`${inputBaseClass}__input`}>
@@ -77,9 +78,9 @@ const Checkbox: React.FC<Props> = (props) => {
/>
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div>
{label && <LabelComp htmlFor={fieldID} label={label} required={required} />}
{label && <LabelComp htmlFor={fieldID} label={label} required={required} i18n={i18n} />}
</CheckboxWrapper>
<FieldDescription description={description} path={path} value={value} />
<FieldDescription description={description} path={path} value={value} i18n={i18n} />
</div>
)
}

View File

@@ -2,12 +2,10 @@
import React, { useCallback } from 'react'
import { getTranslation } from '@payloadcms/translations'
import useField from '../../useField'
import { useTranslation } from '../../../providers/Translation'
import useField from '../../../useField'
import { useTranslation } from '../../../../providers/Translation'
import { Validate } from 'payload/types'
import './index.scss'
export const EmailInput: React.FC<{
name: string
autoComplete?: string

View File

@@ -0,0 +1,34 @@
'use client'
import React from 'react'
import { fieldBaseClass } from '../../shared'
import { useFormFields } from '../../../Form/context'
export const EmailInputWrapper: React.FC<{
className?: string
width?: string
style?: React.CSSProperties
readOnly?: boolean
hasMany?: boolean
children: React.ReactNode
path: string
}> = (props) => {
const { className, readOnly, hasMany, style, width, children, path } = props
const field = useFormFields(([fields]) => fields[path])
const { valid } = field || {}
return (
<div
className={[fieldBaseClass, 'email', className, !valid && 'error', readOnly && 'read-only']
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{children}
</div>
)
}

View File

@@ -1,13 +1,13 @@
import React from 'react'
import type { Props } from './types'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label'
import { fieldBaseClass } from '../shared'
import './index.scss'
import { EmailInput } from './Input'
import { EmailInputWrapper } from './Wrapper'
import './index.scss'
export const Email: React.FC<Props> = (props) => {
const {
@@ -24,8 +24,8 @@ export const Email: React.FC<Props> = (props) => {
label,
path: pathFromProps,
required,
errorMessage,
valid,
i18n,
value,
} = props
const path = pathFromProps || name
@@ -34,24 +34,21 @@ export const Email: React.FC<Props> = (props) => {
const LabelComp = Label || DefaultLabel
return (
<div
className={[
fieldBaseClass,
'email',
className,
// showError && 'error',
readOnly && 'read-only',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
<EmailInputWrapper
className={className}
readOnly={readOnly}
style={style}
width={width}
path={path}
>
<ErrorComp message={errorMessage} showError={!valid} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<div className="input-wrapper">
<ErrorComp path={path} />
<LabelComp
htmlFor={`field-${path.replace(/\./g, '__')}`}
label={label}
required={required}
i18n={i18n}
/>
<div>
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<EmailInput
name={name}
@@ -62,12 +59,8 @@ export const Email: React.FC<Props> = (props) => {
/>
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div>
<FieldDescription
description={description}
path={path}
// value={value}
/>
</div>
<FieldDescription description={description} path={path} i18n={i18n} value={value} />
</EmailInputWrapper>
)
}

View File

@@ -1,16 +1,16 @@
import React from 'react'
import type { Props } from './types'
import { ErrorPill } from '../../../elements/ErrorPill'
import FieldDescription from '../../FieldDescription'
import { createNestedFieldPath } from '../../Form/createNestedFieldPath'
import RenderFields from '../../RenderFields'
import { getNestedFieldState } from '../../WatchChildErrors/getNestedFieldState'
import './index.scss'
import { GroupProvider } from './provider'
import { GroupWrapper } from './Wrapper'
import './index.scss'
const baseClass = 'group-field'
const Group: React.FC<Props> = (props) => {
@@ -28,6 +28,8 @@ const Group: React.FC<Props> = (props) => {
user,
i18n,
payload,
config,
value,
} = props
const path = pathFromProps || name
@@ -69,11 +71,12 @@ const Group: React.FC<Props> = (props) => {
className={`field-description-${path.replace(/\./g, '__')}`}
description={description}
path={path}
value={null}
value={value}
i18n={i18n}
/>
</header>
)}
{groupHasErrors && <ErrorPill count={errorCount} withMessage />}
{groupHasErrors && <ErrorPill count={errorCount} withMessage i18n={i18n} />}
</div>
<RenderFields
fieldSchema={fieldSchema}
@@ -87,6 +90,7 @@ const Group: React.FC<Props> = (props) => {
formState={nestedFieldState}
i18n={i18n}
payload={payload}
config={config}
/>
</div>
</GroupProvider>

View File

@@ -1,10 +1,9 @@
'use client'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from '../../../providers/Translation'
import { useTranslation } from '../../../../providers/Translation'
import { getTranslation } from '@payloadcms/translations'
import useField from '../../useField'
import './index.scss'
import useField from '../../../useField'
import { Validate } from 'payload/types'
export const NumberInput: React.FC<{

View File

@@ -0,0 +1,41 @@
'use client'
import React from 'react'
import { fieldBaseClass } from '../../shared'
import { useFormFields } from '../../../Form/context'
export const NumberInputWrapper: React.FC<{
className?: string
width?: string
style?: React.CSSProperties
readOnly?: boolean
hasMany?: boolean
children: React.ReactNode
path: string
}> = (props) => {
const { className, readOnly, hasMany, style, width, children, path } = props
const field = useFormFields(([fields]) => fields[path])
const { valid } = field || {}
return (
<div
className={[
fieldBaseClass,
'number',
className,
!valid && 'error',
readOnly && 'read-only',
hasMany && 'has-many',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{children}
</div>
)
}

View File

@@ -1,15 +1,15 @@
import React from 'react'
import type { Props } from './types'
import { isNumber } from 'payload/utilities'
import ReactSelect from '../../../elements/ReactSelect'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import DefaultLabel from '../../Label'
import { fieldBaseClass } from '../shared'
import './index.scss'
import { NumberInput } from './Input'
import { NumberInputWrapper } from './Wrapper'
import './index.scss'
const NumberField: React.FC<Props> = (props) => {
const {
@@ -26,14 +26,12 @@ const NumberField: React.FC<Props> = (props) => {
} = {},
hasMany,
label,
max,
maxRows,
min,
minRows,
path: pathFromProps,
required,
valid = true,
errorMessage,
i18n,
value,
} = props
@@ -43,24 +41,21 @@ const NumberField: React.FC<Props> = (props) => {
const path = pathFromProps || name
return (
<div
className={[
fieldBaseClass,
'number',
className,
// showError && 'error',
readOnly && 'read-only',
hasMany && 'has-many',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
<NumberInputWrapper
className={className}
readOnly={readOnly}
hasMany={hasMany}
style={style}
width={width}
path={path}
>
<ErrorComp message={errorMessage} showError={!valid} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<ErrorComp path={path} />
<LabelComp
htmlFor={`field-${path.replace(/\./g, '__')}`}
label={label}
required={required}
i18n={i18n}
/>
{hasMany ? (
<ReactSelect
className={`field-${path.replace(/\./g, '__')}`}
@@ -89,7 +84,7 @@ const NumberField: React.FC<Props> = (props) => {
// value={valueToRender as Option[]}
/>
) : (
<div className="input-wrapper">
<div>
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<NumberInput
path={path}
@@ -104,8 +99,8 @@ const NumberField: React.FC<Props> = (props) => {
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div>
)}
<FieldDescription description={description} value={value} />
</div>
<FieldDescription description={description} value={value} i18n={i18n} />
</NumberInputWrapper>
)
}

View File

@@ -1,8 +1,7 @@
'use client'
import React, { useCallback } from 'react'
import useField from '../../useField'
import './index.scss'
import useField from '../../../useField'
import { Validate } from 'payload/types'
export const PasswordInput: React.FC<{

View File

@@ -0,0 +1,33 @@
'use client'
import React from 'react'
import { fieldBaseClass } from '../../shared'
import { useFormFields } from '../../../Form/context'
export const PasswordInputWrapper: React.FC<{
className?: string
width?: string
style?: React.CSSProperties
children: React.ReactNode
path: string
}> = (props) => {
const { className, style, width, children, path } = props
const field = useFormFields(([fields]) => fields[path])
const { valid } = field || {}
return (
<div
className={[fieldBaseClass, 'password', className, !valid && 'error']
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{children}
</div>
)
}

View File

@@ -1,12 +1,12 @@
import React from 'react'
import type { Props } from './types'
import Error from '../../Error'
import Label from '../../Label'
import './index.scss'
import { fieldBaseClass } from '../shared'
import { PasswordInput } from './Input'
import { PasswordInputWrapper } from './Wrapper'
import './index.scss'
export const Password: React.FC<Props> = (props) => {
const {
@@ -19,33 +19,22 @@ export const Password: React.FC<Props> = (props) => {
required,
style,
width,
i18n,
} = props
const path = pathFromProps || name
return (
<div
className={[
fieldBaseClass,
'password',
className,
// showError && 'error'/
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
<Error
message=""
// message={errorMessage}
// showError={showError}
<PasswordInputWrapper className={className} style={style} width={width} path={path}>
<Error path={path} />
<Label
htmlFor={`field-${path.replace(/\./g, '__')}`}
label={label}
required={required}
i18n={i18n}
/>
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<PasswordInput name={name} autoComplete={autoComplete} disabled={disabled} path={path} />
</div>
</PasswordInputWrapper>
)
}

View File

@@ -2,8 +2,9 @@ import type React from 'react'
import type { Validate } from 'payload/types'
import type { Description } from 'payload/types'
import { FormFieldBase } from '../shared'
export type Props = {
export type Props = FormFieldBase & {
autoComplete?: string
className?: string
description?: Description

View File

@@ -2,14 +2,12 @@
import React, { useCallback } from 'react'
import { useTranslation } from '../../../providers/Translation'
import { useTranslation } from '../../../../providers/Translation'
import type { OptionObject, Validate } from 'payload/types'
import type { Option } from '../../../elements/ReactSelect/types'
import type { Option } from '../../../../elements/ReactSelect/types'
import { getTranslation } from '@payloadcms/translations'
import ReactSelect from '../../../elements/ReactSelect'
import useField from '../../useField'
import './index.scss'
import ReactSelect from '../../../../elements/ReactSelect'
import useField from '../../../useField'
const SelectInput: React.FC<{
readOnly: boolean
@@ -31,7 +29,7 @@ const SelectInput: React.FC<{
[validate, required],
)
const { errorMessage, setValue, showError, value } = useField({
const { setValue, showError, value } = useField({
path,
validate: memoizedValidate,
})

View File

@@ -0,0 +1,35 @@
'use client'
import React from 'react'
import { fieldBaseClass } from '../../shared'
import { useFormFields } from '../../../Form/context'
export const SelectFieldWrapper: React.FC<{
className?: string
width?: string
style?: React.CSSProperties
readOnly?: boolean
children: React.ReactNode
path: string
}> = (props) => {
const { className, readOnly, style, width, children, path } = props
const field = useFormFields(([fields]) => fields[path])
const { valid } = field || {}
return (
<div
className={[fieldBaseClass, 'select', className, !valid && 'error', readOnly && 'read-only']
.filter(Boolean)
.join(' ')}
id={`field-${path.replace(/\./g, '__')}`}
style={{
...style,
width,
}}
>
{children}
</div>
)
}

View File

@@ -6,7 +6,9 @@ import DefaultError from '../../Error'
import DefaultLabel from '../../Label'
import FieldDescription from '../../FieldDescription'
import SelectInput from './Input'
import { fieldBaseClass } from '../shared'
import { SelectFieldWrapper } from './Wrapper'
import './index.scss'
const formatOptions = (options: Option[]): OptionObject[] =>
options.map((option) => {
@@ -25,7 +27,6 @@ export const Select: React.FC<Props> = (props) => {
name,
admin: {
className,
// condition,
description,
isClearable,
isSortable = true,
@@ -39,6 +40,8 @@ export const Select: React.FC<Props> = (props) => {
options,
path: pathFromProps,
required,
i18n,
value,
} = props
const path = pathFromProps || name
@@ -47,27 +50,20 @@ export const Select: React.FC<Props> = (props) => {
const LabelComp = Label || DefaultLabel
return (
<div
className={[
fieldBaseClass,
'select',
className,
// showError && 'error',
readOnly && 'read-only',
]
.filter(Boolean)
.join(' ')}
id={`field-${path.replace(/\./g, '__')}`}
style={{
...style,
width,
}}
<SelectFieldWrapper
className={className}
style={style}
width={width}
path={path}
readOnly={readOnly}
>
{/* <ErrorComp
message={errorMessage}
showError={showError}
/> */}
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<ErrorComp path={path} />
<LabelComp
htmlFor={`field-${path.replace(/\./g, '__')}`}
label={label}
required={required}
i18n={i18n}
/>
<SelectInput
readOnly={readOnly}
isClearable={isClearable}
@@ -76,12 +72,8 @@ export const Select: React.FC<Props> = (props) => {
options={formatOptions(options)}
path={path}
/>
<FieldDescription
description={description}
path={path}
// value={value}
/>
</div>
<FieldDescription description={description} path={path} value={value} i18n={i18n} />
</SelectFieldWrapper>
)
}

View File

@@ -1,5 +1,8 @@
import type { SelectField } from 'payload/types'
import { FormFieldBase } from '../shared'
export type Props = Omit<SelectField, 'type'> & {
path?: string
}
export type Props = FormFieldBase &
Omit<SelectField, 'type'> & {
path?: string
value?: string
}

View File

@@ -4,11 +4,10 @@ import React, { useCallback } from 'react'
import { getTranslation } from '@payloadcms/translations'
import type { SanitizedConfig, Validate } from 'payload/types'
import { useTranslation } from '../../../providers/Translation'
import { isFieldRTL } from '../shared'
import './index.scss'
import useField from '../../useField'
import { useLocale } from '../../../providers/Locale'
import { useTranslation } from '../../../../providers/Translation'
import { isFieldRTL } from '../../shared'
import useField from '../../../useField'
import { useLocale } from '../../../../providers/Locale'
export const TextInput: React.FC<{
name: string
@@ -24,6 +23,7 @@ export const TextInput: React.FC<{
minLength?: number
validate?: Validate
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void
inputRef?: React.MutableRefObject<HTMLInputElement>
}> = (props) => {
const {
path,
@@ -37,6 +37,7 @@ export const TextInput: React.FC<{
validate,
required,
onKeyDown,
inputRef,
} = props
const { i18n } = useTranslation()
@@ -50,13 +51,11 @@ export const TextInput: React.FC<{
[validate, minLength, maxLength, required],
)
const field = useField({
const { setValue, value } = useField({
path,
validate: memoizedValidate,
})
const { setValue, value } = field
const renderRTL = isFieldRTL({
fieldLocalized: localized,
fieldRTL: rtl,
@@ -75,7 +74,7 @@ export const TextInput: React.FC<{
}}
onKeyDown={onKeyDown}
placeholder={getTranslation(placeholder, i18n)}
// ref={inputRef}
ref={inputRef}
type="text"
value={(value as string) || ''}
/>

View File

@@ -1,4 +1,4 @@
@import '../../../scss/styles.scss';
@import '../../../../scss/styles.scss';
.field-type.text {
position: relative;

View File

@@ -0,0 +1,36 @@
'use client'
import React from 'react'
import { fieldBaseClass } from '../../shared'
import { useFormFields } from '../../../Form/context'
import './index.scss'
export const TextInputWrapper: React.FC<{
children: React.ReactNode
className?: string
style?: React.CSSProperties
width?: string
path?: string
readOnly?: boolean
}> = (props) => {
const { children, className, style, width, path, readOnly } = props
const field = useFormFields(([fields]) => fields[path])
const { valid } = field || {}
return (
<div
className={[fieldBaseClass, 'text', className, !valid && 'error', readOnly && 'read-only']
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{children}
</div>
)
}

View File

@@ -1,12 +1,11 @@
import React from 'react'
import type { Props } from './types'
import { fieldBaseClass } from '../shared'
import { TextInput } from './Input'
import FieldDescription from '../../FieldDescription'
import DefaultError from '../../Error'
import DefaultLabel from '../../Label'
import { TextInputWrapper } from './Wrapper'
const Text: React.FC<Props> = (props) => {
const {
@@ -27,8 +26,8 @@ const Text: React.FC<Props> = (props) => {
minLength,
path: pathFromProps,
required,
valid = true,
errorMessage,
value,
i18n,
} = props
const path = pathFromProps || name
@@ -37,18 +36,21 @@ const Text: React.FC<Props> = (props) => {
const LabelComp = Label || DefaultLabel
return (
<div
className={[fieldBaseClass, 'text', className, !valid && 'error', readOnly && 'read-only']
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
<TextInputWrapper
className={className}
style={style}
width={width}
path={path}
readOnly={readOnly}
>
<ErrorComp message={errorMessage} showError={!valid} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<div className="input-wrapper">
<ErrorComp path={path} />
<LabelComp
htmlFor={`field-${path.replace(/\./g, '__')}`}
label={label}
required={required}
i18n={i18n}
/>
<div>
{Array.isArray(beforeInput) && beforeInput.map((Component, i) => <Component key={i} />)}
<TextInput
path={path}
@@ -66,9 +68,10 @@ const Text: React.FC<Props> = (props) => {
className={`field-description-${path.replace(/\./g, '__')}`}
description={description}
path={path}
// value={value}
value={value}
i18n={i18n}
/>
</div>
</TextInputWrapper>
)
}

View File

@@ -1,13 +1,11 @@
'use client'
import React, { useCallback } from 'react'
import { useTranslation } from '../../../providers/Translation'
import { useTranslation } from '../../../../providers/Translation'
import type { TextareaField, Validate } from 'payload/types'
import { getTranslation } from '@payloadcms/translations'
import useField from '../../useField'
import './index.scss'
import useField from '../../../useField'
export type TextAreaInputProps = Omit<TextareaField, 'type'> & {
className?: string

View File

@@ -0,0 +1,34 @@
'use client'
import React from 'react'
import { fieldBaseClass } from '../../shared'
import { useFormFields } from '../../../Form/context'
export const TextareaInputWrapper: React.FC<{
children: React.ReactNode
className?: string
style?: React.CSSProperties
width?: string
path?: string
readOnly?: boolean
}> = (props) => {
const { children, className, style, width, path, readOnly } = props
const field = useFormFields(([fields]) => fields[path])
const { valid } = field || {}
return (
<div
className={[fieldBaseClass, 'textarea', className, !valid && 'error', readOnly && 'read-only']
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{children}
</div>
)
}

View File

@@ -1,10 +1,12 @@
import React from 'react'
import type { Props } from './types'
import { fieldBaseClass, isFieldRTL } from '../shared'
import { isFieldRTL } from '../shared'
import TextareaInput from './Input'
import DefaultError from '../../Error'
import DefaultLabel from '../../Label'
import FieldDescription from '../../FieldDescription'
import { TextareaInputWrapper } from './Wrapper'
import './index.scss'
const Textarea: React.FC<Props> = (props) => {
@@ -27,11 +29,10 @@ const Textarea: React.FC<Props> = (props) => {
minLength,
path: pathFromProps,
required,
valid,
errorMessage,
value,
locale,
config: { localization },
i18n,
} = props
const path = pathFromProps || name
@@ -47,17 +48,20 @@ const Textarea: React.FC<Props> = (props) => {
const LabelComp = Label || DefaultLabel
return (
<div
className={[fieldBaseClass, 'textarea', className, !valid && 'error', readOnly && 'read-only']
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
<TextareaInputWrapper
className={className}
readOnly={readOnly}
style={style}
width={width}
path={path}
>
<ErrorComp message={errorMessage} showError={!valid} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<ErrorComp path={path} />
<LabelComp
htmlFor={`field-${path.replace(/\./g, '__')}`}
label={label}
required={required}
i18n={i18n}
/>
<label className="textarea-outer" htmlFor={`field-${path.replace(/\./g, '__')}`}>
<div className="textarea-inner">
<div className="textarea-clone" data-value={value || placeholder || ''} />
@@ -76,8 +80,8 @@ const Textarea: React.FC<Props> = (props) => {
{Array.isArray(afterInput) && afterInput.map((Component, i) => <Component key={i} />)}
</div>
</label>
<FieldDescription description={description} path={path} value={value} />
</div>
<FieldDescription description={description} path={path} value={value} i18n={i18n} />
</TextareaInputWrapper>
)
}
export default Textarea

View File

@@ -13,11 +13,11 @@ export type FormFieldBase = {
valid?: boolean
errorMessage?: string
user?: User
i18n: I18n
payload: Payload
docPreferences: DocumentPreferences
i18n?: I18n
payload?: Payload
docPreferences?: DocumentPreferences
locale?: Locale
config: SanitizedConfig
config?: SanitizedConfig
}
/**

View File

@@ -24,9 +24,11 @@ const useField = <T,>(options: Options): FieldType<T> => {
const { user } = useAuth()
const { id } = useDocumentInfo()
const operation = useOperation()
const field = useFormFields(([fields]) => fields[path])
const { field, dispatchField } = useFormFields(([fields, dispatch]) => ({
field: fields[path],
dispatchField: dispatch,
}))
const { t } = useTranslation()
const dispatchField = useFormFields(([_, dispatch]) => dispatch)
const config = useConfig()
const { getData, getDataByPath, getSiblingData, setModified } = useForm()

View File

@@ -0,0 +1,56 @@
'use server'
import { getPayload } from 'payload'
import { FormState } from '../../forms/Form/types'
import configPromise from 'payload-config'
import buildStateFromSchema from '../../forms/Form/buildStateFromSchema'
import { reduceFieldsToValues } from '../..'
import { DocumentPreferences } from 'payload/types'
import { Locale } from 'payload/config'
import { User } from 'payload/auth'
import { initTFunction } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/api'
export const getFormStateFromServer = async (
args: {
collectionSlug: string
docPreferences: DocumentPreferences
locale: Locale
id?: string
operation: 'create' | 'update'
user: User
language: string
},
{
formState,
}: {
formState: FormState
},
) => {
const { collectionSlug, docPreferences, locale, id, operation, user, language } = args
const payload = await getPayload({
config: configPromise,
})
const collectionConfig = payload.collections[collectionSlug].config
const data = reduceFieldsToValues(formState, true)
// TODO: memoize the creation of this function based on language
const t = initTFunction({ config: payload.config.i18n, language, translations })
const result = await buildStateFromSchema({
id,
config: payload.config,
data,
fieldSchema: collectionConfig.fields,
locale: locale.code,
operation,
preferences: docPreferences,
t,
user,
})
return result
}

View File

@@ -15,7 +15,8 @@ import { SetStepNav } from './SetStepNav'
// import { Upload } from '../Upload'
import './index.scss'
import { EditViewProps } from '../types'
import { fieldTypes } from '../../exports'
import { fieldTypes } from '../../forms/field-types'
import { getFormStateFromServer } from './action'
import './index.scss'
@@ -93,15 +94,26 @@ export const DefaultEditView: React.FC<EditViewProps> = async (props) => {
// setViewActions(defaultActions)
// }, [id, location.pathname, collectionConfig?.admin?.components?.views?.Edit, setViewActions])
const onChange = getFormStateFromServer.bind(null, {
collectionSlug: collectionConfig?.slug,
id: id || undefined,
locale,
language: i18n.language,
operation,
docPreferences,
user,
})
return (
<main className={classes}>
<OperationProvider operation={operation}>
<Form
action={action}
// action={action}
className={`${baseClass}__form`}
disabled={!hasSavePermission}
initialState={formState}
method={id ? 'PATCH' : 'POST'}
onChange={[onChange]}
// onSuccess={onSave}
>
<FormLoadingOverlayToggle
@@ -115,7 +127,6 @@ export const DefaultEditView: React.FC<EditViewProps> = async (props) => {
}`}
type="withoutNav"
/>
{/* <Meta
description={`${isEditing ? t('general:editing') : t('general:creating')} - ${getTranslation(
collection.labels.singular,

View File

@@ -27,7 +27,7 @@ export type EditViewProps = (
}
) & {
config: SanitizedConfig
action: string
action?: string
apiURL: string
canAccessAdmin?: boolean
data: Document

View File

@@ -6,7 +6,10 @@
"emitDeclarationOnly": true,
"esModuleInterop": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */
"rootDir": "./src" /* Specify the root folder within your source files. */,
"paths": {
"payload-config": ["./src/config.ts"]
}
},
"exclude": [
"dist",