From 67d61df56345e0d8aae95c9d5159ff4ebec9b65e Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Fri, 20 Oct 2023 22:22:41 -0400 Subject: [PATCH] fix: standardizes layout of document fields (#3798) --- .../DocumentFields}/index.scss | 18 +- .../elements/DocumentFields/index.tsx | 87 ++++++++++ .../components/views/Account/Default.tsx | 136 +++++---------- .../views/Account/Settings/index.scss | 45 +++++ .../views/Account/Settings/index.tsx | 42 +++++ .../admin/components/views/Account/index.scss | 42 +---- .../components/views/Global/Default/index.tsx | 88 ++-------- .../views/collections/Edit/Default/index.scss | 163 +----------------- .../views/collections/Edit/Default/index.tsx | 85 +++------ test/access-control/config.ts | 3 - test/admin/collections/Users.ts | 14 +- test/admin/e2e.spec.ts | 4 +- 12 files changed, 283 insertions(+), 444 deletions(-) rename packages/payload/src/admin/components/{views/Global/Default => elements/DocumentFields}/index.scss (91%) create mode 100644 packages/payload/src/admin/components/elements/DocumentFields/index.tsx create mode 100644 packages/payload/src/admin/components/views/Account/Settings/index.scss create mode 100644 packages/payload/src/admin/components/views/Account/Settings/index.tsx diff --git a/packages/payload/src/admin/components/views/Global/Default/index.scss b/packages/payload/src/admin/components/elements/DocumentFields/index.scss similarity index 91% rename from packages/payload/src/admin/components/views/Global/Default/index.scss rename to packages/payload/src/admin/components/elements/DocumentFields/index.scss index bcc8918821..2512d9fa7d 100644 --- a/packages/payload/src/admin/components/views/Global/Default/index.scss +++ b/packages/payload/src/admin/components/elements/DocumentFields/index.scss @@ -1,12 +1,12 @@ -@import '../../../../scss/styles.scss'; +@import '../../../scss/styles.scss'; -.global-default-edit { +.document-fields { width: 100%; display: flex; --doc-sidebar-width: 325px; &--has-sidebar { - .global-default-edit { + .document-fields { &__edit { [dir='ltr'] & { top: 0; @@ -46,10 +46,6 @@ flex-grow: 1; } - &__auth { - margin-bottom: var(--base); - } - &__sidebar-wrap { position: sticky; top: var(--doc-controls-height); @@ -62,9 +58,6 @@ width: 100%; height: 100%; overflow-y: auto; - } - - &__sidebar-sticky-wrap { display: flex; flex-direction: column; min-height: 100%; @@ -88,7 +81,7 @@ display: block; &--has-sidebar { - .global-default-edit { + .document-fields { &__main { width: 100%; } @@ -124,8 +117,6 @@ width: 100%; height: initial; border-left: 0; - margin-top: calc(var(--base) / 2); - width: var(--doc-sidebar-width); } &__form { @@ -136,6 +127,7 @@ padding-top: 0; padding-left: var(--gutter-h); padding-right: var(--gutter-h); + padding-bottom: 0; gap: base(0.5); [dir='ltr'] & { diff --git a/packages/payload/src/admin/components/elements/DocumentFields/index.tsx b/packages/payload/src/admin/components/elements/DocumentFields/index.tsx new file mode 100644 index 0000000000..d70916f042 --- /dev/null +++ b/packages/payload/src/admin/components/elements/DocumentFields/index.tsx @@ -0,0 +1,87 @@ +import React from 'react' + +import type { CollectionPermission, GlobalPermission } from '../../../../auth' +import type { FieldWithPath } from '../../../../fields/config/types' +import type { Description } from '../../forms/FieldDescription/types' + +import RenderFields from '../../forms/RenderFields' +import { filterFields } from '../../forms/RenderFields/filterFields' +import { fieldTypes } from '../../forms/field-types' +import { Gutter } from '../Gutter' +import ViewDescription from '../ViewDescription' +import './index.scss' + +const baseClass = 'document-fields' + +export const DocumentFields: React.FC<{ + AfterFields?: React.FC + BeforeFields?: React.FC + description?: Description + fields: FieldWithPath[] + hasSavePermission: boolean + permissions: CollectionPermission | GlobalPermission +}> = (props) => { + const { AfterFields, BeforeFields, description, fields, hasSavePermission, permissions } = props + + const sidebarFields = filterFields({ + fieldSchema: fields, + fieldTypes, + filter: (field) => field?.admin?.position === 'sidebar', + permissions: permissions.fields, + readOnly: !hasSavePermission, + }) + + const hasSidebar = sidebarFields && sidebarFields.length > 0 + + return ( + +
+
+ +
+ {description && ( +
+ +
+ )} +
+ {BeforeFields && } + + !field.admin.position || + (field.admin.position && field.admin.position !== 'sidebar') + } + permissions={permissions.fields} + readOnly={!hasSavePermission} + /> + {AfterFields && } +
+
+ {hasSidebar && ( +
+
+
+ +
+
+
+ )} +
+
+ ) +} diff --git a/packages/payload/src/admin/components/views/Account/Default.tsx b/packages/payload/src/admin/components/views/Account/Default.tsx index 7cb447f963..f9474fd9b2 100644 --- a/packages/payload/src/admin/components/views/Account/Default.tsx +++ b/packages/payload/src/admin/components/views/Account/Default.tsx @@ -1,24 +1,19 @@ import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import type { Translation } from '../../../../translations/type' import type { CollectionEditViewProps } from '../types' import { DocumentControls } from '../../elements/DocumentControls' +import { DocumentFields } from '../../elements/DocumentFields' import { DocumentHeader } from '../../elements/DocumentHeader' -import { Gutter } from '../../elements/Gutter' import { LoadingOverlayToggle } from '../../elements/Loading' -import ReactSelect from '../../elements/ReactSelect' import Form from '../../forms/Form' -import Label from '../../forms/Label' -import RenderFields from '../../forms/RenderFields' -import { fieldTypes } from '../../forms/field-types' import { LeaveWithoutSaving } from '../../modals/LeaveWithoutSaving' import { useAuth } from '../../utilities/Auth' import Meta from '../../utilities/Meta' import { OperationContext } from '../../utilities/OperationProvider' import Auth from '../collections/Edit/Auth' -import { ToggleTheme } from './ToggleTheme' +import { Settings } from './Settings' import './index.scss' const baseClass = 'account' @@ -39,14 +34,7 @@ const DefaultAccount: React.FC = (props) => { const { auth, fields } = collection const { refreshCookieAsync } = useAuth() - const { i18n, t } = useTranslation('authentication') - - const languageOptions = Object.entries(i18n.options.resources || {}).map( - ([language, resource]) => ({ - label: (resource as Translation).general.thisLanguage, - value: language, - }), - ) + const { t } = useTranslation('authentication') const onSave = useCallback(async () => { await refreshCookieAsync() @@ -55,91 +43,49 @@ const DefaultAccount: React.FC = (props) => { } }, [onSaveFromProps, refreshCookieAsync]) - const classes = [baseClass].filter(Boolean).join(' ') - return ( + {!isLoading && ( -
- -
- - -
- + + {!(collection.versions?.drafts && collection.versions?.drafts?.autosave) && ( + + )} + + + } + BeforeFields={() => ( + - {!(collection.versions?.drafts && collection.versions?.drafts?.autosave) && ( - - )} -
- - - field?.admin?.position !== 'sidebar'} - permissions={permissions?.fields} - readOnly={!hasSavePermission} - /> - - -

{t('general:payloadSettings')}

-
-
- -
-
-
-
-
-
-
- field?.admin?.position === 'sidebar'} - permissions={permissions?.fields} - readOnly={!hasSavePermission} - /> -
-
-
-
- -
-
+ )} + fields={fields} + hasSavePermission={hasSavePermission} + permissions={permissions} + /> + + )}
) diff --git a/packages/payload/src/admin/components/views/Account/Settings/index.scss b/packages/payload/src/admin/components/views/Account/Settings/index.scss new file mode 100644 index 0000000000..16a946826c --- /dev/null +++ b/packages/payload/src/admin/components/views/Account/Settings/index.scss @@ -0,0 +1,45 @@ +@import '../../../../scss/styles.scss'; + +.payload-settings { + position: relative; + + h3 { + margin: 0; + } + + &::before, + &::after { + content: ''; + display: block; + height: 1px; + background: var(--theme-elevation-100); + width: calc(100% + calc(var(--base) * 5)); + left: calc(var(--gutter-h) * -1); + top: 0; + position: absolute; + } + + &::after { + display: none; + bottom: 0; + top: unset; + } + + margin-top: base(3); + padding-top: base(3); + padding-bottom: base(3); + display: flex; + flex-direction: column; + gap: var(--base); + + @include mid-break { + margin-bottom: var(--base); + padding-top: base(2); + margin-top: base(2); + padding-bottom: base(2); + + &::after { + display: block; + } + } +} diff --git a/packages/payload/src/admin/components/views/Account/Settings/index.tsx b/packages/payload/src/admin/components/views/Account/Settings/index.tsx new file mode 100644 index 0000000000..5a641a2903 --- /dev/null +++ b/packages/payload/src/admin/components/views/Account/Settings/index.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' + +import type { Translation } from '../../../../../translations/type' + +import ReactSelect from '../../../elements/ReactSelect' +import Label from '../../../forms/Label' +import { ToggleTheme } from '../ToggleTheme' +import './index.scss' + +const baseClass = 'payload-settings' + +export const Settings: React.FC<{ + className?: string +}> = (props) => { + const { className } = props + + const { i18n, t } = useTranslation('authentication') + + const languageOptions = Object.entries(i18n.options.resources || {}).map( + ([language, resource]) => ({ + label: (resource as Translation).general.thisLanguage, + value: language, + }), + ) + + return ( +
+

{t('general:payloadSettings')}

+
+
+ +
+ ) +} diff --git a/packages/payload/src/admin/components/views/Account/index.scss b/packages/payload/src/admin/components/views/Account/index.scss index 19189d962d..bd1976a24c 100644 --- a/packages/payload/src/admin/components/views/Account/index.scss +++ b/packages/payload/src/admin/components/views/Account/index.scss @@ -1,47 +1,19 @@ @import '../../../scss/styles.scss'; .account { - width: 100%; - padding-bottom: var(--spacing-view-bottom); - - &__form { - height: 100%; - } - - &__edit { - margin-top: calc(var(--base) * 3); - } - &__auth { - margin-bottom: var(--base); + margin-bottom: calc(var(--base) * 2); + margin-top: calc(var(--base) * 0.5); } - &__header { - display: flex; - flex-direction: column; - } - - &__payload-settings { - margin-top: base(3); - padding-top: base(3); - border-top: 1px solid var(--theme-elevation-100); - } - - &__language { - margin-bottom: $baseline; + &___settings { + margin-bottom: calc(var(--base) * 2); } @include small-break { - &__edit { - margin: var(--base) 0; - } - - &__payload-settings { - margin-top: base(1); - padding-top: base(1); - padding-bottom: base(0.5); - border-top: 1px solid var(--theme-elevation-100); - border-bottom: 1px solid var(--theme-elevation-100); + &__auth { + margin-top: 0; + margin-bottom: var(--base); } } } diff --git a/packages/payload/src/admin/components/views/Global/Default/index.tsx b/packages/payload/src/admin/components/views/Global/Default/index.tsx index ad2f503e56..f040b42325 100644 --- a/packages/payload/src/admin/components/views/Global/Default/index.tsx +++ b/packages/payload/src/admin/components/views/Global/Default/index.tsx @@ -5,39 +5,27 @@ import type { GlobalEditViewProps } from '../../types' import { getTranslation } from '../../../../../utilities/getTranslation' import { DocumentControls } from '../../../elements/DocumentControls' -import { Gutter } from '../../../elements/Gutter' -import ViewDescription from '../../../elements/ViewDescription' -import RenderFields from '../../../forms/RenderFields' -import { filterFields } from '../../../forms/RenderFields/filterFields' -import { fieldTypes } from '../../../forms/field-types' +import { DocumentFields } from '../../../elements/DocumentFields' import { LeaveWithoutSaving } from '../../../modals/LeaveWithoutSaving' import Meta from '../../../utilities/Meta' import { SetStepNav } from '../../collections/Edit/SetStepNav' -import './index.scss' - -const baseClass = 'global-default-edit' export const DefaultGlobalEdit: React.FC = (props) => { - const { i18n } = useTranslation('general') - const { apiURL, data, global, permissions } = props + const { i18n } = useTranslation() const { admin: { description } = {}, fields, label } = global const hasSavePermission = permissions?.update?.permission - const sidebarFields = filterFields({ - fieldSchema: fields, - fieldTypes, - filter: (field) => field?.admin?.position === 'sidebar', - permissions: permissions.fields, - readOnly: !hasSavePermission, - }) - - const hasSidebar = sidebarFields && sidebarFields.length > 0 - return ( + + {!(global.versions?.drafts && global.versions?.drafts?.autosave) && } = (props) => { isEditing permissions={permissions} /> -
-
- - {!(global.versions?.drafts && global.versions?.drafts?.autosave) && ( - - )} - -
- {description && ( -
- -
- )} -
- - !field.admin.position || - (field.admin.position && field.admin.position !== 'sidebar') - } - permissions={permissions.fields} - readOnly={!hasSavePermission} - /> -
-
- {hasSidebar && ( -
-
-
-
- -
-
-
-
- )} -
+
) } diff --git a/packages/payload/src/admin/components/views/collections/Edit/Default/index.scss b/packages/payload/src/admin/components/views/collections/Edit/Default/index.scss index a358f5b263..d15995cc80 100644 --- a/packages/payload/src/admin/components/views/collections/Edit/Default/index.scss +++ b/packages/payload/src/admin/components/views/collections/Edit/Default/index.scss @@ -1,168 +1,15 @@ @import '../../../../../scss/styles.scss'; .collection-default-edit { - width: 100%; - display: flex; - --doc-sidebar-width: 500px; - - &--has-sidebar { - .collection-default-edit { - &__edit { - [dir='ltr'] & { - top: 0; - right: 0; - border-right: 1px solid var(--theme-elevation-100); - padding-right: calc(var(--base) * 2); - } - - [dir='rtl'] & { - top: 0; - left: 0; - border-left: 1px solid var(--theme-elevation-100); - padding-left: calc(var(--base) * 2); - } - } - - &__fields { - & > .tabs-field, - & > .group-field { - margin-right: calc(var(--base) * -2); - } - } - } - } - - &__main { - width: 100%; - display: flex; - flex-direction: column; - min-height: 100%; - flex-grow: 1; - } - - &__edit { - padding-top: calc(var(--base) * 1.5); - padding-bottom: var(--spacing-view-bottom); - flex-grow: 1; - } - &__auth { - margin-bottom: var(--base); - } - - &__sidebar-wrap { - position: sticky; - top: var(--doc-controls-height); - height: calc(100vh - var(--doc-controls-height)); - width: var(--doc-sidebar-width); - flex-shrink: 0; - } - - &__sidebar { - width: 100%; - height: 100%; - overflow-y: auto; - } - - &__sidebar-sticky-wrap { - display: flex; - flex-direction: column; - min-height: 100%; - } - - &__sidebar-fields { - display: flex; - flex-direction: column; - gap: var(--base); - padding-top: calc(var(--base) * 1.5); - padding-left: calc(var(--base) * 2); - padding-right: var(--gutter-h); - padding-bottom: var(--spacing-view-bottom); - } - - &__label { - color: var(--theme-elevation-400); - } - - @include large-break { - --doc-sidebar-width: 350px; - } - - @include mid-break { - display: block; - - &--has-sidebar { - .collection-default-edit { - &__main { - width: 100%; - } - - &__edit { - [dir='ltr'] & { - border-right: 0; - padding-right: var(--gutter-h); - } - - [dir='rtl'] & { - border-left: 0; - padding-left: var(--gutter-h); - } - } - - &__fields { - & > .tabs-field, - & > .group-field { - margin-right: calc(var(--gutter-h) * -1); - } - } - } - } - - &__main { - width: 100%; - min-height: initial; - } - - &__sidebar-wrap { - position: static; - width: 100%; - height: initial; - border-left: 0; - margin-top: calc(var(--base) / 2); - } - - &__form { - display: block; - } - - &__sidebar-fields { - padding-top: 0; - padding-left: var(--gutter-h); - padding-right: var(--gutter-h); - gap: base(0.5); - - [dir='ltr'] & { - padding-right: var(--gutter-h); - } - - [dir='rtl'] & { - padding-left: var(--gutter-h); - } - } - - &__sidebar { - padding-bottom: base(3.5); - overflow: visible; - } + margin-bottom: calc(var(--base) * 2); + margin-top: calc(var(--base) * 0.5); } @include small-break { - &__sidebar-wrap { - min-width: initial; - } - - &__edit { - padding-top: calc(var(--base) / 2); + &__auth { + margin-top: 0; + margin-bottom: var(--base); } } } diff --git a/packages/payload/src/admin/components/views/collections/Edit/Default/index.tsx b/packages/payload/src/admin/components/views/collections/Edit/Default/index.tsx index 23ce9817e0..b622876720 100644 --- a/packages/payload/src/admin/components/views/collections/Edit/Default/index.tsx +++ b/packages/payload/src/admin/components/views/collections/Edit/Default/index.tsx @@ -5,10 +5,7 @@ import type { CollectionEditViewProps } from '../../../types' import { getTranslation } from '../../../../../../utilities/getTranslation' import { DocumentControls } from '../../../../elements/DocumentControls' -import { Gutter } from '../../../../elements/Gutter' -import RenderFields from '../../../../forms/RenderFields' -import { filterFields } from '../../../../forms/RenderFields/filterFields' -import { fieldTypes } from '../../../../forms/field-types' +import { DocumentFields } from '../../../../elements/DocumentFields' import { LeaveWithoutSaving } from '../../../../modals/LeaveWithoutSaving' import Meta from '../../../../utilities/Meta' import Auth from '../Auth' @@ -38,18 +35,21 @@ export const DefaultCollectionEdit: React.FC = (props) const operation = isEditing ? 'update' : 'create' - const sidebarFields = filterFields({ - fieldSchema: fields, - fieldTypes, - filter: (field) => field?.admin?.position === 'sidebar', - permissions: permissions.fields, - readOnly: !hasSavePermission, - }) - - const hasSidebar = sidebarFields && sidebarFields.length > 0 - return ( + + {!(collection.versions?.drafts && collection.versions?.drafts?.autosave) && + !disableLeaveWithoutSaving && } = (props) isEditing={isEditing} permissions={permissions} /> -
-
- - {!(collection.versions?.drafts && collection.versions?.drafts?.autosave) && - !disableLeaveWithoutSaving && } - + ( + {auth && ( = (props) /> )} {upload && } - !field?.admin?.position || field?.admin?.position !== 'sidebar'} - permissions={permissions.fields} - readOnly={!hasSavePermission} - /> - -
- {hasSidebar && ( -
-
-
-
- -
-
-
-
+ )} -
+ fields={fields} + hasSavePermission={hasSavePermission} + permissions={permissions} + />
) } diff --git a/test/access-control/config.ts b/test/access-control/config.ts index 6d89064af7..be38197429 100644 --- a/test/access-control/config.ts +++ b/test/access-control/config.ts @@ -283,9 +283,6 @@ export default buildConfigWithDefaults({ name: 'approvedForRemoval', type: 'checkbox', defaultValue: false, - admin: { - position: 'sidebar', - }, }, { name: 'approvedTitle', diff --git a/test/admin/collections/Users.ts b/test/admin/collections/Users.ts index 9469fdbda8..2ea64af34c 100644 --- a/test/admin/collections/Users.ts +++ b/test/admin/collections/Users.ts @@ -6,5 +6,17 @@ export const Users: CollectionConfig = { admin: { useAsTitle: 'email', }, - fields: [], + fields: [ + { + name: 'textField', + type: 'text', + }, + { + name: 'sidebarField', + type: 'text', + admin: { + position: 'sidebar', + }, + }, + ], } diff --git a/test/admin/e2e.spec.ts b/test/admin/e2e.spec.ts index 0f4d17625d..99b0bef8dc 100644 --- a/test/admin/e2e.spec.ts +++ b/test/admin/e2e.spec.ts @@ -395,7 +395,7 @@ describe('admin', () => { test('should allow changing language', async () => { await page.goto(url.account) - const field = page.locator('.account__language .react-select') + const field = page.locator('.payload-settings__language .react-select') await field.click() const options = page.locator('.rs__option') @@ -981,7 +981,7 @@ describe('admin', () => { test('should use fallback language on field titles', async () => { // change language German await page.goto(url.account) - await page.locator('.account__language .react-select').click() + await page.locator('.payload-settings__language .react-select').click() const languageSelect = page.locator('.rs__option') // text field does not have a 'de' label await languageSelect.locator('text=Deutsch').click()