feat: add admin.hidden to collections and globals (#2487)
This commit is contained in:
@@ -63,18 +63,19 @@ You can find an assortment of [example collection configs](https://github.com/pa
|
||||
|
||||
You can customize the way that the Admin panel behaves on a collection-by-collection basis by defining the `admin` property on a collection's config.
|
||||
|
||||
| Option | Description |
|
||||
| --------------------------- | -------------|
|
||||
| `group` | Text used as a label for grouping collection links together in the navigation. |
|
||||
| `hooks` | Admin-specific hooks for this collection. [More](#admin-hooks) |
|
||||
| `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. |
|
||||
| `disableDuplicate ` | Disables the "Duplicate" button while editing documents within this collection. |
|
||||
| Option | Description |
|
||||
|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `group` | Text used as a label for grouping collection links together in the navigation. |
|
||||
| `hidden` | Set to true or a function, called with the current user, returning true to exclude this collection from navigation and admin routing. |
|
||||
| `hooks` | Admin-specific hooks for this collection. [More](#admin-hooks) |
|
||||
| `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. |
|
||||
| `disableDuplicate ` | Disables the "Duplicate" button while editing documents within this collection. |
|
||||
| `enableRichTextRelationship` | The [Rich Text](/docs/fields/rich-text) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. |
|
||||
| `preview` | Function to generate preview URLS within the Admin panel that can point to your app. [More](#preview). |
|
||||
| `components` | Swap in your own React components to be used within this collection. [More](/docs/admin/components#collections) |
|
||||
| `listSearchableFields` | Specify which fields should be searched in the List search view. [More](#list-searchable-fields) |
|
||||
| `preview` | Function to generate preview URLS within the Admin panel that can point to your app. [More](#preview). |
|
||||
| `components` | Swap in your own React components to be used within this collection. [More](/docs/admin/components#collections) |
|
||||
| `listSearchableFields` | Specify which fields should be searched in the List search view. [More](#list-searchable-fields) |
|
||||
|
||||
### Preview
|
||||
|
||||
|
||||
@@ -64,10 +64,11 @@ You can find an [example Global config](https://github.com/payloadcms/public-dem
|
||||
|
||||
You can customize the way that the Admin panel behaves on a Global-by-Global basis by defining the `admin` property on a Global's config.
|
||||
|
||||
| Option | Description |
|
||||
| ------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `components` | Swap in your own React components to be used within this Global. [More](/docs/admin/components#globals) |
|
||||
| `preview` | Function to generate a preview URL within the Admin panel for this global that can point to your app. [More](#preview). |
|
||||
| Option | Description |
|
||||
|--------------|-----------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `hidden` | Set to true or a function, called with the current user, returning true to exclude this global from navigation and admin routing. |
|
||||
| `components` | Swap in your own React components to be used within this Global. [More](/docs/admin/components#globals) |
|
||||
| `preview` | Function to generate a preview URL within the Admin panel for this global that can point to your app. [More](#preview). |
|
||||
|
||||
### Preview
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React, { Suspense, lazy, useState, useEffect } from 'react';
|
||||
import {
|
||||
Route, Switch, withRouter, Redirect,
|
||||
} from 'react-router-dom';
|
||||
import React, { lazy, Suspense, useEffect, useState } from 'react';
|
||||
import { Redirect, Route, Switch, withRouter } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from './utilities/Auth';
|
||||
import { useConfig } from './utilities/Config';
|
||||
@@ -178,82 +176,19 @@ const Routes = () => {
|
||||
</DocumentInfoProvider>
|
||||
</Route>
|
||||
|
||||
{collections.reduce((collectionRoutes, collection) => {
|
||||
const routesToReturn = [
|
||||
...collectionRoutes,
|
||||
<Route
|
||||
key={`${collection.slug}-list`}
|
||||
path={`${match.url}/collections/${collection.slug}`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
if (permissions?.collections?.[collection.slug]?.read?.permission) {
|
||||
return (
|
||||
<List
|
||||
{...routeProps}
|
||||
collection={collection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Unauthorized />;
|
||||
}}
|
||||
/>,
|
||||
<Route
|
||||
key={`${collection.slug}-create`}
|
||||
path={`${match.url}/collections/${collection.slug}/create`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
if (permissions?.collections?.[collection.slug]?.create?.permission) {
|
||||
return (
|
||||
<DocumentInfoProvider collection={collection}>
|
||||
<Edit
|
||||
{...routeProps}
|
||||
collection={collection}
|
||||
/>
|
||||
</DocumentInfoProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <Unauthorized />;
|
||||
}}
|
||||
/>,
|
||||
<Route
|
||||
key={`${collection.slug}-edit`}
|
||||
path={`${match.url}/collections/${collection.slug}/:id`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
const { match: { params: { id } } } = routeProps;
|
||||
if (permissions?.collections?.[collection.slug]?.read?.permission) {
|
||||
return (
|
||||
<DocumentInfoProvider
|
||||
key={`${collection.slug}-edit-${id}-${locale}`}
|
||||
collection={collection}
|
||||
id={id}
|
||||
>
|
||||
<Edit
|
||||
isEditing
|
||||
{...routeProps}
|
||||
collection={collection}
|
||||
/>
|
||||
</DocumentInfoProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <Unauthorized />;
|
||||
}}
|
||||
/>,
|
||||
];
|
||||
|
||||
if (collection.versions) {
|
||||
routesToReturn.push(
|
||||
{collections
|
||||
.filter(({ admin: { hidden } }) => !(typeof hidden === 'function' ? hidden({ user }) : hidden))
|
||||
.reduce((collectionRoutes, collection) => {
|
||||
const routesToReturn = [
|
||||
...collectionRoutes,
|
||||
<Route
|
||||
key={`${collection.slug}-versions`}
|
||||
path={`${match.url}/collections/${collection.slug}/:id/versions`}
|
||||
key={`${collection.slug}-list`}
|
||||
path={`${match.url}/collections/${collection.slug}`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
if (permissions?.collections?.[collection.slug]?.readVersions?.permission) {
|
||||
if (permissions?.collections?.[collection.slug]?.read?.permission) {
|
||||
return (
|
||||
<Versions
|
||||
<List
|
||||
{...routeProps}
|
||||
collection={collection}
|
||||
/>
|
||||
@@ -263,21 +198,15 @@ const Routes = () => {
|
||||
return <Unauthorized />;
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
routesToReturn.push(
|
||||
<Route
|
||||
key={`${collection.slug}-view-version`}
|
||||
path={`${match.url}/collections/${collection.slug}/:id/versions/:versionID`}
|
||||
key={`${collection.slug}-create`}
|
||||
path={`${match.url}/collections/${collection.slug}/create`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
if (permissions?.collections?.[collection.slug]?.readVersions?.permission) {
|
||||
if (permissions?.collections?.[collection.slug]?.create?.permission) {
|
||||
return (
|
||||
<DocumentInfoProvider
|
||||
collection={collection}
|
||||
id={routeProps.match.params.id}
|
||||
>
|
||||
<Version
|
||||
<DocumentInfoProvider collection={collection}>
|
||||
<Edit
|
||||
{...routeProps}
|
||||
collection={collection}
|
||||
/>
|
||||
@@ -288,81 +217,154 @@ const Routes = () => {
|
||||
return <Unauthorized />;
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
return routesToReturn;
|
||||
}, [])}
|
||||
|
||||
{globals && globals.reduce((globalRoutes, global) => {
|
||||
const routesToReturn = [
|
||||
...globalRoutes,
|
||||
<Route
|
||||
key={`${global.slug}`}
|
||||
path={`${match.url}/globals/${global.slug}`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
if (permissions?.globals?.[global.slug]?.read?.permission) {
|
||||
return (
|
||||
<DocumentInfoProvider
|
||||
global={global}
|
||||
key={`${global.slug}-${locale}`}
|
||||
>
|
||||
<EditGlobal
|
||||
{...routeProps}
|
||||
global={global}
|
||||
/>
|
||||
</DocumentInfoProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <Unauthorized />;
|
||||
}}
|
||||
/>,
|
||||
];
|
||||
|
||||
if (global.versions) {
|
||||
routesToReturn.push(
|
||||
<Route
|
||||
key={`${global.slug}-versions`}
|
||||
path={`${match.url}/globals/${global.slug}/versions`}
|
||||
key={`${collection.slug}-edit`}
|
||||
path={`${match.url}/collections/${collection.slug}/:id`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
if (permissions?.globals?.[global.slug]?.readVersions?.permission) {
|
||||
const { match: { params: { id } } } = routeProps;
|
||||
if (permissions?.collections?.[collection.slug]?.read?.permission) {
|
||||
return (
|
||||
<Versions
|
||||
{...routeProps}
|
||||
global={global}
|
||||
/>
|
||||
<DocumentInfoProvider
|
||||
key={`${collection.slug}-edit-${id}-${locale}`}
|
||||
collection={collection}
|
||||
id={id}
|
||||
>
|
||||
<Edit
|
||||
isEditing
|
||||
{...routeProps}
|
||||
collection={collection}
|
||||
/>
|
||||
</DocumentInfoProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <Unauthorized />;
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
routesToReturn.push(
|
||||
];
|
||||
|
||||
if (collection.versions) {
|
||||
routesToReturn.push(
|
||||
<Route
|
||||
key={`${collection.slug}-versions`}
|
||||
path={`${match.url}/collections/${collection.slug}/:id/versions`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
if (permissions?.collections?.[collection.slug]?.readVersions?.permission) {
|
||||
return (
|
||||
<Versions
|
||||
{...routeProps}
|
||||
collection={collection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Unauthorized />;
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
routesToReturn.push(
|
||||
<Route
|
||||
key={`${collection.slug}-view-version`}
|
||||
path={`${match.url}/collections/${collection.slug}/:id/versions/:versionID`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
if (permissions?.collections?.[collection.slug]?.readVersions?.permission) {
|
||||
return (
|
||||
<DocumentInfoProvider
|
||||
collection={collection}
|
||||
id={routeProps.match.params.id}
|
||||
>
|
||||
<Version
|
||||
{...routeProps}
|
||||
collection={collection}
|
||||
/>
|
||||
</DocumentInfoProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <Unauthorized />;
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
return routesToReturn;
|
||||
}, [])}
|
||||
|
||||
{globals && globals
|
||||
.filter(({ admin: { hidden } }) => !(typeof hidden === 'function' ? hidden({ user }) : hidden))
|
||||
.reduce((globalRoutes, global) => {
|
||||
const routesToReturn = [
|
||||
...globalRoutes,
|
||||
<Route
|
||||
key={`${global.slug}-view-version`}
|
||||
path={`${match.url}/globals/${global.slug}/versions/:versionID`}
|
||||
key={`${global.slug}`}
|
||||
path={`${match.url}/globals/${global.slug}`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
if (permissions?.globals?.[global.slug]?.readVersions?.permission) {
|
||||
if (permissions?.globals?.[global.slug]?.read?.permission) {
|
||||
return (
|
||||
<Version
|
||||
{...routeProps}
|
||||
<DocumentInfoProvider
|
||||
global={global}
|
||||
/>
|
||||
key={`${global.slug}-${locale}`}
|
||||
>
|
||||
<EditGlobal
|
||||
{...routeProps}
|
||||
global={global}
|
||||
/>
|
||||
</DocumentInfoProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <Unauthorized />;
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
return routesToReturn;
|
||||
}, [])}
|
||||
];
|
||||
|
||||
if (global.versions) {
|
||||
routesToReturn.push(
|
||||
<Route
|
||||
key={`${global.slug}-versions`}
|
||||
path={`${match.url}/globals/${global.slug}/versions`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
if (permissions?.globals?.[global.slug]?.readVersions?.permission) {
|
||||
return (
|
||||
<Versions
|
||||
{...routeProps}
|
||||
global={global}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Unauthorized />;
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
routesToReturn.push(
|
||||
<Route
|
||||
key={`${global.slug}-view-version`}
|
||||
path={`${match.url}/globals/${global.slug}/versions/:versionID`}
|
||||
exact
|
||||
render={(routeProps) => {
|
||||
if (permissions?.globals?.[global.slug]?.readVersions?.permission) {
|
||||
return (
|
||||
<Version
|
||||
{...routeProps}
|
||||
global={global}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Unauthorized />;
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
return routesToReturn;
|
||||
}, [])}
|
||||
|
||||
<Route path={`${match.url}*`}>
|
||||
<NotFound />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { NavLink, Link, useHistory } from 'react-router-dom';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link, NavLink, useHistory } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import { useAuth } from '../../utilities/Auth';
|
||||
@@ -12,7 +12,7 @@ import Account from '../../graphics/Account';
|
||||
import Localizer from '../Localizer';
|
||||
import NavGroup from '../NavGroup';
|
||||
import Logout from '../Logout';
|
||||
import { groupNavItems, Group, EntityToGroup, EntityType } from '../../../utilities/groupNavItems';
|
||||
import { EntityToGroup, EntityType, Group, groupNavItems } from '../../../utilities/groupNavItems';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
@@ -20,7 +20,7 @@ import './index.scss';
|
||||
const baseClass = 'nav';
|
||||
|
||||
const DefaultNav = () => {
|
||||
const { permissions } = useAuth();
|
||||
const { permissions, user } = useAuth();
|
||||
const [menuActive, setMenuActive] = useState(false);
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const history = useHistory();
|
||||
@@ -47,24 +47,28 @@ const DefaultNav = () => {
|
||||
|
||||
useEffect(() => {
|
||||
setGroups(groupNavItems([
|
||||
...collections.map((collection) => {
|
||||
const entityToGroup: EntityToGroup = {
|
||||
type: EntityType.collection,
|
||||
entity: collection,
|
||||
};
|
||||
...collections
|
||||
.filter(({ admin: { hidden } }) => !(typeof hidden === 'function' ? hidden({ user }) : hidden))
|
||||
.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;
|
||||
}),
|
||||
...globals
|
||||
.filter(({ admin: { hidden } }) => !(typeof hidden === 'function' ? hidden({ user }) : hidden))
|
||||
.map((global) => {
|
||||
const entityToGroup: EntityToGroup = {
|
||||
type: EntityType.global,
|
||||
entity: global,
|
||||
};
|
||||
|
||||
return entityToGroup;
|
||||
}),
|
||||
return entityToGroup;
|
||||
}),
|
||||
], permissions, i18n));
|
||||
}, [collections, globals, permissions, i18n, i18n.language]);
|
||||
}, [collections, globals, permissions, i18n, i18n.language, user]);
|
||||
|
||||
useEffect(() => history.listen(() => {
|
||||
setMenuActive(false);
|
||||
|
||||
@@ -8,7 +8,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 { EntityToGroup, EntityType, Group, groupNavItems } from '../../../utilities/groupNavItems';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
@@ -20,6 +20,7 @@ const Dashboard: React.FC<Props> = (props) => {
|
||||
collections,
|
||||
globals,
|
||||
permissions,
|
||||
user,
|
||||
} = props;
|
||||
|
||||
const { push } = useHistory();
|
||||
@@ -41,24 +42,28 @@ const Dashboard: React.FC<Props> = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
setGroups(groupNavItems([
|
||||
...collections.map((collection) => {
|
||||
const entityToGroup: EntityToGroup = {
|
||||
type: EntityType.collection,
|
||||
entity: collection,
|
||||
};
|
||||
...collections
|
||||
.filter(({ admin: { hidden } }) => !(typeof hidden === 'function' ? hidden({ user }) : hidden))
|
||||
.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;
|
||||
}),
|
||||
...globals
|
||||
.filter(({ admin: { hidden } }) => !(typeof hidden === 'function' ? hidden({ user }) : hidden))
|
||||
.map((global) => {
|
||||
const entityToGroup: EntityToGroup = {
|
||||
type: EntityType.global,
|
||||
entity: global,
|
||||
};
|
||||
|
||||
return entityToGroup;
|
||||
}),
|
||||
return entityToGroup;
|
||||
}),
|
||||
], permissions, i18n));
|
||||
}, [collections, globals, i18n, permissions]);
|
||||
}, [collections, globals, i18n, permissions, user]);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import { useAuth } from '../../utilities/Auth';
|
||||
import { useStepNav } from '../../elements/StepNav';
|
||||
@@ -6,7 +6,7 @@ import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
||||
import DefaultDashboard from './Default';
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const { permissions } = useAuth();
|
||||
const { permissions, user } = useAuth();
|
||||
const { setStepNav } = useStepNav();
|
||||
const [filteredGlobals, setFilteredGlobals] = useState([]);
|
||||
|
||||
@@ -42,6 +42,7 @@ const Dashboard: React.FC = () => {
|
||||
globals: filteredGlobals,
|
||||
collections: collections.filter((collection) => permissions?.collections?.[collection.slug]?.read?.permission),
|
||||
permissions,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
|
||||
import { SanitizedGlobalConfig } from '../../../../globals/config/types';
|
||||
import { Permissions } from '../../../../auth/types';
|
||||
import { Permissions, User } from '../../../../auth/types';
|
||||
|
||||
export type Props = {
|
||||
collections: SanitizedCollectionConfig[],
|
||||
globals: SanitizedGlobalConfig[],
|
||||
permissions: Permissions
|
||||
permissions: Permissions,
|
||||
user: User,
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ const collectionSchema = joi.object().keys({
|
||||
}),
|
||||
timestamps: joi.boolean(),
|
||||
admin: joi.object({
|
||||
hidden: joi.alternatives().try(
|
||||
joi.boolean(),
|
||||
joi.func(),
|
||||
),
|
||||
useAsTitle: joi.string(),
|
||||
defaultColumns: joi.array().items(joi.string()),
|
||||
listSearchableFields: joi.array().items(joi.string()),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Response } from 'express';
|
||||
import { Access, Endpoint, EntityDescription, GeneratePreviewURL } from '../../config/types';
|
||||
import { Field } from '../../fields/config/types';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { Auth, IncomingAuthType } from '../../auth/types';
|
||||
import { Auth, IncomingAuthType, User } from '../../auth/types';
|
||||
import { IncomingUploadType, Upload } from '../../uploads/types';
|
||||
import { IncomingCollectionVersions, SanitizedCollectionVersions } from '../../versions/types';
|
||||
import { Config as GeneratedTypes } from '../../generated-types';
|
||||
@@ -153,6 +153,10 @@ type BeforeDuplicateArgs<T> = {
|
||||
export type BeforeDuplicate<T = any> = (args: BeforeDuplicateArgs<T>) => T | Promise<T>
|
||||
|
||||
export type CollectionAdminOptions = {
|
||||
/**
|
||||
* Exclude the collection from the admin nav and routes
|
||||
*/
|
||||
hidden?: ((args: { user: User }) => boolean) | boolean;
|
||||
/**
|
||||
* Field to use as title in Edit view and first column in List view
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,10 @@ const globalSchema = joi.object().keys({
|
||||
joi.object().pattern(joi.string(), [joi.string()]),
|
||||
),
|
||||
admin: joi.object({
|
||||
hidden: joi.alternatives().try(
|
||||
joi.boolean(),
|
||||
joi.func(),
|
||||
),
|
||||
group: joi.alternatives().try(
|
||||
joi.string(),
|
||||
joi.object().pattern(joi.string(), [joi.string()]),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Model, Document } from 'mongoose';
|
||||
import { Document, Model } from 'mongoose';
|
||||
import { DeepRequired } from 'ts-essentials';
|
||||
import { GraphQLNonNull, GraphQLObjectType } from 'graphql';
|
||||
import { User } from '../../auth/types';
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { Access, Endpoint, EntityDescription, GeneratePreviewURL } from '../../config/types';
|
||||
import { Field } from '../../fields/config/types';
|
||||
@@ -46,6 +47,10 @@ export interface GlobalModel extends Model<Document> {
|
||||
}
|
||||
|
||||
export type GlobalAdminOptions = {
|
||||
/**
|
||||
* Exclude the global from the admin nav and routes
|
||||
*/
|
||||
hidden?: ((args: { user: User }) => boolean) | boolean;
|
||||
/**
|
||||
* Place globals into a navigational group
|
||||
* */
|
||||
|
||||
@@ -7,7 +7,7 @@ import CustomMinimalRoute from './components/views/CustomMinimal';
|
||||
import CustomDefaultRoute from './components/views/CustomDefault';
|
||||
import BeforeLogin from './components/BeforeLogin';
|
||||
import AfterNavLinks from './components/AfterNavLinks';
|
||||
import { slug, globalSlug } from './shared';
|
||||
import { globalSlug, slug } from './shared';
|
||||
import Logout from './components/Logout';
|
||||
import DemoUIFieldField from './components/DemoUIField/Field';
|
||||
import DemoUIFieldCell from './components/DemoUIField/Cell';
|
||||
@@ -68,6 +68,18 @@ export default buildConfig({
|
||||
auth: true,
|
||||
fields: [],
|
||||
},
|
||||
{
|
||||
slug: 'hidden-collection',
|
||||
admin: {
|
||||
hidden: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug,
|
||||
labels: {
|
||||
@@ -176,6 +188,18 @@ export default buildConfig({
|
||||
},
|
||||
],
|
||||
globals: [
|
||||
{
|
||||
slug: 'hidden-global',
|
||||
admin: {
|
||||
hidden: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: globalSlug,
|
||||
label: {
|
||||
|
||||
@@ -111,6 +111,24 @@ describe('admin', () => {
|
||||
await page.locator(`.step-nav >> text=${slug}`).click();
|
||||
expect(page.url()).toContain(url.list);
|
||||
});
|
||||
|
||||
test('should not show hidden collections and globals', async () => {
|
||||
await page.goto(url.admin);
|
||||
|
||||
// nav menu
|
||||
await expect(await page.locator('#nav-hidden-collection')).toBeHidden();
|
||||
await expect(await page.locator('#nav-hidden-global')).toBeHidden();
|
||||
|
||||
// dashboard
|
||||
await expect(await page.locator('#card-hidden-collection')).toBeHidden();
|
||||
await expect(await page.locator('#card-hidden-global')).toBeHidden();
|
||||
|
||||
// routing
|
||||
await page.goto(url.collection('hidden-collection'));
|
||||
await expect(await page.locator('.not-found')).toContainText('Nothing found');
|
||||
await page.goto(url.global('hidden-global'));
|
||||
await expect(await page.locator('.not-found')).toContainText('Nothing found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CRUD', () => {
|
||||
|
||||
@@ -18,6 +18,10 @@ export class AdminUrlUtil {
|
||||
return `${this.list}/${id}`;
|
||||
}
|
||||
|
||||
collection(slug: string): string {
|
||||
return `${this.admin}/collections/${slug}`;
|
||||
}
|
||||
|
||||
global(slug: string): string {
|
||||
return `${this.admin}/globals/${slug}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user