feat(next,ui): improves loading states (#6434)
This commit is contained in:
@@ -17,7 +17,7 @@ export default withBundleAnalyzer(
|
|||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
reactCompiler: false
|
reactCompiler: false,
|
||||||
},
|
},
|
||||||
async redirects() {
|
async redirects() {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -57,11 +57,13 @@ export const RootLayout = async ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const payload = await getPayloadHMR({ config })
|
const payload = await getPayloadHMR({ config })
|
||||||
|
|
||||||
const i18n: I18nClient = await initI18n({
|
const i18n: I18nClient = await initI18n({
|
||||||
config: config.i18n,
|
config: config.i18n,
|
||||||
context: 'client',
|
context: 'client',
|
||||||
language: languageCode,
|
language: languageCode,
|
||||||
})
|
})
|
||||||
|
|
||||||
const clientConfig = await createClientConfig({ config, t: i18n.t })
|
const clientConfig = await createClientConfig({ config, t: i18n.t })
|
||||||
|
|
||||||
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
|
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
|
||||||
|
|||||||
@@ -47,6 +47,20 @@ export const initPage = async ({
|
|||||||
language,
|
language,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const languageOptions = Object.entries(payload.config.i18n.supportedLanguages || {}).reduce(
|
||||||
|
(acc, [language, languageConfig]) => {
|
||||||
|
if (Object.keys(payload.config.i18n.supportedLanguages).includes(language)) {
|
||||||
|
acc.push({
|
||||||
|
label: languageConfig.translations.general.thisLanguage,
|
||||||
|
value: language,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
const req = await createLocalReq(
|
const req = await createLocalReq(
|
||||||
{
|
{
|
||||||
fallbackLocale: null,
|
fallbackLocale: null,
|
||||||
@@ -98,6 +112,7 @@ export const initPage = async ({
|
|||||||
cookies,
|
cookies,
|
||||||
docID,
|
docID,
|
||||||
globalConfig,
|
globalConfig,
|
||||||
|
languageOptions,
|
||||||
locale,
|
locale,
|
||||||
permissions,
|
permissions,
|
||||||
req,
|
req,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { SanitizedConfig } from 'payload/types'
|
import type { SanitizedConfig } from 'payload/types'
|
||||||
|
|
||||||
const authRouteKeys: (keyof SanitizedConfig['admin']['routes'])[] = [
|
const authRouteKeys: (keyof SanitizedConfig['admin']['routes'])[] = [
|
||||||
'account',
|
|
||||||
'createFirstUser',
|
'createFirstUser',
|
||||||
'forgot',
|
'forgot',
|
||||||
'login',
|
'login',
|
||||||
|
|||||||
23
packages/next/src/views/API/LocaleSelector/index.tsx
Normal file
23
packages/next/src/views/API/LocaleSelector/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Select } from '@payloadcms/ui/fields/Select'
|
||||||
|
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const LocaleSelector: React.FC<{
|
||||||
|
localeOptions: {
|
||||||
|
label: Record<string, string> | string
|
||||||
|
value: string
|
||||||
|
}[]
|
||||||
|
onChange: (value: string) => void
|
||||||
|
}> = ({ localeOptions, onChange }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
label={t('general:locale')}
|
||||||
|
name="locale"
|
||||||
|
onChange={(value) => onChange(value)}
|
||||||
|
options={localeOptions}
|
||||||
|
path="locale"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import { CopyToClipboard } from '@payloadcms/ui/elements/CopyToClipboard'
|
|||||||
import { Gutter } from '@payloadcms/ui/elements/Gutter'
|
import { Gutter } from '@payloadcms/ui/elements/Gutter'
|
||||||
import { Checkbox } from '@payloadcms/ui/fields/Checkbox'
|
import { Checkbox } from '@payloadcms/ui/fields/Checkbox'
|
||||||
import { NumberField as NumberInput } from '@payloadcms/ui/fields/Number'
|
import { NumberField as NumberInput } from '@payloadcms/ui/fields/Number'
|
||||||
import { Select } from '@payloadcms/ui/fields/Select'
|
|
||||||
import { Form } from '@payloadcms/ui/forms/Form'
|
import { Form } from '@payloadcms/ui/forms/Form'
|
||||||
import { MinimizeMaximize } from '@payloadcms/ui/icons/MinimizeMaximize'
|
import { MinimizeMaximize } from '@payloadcms/ui/icons/MinimizeMaximize'
|
||||||
import { SetViewActions } from '@payloadcms/ui/providers/Actions'
|
import { SetViewActions } from '@payloadcms/ui/providers/Actions'
|
||||||
@@ -19,6 +18,7 @@ import * as React from 'react'
|
|||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
|
|
||||||
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
|
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
|
||||||
|
import { LocaleSelector } from './LocaleSelector/index.js'
|
||||||
import { RenderJSON } from './RenderJSON/index.js'
|
import { RenderJSON } from './RenderJSON/index.js'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
@@ -173,15 +173,7 @@ export const APIViewClient: React.FC = () => {
|
|||||||
path="authenticated"
|
path="authenticated"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{localeOptions && (
|
{localeOptions && <LocaleSelector localeOptions={localeOptions} onChange={setLocale} />}
|
||||||
<Select
|
|
||||||
label={t('general:locale')}
|
|
||||||
name="locale"
|
|
||||||
onChange={(value) => setLocale(value)}
|
|
||||||
options={localeOptions}
|
|
||||||
path="locale"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label={t('general:depth')}
|
label={t('general:depth')}
|
||||||
max={10}
|
max={10}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
'use client'
|
||||||
|
import type { LanguageOptions } from 'payload/types'
|
||||||
|
|
||||||
|
import { ReactSelect } from '@payloadcms/ui/elements/ReactSelect'
|
||||||
|
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export const LanguageSelector: React.FC<{
|
||||||
|
languageOptions: LanguageOptions
|
||||||
|
}> = (props) => {
|
||||||
|
const { languageOptions } = props
|
||||||
|
|
||||||
|
const { i18n, switchLanguage } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactSelect
|
||||||
|
inputId="language-select"
|
||||||
|
onChange={async ({ value }) => {
|
||||||
|
await switchLanguage(value)
|
||||||
|
}}
|
||||||
|
options={languageOptions}
|
||||||
|
value={languageOptions.find((language) => language.value === i18n.language)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,34 +1,28 @@
|
|||||||
'use client'
|
import type { I18n } from '@payloadcms/translations'
|
||||||
import { ReactSelect } from '@payloadcms/ui/elements/ReactSelect'
|
import type { LanguageOptions } from 'payload/types'
|
||||||
|
|
||||||
import { FieldLabel } from '@payloadcms/ui/forms/FieldLabel'
|
import { FieldLabel } from '@payloadcms/ui/forms/FieldLabel'
|
||||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { ToggleTheme } from '../ToggleTheme/index.js'
|
import { ToggleTheme } from '../ToggleTheme/index.js'
|
||||||
|
import { LanguageSelector } from './LanguageSelector.js'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
const baseClass = 'payload-settings'
|
const baseClass = 'payload-settings'
|
||||||
|
|
||||||
export const Settings: React.FC<{
|
export const Settings: React.FC<{
|
||||||
className?: string
|
className?: string
|
||||||
|
i18n: I18n
|
||||||
|
languageOptions: LanguageOptions
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
const { className } = props
|
const { className, i18n, languageOptions } = props
|
||||||
|
|
||||||
const { i18n, languageOptions, switchLanguage, t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={[baseClass, className].filter(Boolean).join(' ')}>
|
<div className={[baseClass, className].filter(Boolean).join(' ')}>
|
||||||
<h3>{t('general:payloadSettings')}</h3>
|
<h3>{i18n.t('general:payloadSettings')}</h3>
|
||||||
<div className={`${baseClass}__language`}>
|
<div className={`${baseClass}__language`}>
|
||||||
<FieldLabel htmlFor="language-select" label={t('general:language')} />
|
<FieldLabel htmlFor="language-select" label={i18n.t('general:language')} />
|
||||||
<ReactSelect
|
<LanguageSelector languageOptions={languageOptions} />
|
||||||
inputId="language-select"
|
|
||||||
onChange={async ({ value }) => {
|
|
||||||
await switchLanguage(value)
|
|
||||||
}}
|
|
||||||
options={languageOptions}
|
|
||||||
value={languageOptions.find((language) => language.value === i18n.language)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<ToggleTheme />
|
<ToggleTheme />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { FormQueryParamsProvider } from '@payloadcms/ui/providers/FormQueryParam
|
|||||||
import { notFound } from 'next/navigation.js'
|
import { notFound } from 'next/navigation.js'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import { getDocumentData } from '../Document/getDocumentData.js'
|
||||||
import { getDocumentPermissions } from '../Document/getDocumentPermissions.js'
|
import { getDocumentPermissions } from '../Document/getDocumentPermissions.js'
|
||||||
import { EditView } from '../Edit/index.js'
|
import { EditView } from '../Edit/index.js'
|
||||||
import { Settings } from './Settings/index.js'
|
import { Settings } from './Settings/index.js'
|
||||||
@@ -21,6 +22,7 @@ export const Account: React.FC<AdminViewProps> = async ({
|
|||||||
searchParams,
|
searchParams,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
|
languageOptions,
|
||||||
locale,
|
locale,
|
||||||
permissions,
|
permissions,
|
||||||
req,
|
req,
|
||||||
@@ -49,6 +51,13 @@ export const Account: React.FC<AdminViewProps> = async ({
|
|||||||
req,
|
req,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { data, formState } = await getDocumentData({
|
||||||
|
id: user.id,
|
||||||
|
collectionConfig,
|
||||||
|
locale,
|
||||||
|
req,
|
||||||
|
})
|
||||||
|
|
||||||
const viewComponentProps: ServerSideEditViewProps = {
|
const viewComponentProps: ServerSideEditViewProps = {
|
||||||
initPageResult,
|
initPageResult,
|
||||||
params,
|
params,
|
||||||
@@ -58,7 +67,7 @@ export const Account: React.FC<AdminViewProps> = async ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentInfoProvider
|
<DocumentInfoProvider
|
||||||
AfterFields={<Settings />}
|
AfterFields={<Settings i18n={i18n} languageOptions={languageOptions} />}
|
||||||
action={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
|
action={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
|
||||||
apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
|
apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
|
||||||
collectionSlug={userSlug}
|
collectionSlug={userSlug}
|
||||||
@@ -66,6 +75,8 @@ export const Account: React.FC<AdminViewProps> = async ({
|
|||||||
hasPublishPermission={hasPublishPermission}
|
hasPublishPermission={hasPublishPermission}
|
||||||
hasSavePermission={hasSavePermission}
|
hasSavePermission={hasSavePermission}
|
||||||
id={user?.id.toString()}
|
id={user?.id.toString()}
|
||||||
|
initialData={data}
|
||||||
|
initialState={formState}
|
||||||
isEditing
|
isEditing
|
||||||
>
|
>
|
||||||
<DocumentHeader
|
<DocumentHeader
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import type { EntityToGroup, Group } from '@payloadcms/ui/utilities/groupNavItems'
|
|
||||||
import type { Permissions } from 'payload/auth'
|
|
||||||
import type { VisibleEntities } from 'payload/types'
|
|
||||||
|
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
|
||||||
import { Button } from '@payloadcms/ui/elements/Button'
|
|
||||||
import { Card } from '@payloadcms/ui/elements/Card'
|
|
||||||
import { SetViewActions } from '@payloadcms/ui/providers/Actions'
|
|
||||||
import { useAuth } from '@payloadcms/ui/providers/Auth'
|
|
||||||
import { useConfig } from '@payloadcms/ui/providers/Config'
|
|
||||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
|
||||||
import { EntityType, groupNavItems } from '@payloadcms/ui/utilities/groupNavItems'
|
|
||||||
import React, { Fragment, useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
import './index.scss'
|
|
||||||
|
|
||||||
const baseClass = 'dashboard'
|
|
||||||
|
|
||||||
export const DefaultDashboardClient: React.FC<{
|
|
||||||
Link: React.ComponentType
|
|
||||||
permissions: Permissions
|
|
||||||
visibleEntities: VisibleEntities
|
|
||||||
}> = ({ Link, permissions, visibleEntities }) => {
|
|
||||||
const config = useConfig()
|
|
||||||
|
|
||||||
const {
|
|
||||||
collections: collectionsConfig,
|
|
||||||
globals: globalsConfig,
|
|
||||||
routes: { admin },
|
|
||||||
} = config
|
|
||||||
|
|
||||||
const { user } = useAuth()
|
|
||||||
|
|
||||||
const { i18n, t } = useTranslation()
|
|
||||||
|
|
||||||
const [groups, setGroups] = useState<Group[]>([])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const collections = collectionsConfig.filter(
|
|
||||||
(collection) =>
|
|
||||||
permissions?.collections?.[collection.slug]?.read?.permission &&
|
|
||||||
visibleEntities.collections.includes(collection.slug),
|
|
||||||
)
|
|
||||||
|
|
||||||
const globals = globalsConfig.filter(
|
|
||||||
(global) =>
|
|
||||||
permissions?.globals?.[global.slug]?.read?.permission &&
|
|
||||||
visibleEntities.globals.includes(global.slug),
|
|
||||||
)
|
|
||||||
|
|
||||||
setGroups(
|
|
||||||
groupNavItems(
|
|
||||||
[
|
|
||||||
...(collections.map((collection) => {
|
|
||||||
const entityToGroup: EntityToGroup = {
|
|
||||||
type: EntityType.collection,
|
|
||||||
entity: collection,
|
|
||||||
}
|
|
||||||
|
|
||||||
return entityToGroup
|
|
||||||
}) ?? []),
|
|
||||||
...(globals.map((global) => {
|
|
||||||
const entityToGroup: EntityToGroup = {
|
|
||||||
type: EntityType.global,
|
|
||||||
entity: global,
|
|
||||||
}
|
|
||||||
|
|
||||||
return entityToGroup
|
|
||||||
}) ?? []),
|
|
||||||
],
|
|
||||||
permissions,
|
|
||||||
i18n,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}, [permissions, user, i18n, visibleEntities, collectionsConfig, globalsConfig])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<SetViewActions actions={[]} />
|
|
||||||
{groups.map(({ entities, label }, groupIndex) => {
|
|
||||||
return (
|
|
||||||
<div className={`${baseClass}__group`} key={groupIndex}>
|
|
||||||
<h2 className={`${baseClass}__label`}>{label}</h2>
|
|
||||||
<ul className={`${baseClass}__card-list`}>
|
|
||||||
{entities.map(({ type, entity }, entityIndex) => {
|
|
||||||
let title: string
|
|
||||||
let buttonAriaLabel: string
|
|
||||||
let createHREF: string
|
|
||||||
let href: string
|
|
||||||
let hasCreatePermission: boolean
|
|
||||||
|
|
||||||
if (type === EntityType.collection) {
|
|
||||||
title = getTranslation(entity.labels.plural, i18n)
|
|
||||||
buttonAriaLabel = t('general:showAllLabel', { label: title })
|
|
||||||
href = `${admin}/collections/${entity.slug}`
|
|
||||||
createHREF = `${admin}/collections/${entity.slug}/create`
|
|
||||||
hasCreatePermission = permissions?.collections?.[entity.slug]?.create?.permission
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === EntityType.global) {
|
|
||||||
title = getTranslation(entity.label, i18n)
|
|
||||||
buttonAriaLabel = t('general:editLabel', {
|
|
||||||
label: getTranslation(entity.label, i18n),
|
|
||||||
})
|
|
||||||
href = `${admin}/globals/${entity.slug}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={entityIndex}>
|
|
||||||
<Card
|
|
||||||
Link={Link}
|
|
||||||
actions={
|
|
||||||
hasCreatePermission && type === EntityType.collection ? (
|
|
||||||
<Button
|
|
||||||
Link={Link}
|
|
||||||
aria-label={t('general:createNewLabel', {
|
|
||||||
label: getTranslation(entity.labels.singular, i18n),
|
|
||||||
})}
|
|
||||||
buttonStyle="icon-label"
|
|
||||||
el="link"
|
|
||||||
icon="plus"
|
|
||||||
iconStyle="with-border"
|
|
||||||
round
|
|
||||||
to={createHREF}
|
|
||||||
/>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
buttonAriaLabel={buttonAriaLabel}
|
|
||||||
href={href}
|
|
||||||
id={`card-${entity.slug}`}
|
|
||||||
title={title}
|
|
||||||
titleAs="h3"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -2,20 +2,23 @@ import type { Permissions } from 'payload/auth'
|
|||||||
import type { ServerProps } from 'payload/config'
|
import type { ServerProps } from 'payload/config'
|
||||||
import type { VisibleEntities } from 'payload/types'
|
import type { VisibleEntities } from 'payload/types'
|
||||||
|
|
||||||
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
|
import { Button } from '@payloadcms/ui/elements/Button'
|
||||||
|
import { Card } from '@payloadcms/ui/elements/Card'
|
||||||
import { Gutter } from '@payloadcms/ui/elements/Gutter'
|
import { Gutter } from '@payloadcms/ui/elements/Gutter'
|
||||||
import { SetStepNav } from '@payloadcms/ui/elements/StepNav'
|
import { SetStepNav } from '@payloadcms/ui/elements/StepNav'
|
||||||
import { WithServerSideProps } from '@payloadcms/ui/elements/WithServerSideProps'
|
import { WithServerSideProps } from '@payloadcms/ui/elements/WithServerSideProps'
|
||||||
import { SetViewActions } from '@payloadcms/ui/providers/Actions'
|
import { SetViewActions } from '@payloadcms/ui/providers/Actions'
|
||||||
import React from 'react'
|
import { EntityType, type groupNavItems } from '@payloadcms/ui/utilities/groupNavItems'
|
||||||
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
import { DefaultDashboardClient } from './index.client.js'
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
const baseClass = 'dashboard'
|
const baseClass = 'dashboard'
|
||||||
|
|
||||||
export type DashboardProps = ServerProps & {
|
export type DashboardProps = ServerProps & {
|
||||||
Link: React.ComponentType<any>
|
Link: React.ComponentType<any>
|
||||||
|
navGroups?: ReturnType<typeof groupNavItems>
|
||||||
permissions: Permissions
|
permissions: Permissions
|
||||||
visibleEntities: VisibleEntities
|
visibleEntities: VisibleEntities
|
||||||
}
|
}
|
||||||
@@ -24,20 +27,22 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
|||||||
const {
|
const {
|
||||||
Link,
|
Link,
|
||||||
i18n,
|
i18n,
|
||||||
|
i18n: { t },
|
||||||
locale,
|
locale,
|
||||||
|
navGroups,
|
||||||
params,
|
params,
|
||||||
payload: {
|
payload: {
|
||||||
config: {
|
config: {
|
||||||
admin: {
|
admin: {
|
||||||
components: { afterDashboard, beforeDashboard },
|
components: { afterDashboard, beforeDashboard },
|
||||||
},
|
},
|
||||||
|
routes: { admin: adminRoute },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
payload,
|
payload,
|
||||||
permissions,
|
permissions,
|
||||||
searchParams,
|
searchParams,
|
||||||
user,
|
user,
|
||||||
visibleEntities,
|
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const BeforeDashboards = Array.isArray(beforeDashboard)
|
const BeforeDashboards = Array.isArray(beforeDashboard)
|
||||||
@@ -82,12 +87,75 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
|||||||
<SetViewActions actions={[]} />
|
<SetViewActions actions={[]} />
|
||||||
<Gutter className={`${baseClass}__wrap`}>
|
<Gutter className={`${baseClass}__wrap`}>
|
||||||
{Array.isArray(BeforeDashboards) && BeforeDashboards.map((Component) => Component)}
|
{Array.isArray(BeforeDashboards) && BeforeDashboards.map((Component) => Component)}
|
||||||
|
<Fragment>
|
||||||
|
<SetViewActions actions={[]} />
|
||||||
|
{!navGroups || navGroups?.length === 0 ? (
|
||||||
|
<p>no nav groups....</p>
|
||||||
|
) : (
|
||||||
|
navGroups.map(({ entities, label }, groupIndex) => {
|
||||||
|
return (
|
||||||
|
<div className={`${baseClass}__group`} key={groupIndex}>
|
||||||
|
<h2 className={`${baseClass}__label`}>{label}</h2>
|
||||||
|
<ul className={`${baseClass}__card-list`}>
|
||||||
|
{entities.map(({ type, entity }, entityIndex) => {
|
||||||
|
let title: string
|
||||||
|
let buttonAriaLabel: string
|
||||||
|
let createHREF: string
|
||||||
|
let href: string
|
||||||
|
let hasCreatePermission: boolean
|
||||||
|
|
||||||
<DefaultDashboardClient
|
if (type === EntityType.collection) {
|
||||||
|
title = getTranslation(entity.labels.plural, i18n)
|
||||||
|
buttonAriaLabel = t('general:showAllLabel', { label: title })
|
||||||
|
href = `${adminRoute}/collections/${entity.slug}`
|
||||||
|
createHREF = `${adminRoute}/collections/${entity.slug}/create`
|
||||||
|
hasCreatePermission =
|
||||||
|
permissions?.collections?.[entity.slug]?.create?.permission
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === EntityType.global) {
|
||||||
|
title = getTranslation(entity.label, i18n)
|
||||||
|
buttonAriaLabel = t('general:editLabel', {
|
||||||
|
label: getTranslation(entity.label, i18n),
|
||||||
|
})
|
||||||
|
href = `${adminRoute}/globals/${entity.slug}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={entityIndex}>
|
||||||
|
<Card
|
||||||
Link={Link}
|
Link={Link}
|
||||||
permissions={permissions}
|
actions={
|
||||||
visibleEntities={visibleEntities}
|
hasCreatePermission && type === EntityType.collection ? (
|
||||||
|
<Button
|
||||||
|
Link={Link}
|
||||||
|
aria-label={t('general:createNewLabel', {
|
||||||
|
label: getTranslation(entity.labels.singular, i18n),
|
||||||
|
})}
|
||||||
|
buttonStyle="icon-label"
|
||||||
|
el="link"
|
||||||
|
icon="plus"
|
||||||
|
iconStyle="with-border"
|
||||||
|
round
|
||||||
|
to={createHREF}
|
||||||
/>
|
/>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
buttonAriaLabel={buttonAriaLabel}
|
||||||
|
href={href}
|
||||||
|
id={`card-${entity.slug}`}
|
||||||
|
title={title}
|
||||||
|
titleAs="h3"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
{Array.isArray(AfterDashboards) && AfterDashboards.map((Component) => Component)}
|
{Array.isArray(AfterDashboards) && AfterDashboards.map((Component) => Component)}
|
||||||
</Gutter>
|
</Gutter>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import type { EntityToGroup } from '@payloadcms/ui/utilities/groupNavItems'
|
||||||
import type { AdminViewProps } from 'payload/types'
|
import type { AdminViewProps } from 'payload/types'
|
||||||
|
|
||||||
import { HydrateClientUser } from '@payloadcms/ui/elements/HydrateClientUser'
|
import { HydrateClientUser } from '@payloadcms/ui/elements/HydrateClientUser'
|
||||||
import { RenderCustomComponent } from '@payloadcms/ui/elements/RenderCustomComponent'
|
import { RenderCustomComponent } from '@payloadcms/ui/elements/RenderCustomComponent'
|
||||||
|
import { EntityType, groupNavItems } from '@payloadcms/ui/utilities/groupNavItems'
|
||||||
import LinkImport from 'next/link.js'
|
import LinkImport from 'next/link.js'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
@@ -28,10 +30,46 @@ export const Dashboard: React.FC<AdminViewProps> = ({ initPageResult, params, se
|
|||||||
|
|
||||||
const CustomDashboardComponent = config.admin.components?.views?.Dashboard
|
const CustomDashboardComponent = config.admin.components?.views?.Dashboard
|
||||||
|
|
||||||
|
const collections = config.collections.filter(
|
||||||
|
(collection) =>
|
||||||
|
permissions?.collections?.[collection.slug]?.read?.permission &&
|
||||||
|
visibleEntities.collections.includes(collection.slug),
|
||||||
|
)
|
||||||
|
|
||||||
|
const globals = config.globals.filter(
|
||||||
|
(global) =>
|
||||||
|
permissions?.globals?.[global.slug]?.read?.permission &&
|
||||||
|
visibleEntities.globals.includes(global.slug),
|
||||||
|
)
|
||||||
|
|
||||||
|
const navGroups = groupNavItems(
|
||||||
|
[
|
||||||
|
...(collections.map((collection) => {
|
||||||
|
const entityToGroup: EntityToGroup = {
|
||||||
|
type: EntityType.collection,
|
||||||
|
entity: collection,
|
||||||
|
}
|
||||||
|
|
||||||
|
return entityToGroup
|
||||||
|
}) ?? []),
|
||||||
|
...(globals.map((global) => {
|
||||||
|
const entityToGroup: EntityToGroup = {
|
||||||
|
type: EntityType.global,
|
||||||
|
entity: global,
|
||||||
|
}
|
||||||
|
|
||||||
|
return entityToGroup
|
||||||
|
}) ?? []),
|
||||||
|
],
|
||||||
|
permissions,
|
||||||
|
i18n,
|
||||||
|
)
|
||||||
|
|
||||||
const viewComponentProps: DashboardProps = {
|
const viewComponentProps: DashboardProps = {
|
||||||
Link,
|
Link,
|
||||||
i18n,
|
i18n,
|
||||||
locale,
|
locale,
|
||||||
|
navGroups,
|
||||||
params,
|
params,
|
||||||
payload,
|
payload,
|
||||||
permissions,
|
permissions,
|
||||||
|
|||||||
@@ -1,41 +1,45 @@
|
|||||||
import type {
|
import type {
|
||||||
Data,
|
Data,
|
||||||
Payload,
|
PayloadRequestWithData,
|
||||||
PayloadRequest,
|
|
||||||
SanitizedCollectionConfig,
|
SanitizedCollectionConfig,
|
||||||
SanitizedGlobalConfig,
|
SanitizedGlobalConfig,
|
||||||
} from 'payload/types'
|
} from 'payload/types'
|
||||||
|
|
||||||
|
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
|
||||||
|
import { reduceFieldsToValues } from '@payloadcms/ui/utilities/reduceFieldsToValues'
|
||||||
|
|
||||||
export const getDocumentData = async (args: {
|
export const getDocumentData = async (args: {
|
||||||
collectionConfig?: SanitizedCollectionConfig
|
collectionConfig?: SanitizedCollectionConfig
|
||||||
globalConfig?: SanitizedGlobalConfig
|
globalConfig?: SanitizedGlobalConfig
|
||||||
id?: number | string
|
id?: number | string
|
||||||
locale: Locale
|
locale: Locale
|
||||||
payload: Payload
|
req: PayloadRequestWithData
|
||||||
req: PayloadRequest
|
|
||||||
}): Promise<Data> => {
|
}): Promise<Data> => {
|
||||||
const { id, collectionConfig, globalConfig, locale, payload, req } = args
|
const { id, collectionConfig, globalConfig, locale, req } = args
|
||||||
|
|
||||||
let data: Data
|
try {
|
||||||
|
const formState = await buildFormState({
|
||||||
if (collectionConfig && id !== undefined && id !== null) {
|
req: {
|
||||||
data = await payload.findByID({
|
...req,
|
||||||
|
data: {
|
||||||
id,
|
id,
|
||||||
collection: collectionConfig.slug,
|
collectionSlug: collectionConfig?.slug,
|
||||||
depth: 0,
|
globalSlug: globalConfig?.slug,
|
||||||
locale: locale.code,
|
locale: locale.code,
|
||||||
req,
|
operation: (collectionConfig && id) || globalConfig ? 'update' : 'create',
|
||||||
|
schemaPath: collectionConfig?.slug || globalConfig?.slug,
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
if (globalConfig) {
|
const data = reduceFieldsToValues(formState, true)
|
||||||
data = await payload.findGlobal({
|
|
||||||
slug: globalConfig.slug,
|
|
||||||
depth: 0,
|
|
||||||
locale: locale.code,
|
|
||||||
req,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
return {
|
||||||
|
data,
|
||||||
|
formState,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting document data', error) // eslint-disable-line no-console
|
||||||
|
return {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import type {
|
|||||||
SanitizedGlobalConfig,
|
SanitizedGlobalConfig,
|
||||||
} from 'payload/types'
|
} from 'payload/types'
|
||||||
|
|
||||||
|
import { notFound } from 'next/navigation.js'
|
||||||
|
|
||||||
import { APIView as DefaultAPIView } from '../API/index.js'
|
import { APIView as DefaultAPIView } from '../API/index.js'
|
||||||
import { EditView as DefaultEditView } from '../Edit/index.js'
|
import { EditView as DefaultEditView } from '../Edit/index.js'
|
||||||
import { LivePreviewView as DefaultLivePreviewView } from '../LivePreview/index.js'
|
import { LivePreviewView as DefaultLivePreviewView } from '../LivePreview/index.js'
|
||||||
@@ -68,6 +70,9 @@ export const getViewsFromConfig = ({
|
|||||||
const [collectionEntity, collectionSlug, segment3, segment4, segment5, ...remainingSegments] =
|
const [collectionEntity, collectionSlug, segment3, segment4, segment5, ...remainingSegments] =
|
||||||
routeSegments
|
routeSegments
|
||||||
|
|
||||||
|
if (!docPermissions?.read?.permission) {
|
||||||
|
notFound()
|
||||||
|
} else {
|
||||||
// `../:id`, or `../create`
|
// `../:id`, or `../create`
|
||||||
switch (routeSegments.length) {
|
switch (routeSegments.length) {
|
||||||
case 3: {
|
case 3: {
|
||||||
@@ -83,12 +88,8 @@ export const getViewsFromConfig = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
if (docPermissions?.read?.permission) {
|
|
||||||
CustomView = getCustomViewByKey(views, 'Default')
|
CustomView = getCustomViewByKey(views, 'Default')
|
||||||
DefaultView = DefaultEditView
|
DefaultView = DefaultEditView
|
||||||
} else {
|
|
||||||
ErrorView = UnauthorizedView
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,6 +173,7 @@ export const getViewsFromConfig = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (globalConfig) {
|
if (globalConfig) {
|
||||||
const editConfig = globalConfig?.admin?.components?.views?.Edit
|
const editConfig = globalConfig?.admin?.components?.views?.Edit
|
||||||
@@ -185,14 +187,13 @@ export const getViewsFromConfig = ({
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [globalEntity, globalSlug, segment3, ...remainingSegments] = routeSegments
|
const [globalEntity, globalSlug, segment3, ...remainingSegments] = routeSegments
|
||||||
|
|
||||||
|
if (!docPermissions?.read?.permission) {
|
||||||
|
notFound()
|
||||||
|
} else {
|
||||||
switch (routeSegments.length) {
|
switch (routeSegments.length) {
|
||||||
case 2: {
|
case 2: {
|
||||||
if (docPermissions?.read?.permission) {
|
|
||||||
CustomView = getCustomViewByKey(views, 'Default')
|
CustomView = getCustomViewByKey(views, 'Default')
|
||||||
DefaultView = DefaultEditView
|
DefaultView = DefaultEditView
|
||||||
} else {
|
|
||||||
ErrorView = UnauthorizedView
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,6 +265,7 @@ export const getViewsFromConfig = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
CustomView,
|
CustomView,
|
||||||
|
|||||||
@@ -63,12 +63,11 @@ export const Document: React.FC<AdminViewProps> = async ({
|
|||||||
let apiURL: string
|
let apiURL: string
|
||||||
let action: string
|
let action: string
|
||||||
|
|
||||||
const data = await getDocumentData({
|
const { data, formState } = await getDocumentData({
|
||||||
id,
|
id,
|
||||||
collectionConfig,
|
collectionConfig,
|
||||||
globalConfig,
|
globalConfig,
|
||||||
locale,
|
locale,
|
||||||
payload,
|
|
||||||
req,
|
req,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -191,6 +190,8 @@ export const Document: React.FC<AdminViewProps> = async ({
|
|||||||
hasPublishPermission={hasPublishPermission}
|
hasPublishPermission={hasPublishPermission}
|
||||||
hasSavePermission={hasSavePermission}
|
hasSavePermission={hasSavePermission}
|
||||||
id={id}
|
id={id}
|
||||||
|
initialData={data}
|
||||||
|
initialState={formState}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
>
|
>
|
||||||
{!ViewOverride && (
|
{!ViewOverride && (
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const APIKey: React.FC<{ enabled: boolean; readOnly?: boolean }> = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
|
|
||||||
const apiKey = useFormFields(([fields]) => fields[path])
|
const apiKey = useFormFields(([fields]) => (fields && fields[path]) || null)
|
||||||
|
|
||||||
const validate = (val) =>
|
const validate = (val) =>
|
||||||
text(val, {
|
text(val, {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Email } from '@payloadcms/ui/fields/Email'
|
|||||||
import { Password } from '@payloadcms/ui/fields/Password'
|
import { Password } from '@payloadcms/ui/fields/Password'
|
||||||
import { useFormFields, useFormModified } from '@payloadcms/ui/forms/Form'
|
import { useFormFields, useFormModified } from '@payloadcms/ui/forms/Form'
|
||||||
import { useConfig } from '@payloadcms/ui/providers/Config'
|
import { useConfig } from '@payloadcms/ui/providers/Config'
|
||||||
|
import { useDocumentInfo } from '@payloadcms/ui/providers/DocumentInfo'
|
||||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
@@ -32,10 +33,11 @@ export const Auth: React.FC<Props> = (props) => {
|
|||||||
} = props
|
} = props
|
||||||
|
|
||||||
const [changingPassword, setChangingPassword] = useState(requirePassword)
|
const [changingPassword, setChangingPassword] = useState(requirePassword)
|
||||||
const enableAPIKey = useFormFields(([fields]) => fields.enableAPIKey)
|
const enableAPIKey = useFormFields(([fields]) => (fields && fields?.enableAPIKey) || null)
|
||||||
const dispatchFields = useFormFields((reducer) => reducer[1])
|
const dispatchFields = useFormFields((reducer) => reducer[1])
|
||||||
const modified = useFormModified()
|
const modified = useFormModified()
|
||||||
const { i18n, t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
|
const { isInitializing } = useDocumentInfo()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
routes: { api },
|
routes: { api },
|
||||||
@@ -85,12 +87,15 @@ export const Auth: React.FC<Props> = (props) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const disabled = readOnly || isInitializing
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={[baseClass, className].filter(Boolean).join(' ')}>
|
<div className={[baseClass, className].filter(Boolean).join(' ')}>
|
||||||
{!disableLocalStrategy && (
|
{!disableLocalStrategy && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Email
|
<Email
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
|
disabled={disabled}
|
||||||
label={t('general:email')}
|
label={t('general:email')}
|
||||||
name="email"
|
name="email"
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
@@ -100,7 +105,7 @@ export const Auth: React.FC<Props> = (props) => {
|
|||||||
<div className={`${baseClass}__changing-password`}>
|
<div className={`${baseClass}__changing-password`}>
|
||||||
<Password
|
<Password
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
disabled={readOnly}
|
disabled={disabled}
|
||||||
label={t('authentication:newPassword')}
|
label={t('authentication:newPassword')}
|
||||||
name="password"
|
name="password"
|
||||||
required
|
required
|
||||||
@@ -108,12 +113,11 @@ export const Auth: React.FC<Props> = (props) => {
|
|||||||
<ConfirmPassword disabled={readOnly} />
|
<ConfirmPassword disabled={readOnly} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`${baseClass}__controls`}>
|
<div className={`${baseClass}__controls`}>
|
||||||
{changingPassword && !requirePassword && (
|
{changingPassword && !requirePassword && (
|
||||||
<Button
|
<Button
|
||||||
buttonStyle="secondary"
|
buttonStyle="secondary"
|
||||||
disabled={readOnly}
|
disabled={disabled}
|
||||||
onClick={() => handleChangePassword(false)}
|
onClick={() => handleChangePassword(false)}
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
@@ -123,7 +127,7 @@ export const Auth: React.FC<Props> = (props) => {
|
|||||||
{!changingPassword && !requirePassword && (
|
{!changingPassword && !requirePassword && (
|
||||||
<Button
|
<Button
|
||||||
buttonStyle="secondary"
|
buttonStyle="secondary"
|
||||||
disabled={readOnly}
|
disabled={disabled}
|
||||||
id="change-password"
|
id="change-password"
|
||||||
onClick={() => handleChangePassword(true)}
|
onClick={() => handleChangePassword(true)}
|
||||||
size="small"
|
size="small"
|
||||||
@@ -134,7 +138,7 @@ export const Auth: React.FC<Props> = (props) => {
|
|||||||
{operation === 'update' && (
|
{operation === 'update' && (
|
||||||
<Button
|
<Button
|
||||||
buttonStyle="secondary"
|
buttonStyle="secondary"
|
||||||
disabled={readOnly}
|
disabled={disabled}
|
||||||
onClick={() => unlock()}
|
onClick={() => unlock()}
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
@@ -147,6 +151,7 @@ export const Auth: React.FC<Props> = (props) => {
|
|||||||
{useAPIKey && (
|
{useAPIKey && (
|
||||||
<div className={`${baseClass}__api-key`}>
|
<div className={`${baseClass}__api-key`}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
disabled={disabled}
|
||||||
label={t('authentication:enableAPIKey')}
|
label={t('authentication:enableAPIKey')}
|
||||||
name="enableAPIKey"
|
name="enableAPIKey"
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
@@ -155,7 +160,12 @@ export const Auth: React.FC<Props> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{verify && (
|
{verify && (
|
||||||
<Checkbox label={t('authentication:verified')} name="_verified" readOnly={readOnly} />
|
<Checkbox
|
||||||
|
disabled={disabled}
|
||||||
|
label={t('authentication:verified')}
|
||||||
|
name="_verified"
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const SetDocumentStepNav: React.FC<{
|
|||||||
|
|
||||||
const view: string | undefined = props?.view || undefined
|
const view: string | undefined = props?.view || undefined
|
||||||
|
|
||||||
const { isEditing, title } = useDocumentInfo()
|
const { isEditing, isInitializing, title } = useDocumentInfo()
|
||||||
const { isEntityVisible } = useEntityVisibility()
|
const { isEntityVisible } = useEntityVisibility()
|
||||||
const isVisible = isEntityVisible({ collectionSlug, globalSlug })
|
const isVisible = isEntityVisible({ collectionSlug, globalSlug })
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ export const SetDocumentStepNav: React.FC<{
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nav: StepNavItem[] = []
|
const nav: StepNavItem[] = []
|
||||||
|
|
||||||
|
if (!isInitializing) {
|
||||||
if (collectionSlug) {
|
if (collectionSlug) {
|
||||||
nav.push({
|
nav.push({
|
||||||
label: getTranslation(pluralLabel, i18n),
|
label: getTranslation(pluralLabel, i18n),
|
||||||
@@ -71,8 +72,10 @@ export const SetDocumentStepNav: React.FC<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (drawerDepth <= 1) setStepNav(nav)
|
if (drawerDepth <= 1) setStepNav(nav)
|
||||||
|
}
|
||||||
}, [
|
}, [
|
||||||
setStepNav,
|
setStepNav,
|
||||||
|
isInitializing,
|
||||||
isEditing,
|
isEditing,
|
||||||
pluralLabel,
|
pluralLabel,
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { FormProps } from '@payloadcms/ui/forms/Form'
|
|||||||
|
|
||||||
import { DocumentControls } from '@payloadcms/ui/elements/DocumentControls'
|
import { DocumentControls } from '@payloadcms/ui/elements/DocumentControls'
|
||||||
import { DocumentFields } from '@payloadcms/ui/elements/DocumentFields'
|
import { DocumentFields } from '@payloadcms/ui/elements/DocumentFields'
|
||||||
import { FormLoadingOverlayToggle } from '@payloadcms/ui/elements/Loading'
|
|
||||||
import { Upload } from '@payloadcms/ui/elements/Upload'
|
import { Upload } from '@payloadcms/ui/elements/Upload'
|
||||||
import { Form } from '@payloadcms/ui/forms/Form'
|
import { Form } from '@payloadcms/ui/forms/Form'
|
||||||
import { useAuth } from '@payloadcms/ui/providers/Auth'
|
import { useAuth } from '@payloadcms/ui/providers/Auth'
|
||||||
@@ -14,7 +13,6 @@ import { useDocumentInfo } from '@payloadcms/ui/providers/DocumentInfo'
|
|||||||
import { useEditDepth } from '@payloadcms/ui/providers/EditDepth'
|
import { useEditDepth } from '@payloadcms/ui/providers/EditDepth'
|
||||||
import { useFormQueryParams } from '@payloadcms/ui/providers/FormQueryParams'
|
import { useFormQueryParams } from '@payloadcms/ui/providers/FormQueryParams'
|
||||||
import { OperationProvider } from '@payloadcms/ui/providers/Operation'
|
import { OperationProvider } from '@payloadcms/ui/providers/Operation'
|
||||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
|
||||||
import { getFormState } from '@payloadcms/ui/utilities/getFormState'
|
import { getFormState } from '@payloadcms/ui/utilities/getFormState'
|
||||||
import { useRouter } from 'next/navigation.js'
|
import { useRouter } from 'next/navigation.js'
|
||||||
import { useSearchParams } from 'next/navigation.js'
|
import { useSearchParams } from 'next/navigation.js'
|
||||||
@@ -52,6 +50,7 @@ export const DefaultEditView: React.FC = () => {
|
|||||||
initialData: data,
|
initialData: data,
|
||||||
initialState,
|
initialState,
|
||||||
isEditing,
|
isEditing,
|
||||||
|
isInitializing,
|
||||||
onSave: onSaveFromContext,
|
onSave: onSaveFromContext,
|
||||||
} = useDocumentInfo()
|
} = useDocumentInfo()
|
||||||
|
|
||||||
@@ -64,8 +63,6 @@ export const DefaultEditView: React.FC = () => {
|
|||||||
const depth = useEditDepth()
|
const depth = useEditDepth()
|
||||||
const { reportUpdate } = useDocumentEvents()
|
const { reportUpdate } = useDocumentEvents()
|
||||||
|
|
||||||
const { i18n } = useTranslation()
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
admin: { user: userSlug },
|
admin: { user: userSlug },
|
||||||
collections,
|
collections,
|
||||||
@@ -183,23 +180,13 @@ export const DefaultEditView: React.FC = () => {
|
|||||||
action={action}
|
action={action}
|
||||||
className={`${baseClass}__form`}
|
className={`${baseClass}__form`}
|
||||||
disableValidationOnSubmit
|
disableValidationOnSubmit
|
||||||
disabled={!hasSavePermission}
|
disabled={isInitializing || !hasSavePermission}
|
||||||
initialState={initialState}
|
initialState={!isInitializing && initialState}
|
||||||
|
isInitializing={isInitializing}
|
||||||
method={id ? 'PATCH' : 'POST'}
|
method={id ? 'PATCH' : 'POST'}
|
||||||
onChange={[onChange]}
|
onChange={[onChange]}
|
||||||
onSuccess={onSave}
|
onSuccess={onSave}
|
||||||
>
|
>
|
||||||
<FormLoadingOverlayToggle
|
|
||||||
action={operation}
|
|
||||||
// formIsLoading={isLoading}
|
|
||||||
// loadingSuffix={getTranslation(collectionConfig.labels.singular, i18n)}
|
|
||||||
name={`collection-edit--${
|
|
||||||
typeof collectionConfig?.labels?.singular === 'string'
|
|
||||||
? collectionConfig.labels.singular
|
|
||||||
: i18n.t('general:document')
|
|
||||||
}`}
|
|
||||||
type="withoutNav"
|
|
||||||
/>
|
|
||||||
{BeforeDocument}
|
{BeforeDocument}
|
||||||
{preventLeaveWithoutSaving && <LeaveWithoutSaving />}
|
{preventLeaveWithoutSaving && <LeaveWithoutSaving />}
|
||||||
<SetDocumentStepNav
|
<SetDocumentStepNav
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { LoadingOverlay } from '@payloadcms/ui/elements/Loading'
|
|
||||||
import { SetViewActions } from '@payloadcms/ui/providers/Actions'
|
import { SetViewActions } from '@payloadcms/ui/providers/Actions'
|
||||||
import { useComponentMap } from '@payloadcms/ui/providers/ComponentMap'
|
import { useComponentMap } from '@payloadcms/ui/providers/ComponentMap'
|
||||||
import { useDocumentInfo } from '@payloadcms/ui/providers/DocumentInfo'
|
import { useDocumentInfo } from '@payloadcms/ui/providers/DocumentInfo'
|
||||||
@@ -16,9 +15,8 @@ export const EditViewClient: React.FC = () => {
|
|||||||
globalSlug,
|
globalSlug,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Allow the `DocumentInfoProvider` to hydrate
|
if (!Edit) {
|
||||||
if (!Edit || (!collectionSlug && !globalSlug)) {
|
return null
|
||||||
return <LoadingOverlay />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import React, { Fragment } from 'react'
|
|||||||
|
|
||||||
import type { DefaultListViewProps, ListPreferences } from './Default/types.js'
|
import type { DefaultListViewProps, ListPreferences } from './Default/types.js'
|
||||||
|
|
||||||
import { UnauthorizedView } from '../Unauthorized/index.js'
|
|
||||||
import { DefaultListView } from './Default/index.js'
|
import { DefaultListView } from './Default/index.js'
|
||||||
|
|
||||||
export { generateListMetadata } from './meta.js'
|
export { generateListMetadata } from './meta.js'
|
||||||
@@ -41,7 +40,7 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
|||||||
const collectionSlug = collectionConfig?.slug
|
const collectionSlug = collectionConfig?.slug
|
||||||
|
|
||||||
if (!permissions?.collections?.[collectionSlug]?.read?.permission) {
|
if (!permissions?.collections?.[collectionSlug]?.read?.permission) {
|
||||||
return <UnauthorizedView initPageResult={initPageResult} searchParams={searchParams} />
|
notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
let listPreferences: ListPreferences
|
let listPreferences: ListPreferences
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import type { ClientCollectionConfig, ClientConfig, ClientGlobalConfig, Data } f
|
|||||||
|
|
||||||
import { DocumentControls } from '@payloadcms/ui/elements/DocumentControls'
|
import { DocumentControls } from '@payloadcms/ui/elements/DocumentControls'
|
||||||
import { DocumentFields } from '@payloadcms/ui/elements/DocumentFields'
|
import { DocumentFields } from '@payloadcms/ui/elements/DocumentFields'
|
||||||
import { LoadingOverlay } from '@payloadcms/ui/elements/Loading'
|
|
||||||
import { Form } from '@payloadcms/ui/forms/Form'
|
import { Form } from '@payloadcms/ui/forms/Form'
|
||||||
import { SetViewActions } from '@payloadcms/ui/providers/Actions'
|
import { SetViewActions } from '@payloadcms/ui/providers/Actions'
|
||||||
import { useComponentMap } from '@payloadcms/ui/providers/ComponentMap'
|
import { useComponentMap } from '@payloadcms/ui/providers/ComponentMap'
|
||||||
@@ -66,6 +65,7 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
initialData,
|
initialData,
|
||||||
initialState,
|
initialState,
|
||||||
isEditing,
|
isEditing,
|
||||||
|
isInitializing,
|
||||||
onSave: onSaveFromProps,
|
onSave: onSaveFromProps,
|
||||||
} = useDocumentInfo()
|
} = useDocumentInfo()
|
||||||
|
|
||||||
@@ -120,11 +120,6 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
[serverURL, apiRoute, id, operation, schemaPath, getDocPreferences],
|
[serverURL, apiRoute, id, operation, schemaPath, getDocPreferences],
|
||||||
)
|
)
|
||||||
|
|
||||||
// Allow the `DocumentInfoProvider` to hydrate
|
|
||||||
if (!collectionSlug && !globalSlug) {
|
|
||||||
return <LoadingOverlay />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<OperationProvider operation={operation}>
|
<OperationProvider operation={operation}>
|
||||||
@@ -133,6 +128,7 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
className={`${baseClass}__form`}
|
className={`${baseClass}__form`}
|
||||||
disabled={!hasSavePermission}
|
disabled={!hasSavePermission}
|
||||||
initialState={initialState}
|
initialState={initialState}
|
||||||
|
isInitializing={isInitializing}
|
||||||
method={id ? 'PATCH' : 'POST'}
|
method={id ? 'PATCH' : 'POST'}
|
||||||
onChange={[onChange]}
|
onChange={[onChange]}
|
||||||
onSuccess={onSave}
|
onSuccess={onSave}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.
|
|||||||
|
|
||||||
import type { FormState, PayloadRequestWithData } from 'payload/types'
|
import type { FormState, PayloadRequestWithData } from 'payload/types'
|
||||||
|
|
||||||
import { FormLoadingOverlayToggle } from '@payloadcms/ui/elements/Loading'
|
|
||||||
import { Email } from '@payloadcms/ui/fields/Email'
|
import { Email } from '@payloadcms/ui/fields/Email'
|
||||||
import { Password } from '@payloadcms/ui/fields/Password'
|
import { Password } from '@payloadcms/ui/fields/Password'
|
||||||
import { Form } from '@payloadcms/ui/forms/Form'
|
import { Form } from '@payloadcms/ui/forms/Form'
|
||||||
@@ -60,7 +59,6 @@ export const LoginForm: React.FC<{
|
|||||||
redirect={typeof searchParams?.redirect === 'string' ? searchParams.redirect : admin}
|
redirect={typeof searchParams?.redirect === 'string' ? searchParams.redirect : admin}
|
||||||
waitForAutocomplete
|
waitForAutocomplete
|
||||||
>
|
>
|
||||||
<FormLoadingOverlayToggle action="loading" name="login-form" />
|
|
||||||
<div className={`${baseClass}__inputWrap`}>
|
<div className={`${baseClass}__inputWrap`}>
|
||||||
<Email
|
<Email
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { AdminViewProps } from 'payload/types'
|
import type { AdminViewProps } from 'payload/types'
|
||||||
|
|
||||||
import { MinimalTemplate } from '@payloadcms/ui/templates/Minimal'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { LogoutClient } from './LogoutClient.js'
|
import { LogoutClient } from './LogoutClient.js'
|
||||||
@@ -26,7 +25,6 @@ export const LogoutView: React.FC<
|
|||||||
} = initPageResult
|
} = initPageResult
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MinimalTemplate className={baseClass}>
|
|
||||||
<div className={`${baseClass}__wrap`}>
|
<div className={`${baseClass}__wrap`}>
|
||||||
<LogoutClient
|
<LogoutClient
|
||||||
adminRoute={admin}
|
adminRoute={admin}
|
||||||
@@ -34,7 +32,6 @@ export const LogoutView: React.FC<
|
|||||||
redirect={searchParams.redirect as string}
|
redirect={searchParams.redirect as string}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</MinimalTemplate>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,9 +72,9 @@ export const ResetPasswordClient: React.FC<Args> = ({ token }) => {
|
|||||||
|
|
||||||
const PasswordToConfirm = () => {
|
const PasswordToConfirm = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { value: confirmValue } = useFormFields(([fields]) => {
|
const { value: confirmValue } = useFormFields(
|
||||||
return fields['confirm-password']
|
([fields]) => (fields && fields?.['confirm-password']) || null,
|
||||||
})
|
)
|
||||||
|
|
||||||
const validate = React.useCallback(
|
const validate = React.useCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
'use client'
|
||||||
import { ReactSelect } from '@payloadcms/ui/elements/ReactSelect'
|
import { ReactSelect } from '@payloadcms/ui/elements/ReactSelect'
|
||||||
import { useLocale } from '@payloadcms/ui/providers/Locale'
|
import { useLocale } from '@payloadcms/ui/providers/Locale'
|
||||||
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
import { useTranslation } from '@payloadcms/ui/providers/Translation'
|
||||||
|
|||||||
@@ -4,6 +4,12 @@
|
|||||||
* @returns {import('next').NextConfig}
|
* @returns {import('next').NextConfig}
|
||||||
* */
|
* */
|
||||||
export const withPayload = (nextConfig = {}) => {
|
export const withPayload = (nextConfig = {}) => {
|
||||||
|
if (nextConfig.experimental?.staleTimes?.dynamic) {
|
||||||
|
console.warn(
|
||||||
|
'Payload detected a non-zero value for the `staleTimes.dynamic` option in your Next.js config. This may cause stale data to load in the Admin Panel. To clear this warning, remove the `staleTimes.dynamic` option from your Next.js config or set it to 0. In the future, Next.js may support scoping this option to specific routes.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...nextConfig,
|
...nextConfig,
|
||||||
experimental: {
|
experimental: {
|
||||||
|
|||||||
4
packages/payload/src/admin/LanguageOptions.ts
Normal file
4
packages/payload/src/admin/LanguageOptions.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type LanguageOptions = {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
}[]
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export type { LanguageOptions } from './LanguageOptions.js'
|
||||||
export type { RichTextAdapter, RichTextAdapterProvider, RichTextFieldProps } from './RichText.js'
|
export type { RichTextAdapter, RichTextAdapterProvider, RichTextFieldProps } from './RichText.js'
|
||||||
export type { CellComponentProps, DefaultCellComponentProps } from './elements/Cell.js'
|
export type { CellComponentProps, DefaultCellComponentProps } from './elements/Cell.js'
|
||||||
export type { ConditionalDateProps } from './elements/DatePicker.js'
|
export type { ConditionalDateProps } from './elements/DatePicker.js'
|
||||||
@@ -26,6 +27,7 @@ export type {
|
|||||||
} from './forms/FieldDescription.js'
|
} from './forms/FieldDescription.js'
|
||||||
export type { Data, FilterOptionsResult, FormField, FormState, Row } from './forms/Form.js'
|
export type { Data, FilterOptionsResult, FormField, FormState, Row } from './forms/Form.js'
|
||||||
export type { LabelProps, SanitizedLabelProps } from './forms/Label.js'
|
export type { LabelProps, SanitizedLabelProps } from './forms/Label.js'
|
||||||
|
|
||||||
export type { RowLabel, RowLabelComponent } from './forms/RowLabel.js'
|
export type { RowLabel, RowLabelComponent } from './forms/RowLabel.js'
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { SanitizedCollectionConfig } from '../../collections/config/types.j
|
|||||||
import type { Locale } from '../../config/types.js'
|
import type { Locale } from '../../config/types.js'
|
||||||
import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
|
import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
|
||||||
import type { PayloadRequestWithData } from '../../types/index.js'
|
import type { PayloadRequestWithData } from '../../types/index.js'
|
||||||
|
import type { LanguageOptions } from '../LanguageOptions.js'
|
||||||
|
|
||||||
export type AdminViewConfig = {
|
export type AdminViewConfig = {
|
||||||
Component: AdminViewComponent
|
Component: AdminViewComponent
|
||||||
@@ -40,6 +41,7 @@ export type InitPageResult = {
|
|||||||
cookies: Map<string, string>
|
cookies: Map<string, string>
|
||||||
docID?: string
|
docID?: string
|
||||||
globalConfig?: SanitizedGlobalConfig
|
globalConfig?: SanitizedGlobalConfig
|
||||||
|
languageOptions: LanguageOptions
|
||||||
locale: Locale
|
locale: Locale
|
||||||
permissions: Permissions
|
permissions: Permissions
|
||||||
req: PayloadRequestWithData
|
req: PayloadRequestWithData
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const LinkToDoc: CustomComponent<UIField> = () => {
|
|||||||
const { custom } = useFieldProps()
|
const { custom } = useFieldProps()
|
||||||
const { isTestKey, nameOfIDField, stripeResourceType } = custom
|
const { isTestKey, nameOfIDField, stripeResourceType } = custom
|
||||||
|
|
||||||
const field = useFormFields(([fields]) => fields[nameOfIDField])
|
const field = useFormFields(([fields]) => (fields && fields?.[nameOfIDField]) || null)
|
||||||
const { value: stripeID } = field || {}
|
const { value: stripeID } = field || {}
|
||||||
|
|
||||||
const stripeEnv = isTestKey ? 'test/' : ''
|
const stripeEnv = isTestKey ? 'test/' : ''
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ const RichTextField: React.FC<
|
|||||||
path: pathFromProps,
|
path: pathFromProps,
|
||||||
placeholder,
|
placeholder,
|
||||||
plugins,
|
plugins,
|
||||||
readOnly,
|
readOnly: readOnlyFromProps,
|
||||||
required,
|
required,
|
||||||
style,
|
style,
|
||||||
validate = richTextValidate,
|
validate = richTextValidate,
|
||||||
@@ -102,12 +102,16 @@ const RichTextField: React.FC<
|
|||||||
[validate, required, i18n],
|
[validate, required, i18n],
|
||||||
)
|
)
|
||||||
|
|
||||||
const { path: pathFromContext } = useFieldProps()
|
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
||||||
|
|
||||||
const { initialValue, path, schemaPath, setValue, showError, value } = useField({
|
const { formInitializing, initialValue, path, schemaPath, setValue, showError, value } = useField(
|
||||||
|
{
|
||||||
path: pathFromContext || pathFromProps || name,
|
path: pathFromContext || pathFromProps || name,
|
||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const disabled = readOnlyFromProps || readOnlyFromContext || formInitializing
|
||||||
|
|
||||||
const editor = useMemo(() => {
|
const editor = useMemo(() => {
|
||||||
let CreatedEditor = withEnterBreakOut(withHistory(withReact(createEditor())))
|
let CreatedEditor = withEnterBreakOut(withHistory(withReact(createEditor())))
|
||||||
@@ -241,12 +245,12 @@ const RichTextField: React.FC<
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (ops && Array.isArray(ops) && ops.length > 0) {
|
if (ops && Array.isArray(ops) && ops.length > 0) {
|
||||||
if (!readOnly && val !== defaultRichTextValue && val !== value) {
|
if (!disabled && val !== defaultRichTextValue && val !== value) {
|
||||||
setValue(val)
|
setValue(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[editor?.operations, readOnly, setValue, value],
|
[editor?.operations, disabled, setValue, value],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -263,16 +267,16 @@ const RichTextField: React.FC<
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (readOnly) {
|
if (disabled) {
|
||||||
setClickableState('disabled')
|
setClickableState('disabled')
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (readOnly) {
|
if (disabled) {
|
||||||
setClickableState('enabled')
|
setClickableState('enabled')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [readOnly])
|
}, [disabled])
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// // If there is a change to the initial value, we need to reset Slate history
|
// // If there is a change to the initial value, we need to reset Slate history
|
||||||
@@ -289,7 +293,7 @@ const RichTextField: React.FC<
|
|||||||
'field-type',
|
'field-type',
|
||||||
className,
|
className,
|
||||||
showError && 'error',
|
showError && 'error',
|
||||||
readOnly && `${baseClass}--read-only`,
|
disabled && `${baseClass}--read-only`,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')
|
.join(' ')
|
||||||
@@ -344,6 +348,7 @@ const RichTextField: React.FC<
|
|||||||
if (Button) {
|
if (Button) {
|
||||||
return (
|
return (
|
||||||
<ElementButtonProvider
|
<ElementButtonProvider
|
||||||
|
disabled={disabled}
|
||||||
fieldProps={props}
|
fieldProps={props}
|
||||||
key={element.name}
|
key={element.name}
|
||||||
path={path}
|
path={path}
|
||||||
@@ -444,7 +449,7 @@ const RichTextField: React.FC<
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
placeholder={getTranslation(placeholder, i18n)}
|
placeholder={getTranslation(placeholder, i18n)}
|
||||||
readOnly={readOnly}
|
readOnly={disabled}
|
||||||
renderElement={renderElement}
|
renderElement={renderElement}
|
||||||
renderLeaf={renderLeaf}
|
renderLeaf={renderLeaf}
|
||||||
spellCheck
|
spellCheck
|
||||||
|
|||||||
@@ -8,15 +8,26 @@ import { useSlate } from 'slate-react'
|
|||||||
import type { ButtonProps } from './types.js'
|
import type { ButtonProps } from './types.js'
|
||||||
|
|
||||||
import '../buttons.scss'
|
import '../buttons.scss'
|
||||||
|
import { useElementButton } from '../providers/ElementButtonProvider.js'
|
||||||
import { isElementActive } from './isActive.js'
|
import { isElementActive } from './isActive.js'
|
||||||
import { toggleElement } from './toggle.js'
|
import { toggleElement } from './toggle.js'
|
||||||
|
|
||||||
export const baseClass = 'rich-text__button'
|
export const baseClass = 'rich-text__button'
|
||||||
|
|
||||||
export const ElementButton: React.FC<ButtonProps> = (props) => {
|
export const ElementButton: React.FC<ButtonProps> = (props) => {
|
||||||
const { type = 'type', children, className, el = 'button', format, onClick, tooltip } = props
|
const {
|
||||||
|
type = 'type',
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
disabled: disabledFromProps,
|
||||||
|
el = 'button',
|
||||||
|
format,
|
||||||
|
onClick,
|
||||||
|
tooltip,
|
||||||
|
} = props
|
||||||
|
|
||||||
const editor = useSlate()
|
const editor = useSlate()
|
||||||
|
const { disabled: disabledFromContext } = useElementButton()
|
||||||
const [showTooltip, setShowTooltip] = useState(false)
|
const [showTooltip, setShowTooltip] = useState(false)
|
||||||
|
|
||||||
const defaultOnClick = useCallback(
|
const defaultOnClick = useCallback(
|
||||||
@@ -30,9 +41,11 @@ export const ElementButton: React.FC<ButtonProps> = (props) => {
|
|||||||
|
|
||||||
const Tag: ElementType = el
|
const Tag: ElementType = el
|
||||||
|
|
||||||
|
const disabled = disabledFromProps || disabledFromContext
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
{...(el === 'button' && { type: 'button' })}
|
{...(el === 'button' && { type: 'button', disabled })}
|
||||||
className={[
|
className={[
|
||||||
baseClass,
|
baseClass,
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { ElementType } from 'react'
|
|||||||
export type ButtonProps = {
|
export type ButtonProps = {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
|
disabled?: boolean
|
||||||
el?: ElementType
|
el?: ElementType
|
||||||
format: string
|
format: string
|
||||||
onClick?: (e: React.MouseEvent) => void
|
onClick?: (e: React.MouseEvent) => void
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { FormFieldBase } from '@payloadcms/ui/fields/shared'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
type ElementButtonContextType = {
|
type ElementButtonContextType = {
|
||||||
|
disabled?: boolean
|
||||||
fieldProps: FormFieldBase & {
|
fieldProps: FormFieldBase & {
|
||||||
name: string
|
name: string
|
||||||
richTextComponentMap: Map<string, React.ReactNode>
|
richTextComponentMap: Map<string, React.ReactNode>
|
||||||
|
|||||||
@@ -22,16 +22,19 @@ export const heTranslations: DefaultTranslationsObject = {
|
|||||||
failedToUnlock: 'ביטול נעילה נכשל',
|
failedToUnlock: 'ביטול נעילה נכשל',
|
||||||
forceUnlock: 'אלץ ביטול נעילה',
|
forceUnlock: 'אלץ ביטול נעילה',
|
||||||
forgotPassword: 'שכחתי סיסמה',
|
forgotPassword: 'שכחתי סיסמה',
|
||||||
forgotPasswordEmailInstructions: 'אנא הזן את כתובת הדוא"ל שלך למטה. תקבל הודעה עם הוראות לאיפוס הסיסמה שלך.',
|
forgotPasswordEmailInstructions:
|
||||||
|
'אנא הזן את כתובת הדוא"ל שלך למטה. תקבל הודעה עם הוראות לאיפוס הסיסמה שלך.',
|
||||||
forgotPasswordQuestion: 'שכחת סיסמה?',
|
forgotPasswordQuestion: 'שכחת סיסמה?',
|
||||||
generate: 'יצירה',
|
generate: 'יצירה',
|
||||||
generateNewAPIKey: 'יצירת מפתח API חדש',
|
generateNewAPIKey: 'יצירת מפתח API חדש',
|
||||||
generatingNewAPIKeyWillInvalidate: 'יצירת מפתח API חדש תבטל את המפתח הקודם. האם אתה בטוח שברצונך להמשיך?',
|
generatingNewAPIKeyWillInvalidate:
|
||||||
|
'יצירת מפתח API חדש תבטל את המפתח הקודם. האם אתה בטוח שברצונך להמשיך?',
|
||||||
lockUntil: 'נעילה עד',
|
lockUntil: 'נעילה עד',
|
||||||
logBackIn: 'התחברות מחדש',
|
logBackIn: 'התחברות מחדש',
|
||||||
logOut: 'התנתקות',
|
logOut: 'התנתקות',
|
||||||
loggedIn: 'כדי להתחבר עם משתמש אחר, יש להתנתק תחילה.',
|
loggedIn: 'כדי להתחבר עם משתמש אחר, יש להתנתק תחילה.',
|
||||||
loggedInChangePassword: 'כדי לשנות את הסיסמה שלך, יש לעבור ל<a href="{{serverURL}}">חשבון</a> שלך ולערוך את הסיסמה שם.',
|
loggedInChangePassword:
|
||||||
|
'כדי לשנות את הסיסמה שלך, יש לעבור ל<a href="{{serverURL}}">חשבון</a> שלך ולערוך את הסיסמה שם.',
|
||||||
loggedOutInactivity: 'התנתקת בשל חוסר פעילות.',
|
loggedOutInactivity: 'התנתקת בשל חוסר פעילות.',
|
||||||
loggedOutSuccessfully: 'התנתקת בהצלחה.',
|
loggedOutSuccessfully: 'התנתקת בהצלחה.',
|
||||||
loggingOut: 'מתנתק...',
|
loggingOut: 'מתנתק...',
|
||||||
@@ -43,7 +46,8 @@ export const heTranslations: DefaultTranslationsObject = {
|
|||||||
logoutSuccessful: 'התנתקות הצליחה.',
|
logoutSuccessful: 'התנתקות הצליחה.',
|
||||||
logoutUser: 'התנתקות משתמש',
|
logoutUser: 'התנתקות משתמש',
|
||||||
newAPIKeyGenerated: 'נוצר מפתח API חדש.',
|
newAPIKeyGenerated: 'נוצר מפתח API חדש.',
|
||||||
newAccountCreated: 'נוצר חשבון חדש עבורך כדי לגשת אל <a href="{{serverURL}}">{{serverURL}}</a>. אנא לחץ על הקישור הבא או הדבק את ה-URL בדפדפן שלך כדי לאמת את הדוא"ל שלך: <a href="{{verificationURL}}">{{verificationURL}}</a>.<br> לאחר אימות כתובת הדוא"ל, תוכל להתחבר בהצלחה.',
|
newAccountCreated:
|
||||||
|
'נוצר חשבון חדש עבורך כדי לגשת אל <a href="{{serverURL}}">{{serverURL}}</a>. אנא לחץ על הקישור הבא או הדבק את ה-URL בדפדפן שלך כדי לאמת את הדוא"ל שלך: <a href="{{verificationURL}}">{{verificationURL}}</a>.<br> לאחר אימות כתובת הדוא"ל, תוכל להתחבר בהצלחה.',
|
||||||
newPassword: 'סיסמה חדשה',
|
newPassword: 'סיסמה חדשה',
|
||||||
passed: 'אימות הצליח',
|
passed: 'אימות הצליח',
|
||||||
passwordResetSuccessfully: 'איפוס הסיסמה הצליח.',
|
passwordResetSuccessfully: 'איפוס הסיסמה הצליח.',
|
||||||
@@ -61,9 +65,11 @@ export const heTranslations: DefaultTranslationsObject = {
|
|||||||
verify: 'אמת',
|
verify: 'אמת',
|
||||||
verifyUser: 'אמת משתמש',
|
verifyUser: 'אמת משתמש',
|
||||||
verifyYourEmail: 'אמת את כתובת הדוא"ל שלך',
|
verifyYourEmail: 'אמת את כתובת הדוא"ל שלך',
|
||||||
youAreInactive: 'לא היית פעיל לזמן קצר ובקרוב תתנתק אוטומטית כדי לשמור על האבטחה של חשבונך. האם ברצונך להישאר מחובר?',
|
youAreInactive:
|
||||||
youAreReceivingResetPassword: 'קיבלת הודעה זו מכיוון שאתה (או מישהו אחר) ביקשת לאפס את הסיסמה של החשבון שלך. אנא לחץ על הקישור הבא או הדבק אותו בשורת הכתובת בדפדפן שלך כדי להשלים את התהליך:',
|
'לא היית פעיל לזמן קצר ובקרוב תתנתק אוטומטית כדי לשמור על האבטחה של חשבונך. האם ברצונך להישאר מחובר?',
|
||||||
youDidNotRequestPassword: 'אם לא ביקשת זאת, אנא התעלם מההודעה והסיסמה שלך תישאר ללא שינוי.'
|
youAreReceivingResetPassword:
|
||||||
|
'קיבלת הודעה זו מכיוון שאתה (או מישהו אחר) ביקשת לאפס את הסיסמה של החשבון שלך. אנא לחץ על הקישור הבא או הדבק אותו בשורת הכתובת בדפדפן שלך כדי להשלים את התהליך:',
|
||||||
|
youDidNotRequestPassword: 'אם לא ביקשת זאת, אנא התעלם מההודעה והסיסמה שלך תישאר ללא שינוי.',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
accountAlreadyActivated: 'חשבון זה כבר הופעל.',
|
accountAlreadyActivated: 'חשבון זה כבר הופעל.',
|
||||||
@@ -339,7 +345,8 @@ export const heTranslations: DefaultTranslationsObject = {
|
|||||||
type: 'סוג',
|
type: 'סוג',
|
||||||
aboutToPublishSelection: 'אתה עומד לפרסם את כל ה{{label}} שנבחרו. האם אתה בטוח?',
|
aboutToPublishSelection: 'אתה עומד לפרסם את כל ה{{label}} שנבחרו. האם אתה בטוח?',
|
||||||
aboutToRestore: 'אתה עומד לשחזר את מסמך {{label}} למצב שהיה בו בתאריך {{versionDate}}.',
|
aboutToRestore: 'אתה עומד לשחזר את מסמך {{label}} למצב שהיה בו בתאריך {{versionDate}}.',
|
||||||
aboutToRestoreGlobal: 'אתה עומד לשחזר את {{label}} הגלובלי למצב שהיה בו בתאריך {{versionDate}}.',
|
aboutToRestoreGlobal:
|
||||||
|
'אתה עומד לשחזר את {{label}} הגלובלי למצב שהיה בו בתאריך {{versionDate}}.',
|
||||||
aboutToRevertToPublished: 'אתה עומד להחזיר את השינויים במסמך הזה לגרסה שפורסמה. האם אתה בטוח?',
|
aboutToRevertToPublished: 'אתה עומד להחזיר את השינויים במסמך הזה לגרסה שפורסמה. האם אתה בטוח?',
|
||||||
aboutToUnpublish: 'אתה עומד לבטל את הפרסום של מסמך זה. האם אתה בטוח?',
|
aboutToUnpublish: 'אתה עומד לבטל את הפרסום של מסמך זה. האם אתה בטוח?',
|
||||||
aboutToUnpublishSelection: 'אתה עומד לבטל את הפרסום של כל ה{{label}} שנבחרו. האם אתה בטוח?',
|
aboutToUnpublishSelection: 'אתה עומד לבטל את הפרסום של כל ה{{label}} שנבחרו. האם אתה בטוח?',
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export const DocumentFields: React.FC<Args> = ({
|
|||||||
<RenderFields
|
<RenderFields
|
||||||
className={`${baseClass}__fields`}
|
className={`${baseClass}__fields`}
|
||||||
fieldMap={mainFields}
|
fieldMap={mainFields}
|
||||||
|
forceRender={10}
|
||||||
path=""
|
path=""
|
||||||
permissions={docPermissions?.fields}
|
permissions={docPermissions?.fields}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
@@ -76,6 +77,7 @@ export const DocumentFields: React.FC<Args> = ({
|
|||||||
<div className={`${baseClass}__sidebar-fields`}>
|
<div className={`${baseClass}__sidebar-fields`}>
|
||||||
<RenderFields
|
<RenderFields
|
||||||
fieldMap={sidebarFields}
|
fieldMap={sidebarFields}
|
||||||
|
forceRender={10}
|
||||||
path=""
|
path=""
|
||||||
permissions={docPermissions?.fields}
|
permissions={docPermissions?.fields}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
|
|||||||
@@ -82,7 +82,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__count {
|
&__count {
|
||||||
padding: 0px 7px;
|
min-width: 22px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2px 7px;
|
||||||
background-color: var(--theme-elevation-100);
|
background-color: var(--theme-elevation-100);
|
||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import React from 'react'
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
import { useDocumentInfo } from '../../../../../providers/DocumentInfo/index.js'
|
import { useDocumentInfo } from '../../../../../providers/DocumentInfo/index.js'
|
||||||
import { baseClass } from '../../Tab/index.js'
|
import { baseClass } from '../../Tab/index.js'
|
||||||
@@ -7,9 +7,18 @@ import { baseClass } from '../../Tab/index.js'
|
|||||||
export const VersionsPill: React.FC = () => {
|
export const VersionsPill: React.FC = () => {
|
||||||
const { versions } = useDocumentInfo()
|
const { versions } = useDocumentInfo()
|
||||||
|
|
||||||
if (versions?.totalDocs > 0) {
|
// To prevent CLS (versions are currently loaded client-side), render non-breaking space if there are no versions
|
||||||
return <span className={`${baseClass}__count`}>{versions?.totalDocs?.toString()}</span>
|
// The pill is already conditionally rendered to begin with based on whether the document is version-enabled
|
||||||
}
|
// documents that are version enabled _always_ have at least one version
|
||||||
|
const hasVersions = versions?.totalDocs > 0
|
||||||
|
|
||||||
return null
|
return (
|
||||||
|
<span
|
||||||
|
className={[`${baseClass}__count`, hasVersions ? `${baseClass}__count--has-count` : '']
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
>
|
||||||
|
{hasVersions ? versions.totalDocs.toString() : <Fragment> </Fragment>}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { KeyboardEventHandler } from 'react'
|
|||||||
|
|
||||||
import { arrayMove } from '@dnd-kit/sortable'
|
import { arrayMove } from '@dnd-kit/sortable'
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import React from 'react'
|
import React, { useEffect, useId } from 'react'
|
||||||
import Select from 'react-select'
|
import Select from 'react-select'
|
||||||
import CreatableSelect from 'react-select/creatable'
|
import CreatableSelect from 'react-select/creatable'
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ export type { Option } from './types.js'
|
|||||||
|
|
||||||
import { useTranslation } from '../../providers/Translation/index.js'
|
import { useTranslation } from '../../providers/Translation/index.js'
|
||||||
import { DraggableSortable } from '../DraggableSortable/index.js'
|
import { DraggableSortable } from '../DraggableSortable/index.js'
|
||||||
|
import { ShimmerEffect } from '../ShimmerEffect/index.js'
|
||||||
import { ClearIndicator } from './ClearIndicator/index.js'
|
import { ClearIndicator } from './ClearIndicator/index.js'
|
||||||
import { Control } from './Control/index.js'
|
import { Control } from './Control/index.js'
|
||||||
import { DropdownIndicator } from './DropdownIndicator/index.js'
|
import { DropdownIndicator } from './DropdownIndicator/index.js'
|
||||||
@@ -31,6 +32,12 @@ const createOption = (label: string) => ({
|
|||||||
const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
|
const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
|
||||||
const { i18n, t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
const [inputValue, setInputValue] = React.useState('') // for creatable select
|
const [inputValue, setInputValue] = React.useState('') // for creatable select
|
||||||
|
const uuid = useId()
|
||||||
|
const [hasMounted, setHasMounted] = React.useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHasMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
@@ -60,6 +67,10 @@ const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')
|
.join(' ')
|
||||||
|
|
||||||
|
if (!hasMounted) {
|
||||||
|
return <ShimmerEffect height="calc(var(--base) * 2 + 2px)" />
|
||||||
|
}
|
||||||
|
|
||||||
if (!isCreatable) {
|
if (!isCreatable) {
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@@ -83,6 +94,7 @@ const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
|
|||||||
}}
|
}}
|
||||||
filterOption={filterOption}
|
filterOption={filterOption}
|
||||||
getOptionValue={getOptionValue}
|
getOptionValue={getOptionValue}
|
||||||
|
instanceId={uuid}
|
||||||
isClearable={isClearable}
|
isClearable={isClearable}
|
||||||
isDisabled={disabled}
|
isDisabled={disabled}
|
||||||
isSearchable={isSearchable}
|
isSearchable={isSearchable}
|
||||||
@@ -154,6 +166,7 @@ const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
|
|||||||
}}
|
}}
|
||||||
filterOption={filterOption}
|
filterOption={filterOption}
|
||||||
inputValue={inputValue}
|
inputValue={inputValue}
|
||||||
|
instanceId={uuid}
|
||||||
isClearable={isClearable}
|
isClearable={isClearable}
|
||||||
isDisabled={disabled}
|
isDisabled={disabled}
|
||||||
isSearchable={isSearchable}
|
isSearchable={isSearchable}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import React from 'react'
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
||||||
import { IDLabel } from '../IDLabel/index.js'
|
import { IDLabel } from '../IDLabel/index.js'
|
||||||
@@ -18,9 +18,7 @@ export type RenderTitleProps = {
|
|||||||
export const RenderTitle: React.FC<RenderTitleProps> = (props) => {
|
export const RenderTitle: React.FC<RenderTitleProps> = (props) => {
|
||||||
const { className, element = 'h1', fallback, title: titleFromProps } = props
|
const { className, element = 'h1', fallback, title: titleFromProps } = props
|
||||||
|
|
||||||
const documentInfo = useDocumentInfo()
|
const { id, isInitializing, title: titleFromContext } = useDocumentInfo()
|
||||||
|
|
||||||
const { id, title: titleFromContext } = documentInfo
|
|
||||||
|
|
||||||
const title = titleFromProps || titleFromContext || fallback
|
const title = titleFromProps || titleFromContext || fallback
|
||||||
|
|
||||||
@@ -28,6 +26,9 @@ export const RenderTitle: React.FC<RenderTitleProps> = (props) => {
|
|||||||
|
|
||||||
const Tag = element
|
const Tag = element
|
||||||
|
|
||||||
|
// Render and invisible character to prevent layout shift when the title populates from context
|
||||||
|
const EmptySpace = <Fragment> </Fragment>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
className={[className, baseClass, idAsTitle && `${baseClass}--has-id`]
|
className={[className, baseClass, idAsTitle && `${baseClass}--has-id`]
|
||||||
@@ -35,7 +36,13 @@ export const RenderTitle: React.FC<RenderTitleProps> = (props) => {
|
|||||||
.join(' ')}
|
.join(' ')}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
{idAsTitle ? <IDLabel className={`${baseClass}__id`} id={id} /> : title || null}
|
{isInitializing ? (
|
||||||
|
EmptySpace
|
||||||
|
) : (
|
||||||
|
<Fragment>
|
||||||
|
{idAsTitle ? <IDLabel className={`${baseClass}__id`} id={id} /> : title || EmptySpace}
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
</Tag>
|
</Tag>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
|
|||||||
} = props
|
} = props
|
||||||
|
|
||||||
const { indexPath, readOnly: readOnlyFromContext } = useFieldProps()
|
const { indexPath, readOnly: readOnlyFromContext } = useFieldProps()
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
|
||||||
const minRows = minRowsProp ?? required ? 1 : 0
|
const minRows = minRowsProp ?? required ? 1 : 0
|
||||||
|
|
||||||
const { setDocFieldPreferences } = useDocumentInfo()
|
const { setDocFieldPreferences } = useDocumentInfo()
|
||||||
@@ -116,6 +115,8 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
errorPaths,
|
errorPaths,
|
||||||
|
formInitializing,
|
||||||
|
formProcessing,
|
||||||
path,
|
path,
|
||||||
rows = [],
|
rows = [],
|
||||||
schemaPath,
|
schemaPath,
|
||||||
@@ -128,6 +129,8 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
|
|||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing
|
||||||
|
|
||||||
const addRow = useCallback(
|
const addRow = useCallback(
|
||||||
async (rowIndex: number) => {
|
async (rowIndex: number) => {
|
||||||
await addFieldRow({ path, rowIndex, schemaPath })
|
await addFieldRow({ path, rowIndex, schemaPath })
|
||||||
@@ -187,7 +190,7 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
|
|||||||
const fieldErrorCount = errorPaths.length
|
const fieldErrorCount = errorPaths.length
|
||||||
const fieldHasErrors = submitted && errorPaths.length > 0
|
const fieldHasErrors = submitted && errorPaths.length > 0
|
||||||
|
|
||||||
const showRequired = readOnly && rows.length === 0
|
const showRequired = disabled && rows.length === 0
|
||||||
const showMinRows = rows.length < minRows || (required && rows.length === 0)
|
const showMinRows = rows.length < minRows || (required && rows.length === 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -257,7 +260,7 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
|
|||||||
errorPath.startsWith(`${path}.${i}.`),
|
errorPath.startsWith(`${path}.${i}.`),
|
||||||
).length
|
).length
|
||||||
return (
|
return (
|
||||||
<DraggableSortableItem disabled={readOnly || !isSortable} id={row.id} key={row.id}>
|
<DraggableSortableItem disabled={disabled || !isSortable} id={row.id} key={row.id}>
|
||||||
{(draggableSortableItemProps) => (
|
{(draggableSortableItemProps) => (
|
||||||
<ArrayRow
|
<ArrayRow
|
||||||
{...draggableSortableItemProps}
|
{...draggableSortableItemProps}
|
||||||
@@ -274,7 +277,7 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
|
|||||||
moveRow={moveRow}
|
moveRow={moveRow}
|
||||||
path={path}
|
path={path}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
readOnly={readOnly}
|
readOnly={disabled}
|
||||||
removeRow={removeRow}
|
removeRow={removeRow}
|
||||||
row={row}
|
row={row}
|
||||||
rowCount={rows.length}
|
rowCount={rows.length}
|
||||||
@@ -307,7 +310,7 @@ export const _ArrayField: React.FC<ArrayFieldProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</DraggableSortable>
|
</DraggableSortable>
|
||||||
)}
|
)}
|
||||||
{!readOnly && !hasMaxRows && (
|
{!disabled && !hasMaxRows && (
|
||||||
<Button
|
<Button
|
||||||
buttonStyle="icon-label"
|
buttonStyle="icon-label"
|
||||||
className={`${baseClass}__add-row`}
|
className={`${baseClass}__add-row`}
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ const _BlocksField: React.FC<BlocksFieldProps> = (props) => {
|
|||||||
} = props
|
} = props
|
||||||
|
|
||||||
const { indexPath, readOnly: readOnlyFromContext } = useFieldProps()
|
const { indexPath, readOnly: readOnlyFromContext } = useFieldProps()
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
|
||||||
const minRows = minRowsProp ?? required ? 1 : 0
|
const minRows = minRowsProp ?? required ? 1 : 0
|
||||||
|
|
||||||
const { setDocFieldPreferences } = useDocumentInfo()
|
const { setDocFieldPreferences } = useDocumentInfo()
|
||||||
@@ -118,6 +117,8 @@ const _BlocksField: React.FC<BlocksFieldProps> = (props) => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
errorPaths,
|
errorPaths,
|
||||||
|
formInitializing,
|
||||||
|
formProcessing,
|
||||||
path,
|
path,
|
||||||
permissions,
|
permissions,
|
||||||
rows = [],
|
rows = [],
|
||||||
@@ -131,6 +132,8 @@ const _BlocksField: React.FC<BlocksFieldProps> = (props) => {
|
|||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing
|
||||||
|
|
||||||
const addRow = useCallback(
|
const addRow = useCallback(
|
||||||
async (rowIndex: number, blockType: string) => {
|
async (rowIndex: number, blockType: string) => {
|
||||||
await addFieldRow({
|
await addFieldRow({
|
||||||
@@ -201,7 +204,7 @@ const _BlocksField: React.FC<BlocksFieldProps> = (props) => {
|
|||||||
const fieldHasErrors = submitted && fieldErrorCount + (valid ? 0 : 1) > 0
|
const fieldHasErrors = submitted && fieldErrorCount + (valid ? 0 : 1) > 0
|
||||||
|
|
||||||
const showMinRows = rows.length < minRows || (required && rows.length === 0)
|
const showMinRows = rows.length < minRows || (required && rows.length === 0)
|
||||||
const showRequired = readOnly && rows.length === 0
|
const showRequired = disabled && rows.length === 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -274,7 +277,7 @@ const _BlocksField: React.FC<BlocksFieldProps> = (props) => {
|
|||||||
errorPath.startsWith(`${path}.${i}`),
|
errorPath.startsWith(`${path}.${i}`),
|
||||||
).length
|
).length
|
||||||
return (
|
return (
|
||||||
<DraggableSortableItem disabled={readOnly || !isSortable} id={row.id} key={row.id}>
|
<DraggableSortableItem disabled={disabled || !isSortable} id={row.id} key={row.id}>
|
||||||
{(draggableSortableItemProps) => (
|
{(draggableSortableItemProps) => (
|
||||||
<BlockRow
|
<BlockRow
|
||||||
{...draggableSortableItemProps}
|
{...draggableSortableItemProps}
|
||||||
@@ -291,7 +294,7 @@ const _BlocksField: React.FC<BlocksFieldProps> = (props) => {
|
|||||||
moveRow={moveRow}
|
moveRow={moveRow}
|
||||||
path={path}
|
path={path}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
readOnly={readOnly}
|
readOnly={disabled}
|
||||||
removeRow={removeRow}
|
removeRow={removeRow}
|
||||||
row={row}
|
row={row}
|
||||||
rowCount={rows.length}
|
rowCount={rows.length}
|
||||||
@@ -327,7 +330,7 @@ const _BlocksField: React.FC<BlocksFieldProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</DraggableSortable>
|
</DraggableSortable>
|
||||||
)}
|
)}
|
||||||
{!readOnly && !hasMaxRows && (
|
{!disabled && !hasMaxRows && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<DrawerToggler className={`${baseClass}__drawer-toggler`} slug={drawerSlug}>
|
<DrawerToggler className={`${baseClass}__drawer-toggler`} slug={drawerSlug}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -62,20 +62,21 @@ const CheckboxField: React.FC<CheckboxFieldProps> = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
|
||||||
|
|
||||||
const { path, setValue, showError, value } = useField({
|
const { formInitializing, formProcessing, path, setValue, showError, value } = useField({
|
||||||
disableFormData,
|
disableFormData,
|
||||||
path: pathFromContext || pathFromProps || name,
|
path: pathFromContext || pathFromProps || name,
|
||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing
|
||||||
|
|
||||||
const onToggle = useCallback(() => {
|
const onToggle = useCallback(() => {
|
||||||
if (!readOnly) {
|
if (!disabled) {
|
||||||
setValue(!value)
|
setValue(!value)
|
||||||
if (typeof onChangeFromProps === 'function') onChangeFromProps(!value)
|
if (typeof onChangeFromProps === 'function') onChangeFromProps(!value)
|
||||||
}
|
}
|
||||||
}, [onChangeFromProps, readOnly, setValue, value])
|
}, [onChangeFromProps, disabled, setValue, value])
|
||||||
|
|
||||||
const checked = checkedFromProps || Boolean(value)
|
const checked = checkedFromProps || Boolean(value)
|
||||||
|
|
||||||
@@ -89,7 +90,7 @@ const CheckboxField: React.FC<CheckboxFieldProps> = (props) => {
|
|||||||
showError && 'error',
|
showError && 'error',
|
||||||
className,
|
className,
|
||||||
value && `${baseClass}--checked`,
|
value && `${baseClass}--checked`,
|
||||||
readOnly && `${baseClass}--read-only`,
|
disabled && `${baseClass}--read-only`,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
@@ -111,7 +112,7 @@ const CheckboxField: React.FC<CheckboxFieldProps> = (props) => {
|
|||||||
name={path}
|
name={path}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
partialChecked={partialChecked}
|
partialChecked={partialChecked}
|
||||||
readOnly={readOnly}
|
readOnly={disabled}
|
||||||
required={required}
|
required={required}
|
||||||
/>
|
/>
|
||||||
{CustomDescription !== undefined ? (
|
{CustomDescription !== undefined ? (
|
||||||
|
|||||||
@@ -64,13 +64,14 @@ const CodeField: React.FC<CodeFieldProps> = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
|
||||||
|
|
||||||
const { path, setValue, showError, value } = useField({
|
const { formInitializing, formProcessing, path, setValue, showError, value } = useField({
|
||||||
path: pathFromContext || pathFromProps || name,
|
path: pathFromContext || pathFromProps || name,
|
||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
@@ -78,7 +79,7 @@ const CodeField: React.FC<CodeFieldProps> = (props) => {
|
|||||||
baseClass,
|
baseClass,
|
||||||
className,
|
className,
|
||||||
showError && 'error',
|
showError && 'error',
|
||||||
readOnly && 'read-only',
|
disabled && 'read-only',
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
@@ -98,9 +99,9 @@ const CodeField: React.FC<CodeFieldProps> = (props) => {
|
|||||||
{BeforeInput}
|
{BeforeInput}
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
defaultLanguage={prismToMonacoLanguageMap[language] || language}
|
defaultLanguage={prismToMonacoLanguageMap[language] || language}
|
||||||
onChange={readOnly ? () => null : (val) => setValue(val)}
|
onChange={disabled ? () => null : (val) => setValue(val)}
|
||||||
options={editorOptions}
|
options={editorOptions}
|
||||||
readOnly={readOnly}
|
readOnly={disabled}
|
||||||
value={(value as string) || ''}
|
value={(value as string) || ''}
|
||||||
/>
|
/>
|
||||||
{AfterInput}
|
{AfterInput}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import type { FieldMap } from '../../providers/ComponentMap/buildComponentMap/ty
|
|||||||
import type { FormFieldBase } from '../shared/index.js'
|
import type { FormFieldBase } from '../shared/index.js'
|
||||||
|
|
||||||
import { FieldDescription } from '../../forms/FieldDescription/index.js'
|
import { FieldDescription } from '../../forms/FieldDescription/index.js'
|
||||||
|
import { useFormInitializing, useFormProcessing } from '../../forms/Form/context.js'
|
||||||
|
|
||||||
export type CollapsibleFieldProps = FormFieldBase & {
|
export type CollapsibleFieldProps = FormFieldBase & {
|
||||||
fieldMap: FieldMap
|
fieldMap: FieldMap
|
||||||
@@ -52,6 +53,10 @@ const CollapsibleField: React.FC<CollapsibleFieldProps> = (props) => {
|
|||||||
schemaPath,
|
schemaPath,
|
||||||
siblingPermissions,
|
siblingPermissions,
|
||||||
} = useFieldProps()
|
} = useFieldProps()
|
||||||
|
|
||||||
|
const formInitializing = useFormInitializing()
|
||||||
|
const formProcessing = useFormProcessing()
|
||||||
|
|
||||||
const path = pathFromContext || pathFromProps
|
const path = pathFromContext || pathFromProps
|
||||||
|
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
@@ -117,7 +122,7 @@ const CollapsibleField: React.FC<CollapsibleFieldProps> = (props) => {
|
|||||||
|
|
||||||
if (typeof collapsedOnMount !== 'boolean') return null
|
if (typeof collapsedOnMount !== 'boolean') return null
|
||||||
|
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -152,7 +157,7 @@ const CollapsibleField: React.FC<CollapsibleFieldProps> = (props) => {
|
|||||||
margins="small"
|
margins="small"
|
||||||
path={path}
|
path={path}
|
||||||
permissions={siblingPermissions}
|
permissions={siblingPermissions}
|
||||||
readOnly={readOnly}
|
readOnly={disabled}
|
||||||
schemaPath={schemaPath}
|
schemaPath={schemaPath}
|
||||||
/>
|
/>
|
||||||
</CollapsibleElement>
|
</CollapsibleElement>
|
||||||
|
|||||||
@@ -67,12 +67,12 @@ const DateTimeField: React.FC<DateFieldProps> = (props) => {
|
|||||||
|
|
||||||
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
||||||
|
|
||||||
const { path, setValue, showError, value } = useField<Date>({
|
const { formInitializing, formProcessing, path, setValue, showError, value } = useField<Date>({
|
||||||
path: pathFromContext || pathFromProps || name,
|
path: pathFromContext || pathFromProps || name,
|
||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
})
|
||||||
|
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -81,7 +81,7 @@ const DateTimeField: React.FC<DateFieldProps> = (props) => {
|
|||||||
baseClass,
|
baseClass,
|
||||||
className,
|
className,
|
||||||
showError && `${baseClass}--has-error`,
|
showError && `${baseClass}--has-error`,
|
||||||
readOnly && 'read-only',
|
disabled && 'read-only',
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
@@ -102,10 +102,10 @@ const DateTimeField: React.FC<DateFieldProps> = (props) => {
|
|||||||
<DatePickerField
|
<DatePickerField
|
||||||
{...datePickerProps}
|
{...datePickerProps}
|
||||||
onChange={(incomingDate) => {
|
onChange={(incomingDate) => {
|
||||||
if (!readOnly) setValue(incomingDate?.toISOString() || null)
|
if (!disabled) setValue(incomingDate?.toISOString() || null)
|
||||||
}}
|
}}
|
||||||
placeholder={getTranslation(placeholder, i18n)}
|
placeholder={getTranslation(placeholder, i18n)}
|
||||||
readOnly={readOnly}
|
readOnly={disabled}
|
||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
{AfterInput}
|
{AfterInput}
|
||||||
|
|||||||
@@ -60,16 +60,17 @@ const EmailField: React.FC<EmailFieldProps> = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
|
||||||
|
|
||||||
const { path, setValue, showError, value } = useField({
|
const { formInitializing, formProcessing, path, setValue, showError, value } = useField({
|
||||||
path: pathFromContext || pathFromProps || name,
|
path: pathFromContext || pathFromProps || name,
|
||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[fieldBaseClass, 'email', className, showError && 'error', readOnly && 'read-only']
|
className={[fieldBaseClass, 'email', className, showError && 'error', disabled && 'read-only']
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
style={{
|
style={{
|
||||||
@@ -88,7 +89,7 @@ const EmailField: React.FC<EmailFieldProps> = (props) => {
|
|||||||
{BeforeInput}
|
{BeforeInput}
|
||||||
<input
|
<input
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
disabled={readOnly}
|
disabled={disabled}
|
||||||
id={`field-${path.replace(/\./g, '__')}`}
|
id={`field-${path.replace(/\./g, '__')}`}
|
||||||
name={path}
|
name={path}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import { useCollapsible } from '../../elements/Collapsible/provider.js'
|
|||||||
import { ErrorPill } from '../../elements/ErrorPill/index.js'
|
import { ErrorPill } from '../../elements/ErrorPill/index.js'
|
||||||
import { FieldDescription } from '../../forms/FieldDescription/index.js'
|
import { FieldDescription } from '../../forms/FieldDescription/index.js'
|
||||||
import { useFieldProps } from '../../forms/FieldPropsProvider/index.js'
|
import { useFieldProps } from '../../forms/FieldPropsProvider/index.js'
|
||||||
import { useFormSubmitted } from '../../forms/Form/context.js'
|
import {
|
||||||
|
useFormInitializing,
|
||||||
|
useFormProcessing,
|
||||||
|
useFormSubmitted,
|
||||||
|
} from '../../forms/Form/context.js'
|
||||||
import { RenderFields } from '../../forms/RenderFields/index.js'
|
import { RenderFields } from '../../forms/RenderFields/index.js'
|
||||||
import { useField } from '../../forms/useField/index.js'
|
import { useField } from '../../forms/useField/index.js'
|
||||||
import { withCondition } from '../../forms/withCondition/index.js'
|
import { withCondition } from '../../forms/withCondition/index.js'
|
||||||
@@ -54,10 +58,12 @@ const GroupField: React.FC<GroupFieldProps> = (props) => {
|
|||||||
const isWithinRow = useRow()
|
const isWithinRow = useRow()
|
||||||
const isWithinTab = useTabs()
|
const isWithinTab = useTabs()
|
||||||
const { errorPaths } = useField({ path })
|
const { errorPaths } = useField({ path })
|
||||||
|
const formInitializing = useFormInitializing()
|
||||||
|
const formProcessing = useFormProcessing()
|
||||||
const submitted = useFormSubmitted()
|
const submitted = useFormSubmitted()
|
||||||
const errorCount = errorPaths.length
|
const errorCount = errorPaths.length
|
||||||
const fieldHasErrors = submitted && errorCount > 0
|
const fieldHasErrors = submitted && errorCount > 0
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing
|
||||||
|
|
||||||
const isTopLevel = !(isWithinCollapsible || isWithinGroup || isWithinRow)
|
const isTopLevel = !(isWithinCollapsible || isWithinGroup || isWithinRow)
|
||||||
|
|
||||||
@@ -108,7 +114,7 @@ const GroupField: React.FC<GroupFieldProps> = (props) => {
|
|||||||
margins="small"
|
margins="small"
|
||||||
path={path}
|
path={path}
|
||||||
permissions={permissions?.fields}
|
permissions={permissions?.fields}
|
||||||
readOnly={readOnly}
|
readOnly={disabled}
|
||||||
schemaPath={schemaPath}
|
schemaPath={schemaPath}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -64,13 +64,15 @@ const JSONFieldComponent: React.FC<JSONFieldProps> = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
|
||||||
|
|
||||||
const { initialValue, path, setValue, showError, value } = useField<string>({
|
const { formInitializing, formProcessing, initialValue, path, setValue, showError, value } =
|
||||||
|
useField<string>({
|
||||||
path: pathFromContext || pathFromProps || name,
|
path: pathFromContext || pathFromProps || name,
|
||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing
|
||||||
|
|
||||||
const handleMount = useCallback(
|
const handleMount = useCallback(
|
||||||
(editor, monaco) => {
|
(editor, monaco) => {
|
||||||
if (!jsonSchema) return
|
if (!jsonSchema) return
|
||||||
@@ -92,7 +94,7 @@ const JSONFieldComponent: React.FC<JSONFieldProps> = (props) => {
|
|||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(val) => {
|
(val) => {
|
||||||
if (readOnly) return
|
if (disabled) return
|
||||||
setStringValue(val)
|
setStringValue(val)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -103,14 +105,16 @@ const JSONFieldComponent: React.FC<JSONFieldProps> = (props) => {
|
|||||||
setJsonError(e)
|
setJsonError(e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[readOnly, setValue, setStringValue],
|
[disabled, setValue, setStringValue],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasLoadedValue) return
|
if (hasLoadedValue || value === undefined) return
|
||||||
|
|
||||||
setStringValue(
|
setStringValue(
|
||||||
value || initialValue ? JSON.stringify(value ? value : initialValue, null, 2) : '',
|
value || initialValue ? JSON.stringify(value ? value : initialValue, null, 2) : '',
|
||||||
)
|
)
|
||||||
|
|
||||||
setHasLoadedValue(true)
|
setHasLoadedValue(true)
|
||||||
}, [initialValue, value, hasLoadedValue])
|
}, [initialValue, value, hasLoadedValue])
|
||||||
|
|
||||||
@@ -121,7 +125,7 @@ const JSONFieldComponent: React.FC<JSONFieldProps> = (props) => {
|
|||||||
baseClass,
|
baseClass,
|
||||||
className,
|
className,
|
||||||
showError && 'error',
|
showError && 'error',
|
||||||
readOnly && 'read-only',
|
disabled && 'read-only',
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
@@ -145,7 +149,7 @@ const JSONFieldComponent: React.FC<JSONFieldProps> = (props) => {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onMount={handleMount}
|
onMount={handleMount}
|
||||||
options={editorOptions}
|
options={editorOptions}
|
||||||
readOnly={readOnly}
|
readOnly={disabled}
|
||||||
value={stringValue}
|
value={stringValue}
|
||||||
/>
|
/>
|
||||||
{AfterInput}
|
{AfterInput}
|
||||||
|
|||||||
@@ -73,13 +73,16 @@ const NumberFieldComponent: React.FC<NumberFieldProps> = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
|
||||||
|
|
||||||
const { path, setValue, showError, value } = useField<number | number[]>({
|
const { formInitializing, formProcessing, path, setValue, showError, value } = useField<
|
||||||
|
number | number[]
|
||||||
|
>({
|
||||||
path: pathFromContext || pathFromProps || name,
|
path: pathFromContext || pathFromProps || name,
|
||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
const val = parseFloat(e.target.value)
|
const val = parseFloat(e.target.value)
|
||||||
@@ -104,7 +107,7 @@ const NumberFieldComponent: React.FC<NumberFieldProps> = (props) => {
|
|||||||
|
|
||||||
const handleHasManyChange = useCallback(
|
const handleHasManyChange = useCallback(
|
||||||
(selectedOption) => {
|
(selectedOption) => {
|
||||||
if (!readOnly) {
|
if (!disabled) {
|
||||||
let newValue
|
let newValue
|
||||||
if (!selectedOption) {
|
if (!selectedOption) {
|
||||||
newValue = []
|
newValue = []
|
||||||
@@ -117,7 +120,7 @@ const NumberFieldComponent: React.FC<NumberFieldProps> = (props) => {
|
|||||||
setValue(newValue)
|
setValue(newValue)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[readOnly, setValue],
|
[disabled, setValue],
|
||||||
)
|
)
|
||||||
|
|
||||||
// useEffect update valueToRender:
|
// useEffect update valueToRender:
|
||||||
@@ -145,7 +148,7 @@ const NumberFieldComponent: React.FC<NumberFieldProps> = (props) => {
|
|||||||
'number',
|
'number',
|
||||||
className,
|
className,
|
||||||
showError && 'error',
|
showError && 'error',
|
||||||
readOnly && 'read-only',
|
disabled && 'read-only',
|
||||||
hasMany && 'has-many',
|
hasMany && 'has-many',
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -166,7 +169,7 @@ const NumberFieldComponent: React.FC<NumberFieldProps> = (props) => {
|
|||||||
{hasMany ? (
|
{hasMany ? (
|
||||||
<ReactSelect
|
<ReactSelect
|
||||||
className={`field-${path.replace(/\./g, '__')}`}
|
className={`field-${path.replace(/\./g, '__')}`}
|
||||||
disabled={readOnly}
|
disabled={disabled}
|
||||||
filterOption={(_, rawInput) => {
|
filterOption={(_, rawInput) => {
|
||||||
// eslint-disable-next-line no-restricted-globals
|
// eslint-disable-next-line no-restricted-globals
|
||||||
const isOverHasMany = Array.isArray(value) && value.length >= maxRows
|
const isOverHasMany = Array.isArray(value) && value.length >= maxRows
|
||||||
@@ -194,7 +197,7 @@ const NumberFieldComponent: React.FC<NumberFieldProps> = (props) => {
|
|||||||
<div>
|
<div>
|
||||||
{BeforeInput}
|
{BeforeInput}
|
||||||
<input
|
<input
|
||||||
disabled={readOnly}
|
disabled={disabled}
|
||||||
id={`field-${path.replace(/\./g, '__')}`}
|
id={`field-${path.replace(/\./g, '__')}`}
|
||||||
max={max}
|
max={max}
|
||||||
min={min}
|
min={min}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const PasswordField: React.FC<PasswordFieldProps> = (props) => {
|
|||||||
CustomLabel,
|
CustomLabel,
|
||||||
autoComplete,
|
autoComplete,
|
||||||
className,
|
className,
|
||||||
disabled,
|
disabled: disabledFromProps,
|
||||||
errorProps,
|
errorProps,
|
||||||
label,
|
label,
|
||||||
labelProps,
|
labelProps,
|
||||||
@@ -52,14 +52,22 @@ const PasswordField: React.FC<PasswordFieldProps> = (props) => {
|
|||||||
[validate, required],
|
[validate, required],
|
||||||
)
|
)
|
||||||
|
|
||||||
const { formProcessing, path, setValue, showError, value } = useField({
|
const { formInitializing, formProcessing, path, setValue, showError, value } = useField({
|
||||||
path: pathFromProps || name,
|
path: pathFromProps || name,
|
||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const disabled = disabledFromProps || formInitializing || formProcessing
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[fieldBaseClass, 'password', className, showError && 'error']
|
className={[
|
||||||
|
fieldBaseClass,
|
||||||
|
'password',
|
||||||
|
className,
|
||||||
|
showError && 'error',
|
||||||
|
disabled && 'read-only',
|
||||||
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
style={{
|
style={{
|
||||||
@@ -75,10 +83,9 @@ const PasswordField: React.FC<PasswordFieldProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
<div className={`${fieldBaseClass}__wrap`}>
|
<div className={`${fieldBaseClass}__wrap`}>
|
||||||
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
|
<FieldError CustomError={CustomError} path={path} {...(errorProps || {})} />
|
||||||
|
|
||||||
<input
|
<input
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
disabled={formProcessing || disabled}
|
disabled={disabled}
|
||||||
id={`field-${path.replace(/\./g, '__')}`}
|
id={`field-${path.replace(/\./g, '__')}`}
|
||||||
name={path}
|
name={path}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
|
|||||||
@@ -68,9 +68,10 @@ const RadioGroupField: React.FC<RadioFieldProps> = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
formInitializing,
|
||||||
|
formProcessing,
|
||||||
path,
|
path,
|
||||||
setValue,
|
setValue,
|
||||||
showError,
|
showError,
|
||||||
@@ -80,6 +81,8 @@ const RadioGroupField: React.FC<RadioFieldProps> = (props) => {
|
|||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing
|
||||||
|
|
||||||
const value = valueFromContext || valueFromProps
|
const value = valueFromContext || valueFromProps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -90,7 +93,7 @@ const RadioGroupField: React.FC<RadioFieldProps> = (props) => {
|
|||||||
className,
|
className,
|
||||||
`${baseClass}--layout-${layout}`,
|
`${baseClass}--layout-${layout}`,
|
||||||
showError && 'error',
|
showError && 'error',
|
||||||
readOnly && `${baseClass}--read-only`,
|
disabled && `${baseClass}--read-only`,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
@@ -132,13 +135,13 @@ const RadioGroupField: React.FC<RadioFieldProps> = (props) => {
|
|||||||
onChangeFromProps(optionValue)
|
onChangeFromProps(optionValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!readOnly) {
|
if (!disabled) {
|
||||||
setValue(optionValue)
|
setValue(optionValue)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
option={optionIsObject(option) ? option : { label: option, value: option }}
|
option={optionIsObject(option) ? option : { label: option, value: option }}
|
||||||
path={path}
|
path={path}
|
||||||
readOnly={readOnly}
|
readOnly={disabled}
|
||||||
uuid={uuid}
|
uuid={uuid}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { FieldDescription } from '../../forms/FieldDescription/index.js'
|
|||||||
import { FieldError } from '../../forms/FieldError/index.js'
|
import { FieldError } from '../../forms/FieldError/index.js'
|
||||||
import { FieldLabel } from '../../forms/FieldLabel/index.js'
|
import { FieldLabel } from '../../forms/FieldLabel/index.js'
|
||||||
import { useFieldProps } from '../../forms/FieldPropsProvider/index.js'
|
import { useFieldProps } from '../../forms/FieldPropsProvider/index.js'
|
||||||
import { useFormProcessing } from '../../forms/Form/context.js'
|
|
||||||
import { useField } from '../../forms/useField/index.js'
|
import { useField } from '../../forms/useField/index.js'
|
||||||
import { withCondition } from '../../forms/withCondition/index.js'
|
import { withCondition } from '../../forms/withCondition/index.js'
|
||||||
import { useDebouncedCallback } from '../../hooks/useDebouncedCallback.js'
|
import { useDebouncedCallback } from '../../hooks/useDebouncedCallback.js'
|
||||||
@@ -72,7 +71,6 @@ const RelationshipField: React.FC<RelationshipFieldProps> = (props) => {
|
|||||||
const { i18n, t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
const { permissions } = useAuth()
|
const { permissions } = useAuth()
|
||||||
const { code: locale } = useLocale()
|
const { code: locale } = useLocale()
|
||||||
const formProcessing = useFormProcessing()
|
|
||||||
const hasMultipleRelations = Array.isArray(relationTo)
|
const hasMultipleRelations = Array.isArray(relationTo)
|
||||||
const [options, dispatchOptions] = useReducer(optionsReducer, [])
|
const [options, dispatchOptions] = useReducer(optionsReducer, [])
|
||||||
const [lastFullyLoadedRelation, setLastFullyLoadedRelation] = useState(-1)
|
const [lastFullyLoadedRelation, setLastFullyLoadedRelation] = useState(-1)
|
||||||
@@ -93,15 +91,23 @@ const RelationshipField: React.FC<RelationshipFieldProps> = (props) => {
|
|||||||
[validate, required],
|
[validate, required],
|
||||||
)
|
)
|
||||||
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
|
||||||
|
|
||||||
const { filterOptions, initialValue, path, setValue, showError, value } = useField<
|
const {
|
||||||
Value | Value[]
|
filterOptions,
|
||||||
>({
|
formInitializing,
|
||||||
|
formProcessing,
|
||||||
|
initialValue,
|
||||||
|
path,
|
||||||
|
setValue,
|
||||||
|
showError,
|
||||||
|
value,
|
||||||
|
} = useField<Value | Value[]>({
|
||||||
path: pathFromContext || pathFromProps || name,
|
path: pathFromContext || pathFromProps || name,
|
||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const readOnly = readOnlyFromProps || readOnlyFromContext || formInitializing
|
||||||
|
|
||||||
const valueRef = useRef(value)
|
const valueRef = useRef(value)
|
||||||
valueRef.current = value
|
valueRef.current = value
|
||||||
|
|
||||||
|
|||||||
@@ -73,16 +73,17 @@ const SelectField: React.FC<SelectFieldProps> = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
|
||||||
|
|
||||||
const { path, setValue, showError, value } = useField({
|
const { formInitializing, formProcessing, path, setValue, showError, value } = useField({
|
||||||
path: pathFromContext || pathFromProps || name,
|
path: pathFromContext || pathFromProps || name,
|
||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(selectedOption) => {
|
(selectedOption) => {
|
||||||
if (!readOnly) {
|
if (!disabled) {
|
||||||
let newValue
|
let newValue
|
||||||
if (!selectedOption) {
|
if (!selectedOption) {
|
||||||
newValue = null
|
newValue = null
|
||||||
@@ -103,7 +104,7 @@ const SelectField: React.FC<SelectFieldProps> = (props) => {
|
|||||||
setValue(newValue)
|
setValue(newValue)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[readOnly, hasMany, setValue, onChangeFromProps],
|
[disabled, hasMany, setValue, onChangeFromProps],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -125,7 +126,7 @@ const SelectField: React.FC<SelectFieldProps> = (props) => {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
options={options}
|
options={options}
|
||||||
path={path}
|
path={path}
|
||||||
readOnly={readOnly}
|
readOnly={disabled}
|
||||||
required={required}
|
required={required}
|
||||||
showError={showError}
|
showError={showError}
|
||||||
style={style}
|
style={style}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const TabsField: React.FC<TabsFieldProps> = (props) => {
|
|||||||
readOnly: readOnlyFromContext,
|
readOnly: readOnlyFromContext,
|
||||||
schemaPath,
|
schemaPath,
|
||||||
} = useFieldProps()
|
} = useFieldProps()
|
||||||
|
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
const readOnly = readOnlyFromProps || readOnlyFromContext
|
||||||
const path = pathFromContext || pathFromProps || name
|
const path = pathFromContext || pathFromProps || name
|
||||||
const { getPreference, setPreference } = usePreferences()
|
const { getPreference, setPreference } = usePreferences()
|
||||||
|
|||||||
@@ -60,13 +60,14 @@ const TextField: React.FC<TextFieldProps> = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
|
||||||
|
|
||||||
const { formProcessing, path, setValue, showError, value } = useField({
|
const { formInitializing, formProcessing, path, setValue, showError, value } = useField({
|
||||||
path: pathFromContext || pathFromProps || name,
|
path: pathFromContext || pathFromProps || name,
|
||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing
|
||||||
|
|
||||||
const renderRTL = isFieldRTL({
|
const renderRTL = isFieldRTL({
|
||||||
fieldLocalized: localized,
|
fieldLocalized: localized,
|
||||||
fieldRTL: rtl,
|
fieldRTL: rtl,
|
||||||
@@ -80,7 +81,7 @@ const TextField: React.FC<TextFieldProps> = (props) => {
|
|||||||
|
|
||||||
const handleHasManyChange = useCallback(
|
const handleHasManyChange = useCallback(
|
||||||
(selectedOption) => {
|
(selectedOption) => {
|
||||||
if (!readOnly) {
|
if (!disabled) {
|
||||||
let newValue
|
let newValue
|
||||||
if (!selectedOption) {
|
if (!selectedOption) {
|
||||||
newValue = []
|
newValue = []
|
||||||
@@ -93,7 +94,7 @@ const TextField: React.FC<TextFieldProps> = (props) => {
|
|||||||
setValue(newValue)
|
setValue(newValue)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[readOnly, setValue],
|
[disabled, setValue],
|
||||||
)
|
)
|
||||||
|
|
||||||
// useEffect update valueToRender:
|
// useEffect update valueToRender:
|
||||||
@@ -140,7 +141,7 @@ const TextField: React.FC<TextFieldProps> = (props) => {
|
|||||||
}
|
}
|
||||||
path={path}
|
path={path}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
readOnly={formProcessing || readOnly}
|
readOnly={disabled}
|
||||||
required={required}
|
required={required}
|
||||||
rtl={renderRTL}
|
rtl={renderRTL}
|
||||||
showError={showError}
|
showError={showError}
|
||||||
|
|||||||
@@ -66,13 +66,14 @@ const TextareaField: React.FC<TextareaFieldProps> = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
|
||||||
|
|
||||||
const { path, setValue, showError, value } = useField<string>({
|
const { formInitializing, formProcessing, path, setValue, showError, value } = useField<string>({
|
||||||
path: pathFromContext || pathFromProps || name,
|
path: pathFromContext || pathFromProps || name,
|
||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextareaInput
|
<TextareaInput
|
||||||
AfterInput={AfterInput}
|
AfterInput={AfterInput}
|
||||||
@@ -90,7 +91,7 @@ const TextareaField: React.FC<TextareaFieldProps> = (props) => {
|
|||||||
}}
|
}}
|
||||||
path={path}
|
path={path}
|
||||||
placeholder={getTranslation(placeholder, i18n)}
|
placeholder={getTranslation(placeholder, i18n)}
|
||||||
readOnly={readOnly}
|
readOnly={disabled}
|
||||||
required={required}
|
required={required}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
rtl={isRTL}
|
rtl={isRTL}
|
||||||
|
|||||||
@@ -52,13 +52,15 @@ const _Upload: React.FC<UploadFieldProps> = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
const { path: pathFromContext, readOnly: readOnlyFromContext } = useFieldProps()
|
||||||
const readOnly = readOnlyFromProps || readOnlyFromContext
|
|
||||||
|
|
||||||
const { filterOptions, path, setValue, showError, value } = useField<string>({
|
const { filterOptions, formInitializing, formProcessing, path, setValue, showError, value } =
|
||||||
|
useField<string>({
|
||||||
path: pathFromContext || pathFromProps,
|
path: pathFromContext || pathFromProps,
|
||||||
validate: memoizedValidate,
|
validate: memoizedValidate,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(incomingValue) => {
|
(incomingValue) => {
|
||||||
const incomingID = incomingValue?.id || incomingValue
|
const incomingID = incomingValue?.id || incomingValue
|
||||||
@@ -83,7 +85,7 @@ const _Upload: React.FC<UploadFieldProps> = (props) => {
|
|||||||
labelProps={labelProps}
|
labelProps={labelProps}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
path={path}
|
path={path}
|
||||||
readOnly={readOnly}
|
readOnly={disabled}
|
||||||
relationTo={relationTo}
|
relationTo={relationTo}
|
||||||
required={required}
|
required={required}
|
||||||
serverURL={serverURL}
|
serverURL={serverURL}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export type Props = {
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
custom?: Record<any, string>
|
custom?: Record<any, string>
|
||||||
indexPath?: string
|
indexPath?: string
|
||||||
|
isForceRendered?: boolean
|
||||||
path: string
|
path: string
|
||||||
permissions?: FieldPermissions
|
permissions?: FieldPermissions
|
||||||
readOnly: boolean
|
readOnly: boolean
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const FormWatchContext = createContext({} as Context)
|
|||||||
const SubmittedContext = createContext(false)
|
const SubmittedContext = createContext(false)
|
||||||
const ProcessingContext = createContext(false)
|
const ProcessingContext = createContext(false)
|
||||||
const ModifiedContext = createContext(false)
|
const ModifiedContext = createContext(false)
|
||||||
|
const InitializingContext = createContext(false)
|
||||||
const FormFieldsContext = createSelectorContext<FormFieldsContextType>([{}, () => null])
|
const FormFieldsContext = createSelectorContext<FormFieldsContextType>([{}, () => null])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,6 +26,7 @@ const useWatchForm = (): Context => useContext(FormWatchContext)
|
|||||||
const useFormSubmitted = (): boolean => useContext(SubmittedContext)
|
const useFormSubmitted = (): boolean => useContext(SubmittedContext)
|
||||||
const useFormProcessing = (): boolean => useContext(ProcessingContext)
|
const useFormProcessing = (): boolean => useContext(ProcessingContext)
|
||||||
const useFormModified = (): boolean => useContext(ModifiedContext)
|
const useFormModified = (): boolean => useContext(ModifiedContext)
|
||||||
|
const useFormInitializing = (): boolean => useContext(InitializingContext)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get and set the value of a form field based on a selector
|
* Get and set the value of a form field based on a selector
|
||||||
@@ -46,12 +48,14 @@ export {
|
|||||||
FormContext,
|
FormContext,
|
||||||
FormFieldsContext,
|
FormFieldsContext,
|
||||||
FormWatchContext,
|
FormWatchContext,
|
||||||
|
InitializingContext,
|
||||||
ModifiedContext,
|
ModifiedContext,
|
||||||
ProcessingContext,
|
ProcessingContext,
|
||||||
SubmittedContext,
|
SubmittedContext,
|
||||||
useAllFormFields,
|
useAllFormFields,
|
||||||
useForm,
|
useForm,
|
||||||
useFormFields,
|
useFormFields,
|
||||||
|
useFormInitializing,
|
||||||
useFormModified,
|
useFormModified,
|
||||||
useFormProcessing,
|
useFormProcessing,
|
||||||
useFormSubmitted,
|
useFormSubmitted,
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ const { unflatten } = flatleyImport
|
|||||||
import { reduceFieldsToValues } from '../../utilities/reduceFieldsToValues.js'
|
import { reduceFieldsToValues } from '../../utilities/reduceFieldsToValues.js'
|
||||||
|
|
||||||
export const getSiblingData = (fields: FormState, path: string): Data => {
|
export const getSiblingData = (fields: FormState, path: string): Data => {
|
||||||
|
if (!fields) return null
|
||||||
|
|
||||||
if (path.indexOf('.') === -1) {
|
if (path.indexOf('.') === -1) {
|
||||||
return reduceFieldsToValues(fields, true)
|
return reduceFieldsToValues(fields, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const siblingFields = {}
|
const siblingFields = {}
|
||||||
|
|
||||||
// Determine if the last segment of the path is an array-based row
|
// Determine if the last segment of the path is an array-based row
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
FormContext,
|
FormContext,
|
||||||
FormFieldsContext,
|
FormFieldsContext,
|
||||||
FormWatchContext,
|
FormWatchContext,
|
||||||
|
InitializingContext,
|
||||||
ModifiedContext,
|
ModifiedContext,
|
||||||
ProcessingContext,
|
ProcessingContext,
|
||||||
SubmittedContext,
|
SubmittedContext,
|
||||||
@@ -60,6 +61,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
// fields: fieldsFromProps = collection?.fields || global?.fields,
|
// fields: fieldsFromProps = collection?.fields || global?.fields,
|
||||||
handleResponse,
|
handleResponse,
|
||||||
initialState, // fully formed initial field state
|
initialState, // fully formed initial field state
|
||||||
|
isInitializing: initializingFromProps,
|
||||||
onChange,
|
onChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
@@ -86,13 +88,16 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
} = config
|
} = config
|
||||||
|
|
||||||
const [disabled, setDisabled] = useState(disabledFromProps || false)
|
const [disabled, setDisabled] = useState(disabledFromProps || false)
|
||||||
|
const [isMounted, setIsMounted] = useState(false)
|
||||||
const [modified, setModified] = useState(false)
|
const [modified, setModified] = useState(false)
|
||||||
|
const [initializing, setInitializing] = useState(initializingFromProps)
|
||||||
const [processing, setProcessing] = useState(false)
|
const [processing, setProcessing] = useState(false)
|
||||||
const [submitted, setSubmitted] = useState(false)
|
const [submitted, setSubmitted] = useState(false)
|
||||||
const formRef = useRef<HTMLFormElement>(null)
|
const formRef = useRef<HTMLFormElement>(null)
|
||||||
const contextRef = useRef({} as FormContextType)
|
const contextRef = useRef({} as FormContextType)
|
||||||
|
|
||||||
const fieldsReducer = useReducer(fieldReducer, {}, () => initialState)
|
const fieldsReducer = useReducer(fieldReducer, {}, () => initialState)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `fields` is the current, up-to-date state/data of all fields in the form. It can be modified by using dispatchFields,
|
* `fields` is the current, up-to-date state/data of all fields in the form. It can be modified by using dispatchFields,
|
||||||
* which calls the fieldReducer, which then updates the state.
|
* which calls the fieldReducer, which then updates the state.
|
||||||
@@ -489,8 +494,10 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof disabledFromProps === 'boolean') setDisabled(disabledFromProps)
|
if (initializingFromProps !== undefined) {
|
||||||
}, [disabledFromProps])
|
setInitializing(initializingFromProps)
|
||||||
|
}
|
||||||
|
}, [initializingFromProps])
|
||||||
|
|
||||||
contextRef.current.submit = submit
|
contextRef.current.submit = submit
|
||||||
contextRef.current.getFields = getFields
|
contextRef.current.getFields = getFields
|
||||||
@@ -513,6 +520,15 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
contextRef.current.removeFieldRow = removeFieldRow
|
contextRef.current.removeFieldRow = removeFieldRow
|
||||||
contextRef.current.replaceFieldRow = replaceFieldRow
|
contextRef.current.replaceFieldRow = replaceFieldRow
|
||||||
contextRef.current.uuid = uuid
|
contextRef.current.uuid = uuid
|
||||||
|
contextRef.current.initializing = initializing
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof disabledFromProps === 'boolean') setDisabled(disabledFromProps)
|
||||||
|
}, [disabledFromProps])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof submittedFromProps === 'boolean') setSubmitted(submittedFromProps)
|
if (typeof submittedFromProps === 'boolean') setSubmitted(submittedFromProps)
|
||||||
@@ -521,7 +537,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialState) {
|
if (initialState) {
|
||||||
contextRef.current = { ...initContextState } as FormContextType
|
contextRef.current = { ...initContextState } as FormContextType
|
||||||
dispatchFields({ type: 'REPLACE_STATE', state: initialState })
|
dispatchFields({ type: 'REPLACE_STATE', optimize: false, state: initialState })
|
||||||
}
|
}
|
||||||
}, [initialState, dispatchFields])
|
}, [initialState, dispatchFields])
|
||||||
|
|
||||||
@@ -597,6 +613,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SubmittedContext.Provider value={submitted}>
|
<SubmittedContext.Provider value={submitted}>
|
||||||
|
<InitializingContext.Provider value={!isMounted || (isMounted && initializing)}>
|
||||||
<ProcessingContext.Provider value={processing}>
|
<ProcessingContext.Provider value={processing}>
|
||||||
<ModifiedContext.Provider value={modified}>
|
<ModifiedContext.Provider value={modified}>
|
||||||
<FormFieldsContext.Provider value={fieldsReducer}>
|
<FormFieldsContext.Provider value={fieldsReducer}>
|
||||||
@@ -604,6 +621,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
</FormFieldsContext.Provider>
|
</FormFieldsContext.Provider>
|
||||||
</ModifiedContext.Provider>
|
</ModifiedContext.Provider>
|
||||||
</ProcessingContext.Provider>
|
</ProcessingContext.Provider>
|
||||||
|
</InitializingContext.Provider>
|
||||||
</SubmittedContext.Provider>
|
</SubmittedContext.Provider>
|
||||||
</FormWatchContext.Provider>
|
</FormWatchContext.Provider>
|
||||||
</FormContext.Provider>
|
</FormContext.Provider>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const initContextState: Context = {
|
|||||||
getField: (): FormField => undefined,
|
getField: (): FormField => undefined,
|
||||||
getFields: (): FormState => ({}),
|
getFields: (): FormState => ({}),
|
||||||
getSiblingData,
|
getSiblingData,
|
||||||
|
initializing: undefined,
|
||||||
removeFieldRow: () => undefined,
|
removeFieldRow: () => undefined,
|
||||||
replaceFieldRow: () => undefined,
|
replaceFieldRow: () => undefined,
|
||||||
replaceState: () => undefined,
|
replaceState: () => undefined,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export type FormProps = (
|
|||||||
fields?: Field[]
|
fields?: Field[]
|
||||||
handleResponse?: (res: Response) => void
|
handleResponse?: (res: Response) => void
|
||||||
initialState?: FormState
|
initialState?: FormState
|
||||||
|
isInitializing?: boolean
|
||||||
log?: boolean
|
log?: boolean
|
||||||
onChange?: ((args: { formState: FormState }) => Promise<FormState>)[]
|
onChange?: ((args: { formState: FormState }) => Promise<FormState>)[]
|
||||||
onSubmit?: (fields: FormState, data: Data) => void
|
onSubmit?: (fields: FormState, data: Data) => void
|
||||||
@@ -197,6 +198,7 @@ export type Context = {
|
|||||||
getField: GetField
|
getField: GetField
|
||||||
getFields: GetFields
|
getFields: GetFields
|
||||||
getSiblingData: GetSiblingData
|
getSiblingData: GetSiblingData
|
||||||
|
initializing: boolean
|
||||||
removeFieldRow: ({ path, rowIndex }: { path: string; rowIndex: number }) => void
|
removeFieldRow: ({ path, rowIndex }: { path: string; rowIndex: number }) => void
|
||||||
replaceFieldRow: ({
|
replaceFieldRow: ({
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ export const RenderFields: React.FC<Props> = (props) => {
|
|||||||
{
|
{
|
||||||
rootMargin: '1000px',
|
rootMargin: '1000px',
|
||||||
},
|
},
|
||||||
forceRender,
|
Boolean(forceRender),
|
||||||
)
|
)
|
||||||
|
|
||||||
const isIntersecting = Boolean(entry?.isIntersecting)
|
const isIntersecting = Boolean(entry?.isIntersecting)
|
||||||
const isAboveViewport = entry?.boundingClientRect?.top < 0
|
const isAboveViewport = entry?.boundingClientRect?.top < 0
|
||||||
const shouldRender = forceRender || isIntersecting || isAboveViewport
|
const shouldRender = forceRender || isIntersecting || isAboveViewport
|
||||||
@@ -67,6 +68,9 @@ export const RenderFields: React.FC<Props> = (props) => {
|
|||||||
isHidden,
|
isHidden,
|
||||||
} = f
|
} = f
|
||||||
|
|
||||||
|
const forceRenderChildren =
|
||||||
|
(typeof forceRender === 'number' && fieldIndex <= forceRender) || true
|
||||||
|
|
||||||
const name = 'name' in f ? f.name : undefined
|
const name = 'name' in f ? f.name : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -74,7 +78,7 @@ export const RenderFields: React.FC<Props> = (props) => {
|
|||||||
CustomField={CustomField}
|
CustomField={CustomField}
|
||||||
custom={custom}
|
custom={custom}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
fieldComponentProps={fieldComponentProps}
|
fieldComponentProps={{ ...fieldComponentProps, forceRender: forceRenderChildren }}
|
||||||
indexPath={indexPath !== undefined ? `${indexPath}.${fieldIndex}` : `${fieldIndex}`}
|
indexPath={indexPath !== undefined ? `${indexPath}.${fieldIndex}` : `${fieldIndex}`}
|
||||||
isHidden={isHidden}
|
isHidden={isHidden}
|
||||||
key={fieldIndex}
|
key={fieldIndex}
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ import type { FieldMap } from '../../providers/ComponentMap/buildComponentMap/ty
|
|||||||
export type Props = {
|
export type Props = {
|
||||||
className?: string
|
className?: string
|
||||||
fieldMap: FieldMap
|
fieldMap: FieldMap
|
||||||
forceRender?: boolean
|
/**
|
||||||
|
* Controls the rendering behavior of the fields, i.e. defers rendering until they intersect with the viewport using the Intersection Observer API.
|
||||||
|
*
|
||||||
|
* If true, the fields will be rendered immediately, rather than waiting for them to intersect with the viewport.
|
||||||
|
*
|
||||||
|
* If a number is provided, will immediately render fields _up to that index_.
|
||||||
|
*/
|
||||||
|
forceRender?: boolean | number
|
||||||
indexPath?: string
|
indexPath?: string
|
||||||
margins?: 'small' | false
|
margins?: 'small' | false
|
||||||
operation?: Operation
|
operation?: Operation
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React, { forwardRef } from 'react'
|
|||||||
import type { Props } from '../../elements/Button/types.js'
|
import type { Props } from '../../elements/Button/types.js'
|
||||||
|
|
||||||
import { Button } from '../../elements/Button/index.js'
|
import { Button } from '../../elements/Button/index.js'
|
||||||
import { useForm, useFormProcessing } from '../Form/context.js'
|
import { useForm, useFormInitializing, useFormProcessing } from '../Form/context.js'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
const baseClass = 'form-submit'
|
const baseClass = 'form-submit'
|
||||||
@@ -12,9 +12,10 @@ const baseClass = 'form-submit'
|
|||||||
export const FormSubmit = forwardRef<HTMLButtonElement, Props>((props, ref) => {
|
export const FormSubmit = forwardRef<HTMLButtonElement, Props>((props, ref) => {
|
||||||
const { type = 'submit', buttonId: id, children, disabled: disabledFromProps } = props
|
const { type = 'submit', buttonId: id, children, disabled: disabledFromProps } = props
|
||||||
const processing = useFormProcessing()
|
const processing = useFormProcessing()
|
||||||
|
const initializing = useFormInitializing()
|
||||||
const { disabled } = useForm()
|
const { disabled } = useForm()
|
||||||
|
|
||||||
const canSave = !(disabledFromProps || processing || disabled)
|
const canSave = !(disabledFromProps || initializing || processing || disabled)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={baseClass}>
|
<div className={baseClass}>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Data, Field as FieldSchema, PayloadRequestWithData } from 'payload/types'
|
import type { User } from 'payload/auth'
|
||||||
|
import type { Data, Field as FieldSchema } from 'payload/types'
|
||||||
|
|
||||||
import { iterateFields } from './iterateFields.js'
|
import { iterateFields } from './iterateFields.js'
|
||||||
|
|
||||||
@@ -6,17 +7,25 @@ type Args = {
|
|||||||
data: Data
|
data: Data
|
||||||
fields: FieldSchema[]
|
fields: FieldSchema[]
|
||||||
id?: number | string
|
id?: number | string
|
||||||
req: PayloadRequestWithData
|
locale: string | undefined
|
||||||
siblingData: Data
|
siblingData: Data
|
||||||
|
user: User
|
||||||
}
|
}
|
||||||
|
|
||||||
export const calculateDefaultValues = async ({ id, data, fields, req }: Args): Promise<Data> => {
|
export const calculateDefaultValues = async ({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
fields,
|
||||||
|
locale,
|
||||||
|
user,
|
||||||
|
}: Args): Promise<Data> => {
|
||||||
await iterateFields({
|
await iterateFields({
|
||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
fields,
|
fields,
|
||||||
req,
|
locale,
|
||||||
siblingData: data,
|
siblingData: data,
|
||||||
|
user,
|
||||||
})
|
})
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Data, Field, PayloadRequestWithData, TabAsField } from 'payload/types'
|
import type { User } from 'payload/auth'
|
||||||
|
import type { Data, Field, TabAsField } from 'payload/types'
|
||||||
|
|
||||||
import { defaultValuePromise } from './promise.js'
|
import { defaultValuePromise } from './promise.js'
|
||||||
|
|
||||||
@@ -6,16 +7,18 @@ type Args<T> = {
|
|||||||
data: T
|
data: T
|
||||||
fields: (Field | TabAsField)[]
|
fields: (Field | TabAsField)[]
|
||||||
id?: number | string
|
id?: number | string
|
||||||
req: PayloadRequestWithData
|
locale: string | undefined
|
||||||
siblingData: Data
|
siblingData: Data
|
||||||
|
user: User
|
||||||
}
|
}
|
||||||
|
|
||||||
export const iterateFields = async <T>({
|
export const iterateFields = async <T>({
|
||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
fields,
|
fields,
|
||||||
req,
|
locale,
|
||||||
siblingData,
|
siblingData,
|
||||||
|
user,
|
||||||
}: Args<T>): Promise<void> => {
|
}: Args<T>): Promise<void> => {
|
||||||
const promises = []
|
const promises = []
|
||||||
fields.forEach((field) => {
|
fields.forEach((field) => {
|
||||||
@@ -24,8 +27,9 @@ export const iterateFields = async <T>({
|
|||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
field,
|
field,
|
||||||
req,
|
locale,
|
||||||
siblingData,
|
siblingData,
|
||||||
|
user,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
|
import type { User } from 'payload/auth'
|
||||||
import type { Data } from 'payload/types'
|
import type { Data } from 'payload/types'
|
||||||
|
|
||||||
import {
|
import { type Field, type TabAsField, fieldAffectsData, tabHasName } from 'payload/types'
|
||||||
type Field,
|
|
||||||
type PayloadRequestWithData,
|
|
||||||
type TabAsField,
|
|
||||||
fieldAffectsData,
|
|
||||||
tabHasName,
|
|
||||||
} from 'payload/types'
|
|
||||||
import { getDefaultValue } from 'payload/utilities'
|
import { getDefaultValue } from 'payload/utilities'
|
||||||
|
|
||||||
import { iterateFields } from './iterateFields.js'
|
import { iterateFields } from './iterateFields.js'
|
||||||
@@ -15,8 +10,9 @@ type Args<T> = {
|
|||||||
data: T
|
data: T
|
||||||
field: Field | TabAsField
|
field: Field | TabAsField
|
||||||
id?: number | string
|
id?: number | string
|
||||||
req: PayloadRequestWithData
|
locale: string | undefined
|
||||||
siblingData: Data
|
siblingData: Data
|
||||||
|
user: User
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make this works for rich text subfields
|
// TODO: Make this works for rich text subfields
|
||||||
@@ -24,8 +20,9 @@ export const defaultValuePromise = async <T>({
|
|||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
field,
|
field,
|
||||||
req,
|
locale,
|
||||||
siblingData,
|
siblingData,
|
||||||
|
user,
|
||||||
}: Args<T>): Promise<void> => {
|
}: Args<T>): Promise<void> => {
|
||||||
if (fieldAffectsData(field)) {
|
if (fieldAffectsData(field)) {
|
||||||
if (
|
if (
|
||||||
@@ -34,8 +31,8 @@ export const defaultValuePromise = async <T>({
|
|||||||
) {
|
) {
|
||||||
siblingData[field.name] = await getDefaultValue({
|
siblingData[field.name] = await getDefaultValue({
|
||||||
defaultValue: field.defaultValue,
|
defaultValue: field.defaultValue,
|
||||||
locale: req.locale,
|
locale,
|
||||||
user: req.user,
|
user,
|
||||||
value: siblingData[field.name],
|
value: siblingData[field.name],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -52,8 +49,9 @@ export const defaultValuePromise = async <T>({
|
|||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
req,
|
locale,
|
||||||
siblingData: groupData,
|
siblingData: groupData,
|
||||||
|
user,
|
||||||
})
|
})
|
||||||
|
|
||||||
break
|
break
|
||||||
@@ -70,8 +68,9 @@ export const defaultValuePromise = async <T>({
|
|||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
req,
|
locale,
|
||||||
siblingData: row,
|
siblingData: row,
|
||||||
|
user,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -97,8 +96,9 @@ export const defaultValuePromise = async <T>({
|
|||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
fields: block.fields,
|
fields: block.fields,
|
||||||
req,
|
locale,
|
||||||
siblingData: row,
|
siblingData: row,
|
||||||
|
user,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -115,8 +115,9 @@ export const defaultValuePromise = async <T>({
|
|||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
req,
|
locale,
|
||||||
siblingData,
|
siblingData,
|
||||||
|
user,
|
||||||
})
|
})
|
||||||
|
|
||||||
break
|
break
|
||||||
@@ -136,8 +137,9 @@ export const defaultValuePromise = async <T>({
|
|||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
req,
|
locale,
|
||||||
siblingData: tabSiblingData,
|
siblingData: tabSiblingData,
|
||||||
|
user,
|
||||||
})
|
})
|
||||||
|
|
||||||
break
|
break
|
||||||
@@ -148,8 +150,9 @@ export const defaultValuePromise = async <T>({
|
|||||||
id,
|
id,
|
||||||
data,
|
data,
|
||||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||||
req,
|
locale,
|
||||||
siblingData,
|
siblingData,
|
||||||
|
user,
|
||||||
})
|
})
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -41,8 +41,9 @@ export const buildStateFromSchema = async (args: Args): Promise<FormState> => {
|
|||||||
id,
|
id,
|
||||||
data: fullData,
|
data: fullData,
|
||||||
fields: fieldSchema,
|
fields: fieldSchema,
|
||||||
req,
|
locale: req.locale,
|
||||||
siblingData: fullData,
|
siblingData: fullData,
|
||||||
|
user: req.user,
|
||||||
})
|
})
|
||||||
|
|
||||||
await iterateFields({
|
await iterateFields({
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { useFieldProps } from '../FieldPropsProvider/index.js'
|
|||||||
import {
|
import {
|
||||||
useForm,
|
useForm,
|
||||||
useFormFields,
|
useFormFields,
|
||||||
|
useFormInitializing,
|
||||||
useFormModified,
|
useFormModified,
|
||||||
useFormProcessing,
|
useFormProcessing,
|
||||||
useFormSubmitted,
|
useFormSubmitted,
|
||||||
@@ -36,6 +37,7 @@ export const useField = <T,>(options: Options): FieldType<T> => {
|
|||||||
|
|
||||||
const submitted = useFormSubmitted()
|
const submitted = useFormSubmitted()
|
||||||
const processing = useFormProcessing()
|
const processing = useFormProcessing()
|
||||||
|
const initializing = useFormInitializing()
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
const { id } = useDocumentInfo()
|
const { id } = useDocumentInfo()
|
||||||
const operation = useOperation()
|
const operation = useOperation()
|
||||||
@@ -108,6 +110,7 @@ export const useField = <T,>(options: Options): FieldType<T> => {
|
|||||||
errorMessage: field?.errorMessage,
|
errorMessage: field?.errorMessage,
|
||||||
errorPaths: field?.errorPaths || [],
|
errorPaths: field?.errorPaths || [],
|
||||||
filterOptions,
|
filterOptions,
|
||||||
|
formInitializing: initializing,
|
||||||
formProcessing: processing,
|
formProcessing: processing,
|
||||||
formSubmitted: submitted,
|
formSubmitted: submitted,
|
||||||
initialValue,
|
initialValue,
|
||||||
@@ -137,6 +140,7 @@ export const useField = <T,>(options: Options): FieldType<T> => {
|
|||||||
readOnly,
|
readOnly,
|
||||||
permissions,
|
permissions,
|
||||||
filterOptions,
|
filterOptions,
|
||||||
|
initializing,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type FieldType<T> = {
|
|||||||
errorMessage?: string
|
errorMessage?: string
|
||||||
errorPaths?: string[]
|
errorPaths?: string[]
|
||||||
filterOptions?: FilterOptionsResult
|
filterOptions?: FilterOptionsResult
|
||||||
|
formInitializing: boolean
|
||||||
formProcessing: boolean
|
formProcessing: boolean
|
||||||
formSubmitted: boolean
|
formSubmitted: boolean
|
||||||
initialValue?: T
|
initialValue?: T
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import React, { createContext, useCallback, useContext, useEffect, useRef, useSt
|
|||||||
|
|
||||||
import type { DocumentInfoContext, DocumentInfoProps } from './types.js'
|
import type { DocumentInfoContext, DocumentInfoProps } from './types.js'
|
||||||
|
|
||||||
import { LoadingOverlay } from '../../elements/Loading/index.js'
|
|
||||||
import { formatDocTitle } from '../../utilities/formatDocTitle.js'
|
import { formatDocTitle } from '../../utilities/formatDocTitle.js'
|
||||||
import { getFormState } from '../../utilities/getFormState.js'
|
import { getFormState } from '../../utilities/getFormState.js'
|
||||||
import { hasSavePermission as getHasSavePermission } from '../../utilities/hasSavePermission.js'
|
import { hasSavePermission as getHasSavePermission } from '../../utilities/hasSavePermission.js'
|
||||||
@@ -39,29 +38,12 @@ export const DocumentInfoProvider: React.FC<
|
|||||||
globalSlug,
|
globalSlug,
|
||||||
hasPublishPermission: hasPublishPermissionFromProps,
|
hasPublishPermission: hasPublishPermissionFromProps,
|
||||||
hasSavePermission: hasSavePermissionFromProps,
|
hasSavePermission: hasSavePermissionFromProps,
|
||||||
|
initialData: initialDataFromProps,
|
||||||
|
initialState: initialStateFromProps,
|
||||||
onLoadError,
|
onLoadError,
|
||||||
onSave: onSaveFromProps,
|
onSave: onSaveFromProps,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [isError, setIsError] = useState(false)
|
|
||||||
const [documentTitle, setDocumentTitle] = useState('')
|
|
||||||
const [data, setData] = useState<Data>()
|
|
||||||
const [initialState, setInitialState] = useState<FormState>()
|
|
||||||
const [publishedDoc, setPublishedDoc] = useState<TypeWithID & TypeWithTimestamps>(null)
|
|
||||||
const [versions, setVersions] = useState<PaginatedDocs<TypeWithVersion<any>>>(null)
|
|
||||||
const [docPermissions, setDocPermissions] = useState<DocumentPermissions>(null)
|
|
||||||
const [hasSavePermission, setHasSavePermission] = useState<boolean>(null)
|
|
||||||
const [hasPublishPermission, setHasPublishPermission] = useState<boolean>(null)
|
|
||||||
const hasInitializedDocPermissions = useRef(false)
|
|
||||||
const [unpublishedVersions, setUnpublishedVersions] =
|
|
||||||
useState<PaginatedDocs<TypeWithVersion<any>>>(null)
|
|
||||||
|
|
||||||
const { getPreference, setPreference } = usePreferences()
|
|
||||||
const { i18n } = useTranslation()
|
|
||||||
const { permissions } = useAuth()
|
|
||||||
const { code: locale } = useLocale()
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
admin: { dateFormat },
|
admin: { dateFormat },
|
||||||
collections,
|
collections,
|
||||||
@@ -73,6 +55,43 @@ export const DocumentInfoProvider: React.FC<
|
|||||||
const collectionConfig = collections.find((c) => c.slug === collectionSlug)
|
const collectionConfig = collections.find((c) => c.slug === collectionSlug)
|
||||||
const globalConfig = globals.find((g) => g.slug === globalSlug)
|
const globalConfig = globals.find((g) => g.slug === globalSlug)
|
||||||
const docConfig = collectionConfig || globalConfig
|
const docConfig = collectionConfig || globalConfig
|
||||||
|
|
||||||
|
const { i18n } = useTranslation()
|
||||||
|
|
||||||
|
const [documentTitle, setDocumentTitle] = useState(() => {
|
||||||
|
if (!initialDataFromProps) return ''
|
||||||
|
|
||||||
|
return formatDocTitle({
|
||||||
|
collectionConfig,
|
||||||
|
data: { ...initialDataFromProps, id },
|
||||||
|
dateFormat,
|
||||||
|
fallback: id?.toString(),
|
||||||
|
globalConfig,
|
||||||
|
i18n,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [isError, setIsError] = useState(false)
|
||||||
|
const [data, setData] = useState<Data>(initialDataFromProps)
|
||||||
|
const [initialState, setInitialState] = useState<FormState>(initialStateFromProps)
|
||||||
|
const [publishedDoc, setPublishedDoc] = useState<TypeWithID & TypeWithTimestamps>(null)
|
||||||
|
const [versions, setVersions] = useState<PaginatedDocs<TypeWithVersion<any>>>(null)
|
||||||
|
const [docPermissions, setDocPermissions] = useState<DocumentPermissions>(docPermissionsFromProps)
|
||||||
|
const [hasSavePermission, setHasSavePermission] = useState<boolean>(hasSavePermissionFromProps)
|
||||||
|
const [hasPublishPermission, setHasPublishPermission] = useState<boolean>(
|
||||||
|
hasPublishPermissionFromProps,
|
||||||
|
)
|
||||||
|
const isInitializing = initialState === undefined || data === undefined
|
||||||
|
const hasInitializedDocPermissions = useRef(false)
|
||||||
|
const [unpublishedVersions, setUnpublishedVersions] =
|
||||||
|
useState<PaginatedDocs<TypeWithVersion<any>>>(null)
|
||||||
|
|
||||||
|
const { getPreference, setPreference } = usePreferences()
|
||||||
|
const { permissions } = useAuth()
|
||||||
|
const { code: locale } = useLocale()
|
||||||
|
const prevLocale = useRef(locale)
|
||||||
|
|
||||||
const versionsConfig = docConfig?.versions
|
const versionsConfig = docConfig?.versions
|
||||||
|
|
||||||
const baseURL = `${serverURL}${api}`
|
const baseURL = `${serverURL}${api}`
|
||||||
@@ -296,7 +315,7 @@ export const DocumentInfoProvider: React.FC<
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[serverURL, api, permissions, i18n.language, locale, collectionSlug, globalSlug, isEditing],
|
[serverURL, api, permissions, i18n.language, locale, collectionSlug, globalSlug],
|
||||||
)
|
)
|
||||||
|
|
||||||
const getDocPreferences = useCallback(() => {
|
const getDocPreferences = useCallback(() => {
|
||||||
@@ -372,6 +391,14 @@ export const DocumentInfoProvider: React.FC<
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
|
const localeChanged = locale !== prevLocale.current
|
||||||
|
|
||||||
|
if (
|
||||||
|
initialStateFromProps === undefined ||
|
||||||
|
initialDataFromProps === undefined ||
|
||||||
|
localeChanged
|
||||||
|
) {
|
||||||
|
if (localeChanged) prevLocale.current = locale
|
||||||
|
|
||||||
const getInitialState = async () => {
|
const getInitialState = async () => {
|
||||||
setIsError(false)
|
setIsError(false)
|
||||||
@@ -408,11 +435,23 @@ export const DocumentInfoProvider: React.FC<
|
|||||||
}
|
}
|
||||||
|
|
||||||
void getInitialState()
|
void getInitialState()
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
abortController.abort()
|
abortController.abort()
|
||||||
}
|
}
|
||||||
}, [api, operation, collectionSlug, serverURL, id, globalSlug, locale, onLoadError])
|
}, [
|
||||||
|
api,
|
||||||
|
operation,
|
||||||
|
collectionSlug,
|
||||||
|
serverURL,
|
||||||
|
id,
|
||||||
|
globalSlug,
|
||||||
|
locale,
|
||||||
|
onLoadError,
|
||||||
|
initialDataFromProps,
|
||||||
|
initialStateFromProps,
|
||||||
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void getVersions()
|
void getVersions()
|
||||||
@@ -445,10 +484,6 @@ export const DocumentInfoProvider: React.FC<
|
|||||||
hasPublishPermission === null
|
hasPublishPermission === null
|
||||||
) {
|
) {
|
||||||
await getDocPermissions(data)
|
await getDocPermissions(data)
|
||||||
} else {
|
|
||||||
setDocPermissions(docPermissions)
|
|
||||||
setHasSavePermission(hasSavePermission)
|
|
||||||
setHasPublishPermission(hasPublishPermission)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,10 +504,6 @@ export const DocumentInfoProvider: React.FC<
|
|||||||
|
|
||||||
if (isError) notFound()
|
if (isError) notFound()
|
||||||
|
|
||||||
if (!initialState || isLoading) {
|
|
||||||
return <LoadingOverlay />
|
|
||||||
}
|
|
||||||
|
|
||||||
const value: DocumentInfoContext = {
|
const value: DocumentInfoContext = {
|
||||||
...props,
|
...props,
|
||||||
docConfig,
|
docConfig,
|
||||||
@@ -484,6 +515,8 @@ export const DocumentInfoProvider: React.FC<
|
|||||||
hasSavePermission,
|
hasSavePermission,
|
||||||
initialData: data,
|
initialData: data,
|
||||||
initialState,
|
initialState,
|
||||||
|
isInitializing,
|
||||||
|
isLoading,
|
||||||
onSave,
|
onSave,
|
||||||
publishedDoc,
|
publishedDoc,
|
||||||
setDocFieldPreferences,
|
setDocFieldPreferences,
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export type DocumentInfoProps = {
|
|||||||
hasPublishPermission?: boolean
|
hasPublishPermission?: boolean
|
||||||
hasSavePermission?: boolean
|
hasSavePermission?: boolean
|
||||||
id: null | number | string
|
id: null | number | string
|
||||||
|
initialData?: Data
|
||||||
|
initialState?: FormState
|
||||||
isEditing?: boolean
|
isEditing?: boolean
|
||||||
onLoadError?: (data?: any) => Promise<void> | void
|
onLoadError?: (data?: any) => Promise<void> | void
|
||||||
onSave?: (data: Data) => Promise<void> | void
|
onSave?: (data: Data) => Promise<void> | void
|
||||||
@@ -41,6 +43,8 @@ export type DocumentInfoContext = DocumentInfoProps & {
|
|||||||
getVersions: () => Promise<void>
|
getVersions: () => Promise<void>
|
||||||
initialData: Data
|
initialData: Data
|
||||||
initialState?: FormState
|
initialState?: FormState
|
||||||
|
isInitializing: boolean
|
||||||
|
isLoading: boolean
|
||||||
preferencesKey?: string
|
preferencesKey?: string
|
||||||
publishedDoc?: TypeWithID & TypeWithTimestamps & { _status?: string }
|
publishedDoc?: TypeWithID & TypeWithTimestamps & { _status?: string }
|
||||||
setDocFieldPreferences: (
|
setDocFieldPreferences: (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { I18nClient, Language } from '@payloadcms/translations'
|
import type { I18nClient, Language } from '@payloadcms/translations'
|
||||||
import type { ClientConfig } from 'payload/types'
|
import type { ClientConfig, LanguageOptions } from 'payload/types'
|
||||||
|
|
||||||
import * as facelessUIImport from '@faceless-ui/modal'
|
import * as facelessUIImport from '@faceless-ui/modal'
|
||||||
import * as facelessUIImport3 from '@faceless-ui/scroll-info'
|
import * as facelessUIImport3 from '@faceless-ui/scroll-info'
|
||||||
@@ -10,7 +10,6 @@ import { Slide, ToastContainer } from 'react-toastify'
|
|||||||
|
|
||||||
import type { ComponentMap } from '../ComponentMap/buildComponentMap/types.js'
|
import type { ComponentMap } from '../ComponentMap/buildComponentMap/types.js'
|
||||||
import type { Theme } from '../Theme/index.js'
|
import type { Theme } from '../Theme/index.js'
|
||||||
import type { LanguageOptions } from '../Translation/index.js'
|
|
||||||
|
|
||||||
import { LoadingOverlayProvider } from '../../elements/LoadingOverlay/index.js'
|
import { LoadingOverlayProvider } from '../../elements/LoadingOverlay/index.js'
|
||||||
import { NavProvider } from '../../elements/Nav/context.js'
|
import { NavProvider } from '../../elements/Nav/context.js'
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type {
|
|||||||
TFunction,
|
TFunction,
|
||||||
} from '@payloadcms/translations'
|
} from '@payloadcms/translations'
|
||||||
import type { Locale } from 'date-fns'
|
import type { Locale } from 'date-fns'
|
||||||
import type { ClientConfig } from 'payload/types'
|
import type { ClientConfig, LanguageOptions } from 'payload/types'
|
||||||
|
|
||||||
import { t } from '@payloadcms/translations'
|
import { t } from '@payloadcms/translations'
|
||||||
import { importDateFNSLocale } from '@payloadcms/translations'
|
import { importDateFNSLocale } from '@payloadcms/translations'
|
||||||
@@ -16,11 +16,6 @@ import React, { createContext, useContext, useEffect, useState } from 'react'
|
|||||||
|
|
||||||
import { useRouteCache } from '../RouteCache/index.js'
|
import { useRouteCache } from '../RouteCache/index.js'
|
||||||
|
|
||||||
export type LanguageOptions = {
|
|
||||||
label: string
|
|
||||||
value: string
|
|
||||||
}[]
|
|
||||||
|
|
||||||
type ContextType<
|
type ContextType<
|
||||||
TAdditionalTranslations = {},
|
TAdditionalTranslations = {},
|
||||||
TAdditionalClientTranslationKeys extends string = never,
|
TAdditionalClientTranslationKeys extends string = never,
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ export const getFormState = async (args: {
|
|||||||
onError?: (data?: any) => Promise<void> | void
|
onError?: (data?: any) => Promise<void> | void
|
||||||
serverURL: SanitizedConfig['serverURL']
|
serverURL: SanitizedConfig['serverURL']
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
|
token?: string
|
||||||
}): Promise<FormState> => {
|
}): Promise<FormState> => {
|
||||||
const { apiRoute, body, onError, serverURL, signal } = args
|
const { apiRoute, body, onError, serverURL, signal, token } = args
|
||||||
|
|
||||||
const res = await fetch(`${serverURL}${apiRoute}/form-state`, {
|
const res = await fetch(`${serverURL}${apiRoute}/form-state`, {
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
...(token ? { Authorization: `JWT ${token}` } : {}),
|
||||||
},
|
},
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
signal,
|
signal,
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export const reduceFieldsToValues = (
|
|||||||
): Data => {
|
): Data => {
|
||||||
let data = {}
|
let data = {}
|
||||||
|
|
||||||
|
if (!fields) return data
|
||||||
|
|
||||||
Object.keys(fields).forEach((key) => {
|
Object.keys(fields).forEach((key) => {
|
||||||
if (ignoreDisableFormData === true || !fields[key]?.disableFormData) {
|
if (ignoreDisableFormData === true || !fields[key]?.disableFormData) {
|
||||||
data[key] = fields[key]?.value
|
data[key] = fields[key]?.value
|
||||||
|
|||||||
@@ -170,12 +170,12 @@ describe('access control', () => {
|
|||||||
|
|
||||||
test('should not have list url', async () => {
|
test('should not have list url', async () => {
|
||||||
await page.goto(restrictedUrl.list)
|
await page.goto(restrictedUrl.list)
|
||||||
await expect(page.locator('.unauthorized')).toBeVisible()
|
await expect(page.locator('.not-found')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should not have create url', async () => {
|
test('should not have create url', async () => {
|
||||||
await page.goto(restrictedUrl.create)
|
await page.goto(restrictedUrl.create)
|
||||||
await expect(page.locator('.unauthorized')).toBeVisible()
|
await expect(page.locator('.not-found')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should not have access to existing doc', async () => {
|
test('should not have access to existing doc', async () => {
|
||||||
@@ -321,13 +321,12 @@ describe('access control', () => {
|
|||||||
name: 'unrestricted-123',
|
name: 'unrestricted-123',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await page.goto(unrestrictedURL.edit(unrestrictedDoc.id.toString()))
|
await page.goto(unrestrictedURL.edit(unrestrictedDoc.id.toString()))
|
||||||
|
const field = page.locator('#field-userRestrictedDocs')
|
||||||
|
await expect(field.locator('input')).toBeEnabled()
|
||||||
const addDocButton = page.locator(
|
const addDocButton = page.locator(
|
||||||
'#userRestrictedDocs-add-new button.relationship-add-new__add-button.doc-drawer__toggler',
|
'#userRestrictedDocs-add-new button.relationship-add-new__add-button.doc-drawer__toggler',
|
||||||
)
|
)
|
||||||
|
|
||||||
await addDocButton.click()
|
await addDocButton.click()
|
||||||
const documentDrawer = page.locator('[id^=doc-drawer_user-restricted-collection_1_]')
|
const documentDrawer = page.locator('[id^=doc-drawer_user-restricted-collection_1_]')
|
||||||
await expect(documentDrawer).toBeVisible()
|
await expect(documentDrawer).toBeVisible()
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import React from 'react'
|
|||||||
|
|
||||||
export const FieldDescriptionComponent: DescriptionComponent = () => {
|
export const FieldDescriptionComponent: DescriptionComponent = () => {
|
||||||
const { path } = useFieldProps()
|
const { path } = useFieldProps()
|
||||||
const { value } = useFormFields(([fields]) => fields[path])
|
const field = useFormFields(([fields]) => (fields && fields?.[path]) || null)
|
||||||
|
const { value } = field || {}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`field-description-${path}`}>
|
<div className={`field-description-${path}`}>
|
||||||
|
|||||||
@@ -23,16 +23,15 @@ export const ClientForm: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<CustomPassword />
|
<CustomPassword />
|
||||||
<ConfirmPassword />
|
<ConfirmPassword />
|
||||||
|
|
||||||
<FormSubmit>Submit</FormSubmit>
|
<FormSubmit>Submit</FormSubmit>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomPassword: React.FC = () => {
|
const CustomPassword: React.FC = () => {
|
||||||
const confirmPassword = useFormFields(([fields]) => {
|
const confirmPassword = useFormFields(
|
||||||
return fields['confirm-password']
|
([fields]) => (fields && fields?.['confirm-password']) || null,
|
||||||
})
|
)
|
||||||
|
|
||||||
const confirmValue = confirmPassword.value
|
const confirmValue = confirmPassword.value
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ describe('admin2', () => {
|
|||||||
|
|
||||||
// prefill search with "a" from the query param
|
// prefill search with "a" from the query param
|
||||||
await page.goto(`${postsUrl.list}?search=dennis`)
|
await page.goto(`${postsUrl.list}?search=dennis`)
|
||||||
await page.waitForURL(`${postsUrl.list}?search=dennis`)
|
await page.waitForURL(new RegExp(`${postsUrl.list}\\?search=dennis`))
|
||||||
|
|
||||||
// input should be filled out, list should filter
|
// input should be filled out, list should filter
|
||||||
await expect(page.locator('.search-filter__input')).toHaveValue('dennis')
|
await expect(page.locator('.search-filter__input')).toHaveValue('dennis')
|
||||||
@@ -624,7 +624,7 @@ describe('admin2', () => {
|
|||||||
|
|
||||||
test('should delete many', async () => {
|
test('should delete many', async () => {
|
||||||
await page.goto(postsUrl.list)
|
await page.goto(postsUrl.list)
|
||||||
await page.waitForURL(postsUrl.list)
|
await page.waitForURL(new RegExp(postsUrl.list))
|
||||||
// delete should not appear without selection
|
// delete should not appear without selection
|
||||||
await expect(page.locator('#confirm-delete')).toHaveCount(0)
|
await expect(page.locator('#confirm-delete')).toHaveCount(0)
|
||||||
// select one row
|
// select one row
|
||||||
|
|||||||
@@ -119,22 +119,18 @@ describe('auth', () => {
|
|||||||
await page.locator('#change-password').click()
|
await page.locator('#change-password').click()
|
||||||
await page.locator('#field-password').fill('password')
|
await page.locator('#field-password').fill('password')
|
||||||
await page.locator('#field-confirm-password').fill('password')
|
await page.locator('#field-confirm-password').fill('password')
|
||||||
|
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
await expect(page.locator('#field-email')).toHaveValue(emailBeforeSave)
|
await expect(page.locator('#field-email')).toHaveValue(emailBeforeSave)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should have up-to-date user in `useAuth` hook', async () => {
|
test('should have up-to-date user in `useAuth` hook', async () => {
|
||||||
await page.goto(url.account)
|
await page.goto(url.account)
|
||||||
|
await page.waitForURL(url.account)
|
||||||
await expect(page.locator('#users-api-result')).toHaveText('Hello, world!')
|
await expect(page.locator('#users-api-result')).toHaveText('Hello, world!')
|
||||||
await expect(page.locator('#use-auth-result')).toHaveText('Hello, world!')
|
await expect(page.locator('#use-auth-result')).toHaveText('Hello, world!')
|
||||||
|
|
||||||
const field = page.locator('#field-custom')
|
const field = page.locator('#field-custom')
|
||||||
await field.fill('Goodbye, world!')
|
await field.fill('Goodbye, world!')
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
await expect(page.locator('#users-api-result')).toHaveText('Goodbye, world!')
|
await expect(page.locator('#users-api-result')).toHaveText('Goodbye, world!')
|
||||||
await expect(page.locator('#use-auth-result')).toHaveText('Goodbye, world!')
|
await expect(page.locator('#use-auth-result')).toHaveText('Goodbye, world!')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import type {
|
|||||||
import {
|
import {
|
||||||
ensureAutoLoginAndCompilationIsDone,
|
ensureAutoLoginAndCompilationIsDone,
|
||||||
initPageConsoleErrorCatch,
|
initPageConsoleErrorCatch,
|
||||||
|
openCreateDocDrawer,
|
||||||
openDocControls,
|
openDocControls,
|
||||||
openDocDrawer,
|
openDocDrawer,
|
||||||
saveDocAndAssert,
|
saveDocAndAssert,
|
||||||
@@ -141,19 +142,13 @@ describe('fields - relationship', () => {
|
|||||||
|
|
||||||
test('should create relationship', async () => {
|
test('should create relationship', async () => {
|
||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
|
|
||||||
const field = page.locator('#field-relationship')
|
const field = page.locator('#field-relationship')
|
||||||
|
await expect(field.locator('input')).toBeEnabled()
|
||||||
await field.click({ delay: 100 })
|
await field.click({ delay: 100 })
|
||||||
|
|
||||||
const options = page.locator('.rs__option')
|
const options = page.locator('.rs__option')
|
||||||
|
|
||||||
await expect(options).toHaveCount(2) // two docs
|
await expect(options).toHaveCount(2) // two docs
|
||||||
|
|
||||||
// Select a relationship
|
|
||||||
await options.nth(0).click()
|
await options.nth(0).click()
|
||||||
await expect(field).toContainText(relationOneDoc.id)
|
await expect(field).toContainText(relationOneDoc.id)
|
||||||
|
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -186,30 +181,20 @@ describe('fields - relationship', () => {
|
|||||||
|
|
||||||
test('should create hasMany relationship', async () => {
|
test('should create hasMany relationship', async () => {
|
||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
|
|
||||||
const field = page.locator('#field-relationshipHasMany')
|
const field = page.locator('#field-relationshipHasMany')
|
||||||
|
await expect(field.locator('input')).toBeEnabled()
|
||||||
await field.click({ delay: 100 })
|
await field.click({ delay: 100 })
|
||||||
|
|
||||||
const options = page.locator('.rs__option')
|
const options = page.locator('.rs__option')
|
||||||
|
|
||||||
await expect(options).toHaveCount(2) // Two relationship options
|
await expect(options).toHaveCount(2) // Two relationship options
|
||||||
|
|
||||||
const values = page.locator('#field-relationshipHasMany .relationship--multi-value-label__text')
|
const values = page.locator('#field-relationshipHasMany .relationship--multi-value-label__text')
|
||||||
|
|
||||||
// Add one relationship
|
|
||||||
await options.locator(`text=${relationOneDoc.id}`).click()
|
await options.locator(`text=${relationOneDoc.id}`).click()
|
||||||
await expect(values).toHaveText([relationOneDoc.id])
|
await expect(values).toHaveText([relationOneDoc.id])
|
||||||
await expect(values).not.toHaveText([anotherRelationOneDoc.id])
|
await expect(values).not.toHaveText([anotherRelationOneDoc.id])
|
||||||
|
|
||||||
// Add second relationship
|
|
||||||
await field.click({ delay: 100 })
|
await field.click({ delay: 100 })
|
||||||
await options.locator(`text=${anotherRelationOneDoc.id}`).click()
|
await options.locator(`text=${anotherRelationOneDoc.id}`).click()
|
||||||
await expect(values).toHaveText([relationOneDoc.id, anotherRelationOneDoc.id])
|
await expect(values).toHaveText([relationOneDoc.id, anotherRelationOneDoc.id])
|
||||||
|
|
||||||
// No options left
|
|
||||||
await field.locator('.rs__input').click({ delay: 100 })
|
await field.locator('.rs__input').click({ delay: 100 })
|
||||||
await expect(page.locator('.rs__menu')).toHaveText('No options')
|
await expect(page.locator('.rs__menu')).toHaveText('No options')
|
||||||
|
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
await wait(200)
|
await wait(200)
|
||||||
await expect(values).toHaveText([relationOneDoc.id, anotherRelationOneDoc.id])
|
await expect(values).toHaveText([relationOneDoc.id, anotherRelationOneDoc.id])
|
||||||
@@ -257,49 +242,30 @@ describe('fields - relationship', () => {
|
|||||||
async function runFilterOptionsTest(fieldName: string) {
|
async function runFilterOptionsTest(fieldName: string) {
|
||||||
await page.reload()
|
await page.reload()
|
||||||
await page.goto(url.edit(docWithExistingRelations.id))
|
await page.goto(url.edit(docWithExistingRelations.id))
|
||||||
|
|
||||||
// fill the first relation field
|
|
||||||
const field = page.locator('#field-relationship')
|
const field = page.locator('#field-relationship')
|
||||||
|
await expect(field.locator('input')).toBeEnabled()
|
||||||
await field.click({ delay: 100 })
|
await field.click({ delay: 100 })
|
||||||
const options = page.locator('.rs__option')
|
const options = page.locator('.rs__option')
|
||||||
|
|
||||||
await options.nth(0).click()
|
await options.nth(0).click()
|
||||||
await expect(field).toContainText(relationOneDoc.id)
|
await expect(field).toContainText(relationOneDoc.id)
|
||||||
|
|
||||||
// then verify that the filtered field's options match
|
|
||||||
let filteredField = page.locator(`#field-${fieldName} .react-select`)
|
let filteredField = page.locator(`#field-${fieldName} .react-select`)
|
||||||
await filteredField.click({ delay: 100 })
|
await filteredField.click({ delay: 100 })
|
||||||
let filteredOptions = filteredField.locator('.rs__option')
|
let filteredOptions = filteredField.locator('.rs__option')
|
||||||
await expect(filteredOptions).toHaveCount(1) // one doc
|
await expect(filteredOptions).toHaveCount(1) // one doc
|
||||||
await filteredOptions.nth(0).click()
|
await filteredOptions.nth(0).click()
|
||||||
await expect(filteredField).toContainText(relationOneDoc.id)
|
await expect(filteredField).toContainText(relationOneDoc.id)
|
||||||
|
|
||||||
// change the first relation field
|
|
||||||
await field.click({ delay: 100 })
|
await field.click({ delay: 100 })
|
||||||
await options.nth(1).click()
|
await options.nth(1).click()
|
||||||
await expect(field).toContainText(anotherRelationOneDoc.id)
|
await expect(field).toContainText(anotherRelationOneDoc.id)
|
||||||
|
await wait(2000) // Need to wait form state to come back before clicking save
|
||||||
// Need to wait form state to come back
|
|
||||||
// before clicking save
|
|
||||||
await wait(2000)
|
|
||||||
|
|
||||||
// Now, save the document. This should fail, as the filitered field doesn't match the selected relationship value
|
|
||||||
await page.locator('#action-save').click()
|
await page.locator('#action-save').click()
|
||||||
await expect(page.locator('.Toastify')).toContainText(`is invalid: ${fieldName}`)
|
await expect(page.locator('.Toastify')).toContainText(`is invalid: ${fieldName}`)
|
||||||
|
|
||||||
// then verify that the filtered field's options match
|
|
||||||
filteredField = page.locator(`#field-${fieldName} .react-select`)
|
filteredField = page.locator(`#field-${fieldName} .react-select`)
|
||||||
|
|
||||||
await filteredField.click({ delay: 100 })
|
await filteredField.click({ delay: 100 })
|
||||||
|
|
||||||
filteredOptions = filteredField.locator('.rs__option')
|
filteredOptions = filteredField.locator('.rs__option')
|
||||||
|
|
||||||
await expect(filteredOptions).toHaveCount(2) // two options because the currently selected option is still there
|
await expect(filteredOptions).toHaveCount(2) // two options because the currently selected option is still there
|
||||||
await filteredOptions.nth(1).click()
|
await filteredOptions.nth(1).click()
|
||||||
await expect(filteredField).toContainText(anotherRelationOneDoc.id)
|
await expect(filteredField).toContainText(anotherRelationOneDoc.id)
|
||||||
|
|
||||||
// Now, saving the document should succeed
|
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,30 +399,17 @@ describe('fields - relationship', () => {
|
|||||||
|
|
||||||
test('should open document drawer and append newly created docs onto the parent field', async () => {
|
test('should open document drawer and append newly created docs onto the parent field', async () => {
|
||||||
await page.goto(url.edit(docWithExistingRelations.id))
|
await page.goto(url.edit(docWithExistingRelations.id))
|
||||||
|
await openCreateDocDrawer(page, '#field-relationshipHasMany')
|
||||||
const field = page.locator('#field-relationshipHasMany')
|
|
||||||
|
|
||||||
// open the document drawer
|
|
||||||
const addNewButton = field.locator(
|
|
||||||
'button.relationship-add-new__add-button.doc-drawer__toggler',
|
|
||||||
)
|
|
||||||
await addNewButton.click()
|
|
||||||
const documentDrawer = page.locator('[id^=doc-drawer_relation-one_1_]')
|
const documentDrawer = page.locator('[id^=doc-drawer_relation-one_1_]')
|
||||||
await expect(documentDrawer).toBeVisible()
|
await expect(documentDrawer).toBeVisible()
|
||||||
|
|
||||||
// fill in the field and save the document, keep the drawer open for further testing
|
|
||||||
const drawerField = documentDrawer.locator('#field-name')
|
const drawerField = documentDrawer.locator('#field-name')
|
||||||
await drawerField.fill('Newly created document')
|
await drawerField.fill('Newly created document')
|
||||||
const saveButton = documentDrawer.locator('#action-save')
|
const saveButton = documentDrawer.locator('#action-save')
|
||||||
await saveButton.click()
|
await saveButton.click()
|
||||||
await expect(page.locator('.Toastify')).toContainText('successfully')
|
await expect(page.locator('.Toastify')).toContainText('successfully')
|
||||||
|
|
||||||
// count the number of values in the field to ensure only one was added
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('#field-relationshipHasMany .value-container .rs__multi-value'),
|
page.locator('#field-relationshipHasMany .value-container .rs__multi-value'),
|
||||||
).toHaveCount(1)
|
).toHaveCount(1)
|
||||||
|
|
||||||
// save the same document again to ensure the relationship field doesn't receive duplicative values
|
|
||||||
await drawerField.fill('Updated document')
|
await drawerField.fill('Updated document')
|
||||||
await saveButton.click()
|
await saveButton.click()
|
||||||
await expect(page.locator('.Toastify')).toContainText('Updated successfully')
|
await expect(page.locator('.Toastify')).toContainText('Updated successfully')
|
||||||
@@ -469,12 +422,9 @@ describe('fields - relationship', () => {
|
|||||||
describe('existing relationships', () => {
|
describe('existing relationships', () => {
|
||||||
test('should highlight existing relationship', async () => {
|
test('should highlight existing relationship', async () => {
|
||||||
await page.goto(url.edit(docWithExistingRelations.id))
|
await page.goto(url.edit(docWithExistingRelations.id))
|
||||||
|
|
||||||
const field = page.locator('#field-relationship')
|
const field = page.locator('#field-relationship')
|
||||||
|
await expect(field.locator('input')).toBeEnabled()
|
||||||
// Check dropdown options
|
|
||||||
await field.click({ delay: 100 })
|
await field.click({ delay: 100 })
|
||||||
|
|
||||||
await expect(page.locator('.rs__option--is-selected')).toHaveCount(1)
|
await expect(page.locator('.rs__option--is-selected')).toHaveCount(1)
|
||||||
await expect(page.locator('.rs__option--is-selected')).toHaveText(relationOneDoc.id)
|
await expect(page.locator('.rs__option--is-selected')).toHaveText(relationOneDoc.id)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
ensureAutoLoginAndCompilationIsDone,
|
ensureAutoLoginAndCompilationIsDone,
|
||||||
exactText,
|
exactText,
|
||||||
initPageConsoleErrorCatch,
|
initPageConsoleErrorCatch,
|
||||||
openDocDrawer,
|
openCreateDocDrawer,
|
||||||
saveDocAndAssert,
|
saveDocAndAssert,
|
||||||
saveDocHotkeyAndAssert,
|
saveDocHotkeyAndAssert,
|
||||||
} from '../../../helpers.js'
|
} from '../../../helpers.js'
|
||||||
@@ -77,26 +77,20 @@ describe('relationship', () => {
|
|||||||
|
|
||||||
test('should create inline relationship within field with many relations', async () => {
|
test('should create inline relationship within field with many relations', async () => {
|
||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
|
await openCreateDocDrawer(page, '#field-relationship')
|
||||||
await openDocDrawer(page, '#relationship-add-new .relationship-add-new__add-button')
|
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.locator('#field-relationship .relationship-add-new__relation-button--text-fields')
|
.locator('#field-relationship .relationship-add-new__relation-button--text-fields')
|
||||||
.click()
|
.click()
|
||||||
|
|
||||||
const textField = page.locator('.drawer__content #field-text')
|
const textField = page.locator('.drawer__content #field-text')
|
||||||
|
await expect(textField).toBeEnabled()
|
||||||
const textValue = 'hello'
|
const textValue = 'hello'
|
||||||
|
|
||||||
await textField.fill(textValue)
|
await textField.fill(textValue)
|
||||||
|
|
||||||
await page.locator('[id^=doc-drawer_text-fields_1_] #action-save').click()
|
await page.locator('[id^=doc-drawer_text-fields_1_] #action-save').click()
|
||||||
await expect(page.locator('.Toastify')).toContainText('successfully')
|
await expect(page.locator('.Toastify')).toContainText('successfully')
|
||||||
await page.locator('[id^=close-drawer__doc-drawer_text-fields_1_]').click()
|
await page.locator('[id^=close-drawer__doc-drawer_text-fields_1_]').click()
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('#field-relationship .relationship--single-value__text'),
|
page.locator('#field-relationship .relationship--single-value__text'),
|
||||||
).toContainText(textValue)
|
).toContainText(textValue)
|
||||||
|
|
||||||
await page.locator('#action-save').click()
|
await page.locator('#action-save').click()
|
||||||
await expect(page.locator('.Toastify')).toContainText('successfully')
|
await expect(page.locator('.Toastify')).toContainText('successfully')
|
||||||
})
|
})
|
||||||
@@ -105,7 +99,7 @@ describe('relationship', () => {
|
|||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
await page.waitForURL(`**/${url.create}`)
|
await page.waitForURL(`**/${url.create}`)
|
||||||
// Open first modal
|
// Open first modal
|
||||||
await openDocDrawer(page, '#relationToSelf-add-new .relationship-add-new__add-button')
|
await openCreateDocDrawer(page, '#field-relationToSelf')
|
||||||
|
|
||||||
// Fill first modal's required relationship field
|
// Fill first modal's required relationship field
|
||||||
await page.locator('[id^=doc-drawer_relationship-fields_1_] #field-relationship').click()
|
await page.locator('[id^=doc-drawer_relationship-fields_1_] #field-relationship').click()
|
||||||
@@ -115,11 +109,10 @@ describe('relationship', () => {
|
|||||||
)
|
)
|
||||||
.click()
|
.click()
|
||||||
|
|
||||||
// Open second modal
|
const secondModalButton = page.locator(
|
||||||
await openDocDrawer(
|
|
||||||
page,
|
|
||||||
'[id^=doc-drawer_relationship-fields_1_] #relationToSelf-add-new button',
|
'[id^=doc-drawer_relationship-fields_1_] #relationToSelf-add-new button',
|
||||||
)
|
)
|
||||||
|
await secondModalButton.click()
|
||||||
|
|
||||||
// Fill second modal's required relationship field
|
// Fill second modal's required relationship field
|
||||||
await page.locator('[id^=doc-drawer_relationship-fields_2_] #field-relationship').click()
|
await page.locator('[id^=doc-drawer_relationship-fields_2_] #field-relationship').click()
|
||||||
@@ -249,7 +242,7 @@ describe('relationship', () => {
|
|||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
await page.waitForURL(`**/${url.create}`)
|
await page.waitForURL(`**/${url.create}`)
|
||||||
// First fill out the relationship field, as it's required
|
// First fill out the relationship field, as it's required
|
||||||
await openDocDrawer(page, '#relationship-add-new .relationship-add-new__add-button')
|
await openCreateDocDrawer(page, '#field-relationship')
|
||||||
await page
|
await page
|
||||||
.locator('#field-relationship .relationship-add-new__relation-button--text-fields')
|
.locator('#field-relationship .relationship-add-new__relation-button--text-fields')
|
||||||
.click()
|
.click()
|
||||||
@@ -264,7 +257,7 @@ describe('relationship', () => {
|
|||||||
|
|
||||||
// Create a new doc for the `relationshipHasMany` field
|
// Create a new doc for the `relationshipHasMany` field
|
||||||
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).not.toContain('create')
|
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).not.toContain('create')
|
||||||
await openDocDrawer(page, '#field-relationshipHasMany .relationship-add-new__add-button')
|
await openCreateDocDrawer(page, '#field-relationshipHasMany')
|
||||||
const value = 'Hello, world!'
|
const value = 'Hello, world!'
|
||||||
await page.locator('.drawer__content #field-text').fill(value)
|
await page.locator('.drawer__content #field-text').fill(value)
|
||||||
|
|
||||||
@@ -313,7 +306,7 @@ describe('relationship', () => {
|
|||||||
test('should save using hotkey in edit document drawer', async () => {
|
test('should save using hotkey in edit document drawer', async () => {
|
||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
// First fill out the relationship field, as it's required
|
// First fill out the relationship field, as it's required
|
||||||
await openDocDrawer(page, '#relationship-add-new .relationship-add-new__add-button')
|
await openCreateDocDrawer(page, '#field-relationship')
|
||||||
await page.locator('#field-relationship .value-container').click()
|
await page.locator('#field-relationship .value-container').click()
|
||||||
await wait(500)
|
await wait(500)
|
||||||
// Select "Seeded text document" relationship
|
// Select "Seeded text document" relationship
|
||||||
@@ -354,7 +347,7 @@ describe('relationship', () => {
|
|||||||
test.skip('should bypass min rows validation when no rows present and field is not required', async () => {
|
test.skip('should bypass min rows validation when no rows present and field is not required', async () => {
|
||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
// First fill out the relationship field, as it's required
|
// First fill out the relationship field, as it's required
|
||||||
await openDocDrawer(page, '#relationship-add-new .relationship-add-new__add-button')
|
await openCreateDocDrawer(page, '#field-relationship')
|
||||||
await page.locator('#field-relationship .value-container').click()
|
await page.locator('#field-relationship .value-container').click()
|
||||||
await page.getByText('Seeded text document', { exact: true }).click()
|
await page.getByText('Seeded text document', { exact: true }).click()
|
||||||
|
|
||||||
@@ -366,7 +359,7 @@ describe('relationship', () => {
|
|||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
await page.waitForURL(url.create)
|
await page.waitForURL(url.create)
|
||||||
// First fill out the relationship field, as it's required
|
// First fill out the relationship field, as it's required
|
||||||
await openDocDrawer(page, '#relationship-add-new .relationship-add-new__add-button')
|
await openCreateDocDrawer(page, '#field-relationship')
|
||||||
await page.locator('#field-relationship .value-container').click()
|
await page.locator('#field-relationship .value-container').click()
|
||||||
await page.getByText('Seeded text document', { exact: true }).click()
|
await page.getByText('Seeded text document', { exact: true }).click()
|
||||||
|
|
||||||
@@ -425,7 +418,7 @@ describe('relationship', () => {
|
|||||||
await createRelationshipFieldDoc({ value: textDoc.id, relationTo: 'text-fields' })
|
await createRelationshipFieldDoc({ value: textDoc.id, relationTo: 'text-fields' })
|
||||||
|
|
||||||
await page.goto(url.list)
|
await page.goto(url.list)
|
||||||
await page.waitForURL(url.list)
|
await page.waitForURL(new RegExp(url.list))
|
||||||
await wait(400)
|
await wait(400)
|
||||||
|
|
||||||
await page.locator('.list-controls__toggle-columns').click()
|
await page.locator('.list-controls__toggle-columns').click()
|
||||||
@@ -439,6 +432,7 @@ describe('relationship', () => {
|
|||||||
|
|
||||||
await wait(400)
|
await wait(400)
|
||||||
const conditionField = page.locator('.condition__field')
|
const conditionField = page.locator('.condition__field')
|
||||||
|
await expect(conditionField.locator('input')).toBeEnabled()
|
||||||
await conditionField.click()
|
await conditionField.click()
|
||||||
await wait(400)
|
await wait(400)
|
||||||
|
|
||||||
@@ -447,6 +441,7 @@ describe('relationship', () => {
|
|||||||
await wait(400)
|
await wait(400)
|
||||||
|
|
||||||
const operatorField = page.locator('.condition__operator')
|
const operatorField = page.locator('.condition__operator')
|
||||||
|
await expect(operatorField.locator('input')).toBeEnabled()
|
||||||
await operatorField.click()
|
await operatorField.click()
|
||||||
await wait(400)
|
await wait(400)
|
||||||
|
|
||||||
@@ -455,6 +450,7 @@ describe('relationship', () => {
|
|||||||
await wait(400)
|
await wait(400)
|
||||||
|
|
||||||
const valueField = page.locator('.condition__value')
|
const valueField = page.locator('.condition__value')
|
||||||
|
await expect(valueField.locator('input')).toBeEnabled()
|
||||||
await valueField.click()
|
await valueField.click()
|
||||||
await wait(400)
|
await wait(400)
|
||||||
|
|
||||||
|
|||||||
@@ -183,8 +183,6 @@ describe('Rich Text', () => {
|
|||||||
|
|
||||||
test('should not create new url link when read only', async () => {
|
test('should not create new url link when read only', async () => {
|
||||||
await navigateToRichTextFields()
|
await navigateToRichTextFields()
|
||||||
|
|
||||||
// Attempt to open link popup
|
|
||||||
const modalTrigger = page.locator('.rich-text--read-only .rich-text__toolbar button .link')
|
const modalTrigger = page.locator('.rich-text--read-only .rich-text__toolbar button .link')
|
||||||
await expect(modalTrigger).toBeDisabled()
|
await expect(modalTrigger).toBeDisabled()
|
||||||
})
|
})
|
||||||
@@ -421,19 +419,14 @@ describe('Rich Text', () => {
|
|||||||
})
|
})
|
||||||
test('should not take value from previous block', async () => {
|
test('should not take value from previous block', async () => {
|
||||||
await navigateToRichTextFields()
|
await navigateToRichTextFields()
|
||||||
|
await page.locator('#field-blocks').scrollIntoViewIfNeeded()
|
||||||
// check first block value
|
await expect(page.locator('#field-blocks__0__text')).toBeVisible()
|
||||||
const textField = page.locator('#field-blocks__0__text')
|
await expect(page.locator('#field-blocks__0__text')).toHaveValue('Regular text')
|
||||||
await expect(textField).toHaveValue('Regular text')
|
|
||||||
|
|
||||||
// remove the first block
|
|
||||||
const editBlock = page.locator('#blocks-row-0 .popup-button')
|
const editBlock = page.locator('#blocks-row-0 .popup-button')
|
||||||
await editBlock.click()
|
await editBlock.click()
|
||||||
const removeButton = page.locator('#blocks-row-0').getByRole('button', { name: 'Remove' })
|
const removeButton = page.locator('#blocks-row-0').getByRole('button', { name: 'Remove' })
|
||||||
await expect(removeButton).toBeVisible()
|
await expect(removeButton).toBeVisible()
|
||||||
await removeButton.click()
|
await removeButton.click()
|
||||||
|
|
||||||
// check new first block value
|
|
||||||
const richTextField = page.locator('#field-blocks__0__text')
|
const richTextField = page.locator('#field-blocks__0__text')
|
||||||
const richTextValue = await richTextField.innerText()
|
const richTextValue = await richTextField.innerText()
|
||||||
expect(richTextValue).toContain('Rich text')
|
expect(richTextValue).toContain('Rich text')
|
||||||
|
|||||||
@@ -146,12 +146,13 @@ describe('fields', () => {
|
|||||||
|
|
||||||
test('should create', async () => {
|
test('should create', async () => {
|
||||||
const input = '{"foo": "bar"}'
|
const input = '{"foo": "bar"}'
|
||||||
|
|
||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
const json = page.locator('.json-field .inputarea')
|
await page.waitForURL(url.create)
|
||||||
await json.fill(input)
|
await expect(() => expect(page.locator('.json-field .code-editor')).toBeVisible()).toPass({
|
||||||
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
await saveDocAndAssert(page, '.form-submit button')
|
})
|
||||||
|
await page.locator('.json-field .inputarea').fill(input)
|
||||||
|
await saveDocAndAssert(page)
|
||||||
await expect(page.locator('.json-field')).toContainText('"foo": "bar"')
|
await expect(page.locator('.json-field')).toContainText('"foo": "bar"')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -256,13 +257,15 @@ describe('fields', () => {
|
|||||||
|
|
||||||
test('should have disabled admin sorting', async () => {
|
test('should have disabled admin sorting', async () => {
|
||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
const field = page.locator('#field-disableSort .array-actions__action-chevron')
|
const field = page.locator('#field-disableSort > div > div > .array-actions__action-chevron')
|
||||||
expect(await field.count()).toEqual(0)
|
expect(await field.count()).toEqual(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('the drag handle should be hidden', async () => {
|
test('the drag handle should be hidden', async () => {
|
||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
const field = page.locator('#field-disableSort .collapsible__drag')
|
const field = page.locator(
|
||||||
|
'#field-disableSort > .blocks-field__rows > div > div > .collapsible__drag',
|
||||||
|
)
|
||||||
expect(await field.count()).toEqual(0)
|
expect(await field.count()).toEqual(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -275,13 +278,15 @@ describe('fields', () => {
|
|||||||
|
|
||||||
test('should have disabled admin sorting', async () => {
|
test('should have disabled admin sorting', async () => {
|
||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
const field = page.locator('#field-disableSort .array-actions__action-chevron')
|
const field = page.locator('#field-disableSort > div > div > .array-actions__action-chevron')
|
||||||
expect(await field.count()).toEqual(0)
|
expect(await field.count()).toEqual(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('the drag handle should be hidden', async () => {
|
test('the drag handle should be hidden', async () => {
|
||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
const field = page.locator('#field-disableSort .collapsible__drag')
|
const field = page.locator(
|
||||||
|
'#field-disableSort > .blocks-field__rows > div > div > .collapsible__drag',
|
||||||
|
)
|
||||||
expect(await field.count()).toEqual(0)
|
expect(await field.count()).toEqual(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ export async function ensureAutoLoginAndCompilationIsDone({
|
|||||||
await page.goto(adminURL)
|
await page.goto(adminURL)
|
||||||
await page.waitForURL(adminURL)
|
await page.waitForURL(adminURL)
|
||||||
|
|
||||||
|
await expect(() => expect(page.locator('.template-default')).toBeVisible()).toPass({
|
||||||
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
|
})
|
||||||
|
|
||||||
await expect(() => expect(page.url()).not.toContain(`${adminRoute}${loginRoute}`)).toPass({
|
await expect(() => expect(page.url()).not.toContain(`${adminRoute}${loginRoute}`)).toPass({
|
||||||
timeout: POLL_TOPASS_TIMEOUT,
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
})
|
})
|
||||||
@@ -85,7 +89,6 @@ export async function ensureAutoLoginAndCompilationIsDone({
|
|||||||
timeout: POLL_TOPASS_TIMEOUT,
|
timeout: POLL_TOPASS_TIMEOUT,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check if hero is there
|
|
||||||
await expect(page.locator('.dashboard__label').first()).toBeVisible()
|
await expect(page.locator('.dashboard__label').first()).toBeVisible()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +203,16 @@ export async function openDocDrawer(page: Page, selector: string): Promise<void>
|
|||||||
await wait(500) // wait for drawer form state to initialize
|
await wait(500) // wait for drawer form state to initialize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function openCreateDocDrawer(page: Page, fieldSelector: string): Promise<void> {
|
||||||
|
await wait(500) // wait for parent form state to initialize
|
||||||
|
const relationshipField = page.locator(fieldSelector)
|
||||||
|
await expect(relationshipField.locator('input')).toBeEnabled()
|
||||||
|
const addNewButton = relationshipField.locator('.relationship-add-new__add-button')
|
||||||
|
await expect(addNewButton).toBeVisible()
|
||||||
|
await addNewButton.click()
|
||||||
|
await wait(500) // wait for drawer form state to initialize
|
||||||
|
}
|
||||||
|
|
||||||
export async function closeNav(page: Page): Promise<void> {
|
export async function closeNav(page: Page): Promise<void> {
|
||||||
if (!(await page.locator('.template-default.template-default--nav-open').isVisible())) return
|
if (!(await page.locator('.template-default.template-default--nav-open').isVisible())) return
|
||||||
await page.locator('.nav-toggler >> visible=true').click()
|
await page.locator('.nav-toggler >> visible=true').click()
|
||||||
|
|||||||
@@ -123,27 +123,15 @@ describe('Localization', () => {
|
|||||||
|
|
||||||
test('create arabic post, add english', async () => {
|
test('create arabic post, add english', async () => {
|
||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
|
|
||||||
const newLocale = 'ar'
|
const newLocale = 'ar'
|
||||||
|
|
||||||
// Change to Arabic
|
|
||||||
await changeLocale(page, newLocale)
|
await changeLocale(page, newLocale)
|
||||||
|
|
||||||
await fillValues({ description, title: arabicTitle })
|
await fillValues({ description, title: arabicTitle })
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
// Change back to English
|
|
||||||
await changeLocale(page, defaultLocale)
|
await changeLocale(page, defaultLocale)
|
||||||
|
|
||||||
// Localized field should not be populated
|
|
||||||
await expect(page.locator('#field-title')).toBeEmpty()
|
await expect(page.locator('#field-title')).toBeEmpty()
|
||||||
await expect(page.locator('#field-description')).toHaveValue(description)
|
await expect(page.locator('#field-description')).toHaveValue(description)
|
||||||
|
|
||||||
// Add English
|
|
||||||
|
|
||||||
await fillValues({ description, title })
|
await fillValues({ description, title })
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
await expect(page.locator('#field-title')).toHaveValue(title)
|
await expect(page.locator('#field-title')).toHaveValue(title)
|
||||||
await expect(page.locator('#field-description')).toHaveValue(description)
|
await expect(page.locator('#field-description')).toHaveValue(description)
|
||||||
})
|
})
|
||||||
@@ -175,56 +163,45 @@ describe('Localization', () => {
|
|||||||
await page.goto(url.edit(id))
|
await page.goto(url.edit(id))
|
||||||
await page.waitForURL(`**${url.edit(id)}`)
|
await page.waitForURL(`**${url.edit(id)}`)
|
||||||
await openDocControls(page)
|
await openDocControls(page)
|
||||||
|
|
||||||
// duplicate document
|
|
||||||
await page.locator('#action-duplicate').click()
|
await page.locator('#action-duplicate').click()
|
||||||
await expect(page.locator('.Toastify')).toContainText('successfully')
|
await expect(page.locator('.Toastify')).toContainText('successfully')
|
||||||
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).not.toContain(id)
|
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).not.toContain(id)
|
||||||
|
|
||||||
// check fields
|
|
||||||
await expect(page.locator('#field-title')).toHaveValue(englishTitle)
|
await expect(page.locator('#field-title')).toHaveValue(englishTitle)
|
||||||
await changeLocale(page, spanishLocale)
|
await changeLocale(page, spanishLocale)
|
||||||
|
await expect(page.locator('#field-title')).toBeEnabled()
|
||||||
await expect(page.locator('#field-title')).toHaveValue(spanishTitle)
|
await expect(page.locator('#field-title')).toHaveValue(spanishTitle)
|
||||||
|
await expect(page.locator('#field-localizedCheckbox')).toBeEnabled()
|
||||||
|
await page.reload() // TODO: remove this line, the checkbox _is not_ checked, but Playwright is unable to detect it without a reload for some reason
|
||||||
await expect(page.locator('#field-localizedCheckbox')).not.toBeChecked()
|
await expect(page.locator('#field-localizedCheckbox')).not.toBeChecked()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should duplicate localized checkbox correctly', async () => {
|
test('should duplicate localized checkbox correctly', async () => {
|
||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
await page.waitForURL(url.create)
|
await page.waitForURL(url.create)
|
||||||
|
|
||||||
await changeLocale(page, defaultLocale)
|
await changeLocale(page, defaultLocale)
|
||||||
await fillValues({ description, title: englishTitle })
|
await fillValues({ description, title: englishTitle })
|
||||||
|
await expect(page.locator('#field-localizedCheckbox')).toBeEnabled()
|
||||||
await page.locator('#field-localizedCheckbox').click()
|
await page.locator('#field-localizedCheckbox').click()
|
||||||
|
|
||||||
await page.locator('#action-save').click()
|
await page.locator('#action-save').click()
|
||||||
// wait for navigation to update route
|
|
||||||
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).not.toContain('create')
|
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).not.toContain('create')
|
||||||
const collectionUrl = page.url()
|
const collectionUrl = page.url()
|
||||||
// ensure spanish is not checked
|
|
||||||
await changeLocale(page, spanishLocale)
|
await changeLocale(page, spanishLocale)
|
||||||
|
await expect(page.locator('#field-localizedCheckbox')).toBeEnabled()
|
||||||
|
await page.reload() // TODO: remove this line, the checkbox _is not_ checked, but Playwright is unable to detect it without a reload for some reason
|
||||||
await expect(page.locator('#field-localizedCheckbox')).not.toBeChecked()
|
await expect(page.locator('#field-localizedCheckbox')).not.toBeChecked()
|
||||||
|
|
||||||
// duplicate doc
|
|
||||||
await changeLocale(page, defaultLocale)
|
await changeLocale(page, defaultLocale)
|
||||||
await openDocControls(page)
|
await openDocControls(page)
|
||||||
await page.locator('#action-duplicate').click()
|
await page.locator('#action-duplicate').click()
|
||||||
|
|
||||||
// wait for navigation to update route
|
|
||||||
await expect
|
await expect
|
||||||
.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT })
|
.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT })
|
||||||
.not.toContain(collectionUrl)
|
.not.toContain(collectionUrl)
|
||||||
|
|
||||||
// finally change locale to spanish
|
|
||||||
await changeLocale(page, spanishLocale)
|
await changeLocale(page, spanishLocale)
|
||||||
|
await expect(page.locator('#field-localizedCheckbox')).toBeEnabled()
|
||||||
|
await page.reload() // TODO: remove this line, the checkbox _is not_ checked, but Playwright is unable to detect it without a reload for some reason
|
||||||
await expect(page.locator('#field-localizedCheckbox')).not.toBeChecked()
|
await expect(page.locator('#field-localizedCheckbox')).not.toBeChecked()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should duplicate even if missing some localized data', async () => {
|
test('should duplicate even if missing some localized data', async () => {
|
||||||
// create a localized required doc
|
|
||||||
await page.goto(urlWithRequiredLocalizedFields.create)
|
await page.goto(urlWithRequiredLocalizedFields.create)
|
||||||
await changeLocale(page, defaultLocale)
|
await changeLocale(page, defaultLocale)
|
||||||
await page.locator('#field-title').fill(englishTitle)
|
await page.locator('#field-title').fill(englishTitle)
|
||||||
@@ -233,21 +210,12 @@ describe('Localization', () => {
|
|||||||
await page.fill('#field-layout__0__text', 'test')
|
await page.fill('#field-layout__0__text', 'test')
|
||||||
await expect(page.locator('#field-layout__0__text')).toHaveValue('test')
|
await expect(page.locator('#field-layout__0__text')).toHaveValue('test')
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
const originalID = await page.locator('.id-label').innerText()
|
const originalID = await page.locator('.id-label').innerText()
|
||||||
|
|
||||||
// duplicate
|
|
||||||
await openDocControls(page)
|
await openDocControls(page)
|
||||||
await page.locator('#action-duplicate').click()
|
await page.locator('#action-duplicate').click()
|
||||||
await expect(page.locator('.id-label')).not.toContainText(originalID)
|
await expect(page.locator('.id-label')).not.toContainText(originalID)
|
||||||
|
|
||||||
// verify that the locale did copy
|
|
||||||
await expect(page.locator('#field-title')).toHaveValue(englishTitle)
|
await expect(page.locator('#field-title')).toHaveValue(englishTitle)
|
||||||
|
|
||||||
// await the success toast
|
|
||||||
await expect(page.locator('.Toastify')).toContainText('successfully duplicated')
|
await expect(page.locator('.Toastify')).toContainText('successfully duplicated')
|
||||||
|
|
||||||
// expect that the document has a new id
|
|
||||||
await expect(page.locator('.id-label')).not.toContainText(originalID)
|
await expect(page.locator('.id-label')).not.toContainText(originalID)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -107,9 +107,6 @@ test.describe('Form Builder', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('can create form submission', async () => {
|
test('can create form submission', async () => {
|
||||||
await page.goto(submissionsUrl.list)
|
|
||||||
await page.waitForURL(submissionsUrl.list)
|
|
||||||
|
|
||||||
const { docs } = await payload.find({
|
const { docs } = await payload.find({
|
||||||
collection: 'forms',
|
collection: 'forms',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ describe('uploads', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Should render adminThumbnail when using a function', async () => {
|
test('should render adminThumbnail when using a function', async () => {
|
||||||
await page.reload() // Flakey test, it likely has to do with the test that comes before it. Trace viewer is not helpful when it fails.
|
await page.reload() // Flakey test, it likely has to do with the test that comes before it. Trace viewer is not helpful when it fails.
|
||||||
await page.goto(adminThumbnailFunctionURL.list)
|
await page.goto(adminThumbnailFunctionURL.list)
|
||||||
await page.waitForURL(adminThumbnailFunctionURL.list)
|
await page.waitForURL(adminThumbnailFunctionURL.list)
|
||||||
@@ -267,7 +267,7 @@ describe('uploads', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Should render adminThumbnail when using a specific size', async () => {
|
test('should render adminThumbnail when using a specific size', async () => {
|
||||||
await page.goto(adminThumbnailSizeURL.list)
|
await page.goto(adminThumbnailSizeURL.list)
|
||||||
await page.waitForURL(adminThumbnailSizeURL.list)
|
await page.waitForURL(adminThumbnailSizeURL.list)
|
||||||
|
|
||||||
@@ -280,7 +280,7 @@ describe('uploads', () => {
|
|||||||
await expect(audioUploadImage).toBeVisible()
|
await expect(audioUploadImage).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Should detect correct mimeType', async () => {
|
test('should detect correct mimeType', async () => {
|
||||||
await page.goto(mediaURL.create)
|
await page.goto(mediaURL.create)
|
||||||
await page.waitForURL(mediaURL.create)
|
await page.waitForURL(mediaURL.create)
|
||||||
await page.setInputFiles('input[type="file"]', path.resolve(dirname, './image.png'))
|
await page.setInputFiles('input[type="file"]', path.resolve(dirname, './image.png'))
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import type { Page } from '@playwright/test'
|
|||||||
|
|
||||||
import { expect, test } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { wait } from 'payload/utilities'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
||||||
@@ -162,31 +163,19 @@ describe('versions', () => {
|
|||||||
const title = 'autosave title'
|
const title = 'autosave title'
|
||||||
const description = 'autosave description'
|
const description = 'autosave description'
|
||||||
await page.goto(autosaveURL.create)
|
await page.goto(autosaveURL.create)
|
||||||
|
// gets redirected from /create to /slug/id due to autosave
|
||||||
// fill the fields
|
await page.waitForURL(new RegExp(`${autosaveURL.edit('')}`))
|
||||||
|
await wait(500)
|
||||||
|
await expect(page.locator('#field-title')).toBeEnabled()
|
||||||
await page.locator('#field-title').fill(title)
|
await page.locator('#field-title').fill(title)
|
||||||
|
await expect(page.locator('#field-description')).toBeEnabled()
|
||||||
await page.locator('#field-description').fill(description)
|
await page.locator('#field-description').fill(description)
|
||||||
|
|
||||||
// wait for autosave
|
|
||||||
await waitForAutoSaveToRunAndComplete(page)
|
await waitForAutoSaveToRunAndComplete(page)
|
||||||
|
|
||||||
// go to list
|
|
||||||
await page.goto(autosaveURL.list)
|
await page.goto(autosaveURL.list)
|
||||||
|
|
||||||
// expect the status to be draft
|
|
||||||
await expect(findTableCell(page, '_status', title)).toContainText('Draft')
|
await expect(findTableCell(page, '_status', title)).toContainText('Draft')
|
||||||
|
|
||||||
// select the row
|
|
||||||
// await page.locator('.row-1 .select-row__checkbox').click()
|
|
||||||
await selectTableRow(page, title)
|
await selectTableRow(page, title)
|
||||||
|
|
||||||
// click the publish many
|
|
||||||
await page.locator('.publish-many__toggle').click()
|
await page.locator('.publish-many__toggle').click()
|
||||||
|
|
||||||
// confirm the dialog
|
|
||||||
await page.locator('#confirm-publish').click()
|
await page.locator('#confirm-publish').click()
|
||||||
|
|
||||||
// expect the status to be published
|
|
||||||
await expect(findTableCell(page, '_status', title)).toContainText('Published')
|
await expect(findTableCell(page, '_status', title)).toContainText('Published')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -278,21 +267,19 @@ describe('versions', () => {
|
|||||||
|
|
||||||
test('collection — tab displays proper number of versions', async () => {
|
test('collection — tab displays proper number of versions', async () => {
|
||||||
await page.goto(url.list)
|
await page.goto(url.list)
|
||||||
|
|
||||||
const linkToDoc = page
|
const linkToDoc = page
|
||||||
.locator('tbody tr .cell-title a', {
|
.locator('tbody tr .cell-title a', {
|
||||||
hasText: exactText('Title With Many Versions 11'),
|
hasText: exactText('Title With Many Versions 11'),
|
||||||
})
|
})
|
||||||
.first()
|
.first()
|
||||||
|
|
||||||
expect(linkToDoc).toBeTruthy()
|
expect(linkToDoc).toBeTruthy()
|
||||||
await linkToDoc.click()
|
await linkToDoc.click()
|
||||||
|
|
||||||
const versionsTab = page.locator('.doc-tab', {
|
const versionsTab = page.locator('.doc-tab', {
|
||||||
hasText: 'Versions',
|
hasText: 'Versions',
|
||||||
})
|
})
|
||||||
await versionsTab.waitFor({ state: 'visible' })
|
await versionsTab.waitFor({ state: 'visible' })
|
||||||
|
const versionsPill = versionsTab.locator('.doc-tab__count--has-count')
|
||||||
|
await versionsPill.waitFor({ state: 'visible' })
|
||||||
const versionCount = await versionsTab.locator('.doc-tab__count').first().textContent()
|
const versionCount = await versionsTab.locator('.doc-tab__count').first().textContent()
|
||||||
expect(versionCount).toBe('11')
|
expect(versionCount).toBe('11')
|
||||||
})
|
})
|
||||||
@@ -322,32 +309,21 @@ describe('versions', () => {
|
|||||||
test('should restore version with correct data', async () => {
|
test('should restore version with correct data', async () => {
|
||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
await page.waitForURL(url.create)
|
await page.waitForURL(url.create)
|
||||||
|
|
||||||
// publish a doc
|
|
||||||
await page.locator('#field-title').fill('v1')
|
await page.locator('#field-title').fill('v1')
|
||||||
await page.locator('#field-description').fill('hello')
|
await page.locator('#field-description').fill('hello')
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
// save a draft
|
|
||||||
await page.locator('#field-title').fill('v2')
|
await page.locator('#field-title').fill('v2')
|
||||||
await saveDocAndAssert(page, '#action-save-draft')
|
await saveDocAndAssert(page, '#action-save-draft')
|
||||||
|
|
||||||
// go to versions list view
|
|
||||||
const savedDocURL = page.url()
|
const savedDocURL = page.url()
|
||||||
await page.goto(`${savedDocURL}/versions`)
|
await page.goto(`${savedDocURL}/versions`)
|
||||||
await page.waitForURL(`${savedDocURL}/versions`)
|
await page.waitForURL(new RegExp(`${savedDocURL}/versions`))
|
||||||
|
|
||||||
// select the first version (row 2)
|
|
||||||
const row2 = page.locator('tbody .row-2')
|
const row2 = page.locator('tbody .row-2')
|
||||||
const versionID = await row2.locator('.cell-id').textContent()
|
const versionID = await row2.locator('.cell-id').textContent()
|
||||||
await page.goto(`${savedDocURL}/versions/${versionID}`)
|
await page.goto(`${savedDocURL}/versions/${versionID}`)
|
||||||
await page.waitForURL(`${savedDocURL}/versions/${versionID}`)
|
await page.waitForURL(new RegExp(`${savedDocURL}/versions/${versionID}`))
|
||||||
|
|
||||||
// restore doc
|
|
||||||
await page.locator('.pill.restore-version').click()
|
await page.locator('.pill.restore-version').click()
|
||||||
await page.locator('button:has-text("Confirm")').click()
|
await page.locator('button:has-text("Confirm")').click()
|
||||||
await page.waitForURL(savedDocURL)
|
await page.waitForURL(new RegExp(savedDocURL))
|
||||||
|
|
||||||
await expect(page.locator('#field-title')).toHaveValue('v1')
|
await expect(page.locator('#field-title')).toHaveValue('v1')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -385,16 +361,12 @@ describe('versions', () => {
|
|||||||
|
|
||||||
test('global - should autosave', async () => {
|
test('global - should autosave', async () => {
|
||||||
const url = new AdminUrlUtil(serverURL, autoSaveGlobalSlug)
|
const url = new AdminUrlUtil(serverURL, autoSaveGlobalSlug)
|
||||||
// fill out global title and wait for autosave
|
|
||||||
await page.goto(url.global(autoSaveGlobalSlug))
|
await page.goto(url.global(autoSaveGlobalSlug))
|
||||||
await page.waitForURL(`**/${autoSaveGlobalSlug}`)
|
await page.waitForURL(`**/${autoSaveGlobalSlug}`)
|
||||||
const titleField = page.locator('#field-title')
|
const titleField = page.locator('#field-title')
|
||||||
|
|
||||||
await titleField.fill('global title')
|
await titleField.fill('global title')
|
||||||
await waitForAutoSaveToRunAndComplete(page)
|
await waitForAutoSaveToRunAndComplete(page)
|
||||||
await expect(titleField).toHaveValue('global title')
|
await expect(titleField).toHaveValue('global title')
|
||||||
|
|
||||||
// refresh the page and ensure value autosaved
|
|
||||||
await page.goto(url.global(autoSaveGlobalSlug))
|
await page.goto(url.global(autoSaveGlobalSlug))
|
||||||
await expect(page.locator('#field-title')).toHaveValue('global title')
|
await expect(page.locator('#field-title')).toHaveValue('global title')
|
||||||
})
|
})
|
||||||
@@ -405,41 +377,25 @@ describe('versions', () => {
|
|||||||
const englishTitle = 'english title'
|
const englishTitle = 'english title'
|
||||||
const spanishTitle = 'spanish title'
|
const spanishTitle = 'spanish title'
|
||||||
const newDescription = 'new description'
|
const newDescription = 'new description'
|
||||||
|
|
||||||
await page.goto(autosaveURL.create)
|
await page.goto(autosaveURL.create)
|
||||||
// gets redirected from /create to /slug/id due to autosave
|
// gets redirected from /create to /slug/id due to autosave
|
||||||
await page.waitForURL(`${autosaveURL.list}/**`)
|
await page.waitForURL(new RegExp(`${autosaveURL.edit('')}`))
|
||||||
await expect(() => expect(page.url()).not.toContain(`/create`)).toPass({
|
await wait(500)
|
||||||
timeout: POLL_TOPASS_TIMEOUT,
|
|
||||||
})
|
|
||||||
const titleField = page.locator('#field-title')
|
const titleField = page.locator('#field-title')
|
||||||
const descriptionField = page.locator('#field-description')
|
await expect(titleField).toBeEnabled()
|
||||||
|
|
||||||
// fill out en doc
|
|
||||||
await titleField.fill(englishTitle)
|
await titleField.fill(englishTitle)
|
||||||
|
const descriptionField = page.locator('#field-description')
|
||||||
|
await expect(descriptionField).toBeEnabled()
|
||||||
await descriptionField.fill('description')
|
await descriptionField.fill('description')
|
||||||
await waitForAutoSaveToRunAndComplete(page)
|
await waitForAutoSaveToRunAndComplete(page)
|
||||||
|
|
||||||
// change locale to spanish
|
|
||||||
await changeLocale(page, es)
|
await changeLocale(page, es)
|
||||||
// set localized title field
|
|
||||||
await titleField.fill(spanishTitle)
|
await titleField.fill(spanishTitle)
|
||||||
await waitForAutoSaveToRunAndComplete(page)
|
await waitForAutoSaveToRunAndComplete(page)
|
||||||
|
|
||||||
// change locale back to en
|
|
||||||
await changeLocale(page, en)
|
await changeLocale(page, en)
|
||||||
// verify en loads its own title
|
|
||||||
await expect(titleField).toHaveValue(englishTitle)
|
await expect(titleField).toHaveValue(englishTitle)
|
||||||
// change non-localized description field
|
|
||||||
await descriptionField.fill(newDescription)
|
await descriptionField.fill(newDescription)
|
||||||
await waitForAutoSaveToRunAndComplete(page)
|
await waitForAutoSaveToRunAndComplete(page)
|
||||||
|
|
||||||
// change locale to spanish
|
|
||||||
await changeLocale(page, es)
|
await changeLocale(page, es)
|
||||||
|
|
||||||
// reload page in spanish
|
|
||||||
// title should not be english title
|
|
||||||
// description should be new description
|
|
||||||
await page.reload()
|
await page.reload()
|
||||||
await expect(titleField).toHaveValue(spanishTitle)
|
await expect(titleField).toHaveValue(spanishTitle)
|
||||||
await expect(descriptionField).toHaveValue(newDescription)
|
await expect(descriptionField).toHaveValue(newDescription)
|
||||||
@@ -487,41 +443,30 @@ describe('versions', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('collection — autosave should only update the current document', async () => {
|
test('collection — autosave should only update the current document', async () => {
|
||||||
// create and save first doc
|
|
||||||
await page.goto(autosaveURL.create)
|
await page.goto(autosaveURL.create)
|
||||||
// Should redirect from /create to /[collectionslug]/[new id] due to auto-save
|
// gets redirected from /create to /slug/id due to autosave
|
||||||
await page.waitForURL(`${autosaveURL.list}/**`)
|
await page.waitForURL(new RegExp(`${autosaveURL.edit('')}`))
|
||||||
await expect(() => expect(page.url()).not.toContain(`/create`)).toPass({
|
await wait(500)
|
||||||
timeout: POLL_TOPASS_TIMEOUT,
|
await expect(page.locator('#field-title')).toBeEnabled()
|
||||||
}) // Make sure this doesnt match for list view and /create view, but ONLY for the ID edit view
|
|
||||||
|
|
||||||
await page.locator('#field-title').fill('first post title')
|
await page.locator('#field-title').fill('first post title')
|
||||||
|
await expect(page.locator('#field-description')).toBeEnabled()
|
||||||
await page.locator('#field-description').fill('first post description')
|
await page.locator('#field-description').fill('first post description')
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
await waitForAutoSaveToComplete(page) // Make sure nothing is auto-saving before next steps
|
await waitForAutoSaveToComplete(page) // Make sure nothing is auto-saving before next steps
|
||||||
|
|
||||||
// create and save second doc
|
|
||||||
await page.goto(autosaveURL.create)
|
await page.goto(autosaveURL.create)
|
||||||
// Should redirect from /create to /[collectionslug]/[new id] due to auto-save
|
// gets redirected from /create to /slug/id due to autosave
|
||||||
await page.waitForURL(`${autosaveURL.list}/**`)
|
await page.waitForURL(new RegExp(`${autosaveURL.edit('')}`))
|
||||||
await expect(() => expect(page.url()).not.toContain(`/create`)).toPass({
|
|
||||||
timeout: POLL_TOPASS_TIMEOUT,
|
|
||||||
}) // Make sure this doesnt match for list view and /create view, but only for the ID edit view
|
|
||||||
|
|
||||||
await waitForAutoSaveToComplete(page) // Make sure nothing is auto-saving before next steps
|
await waitForAutoSaveToComplete(page) // Make sure nothing is auto-saving before next steps
|
||||||
|
await wait(500)
|
||||||
|
await expect(page.locator('#field-title')).toBeEnabled()
|
||||||
await page.locator('#field-title').fill('second post title')
|
await page.locator('#field-title').fill('second post title')
|
||||||
|
await expect(page.locator('#field-description')).toBeEnabled()
|
||||||
await page.locator('#field-description').fill('second post description')
|
await page.locator('#field-description').fill('second post description')
|
||||||
// publish changes
|
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
await waitForAutoSaveToComplete(page) // Make sure nothing is auto-saving before next steps
|
await waitForAutoSaveToComplete(page) // Make sure nothing is auto-saving before next steps
|
||||||
|
|
||||||
// update second doc and wait for autosave
|
|
||||||
await page.locator('#field-title').fill('updated second post title')
|
await page.locator('#field-title').fill('updated second post title')
|
||||||
await page.locator('#field-description').fill('updated second post description')
|
await page.locator('#field-description').fill('updated second post description')
|
||||||
await waitForAutoSaveToRunAndComplete(page)
|
await waitForAutoSaveToRunAndComplete(page)
|
||||||
|
|
||||||
// verify that the first doc is unchanged
|
|
||||||
await page.goto(autosaveURL.list)
|
await page.goto(autosaveURL.list)
|
||||||
const secondRowLink = page.locator('tbody tr:nth-child(2) .cell-title a')
|
const secondRowLink = page.locator('tbody tr:nth-child(2) .cell-title a')
|
||||||
const docURL = await secondRowLink.getAttribute('href')
|
const docURL = await secondRowLink.getAttribute('href')
|
||||||
|
|||||||
Reference in New Issue
Block a user