feat: collection groups

This commit is contained in:
Dan Ribbens
2022-09-10 10:00:08 -04:00
parent c7851f8189
commit dffeaf6a69
8 changed files with 242 additions and 22 deletions

View File

@@ -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:**

View File

@@ -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 { SanitizedCollectionConfig } from '../../../../collections/config/types';
import './index.scss';
@@ -36,7 +38,21 @@ const DefaultNav = () => {
const classes = [
baseClass,
menuActive && `${baseClass}--menu-active`,
].filter(Boolean).join(' ');
].filter(Boolean)
.join(' ');
const groupedCollections: Record<string, SanitizedCollectionConfig[]> = collections.reduce((acc, currentValue) => {
if (currentValue.admin.group) {
if (acc[currentValue.admin.group]) {
acc[currentValue.admin.group].push(currentValue);
} else {
acc[currentValue.admin.group] = [currentValue];
}
} else {
acc[''].push(currentValue);
}
return acc;
}, { '': [] });
useEffect(() => history.listen(() => {
setMenuActive(false);
@@ -68,31 +84,39 @@ const DefaultNav = () => {
<div 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}`;
<nav className={`${baseClass}__collections`}>
{Object.entries(groupedCollections)
.map(([group, groupCollections]) => (
<NavGroup
key={group}
label={group}
>
{groupCollections.map((collection, i) => {
const href = `${admin}/collections/${collection.slug}`;
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;
})}
if (permissions?.collections?.[collection.slug]?.read.permission) {
return (
<NavLink
id={`nav-${collection.slug}`}
className={`${baseClass}__link`}
activeClassName="active"
key={i}
to={href}
>
<Chevron />
{collection.labels.plural}
</NavLink>
);
}
return null;
})}
</NavGroup>
))}
</nav>
{(globals && globals.length > 0) && (
<React.Fragment>
<span className={`${baseClass}__label`}>Globals</span>
<nav>
<nav className={`${baseClass}__globals`}>
{globals.map((global, i) => {
const href = `${admin}/globals/${global.slug}`;

View File

@@ -0,0 +1,32 @@
@import '../../../scss/styles.scss';
.nav-group {
&__toggle {
cursor: pointer;
color: var(--theme-elevation-600);
background: transparent;
padding-left: 0;
border: 0;
margin-top: base(.25);
}
&__indicator {
transform: rotate(.5turn);
.stroke {
stroke: var(--theme-elevation-600);
}
}
&--collapsed {
.collapsible__toggle {
border-bottom-right-radius: $style-radius-s;
border-bottom-left-radius: $style-radius-s;
}
.nav-group__indicator {
transform: rotate(0turn);
}
}
}

View File

@@ -0,0 +1,83 @@
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';
const preferencesKey = 'collapsed-nav-groups';
const NavGroup: React.FC<{ children: React.ReactNode, label: string}> = ({
children,
label,
}) => {
const [collapsed, setCollapsed] = useState(true);
const [animate, setAnimate] = useState(false);
const { getPreference, setPreference } = usePreferences();
useEffect(() => {
if (label) {
const setCollapsedFromPreferences = async () => {
const preferences = await getPreference(preferencesKey) || [];
setCollapsed(preferences.indexOf(label) !== -1);
};
setCollapsedFromPreferences();
}
}, [getPreference, label]);
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}
>
{label}
<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;

View File

@@ -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,

View File

@@ -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
*/

View File

@@ -61,6 +61,54 @@ 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: [
{

View File

@@ -56,6 +56,33 @@ 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 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();