chore: finishes collapsible nav groups
This commit is contained in:
@@ -81,33 +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;
|
||||
width: 100%;
|
||||
|
||||
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 {
|
||||
@@ -115,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,4 +180,4 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,7 @@ 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 { SanitizedGlobalConfig } from '../../../../globals/config/types';
|
||||
import { groupNavItems, Group, EntityToGroup, EntityType } from '../../../utilities/groupNavItems';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -21,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,
|
||||
@@ -42,23 +42,26 @@ const DefaultNav = () => {
|
||||
].filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
const groupNavItems = (items) => {
|
||||
return items.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(() => {
|
||||
setGroups(groupNavItems([
|
||||
...collections.map((collection) => {
|
||||
const entityToGroup: EntityToGroup = {
|
||||
type: EntityType.collection,
|
||||
entity: collection,
|
||||
};
|
||||
|
||||
const groupedCollections: Record<string, SanitizedCollectionConfig[]> = groupNavItems(collections);
|
||||
const groupedGlobals: Record<string, SanitizedGlobalConfig[]> = groupNavItems(globals);
|
||||
return entityToGroup;
|
||||
}),
|
||||
...globals.map((global) => {
|
||||
const entityToGroup: EntityToGroup = {
|
||||
type: EntityType.global,
|
||||
entity: global,
|
||||
};
|
||||
|
||||
return entityToGroup;
|
||||
}),
|
||||
], permissions));
|
||||
}, [collections, globals, permissions]);
|
||||
|
||||
useEffect(() => history.listen(() => {
|
||||
setMenuActive(false);
|
||||
@@ -87,78 +90,44 @@ const DefaultNav = () => {
|
||||
)}
|
||||
</button>
|
||||
</header>
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<nav className={`${baseClass}__wrap`}>
|
||||
{Array.isArray(beforeNavLinks) && beforeNavLinks.map((Component, i) => <Component key={i} />)}
|
||||
{ groupedCollections[''].length > 0 && (
|
||||
<span className={`${baseClass}__label`}>Collections</span>
|
||||
) }
|
||||
<nav className={`${baseClass}__collections`}>
|
||||
{Object.entries(groupedCollections)
|
||||
.map(([group, groupCollections]) => (
|
||||
<NavGroup
|
||||
key={group}
|
||||
label={group}
|
||||
type="collections"
|
||||
>
|
||||
{groupCollections.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}`}
|
||||
className={`${baseClass}__link`}
|
||||
activeClassName="active"
|
||||
key={i}
|
||||
to={href}
|
||||
>
|
||||
<Chevron />
|
||||
{collection.labels.plural}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</NavGroup>
|
||||
))}
|
||||
</nav>
|
||||
{(globals && globals.length > 0) && (
|
||||
<React.Fragment>
|
||||
{ groupedGlobals[''].length > 0 && (
|
||||
<span className={`${baseClass}__label`}>Globals</span>
|
||||
) }
|
||||
<nav className={`${baseClass}__globals`}>
|
||||
{Object.entries(groupedGlobals)
|
||||
.map(([group, globalsGroup]) => (
|
||||
<NavGroup
|
||||
key={group}
|
||||
label={group}
|
||||
type="globals"
|
||||
if (type === EntityType.collection) {
|
||||
href = `${admin}/collections/${entity.slug}`;
|
||||
entityLabel = entity.labels.plural;
|
||||
id = `nav-${entity.slug}`;
|
||||
}
|
||||
|
||||
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}
|
||||
>
|
||||
{globalsGroup.map((global, i) => {
|
||||
const href = `${admin}/globals/${global.slug}`;
|
||||
|
||||
if (permissions?.globals?.[global.slug]?.read.permission) {
|
||||
return (
|
||||
<NavLink
|
||||
id={`nav-global-${global.slug}`}
|
||||
className={`${baseClass}__link`}
|
||||
activeClassName="active"
|
||||
key={i}
|
||||
to={href}
|
||||
>
|
||||
<Chevron />
|
||||
{global.label}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</NavGroup>
|
||||
))}
|
||||
</nav>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<Chevron />
|
||||
{entityLabel}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</NavGroup>
|
||||
);
|
||||
})}
|
||||
{Array.isArray(afterNavLinks) && afterNavLinks.map((Component, i) => <Component key={i} />)}
|
||||
<div className={`${baseClass}__controls`}>
|
||||
<Localizer />
|
||||
@@ -175,7 +144,7 @@ const DefaultNav = () => {
|
||||
<LogOut />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
.nav-group {
|
||||
width: 100%;
|
||||
margin-bottom: base(.5);
|
||||
|
||||
&__toggle {
|
||||
cursor: pointer;
|
||||
@@ -13,10 +14,17 @@
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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);
|
||||
}
|
||||
@@ -24,11 +32,10 @@
|
||||
}
|
||||
|
||||
&__indicator {
|
||||
transform: rotate(.5turn);
|
||||
margin-left: auto;
|
||||
|
||||
.stroke {
|
||||
stroke: var(--theme-elevation-400);
|
||||
stroke: var(--theme-elevation-200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +46,8 @@
|
||||
}
|
||||
|
||||
.nav-group__indicator {
|
||||
transform: rotate(0turn);
|
||||
transform: rotate(.5turn);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,20 @@ import './index.scss';
|
||||
|
||||
const baseClass = 'nav-group';
|
||||
|
||||
const NavGroup: React.FC<{ children: React.ReactNode, label: string, type: string}> = ({
|
||||
type Props = {
|
||||
children: React.ReactNode,
|
||||
label: string,
|
||||
}
|
||||
|
||||
const NavGroup: React.FC<Props> = ({
|
||||
children,
|
||||
label,
|
||||
type,
|
||||
}) => {
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const [animate, setAnimate] = useState(false);
|
||||
const { getPreference, setPreference } = usePreferences();
|
||||
|
||||
const preferencesKey = `collapsed-${type}-groups`;
|
||||
const preferencesKey = `collapsed-${label}-groups`;
|
||||
|
||||
useEffect(() => {
|
||||
if (label) {
|
||||
@@ -60,7 +64,9 @@ const NavGroup: React.FC<{ children: React.ReactNode, label: string, type: strin
|
||||
.join(' ')}
|
||||
onClick={toggleCollapsed}
|
||||
>
|
||||
{label}
|
||||
<div className={`${baseClass}__label`}>
|
||||
{label}
|
||||
</div>
|
||||
<Chevron className={`${baseClass}__indicator`} />
|
||||
</button>
|
||||
<AnimateHeight
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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`}
|
||||
>
|
||||
|
||||
@@ -50,9 +50,6 @@ export default buildConfig({
|
||||
collections: [
|
||||
{
|
||||
slug: 'users',
|
||||
admin: {
|
||||
group: 'One',
|
||||
},
|
||||
auth: true,
|
||||
fields: [],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user