chore: finishes collapsible nav groups

This commit is contained in:
James
2022-09-12 12:45:06 -07:00
parent 813fa1571c
commit 78f39af5cf
9 changed files with 229 additions and 169 deletions

View File

@@ -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;
}
}
}
}

View File

@@ -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>
);

View File

@@ -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);
}
}
}
}

View File

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

View File

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

View File

@@ -57,4 +57,4 @@
margin: 0 base(.25) base(.5);
}
}
}
}

View 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);
}

View File

@@ -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`}
>

View File

@@ -50,9 +50,6 @@ export default buildConfig({
collections: [
{
slug: 'users',
admin: {
group: 'One',
},
auth: true,
fields: [],
},