Merge pull request #1102 from payloadcms/feat/group-collections
This commit is contained in:
@@ -61,6 +61,7 @@ You can customize the way that the Admin panel behaves on a collection-by-collec
|
||||
|
||||
| Option | Description |
|
||||
| ---------------------------- | -------------|
|
||||
| `group` | Text used as a label for grouping collection links together in the navigation. |
|
||||
| `useAsTitle` | Specify a top-level field to use for a document title throughout the Admin panel. If no field is defined, the ID of the document is used as the title. |
|
||||
| `description` | Text or React component to display below the Collection label in the List view to give editors more information. |
|
||||
| `defaultColumns` | Array of field names that correspond to which columns to show by default in this collection's List view. |
|
||||
@@ -78,7 +79,7 @@ If the function is specified, a Preview button will automatically appear in the
|
||||
**The preview function accepts two arguments:**
|
||||
|
||||
1. The document being edited
|
||||
1. An `options` object, containing `locale` and `token` properties. The `token` is the currently logged in user's JWT.
|
||||
1. An `options` object, containing `locale` and `token` properties. The `token` is the currently logged-in user's JWT.
|
||||
|
||||
**Example collection with preview function:**
|
||||
|
||||
|
||||
@@ -81,32 +81,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
padding: base(.125) 0;
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:active {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
margin: base(.25) 0 $baseline;
|
||||
|
||||
a {
|
||||
position: relative;
|
||||
padding: base(.125) base(1.5) base(.125) 0;
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
|
||||
svg {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: - base(.5);
|
||||
transform: rotate(-90deg);
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -114,12 +98,24 @@
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: normal;
|
||||
padding-left: base(.6);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
opacity: 1;
|
||||
}
|
||||
&__link {
|
||||
svg {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: - base(.5);
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
&.active {
|
||||
svg {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,4 +180,4 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import CloseMenu from '../../icons/CloseMenu';
|
||||
import Icon from '../../graphics/Icon';
|
||||
import Account from '../../graphics/Account';
|
||||
import Localizer from '../Localizer';
|
||||
import NavGroup from '../NavGroup';
|
||||
import { groupNavItems, Group, EntityToGroup, EntityType } from '../../../utilities/groupNavItems';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -18,6 +20,7 @@ const baseClass = 'nav';
|
||||
const DefaultNav = () => {
|
||||
const { permissions } = useAuth();
|
||||
const [menuActive, setMenuActive] = useState(false);
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const history = useHistory();
|
||||
const {
|
||||
collections,
|
||||
@@ -36,7 +39,29 @@ const DefaultNav = () => {
|
||||
const classes = [
|
||||
baseClass,
|
||||
menuActive && `${baseClass}--menu-active`,
|
||||
].filter(Boolean).join(' ');
|
||||
].filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
useEffect(() => {
|
||||
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));
|
||||
}, [collections, globals, permissions]);
|
||||
|
||||
useEffect(() => history.listen(() => {
|
||||
setMenuActive(false);
|
||||
@@ -65,56 +90,44 @@ const DefaultNav = () => {
|
||||
)}
|
||||
</button>
|
||||
</header>
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<nav className={`${baseClass}__wrap`}>
|
||||
{Array.isArray(beforeNavLinks) && beforeNavLinks.map((Component, i) => <Component key={i} />)}
|
||||
<span className={`${baseClass}__label`}>Collections</span>
|
||||
<nav>
|
||||
{collections && collections.map((collection, i) => {
|
||||
const href = `${admin}/collections/${collection.slug}`;
|
||||
{groups.map(({ label, entities }, key) => {
|
||||
return (
|
||||
<NavGroup {...{ key, label }}>
|
||||
{entities.map(({ entity, type }, i) => {
|
||||
let entityLabel: string;
|
||||
let href: string;
|
||||
let id: string;
|
||||
|
||||
if (permissions?.collections?.[collection.slug]?.read.permission) {
|
||||
return (
|
||||
<NavLink
|
||||
id={`nav-${collection.slug}`}
|
||||
activeClassName="active"
|
||||
key={i}
|
||||
to={href}
|
||||
>
|
||||
<Chevron />
|
||||
{collection.labels.plural}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</nav>
|
||||
{(globals && globals.length > 0) && (
|
||||
<React.Fragment>
|
||||
<span className={`${baseClass}__label`}>Globals</span>
|
||||
<nav>
|
||||
{globals.map((global, i) => {
|
||||
const href = `${admin}/globals/${global.slug}`;
|
||||
|
||||
if (permissions?.globals?.[global.slug].read.permission) {
|
||||
return (
|
||||
<NavLink
|
||||
id={`nav-global-${global.slug}`}
|
||||
activeClassName="active"
|
||||
key={i}
|
||||
to={href}
|
||||
>
|
||||
<Chevron />
|
||||
{global.label}
|
||||
</NavLink>
|
||||
);
|
||||
if (type === EntityType.collection) {
|
||||
href = `${admin}/collections/${entity.slug}`;
|
||||
entityLabel = entity.labels.plural;
|
||||
id = `nav-${entity.slug}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
if (type === EntityType.global) {
|
||||
href = `${admin}/globals/${entity.slug}`;
|
||||
entityLabel = entity.label;
|
||||
id = `nav-global-${entity.slug}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
id={id}
|
||||
className={`${baseClass}__link`}
|
||||
activeClassName="active"
|
||||
key={i}
|
||||
to={href}
|
||||
>
|
||||
<Chevron />
|
||||
{entityLabel}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</NavGroup>
|
||||
);
|
||||
})}
|
||||
{Array.isArray(afterNavLinks) && afterNavLinks.map((Component, i) => <Component key={i} />)}
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Localizer />
|
||||
@@ -131,7 +144,7 @@ const DefaultNav = () => {
|
||||
<LogOut />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
53
src/admin/components/elements/NavGroup/index.scss
Normal file
53
src/admin/components/elements/NavGroup/index.scss
Normal file
@@ -0,0 +1,53 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.nav-group {
|
||||
width: 100%;
|
||||
margin-bottom: base(.5);
|
||||
|
||||
&__toggle {
|
||||
cursor: pointer;
|
||||
color: var(--theme-elevation-400);
|
||||
background: transparent;
|
||||
padding-left: 0;
|
||||
border: 0;
|
||||
margin-top: base(.25);
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-right: base(.5);
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
margin-top: base(-.2);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-elevation-1000);
|
||||
|
||||
.stroke {
|
||||
stroke: var(--theme-elevation-1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__indicator {
|
||||
margin-left: auto;
|
||||
|
||||
.stroke {
|
||||
stroke: var(--theme-elevation-200);
|
||||
}
|
||||
}
|
||||
|
||||
&--collapsed {
|
||||
.collapsible__toggle {
|
||||
border-bottom-right-radius: $style-radius-s;
|
||||
border-bottom-left-radius: $style-radius-s;
|
||||
}
|
||||
|
||||
.nav-group__indicator {
|
||||
transform: rotate(.5turn);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
91
src/admin/components/elements/NavGroup/index.tsx
Normal file
91
src/admin/components/elements/NavGroup/index.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import AnimateHeight from 'react-animate-height';
|
||||
import Chevron from '../../icons/Chevron';
|
||||
import { usePreferences } from '../../utilities/Preferences';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'nav-group';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode,
|
||||
label: string,
|
||||
}
|
||||
|
||||
const NavGroup: React.FC<Props> = ({
|
||||
children,
|
||||
label,
|
||||
}) => {
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const [animate, setAnimate] = useState(false);
|
||||
const { getPreference, setPreference } = usePreferences();
|
||||
|
||||
const preferencesKey = `collapsed-${label}-groups`;
|
||||
|
||||
useEffect(() => {
|
||||
if (label) {
|
||||
const setCollapsedFromPreferences = async () => {
|
||||
const preferences = await getPreference(preferencesKey) || [];
|
||||
setCollapsed(preferences.indexOf(label) !== -1);
|
||||
};
|
||||
setCollapsedFromPreferences();
|
||||
}
|
||||
}, [getPreference, label, preferencesKey]);
|
||||
|
||||
if (label) {
|
||||
const toggleCollapsed = async () => {
|
||||
setAnimate(true);
|
||||
let preferences: string[] = await getPreference(preferencesKey) || [];
|
||||
if (collapsed) {
|
||||
preferences = preferences.filter((preference) => label !== preference);
|
||||
} else {
|
||||
preferences.push(label);
|
||||
}
|
||||
setPreference(preferencesKey, preferences);
|
||||
setCollapsed(!collapsed);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`nav-group-${label}`}
|
||||
className={[
|
||||
`${baseClass}`,
|
||||
`${label}`,
|
||||
collapsed && `${baseClass}--collapsed`,
|
||||
].filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={[
|
||||
`${baseClass}__toggle`,
|
||||
`${baseClass}__toggle--${collapsed ? 'collapsed' : 'open'}`,
|
||||
].filter(Boolean)
|
||||
.join(' ')}
|
||||
onClick={toggleCollapsed}
|
||||
>
|
||||
<div className={`${baseClass}__label`}>
|
||||
{label}
|
||||
</div>
|
||||
<Chevron className={`${baseClass}__indicator`} />
|
||||
</button>
|
||||
<AnimateHeight
|
||||
height={collapsed ? 0 : 'auto'}
|
||||
duration={animate ? 200 : 0}
|
||||
>
|
||||
<div className={`${baseClass}__content`}>
|
||||
{children}
|
||||
</div>
|
||||
</AnimateHeight>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{children}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavGroup;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
|
||||
@@ -7,6 +7,7 @@ import Card from '../../elements/Card';
|
||||
import Button from '../../elements/Button';
|
||||
import { Props } from './types';
|
||||
import { Gutter } from '../../elements/Gutter';
|
||||
import { groupNavItems, Group, EntityToGroup, EntityType } from '../../../utilities/groupNavItems';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -33,52 +34,81 @@ const Dashboard: React.FC<Props> = (props) => {
|
||||
},
|
||||
} = useConfig();
|
||||
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
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));
|
||||
}, [collections, globals, permissions]);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Eyebrow />
|
||||
<Gutter className={`${baseClass}__wrap`}>
|
||||
{Array.isArray(beforeDashboard) && beforeDashboard.map((Component, i) => <Component key={i} />)}
|
||||
<h2 className={`${baseClass}__label`}>Collections</h2>
|
||||
<ul className={`${baseClass}__card-list`}>
|
||||
{collections.map((collection) => {
|
||||
const hasCreatePermission = permissions?.collections?.[collection.slug]?.create?.permission;
|
||||
{groups.map(({ label, entities }, groupIndex) => {
|
||||
return (
|
||||
<React.Fragment key={groupIndex}>
|
||||
<h2 className={`${baseClass}__label`}>{label}</h2>
|
||||
<ul className={`${baseClass}__card-list`}>
|
||||
{entities.map(({ entity, type }, entityIndex) => {
|
||||
let title: string;
|
||||
let createHREF: string;
|
||||
let onClick: () => void;
|
||||
let hasCreatePermission: boolean;
|
||||
|
||||
return (
|
||||
<li key={collection.slug}>
|
||||
<Card
|
||||
title={collection.labels.plural}
|
||||
id={`card-${collection.slug}`}
|
||||
onClick={() => push({ pathname: `${admin}/collections/${collection.slug}` })}
|
||||
actions={hasCreatePermission ? (
|
||||
<Button
|
||||
el="link"
|
||||
to={`${admin}/collections/${collection.slug}/create`}
|
||||
icon="plus"
|
||||
round
|
||||
buttonStyle="icon-label"
|
||||
iconStyle="with-border"
|
||||
/>
|
||||
) : undefined}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{(globals.length > 0) && (
|
||||
<React.Fragment>
|
||||
<h2 className={`${baseClass}__label`}>Globals</h2>
|
||||
<ul className={`${baseClass}__card-list`}>
|
||||
{globals.map((global) => (
|
||||
<li key={global.slug}>
|
||||
<Card
|
||||
title={global.label}
|
||||
onClick={() => push({ pathname: `${admin}/globals/${global.slug}` })}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
)}
|
||||
if (type === EntityType.collection) {
|
||||
title = entity.labels.plural;
|
||||
onClick = () => push({ pathname: `${admin}/collections/${entity.slug}` });
|
||||
createHREF = `${admin}/collections/${entity.slug}/create`;
|
||||
hasCreatePermission = permissions?.collections?.[entity.slug]?.create?.permission;
|
||||
}
|
||||
|
||||
if (type === EntityType.global) {
|
||||
title = entity.label;
|
||||
onClick = () => push({ pathname: `${admin}/globals/${global.slug}` });
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={entityIndex}>
|
||||
<Card
|
||||
title={title}
|
||||
id={`card-${entity.slug}`}
|
||||
onClick={onClick}
|
||||
actions={(hasCreatePermission && type === EntityType.collection) ? (
|
||||
<Button
|
||||
el="link"
|
||||
to={createHREF}
|
||||
icon="plus"
|
||||
round
|
||||
buttonStyle="icon-label"
|
||||
iconStyle="with-border"
|
||||
/>
|
||||
) : undefined}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{Array.isArray(afterDashboard) && afterDashboard.map((Component, i) => <Component key={i} />)}
|
||||
</Gutter>
|
||||
</div>
|
||||
|
||||
@@ -57,4 +57,4 @@
|
||||
margin: 0 base(.25) base(.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/admin/utilities/groupNavItems.ts
Normal file
54
src/admin/utilities/groupNavItems.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Permissions } from '../../auth';
|
||||
import { SanitizedCollectionConfig } from '../../collections/config/types';
|
||||
import { SanitizedGlobalConfig } from '../../globals/config/types';
|
||||
|
||||
export enum EntityType {
|
||||
collection = 'Collections',
|
||||
global = 'Globals'
|
||||
}
|
||||
|
||||
export type EntityToGroup = {
|
||||
type: EntityType.collection
|
||||
entity: SanitizedCollectionConfig
|
||||
} | {
|
||||
type: EntityType.global
|
||||
entity: SanitizedGlobalConfig
|
||||
}
|
||||
|
||||
export type Group = {
|
||||
label: string
|
||||
entities: EntityToGroup[]
|
||||
}
|
||||
|
||||
export function groupNavItems(entities: EntityToGroup[], permissions: Permissions): Group[] {
|
||||
const result = entities.reduce((groups, entityToGroup) => {
|
||||
if (permissions?.[entityToGroup.type.toLowerCase()]?.[entityToGroup.entity.slug]?.read.permission) {
|
||||
if (entityToGroup.entity.admin.group) {
|
||||
const existingGroup = groups.find((group) => group.label === entityToGroup.entity.admin.group);
|
||||
let matchedGroup: Group = existingGroup;
|
||||
if (!existingGroup) {
|
||||
matchedGroup = { label: entityToGroup.entity.admin.group, entities: [] };
|
||||
groups.push(matchedGroup);
|
||||
}
|
||||
|
||||
matchedGroup.entities.push(entityToGroup);
|
||||
} else {
|
||||
const defaultGroup = groups.find((group) => group.label === entityToGroup.type);
|
||||
defaultGroup.entities.push(entityToGroup);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}, [
|
||||
{
|
||||
label: 'Collections',
|
||||
entities: [],
|
||||
},
|
||||
{
|
||||
label: 'Globals',
|
||||
entities: [],
|
||||
},
|
||||
]);
|
||||
|
||||
return result.filter((group) => group.entities.length > 0);
|
||||
}
|
||||
@@ -26,6 +26,7 @@ const collectionSchema = joi.object().keys({
|
||||
admin: joi.object({
|
||||
useAsTitle: joi.string(),
|
||||
defaultColumns: joi.array().items(joi.string()),
|
||||
group: joi.string(),
|
||||
description: joi.alternatives().try(
|
||||
joi.string(),
|
||||
componentSchema,
|
||||
|
||||
@@ -151,6 +151,10 @@ export type CollectionAdminOptions = {
|
||||
* Default columns to show in list view
|
||||
*/
|
||||
defaultColumns?: string[];
|
||||
/**
|
||||
* Place collections into a navigational group
|
||||
*/
|
||||
group?: string;
|
||||
/**
|
||||
* Custom description for collection
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,7 @@ const globalSchema = joi.object().keys({
|
||||
slug: joi.string().required(),
|
||||
label: joi.string(),
|
||||
admin: joi.object({
|
||||
group: joi.string(),
|
||||
hideAPIURL: joi.boolean(),
|
||||
description: joi.alternatives().try(
|
||||
joi.string(),
|
||||
|
||||
@@ -65,6 +65,7 @@ export type GlobalConfig = {
|
||||
fields: Field[];
|
||||
admin?: {
|
||||
description?: string | (() => string);
|
||||
group?: string;
|
||||
hideAPIURL?: boolean;
|
||||
components?: {
|
||||
views?: {
|
||||
|
||||
@@ -19,6 +19,7 @@ const AfterNavLinks: React.FC = () => {
|
||||
<span className="nav__label">Custom Routes</span>
|
||||
<nav>
|
||||
<NavLink
|
||||
className="nav__link"
|
||||
activeClassName="active"
|
||||
to={`${adminRoute}/custom-default-route`}
|
||||
>
|
||||
@@ -26,6 +27,7 @@ const AfterNavLinks: React.FC = () => {
|
||||
Default Template
|
||||
</NavLink>
|
||||
<NavLink
|
||||
className="nav__link"
|
||||
activeClassName="active"
|
||||
to={`${adminRoute}/custom-minimal-route`}
|
||||
>
|
||||
|
||||
@@ -48,8 +48,16 @@ export default buildConfig({
|
||||
},
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
fields: [],
|
||||
},
|
||||
{
|
||||
slug,
|
||||
admin: {
|
||||
group: 'One',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
@@ -61,10 +69,85 @@ export default buildConfig({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'group-one-collection-ones',
|
||||
admin: {
|
||||
group: 'One',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'group-one-collection-twos',
|
||||
admin: {
|
||||
group: 'One',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'group-two-collection-ones',
|
||||
admin: {
|
||||
group: 'Two',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'group-two-collection-twos',
|
||||
admin: {
|
||||
group: 'Two',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
globals: [
|
||||
{
|
||||
slug: globalSlug,
|
||||
admin: {
|
||||
group: 'Group',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'group-globals-one',
|
||||
admin: {
|
||||
group: 'Group',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'group-globals-two',
|
||||
admin: {
|
||||
group: 'Group',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
|
||||
@@ -56,6 +56,48 @@ describe('admin', () => {
|
||||
expect(page.url()).toContain(url.list);
|
||||
});
|
||||
|
||||
test('should collapse and expand collection groups', async () => {
|
||||
await page.goto(url.admin);
|
||||
const navGroup = page.locator('#nav-group-One .nav-group__toggle');
|
||||
const link = await page.locator('#nav-group-one-collection-ones');
|
||||
|
||||
await expect(navGroup).toContainText('One');
|
||||
await expect(link).toBeVisible();
|
||||
|
||||
await navGroup.click();
|
||||
await expect(link).not.toBeVisible();
|
||||
|
||||
await navGroup.click();
|
||||
await expect(link).toBeVisible();
|
||||
});
|
||||
|
||||
test('should collapse and expand globals groups', async () => {
|
||||
await page.goto(url.admin);
|
||||
const navGroup = page.locator('#nav-group-Group .nav-group__toggle');
|
||||
const link = await page.locator('#nav-global-group-globals-one');
|
||||
|
||||
await expect(navGroup).toContainText('Group');
|
||||
await expect(link).toBeVisible();
|
||||
|
||||
await navGroup.click();
|
||||
await expect(link).not.toBeVisible();
|
||||
|
||||
await navGroup.click();
|
||||
await expect(link).toBeVisible();
|
||||
});
|
||||
|
||||
test('should save nav group collapse preferences', async () => {
|
||||
await page.goto(url.admin);
|
||||
|
||||
const navGroup = page.locator('#nav-group-One .nav-group__toggle');
|
||||
await navGroup.click();
|
||||
|
||||
await page.goto(url.admin);
|
||||
|
||||
const link = await page.locator('#nav-group-one-collection-ones');
|
||||
await expect(link).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('breadcrumbs - from list to dashboard', async () => {
|
||||
await page.goto(url.list);
|
||||
await page.locator('.step-nav a[href="/admin"]').click();
|
||||
|
||||
Reference in New Issue
Block a user