Feat/custom admin buttons (#2613)

This commit is contained in:
Jarrod Flesch
2023-05-03 17:21:38 -04:00
committed by GitHub
parent 6f82cefdc5
commit acfb9bca45
21 changed files with 402 additions and 95 deletions

View File

@@ -21,22 +21,22 @@ To swap in your own React component, first, consult the list of available compon
You can override a set of admin panel-wide components by providing a component to your base Payload config's `admin.components` property. The following options are available:
| Path | Description |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`Nav`** | Contains the sidebar and mobile Nav in its entirety. |
| **`logout.Button`** | A custom React component.
| **`BeforeDashboard`** | Array of components to inject into the built-in Dashboard, _before_ the default dashboard contents. |
| **`AfterDashboard`** | Array of components to inject into the built-in Dashboard, _after_ the default dashboard contents. [Demo](https://github.com/payloadcms/payload/tree/master/test/admin/components/AfterDashboard/index.tsx) |
| **`BeforeLogin`** | Array of components to inject into the built-in Login, _before_ the default login form. |
| **`AfterLogin`** | Array of components to inject into the built-in Login, _after_ the default login form. |
| **`BeforeNavLinks`** | Array of components to inject into the built-in Nav, _before_ the links themselves. |
| **`AfterNavLinks`** | Array of components to inject into the built-in Nav, _after_ the links. |
| **`views.Account`** | The Account view is used to show the currently logged in user's Account page. |
| **`views.Dashboard`** | The main landing page of the Admin panel. |
| **`graphics.Icon`** | Used as a graphic within the `Nav` component. Often represents a condensed version of a full logo. |
| **`graphics.Logo`** | The full logo to be used in contexts like the `Login` view. |
| **`routes`** | Define your own routes to add to the Payload Admin UI. [More](#custom-routes) |
| **`providers`** | Define your own provider components that will wrap the Payload Admin UI. [More](#custom-providers) |
| Path | Description |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`Nav`** | Contains the sidebar and mobile Nav in its entirety. |
| **`logout.Button`** | A custom React component. |
| **`BeforeDashboard`** | Array of components to inject into the built-in Dashboard, _before_ the default dashboard contents. |
| **`AfterDashboard`** | Array of components to inject into the built-in Dashboard, _after_ the default dashboard contents. [Demo](https://github.com/payloadcms/payload/tree/master/test/admin/components/AfterDashboard/index.tsx) |
| **`BeforeLogin`** | Array of components to inject into the built-in Login, _before_ the default login form. |
| **`AfterLogin`** | Array of components to inject into the built-in Login, _after_ the default login form. |
| **`BeforeNavLinks`** | Array of components to inject into the built-in Nav, _before_ the links themselves. |
| **`AfterNavLinks`** | Array of components to inject into the built-in Nav, _after_ the links. |
| **`views.Account`** | The Account view is used to show the currently logged in user's Account page. |
| **`views.Dashboard`** | The main landing page of the Admin panel. |
| **`graphics.Icon`** | Used as a graphic within the `Nav` component. Often represents a condensed version of a full logo. |
| **`graphics.Logo`** | The full logo to be used in contexts like the `Login` view. |
| **`routes`** | Define your own routes to add to the Payload Admin UI. [More](#custom-routes) |
| **`providers`** | Define your own provider components that will wrap the Payload Admin UI. [More](#custom-providers) |
#### Full example:
@@ -77,18 +77,76 @@ _For more examples regarding how to customize components, look at the following
You can override components on a Collection-by-Collection basis via each Collection's `admin` property.
| Path | Description |
| ---------------- | ------------------------------------------------------------------------------------------------ |
| **`views.Edit`** | Used while a document within this Collection is being edited. |
| **`views.List`** | The `List` view is used to render a paginated, filterable table of Documents in this Collection. |
| Path | Description |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| **`views.Edit`** | Used while a document within this Collection is being edited. |
| **`views.List`** | The `List` view is used to render a paginated, filterable table of Documents in this Collection. |
| **`elements.SaveButton`** | Replace the default `Save` button with a custom component. Drafts must be disabled |
| **`elements.SaveDraftButton`** | Replace the default `Save Draft` button with a custom component. Drafts must be enabled and autosave must be disabled. |
| **`elements.PublishButton`** | Replace the default `Publish` button with a custom component. Drafts must be enabled. |
| **`elements.PreviewButton`** | Replace the default `Preview` button with a custom component. |
#### Examples
```tsx
// Custom Buttons
import * as React from "react";
import {
CustomSaveButtonProps,
CustomSaveDraftButtonProps,
CustomPublishButtonProps,
CustomPreviewButtonProps,
} from "payload/types";
export const CustomSaveButton: CustomSaveButtonProps = ({
DefaultButton,
label,
}) => {
return <DefaultButton label={label} />;
};
export const CustomSaveDraftButton: CustomSaveDraftButtonProps = ({
DefaultButton,
disabled,
label,
saveDraft,
}) => {
return (
<DefaultButton label={label} disabled={disabled} saveDraft={saveDraft} />
);
};
export const CustomPublishButton: CustomPublishButtonProps = ({
DefaultButton,
disabled,
label,
publish,
}) => {
return <DefaultButton label={label} disabled={disabled} publish={publish} />;
};
export const CustomPreviewButton: CustomPreviewButtonProps = ({
DefaultButton,
disabled,
label,
preview,
}) => {
return <DefaultButton label={label} disabled={disabled} preview={preview} />;
};
```
### Globals
As with Collections, You can override components on a global-by-global basis via their `admin` property.
| Path | Description |
| ---------------- | --------------------------------------- |
| **`views.Edit`** | Used while this Global is being edited. |
| Path | Description |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| **`views.Edit`** | Used while this Global is being edited. |
| **`elements.SaveButton`** | Replace the default `Save` button with a custom component. Drafts must be disabled |
| **`elements.SaveDraftButton`** | Replace the default `Save Draft` button with a custom component. Drafts must be enabled and autosave must be disabled. |
| **`elements.PublishButton`** | Replace the default `Publish` button with a custom component. Drafts must be enabled. |
| **`elements.PreviewButton`** | Replace the default `Preview` button with a custom component. |
### Fields
@@ -163,7 +221,11 @@ const CustomTextField: React.FC<Props> = ({ path }) => {
<Banner type="success">
For more information regarding the hooks that are available to you while you
build custom components, including the <strong>useField</strong> hook, <a href="/docs/admin/hooks" style={{ color: "black" }}>click here</a>.
build custom components, including the <strong>useField</strong> hook,{" "}
<a href="/docs/admin/hooks" style={{ color: "black" }}>
click here
</a>
.
</Banner>
## Custom routes
@@ -232,19 +294,20 @@ To make use of Payload SCSS variables / mixins to use directly in your own compo
When developing custom components you can support multiple languages to be consistent with Payload's i18n support. The best way to do this is to add your translation resources to the [i18n configuration](https://payloadcms.com/docs/configuration/i18n) and import `useTranslation` from `react-i18next` in your components.
For example:
```tsx
import { useTranslation } from 'react-i18next';
import { useTranslation } from "react-i18next";
const CustomComponent: React.FC = () => {
// highlight-start
const { t, i18n } = useTranslation('namespace1');
const { t, i18n } = useTranslation("namespace1");
// highlight-end
return (
<ul>
<li>{ t('key', { variable: 'value' }) }</li>
<li>{ t('namespace2:key', { variable: 'value' }) }</li>
<li>{ i18n.language }</li>
<li>{t("key", { variable: "value" })}</li>
<li>{t("namespace2:key", { variable: "value" })}</li>
<li>{i18n.language}</li>
</ul>
);
};

View File

@@ -1 +0,0 @@
@import '../../../scss/styles.scss';

View File

@@ -1,22 +1,45 @@
import React, { useCallback, useRef, useState } from 'react';
import React, { useRef, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-toastify';
import { GeneratePreviewURL } from '../../../../config/types';
import { useAuth } from '../../utilities/Auth';
import Button from '../Button';
import { Props } from './types';
import { useLocale } from '../../utilities/Locale';
import { useDocumentInfo } from '../../utilities/DocumentInfo';
import { useConfig } from '../../utilities/Config';
import './index.scss';
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
const baseClass = 'preview-btn';
const PreviewButton: React.FC<Props> = (props) => {
const {
generatePreviewURL,
} = props;
export type CustomPreviewButtonProps = React.ComponentType<DefaultPreviewButtonProps & {
DefaultButton: React.ComponentType<DefaultPreviewButtonProps>;
}>
export type DefaultPreviewButtonProps = {
preview: () => void;
disabled: boolean;
label: string;
};
const DefaultPreviewButton: React.FC<DefaultPreviewButtonProps> = ({ preview, disabled, label }) => {
return (
<Button
className={baseClass}
buttonStyle="secondary"
onClick={preview}
disabled={disabled}
>
{label}
</Button>
);
};
type Props = {
CustomComponent?: CustomPreviewButtonProps
generatePreviewURL?: GeneratePreviewURL
}
const PreviewButton: React.FC<Props> = ({
CustomComponent,
generatePreviewURL,
}) => {
const { id, collection, global } = useDocumentInfo();
const [isLoading, setIsLoading] = useState(false);
@@ -29,7 +52,7 @@ const PreviewButton: React.FC<Props> = (props) => {
// we need to regenerate the preview URL every time the button is clicked
// to do this we need to fetch the document data fresh from the API
// this will ensure the latest data is used when generating the preview URL
const handleClick = useCallback(async () => {
const preview = useCallback(async () => {
if (!generatePreviewURL || isGeneratingPreviewURL.current) return;
isGeneratingPreviewURL.current = true;
@@ -54,14 +77,16 @@ const PreviewButton: React.FC<Props> = (props) => {
}, [serverURL, api, collection, global, id, generatePreviewURL, locale, token, t]);
return (
<Button
className={baseClass}
buttonStyle="secondary"
onClick={handleClick}
disabled={isLoading || !generatePreviewURL}
>
{isLoading ? t('general:loading') : t('preview')}
</Button>
<RenderCustomComponent
CustomComponent={CustomComponent}
DefaultComponent={DefaultPreviewButton}
componentProps={{
preview,
disabled: isLoading || !generatePreviewURL,
label: isLoading ? t('general:loading') : t('preview'),
DefaultButton: DefaultPreviewButton,
}}
/>
);
};

View File

@@ -1,5 +0,0 @@
import { GeneratePreviewURL } from '../../../../config/types';
export type Props = {
generatePreviewURL?: GeneratePreviewURL,
}

View File

@@ -1,11 +1,34 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import FormSubmit from '../../forms/Submit';
import { Props } from './types';
import { useDocumentInfo } from '../../utilities/DocumentInfo';
import { useForm, useFormModified } from '../../forms/Form/context';
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
const Publish: React.FC<Props> = () => {
export type CustomPublishButtonProps = React.ComponentType<DefaultPublishButtonProps & {
DefaultButton: React.ComponentType<DefaultPublishButtonProps>;
}>
export type DefaultPublishButtonProps = {
publish: () => void;
disabled: boolean;
label: string;
};
const DefaultPublishButton: React.FC<DefaultPublishButtonProps> = ({ disabled, publish, label }) => {
return (
<FormSubmit
type="button"
onClick={publish}
disabled={disabled}
>
{label}
</FormSubmit>
);
};
type Props = {
CustomComponent?: CustomPublishButtonProps
}
export const Publish: React.FC<Props> = ({ CustomComponent }) => {
const { unpublishedVersions, publishedDoc } = useDocumentInfo();
const { submit } = useForm();
const modified = useFormModified();
@@ -23,14 +46,15 @@ const Publish: React.FC<Props> = () => {
}, [submit]);
return (
<FormSubmit
type="button"
onClick={publish}
disabled={!canPublish}
>
{t('publishChanges')}
</FormSubmit>
<RenderCustomComponent
CustomComponent={CustomComponent}
DefaultComponent={DefaultPublishButton}
componentProps={{
publish,
disabled: !canPublish,
label: t('publishChanges'),
DefaultButton: DefaultPublishButton,
}}
/>
);
};
export default Publish;

View File

@@ -1 +0,0 @@
export type Props = {}

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import FormSubmit from '../../forms/Submit';
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
export type CustomSaveButtonProps = React.ComponentType<DefaultSaveButtonProps & {
DefaultButton: React.ComponentType<DefaultSaveButtonProps>;
}>
type DefaultSaveButtonProps = {
label: string;
};
const DefaultSaveButton: React.FC<DefaultSaveButtonProps> = ({ label }) => {
return (
<FormSubmit buttonId="action-save">{label}</FormSubmit>
);
};
type Props = {
CustomComponent?: CustomSaveButtonProps;
}
export const Save: React.FC<Props> = ({ CustomComponent }) => {
const { t } = useTranslation('general');
return (
<RenderCustomComponent
CustomComponent={CustomComponent}
DefaultComponent={DefaultSaveButton}
componentProps={{
label: t('save'),
DefaultButton: DefaultSaveButton,
}}
/>
);
};

View File

@@ -5,12 +5,37 @@ import FormSubmit from '../../forms/Submit';
import { useForm, useFormModified } from '../../forms/Form/context';
import { useDocumentInfo } from '../../utilities/DocumentInfo';
import { useLocale } from '../../utilities/Locale';
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
import './index.scss';
const baseClass = 'save-draft';
const SaveDraft: React.FC = () => {
export type CustomSaveDraftButtonProps = React.ComponentType<DefaultSaveDraftButtonProps & {
DefaultButton: React.ComponentType<DefaultSaveDraftButtonProps>;
}>
export type DefaultSaveDraftButtonProps = {
saveDraft: () => void;
disabled: boolean;
label: string;
};
const DefaultSaveDraftButton: React.FC<DefaultSaveDraftButtonProps> = ({ disabled, saveDraft, label }) => {
return (
<FormSubmit
className={baseClass}
type="button"
buttonStyle="secondary"
onClick={saveDraft}
disabled={disabled}
>
{label}
</FormSubmit>
);
};
type Props = {
CustomComponent?: CustomSaveDraftButtonProps
}
export const SaveDraft: React.FC<Props> = ({ CustomComponent }) => {
const { serverURL, routes: { api } } = useConfig();
const { submit } = useForm();
const { collection, global, id } = useDocumentInfo();
@@ -45,16 +70,15 @@ const SaveDraft: React.FC = () => {
}, [submit, collection, global, serverURL, api, locale, id]);
return (
<FormSubmit
className={baseClass}
type="button"
buttonStyle="secondary"
onClick={saveDraft}
disabled={!canSaveDraft}
>
{t('saveDraft')}
</FormSubmit>
<RenderCustomComponent
CustomComponent={CustomComponent}
DefaultComponent={DefaultSaveDraftButton}
componentProps={{
saveDraft,
disabled: !canSaveDraft,
label: t('saveDraft'),
DefaultButton: DefaultSaveDraftButton,
}}
/>
);
};
export default SaveDraft;

View File

@@ -0,0 +1,4 @@
export { CustomPublishButtonProps } from './Publish';
export { CustomSaveButtonProps } from './Save';
export { CustomSaveDraftButtonProps } from './SaveDraft';
export { CustomPreviewButtonProps } from './PreviewButton';

View File

@@ -5,7 +5,7 @@ import { useConfig } from '../../utilities/Config';
import Eyebrow from '../../elements/Eyebrow';
import Form from '../../forms/Form';
import PreviewButton from '../../elements/PreviewButton';
import FormSubmit from '../../forms/Submit';
import { Save } from '../../elements/Save';
import RenderFields from '../../forms/RenderFields';
import CopyToClipboard from '../../elements/CopyToClipboard';
import fieldTypes from '../../forms/field-types';
@@ -147,10 +147,13 @@ const DefaultAccount: React.FC<Props> = (props) => {
{(preview && (!collection.versions?.drafts || collection.versions?.drafts?.autosave)) && (
<PreviewButton
generatePreviewURL={preview}
CustomComponent={collection?.admin?.components?.elements?.PreviewButton}
/>
)}
{hasSavePermission && (
<FormSubmit buttonId="action-save">{t('general:save')}</FormSubmit>
<Save
CustomComponent={collection?.admin?.components?.elements?.SaveButton}
/>
)}
</div>
<div className={`${baseClass}__sidebar-fields`}>

View File

@@ -4,7 +4,6 @@ import { useConfig } from '../../utilities/Config';
import Eyebrow from '../../elements/Eyebrow';
import Form from '../../forms/Form';
import PreviewButton from '../../elements/PreviewButton';
import FormSubmit from '../../forms/Submit';
import RenderFields from '../../forms/RenderFields';
import CopyToClipboard from '../../elements/CopyToClipboard';
import Meta from '../../utilities/Meta';
@@ -14,8 +13,9 @@ import VersionsCount from '../../elements/VersionsCount';
import { Props } from './types';
import ViewDescription from '../../elements/ViewDescription';
import { useDocumentInfo } from '../../utilities/DocumentInfo';
import SaveDraft from '../../elements/SaveDraft';
import Publish from '../../elements/Publish';
import { SaveDraft } from '../../elements/SaveDraft';
import { Publish } from '../../elements/Publish';
import { Save } from '../../elements/Save';
import Status from '../../elements/Status';
import Autosave from '../../elements/Autosave';
import { OperationContext } from '../../utilities/OperationProvider';
@@ -106,20 +106,29 @@ const DefaultGlobalView: React.FC<Props> = (props) => {
{(preview && (!global.versions?.drafts || global.versions?.drafts?.autosave)) && (
<PreviewButton
generatePreviewURL={preview}
CustomComponent={global?.admin?.components?.elements?.PreviewButton}
/>
)}
{hasSavePermission && (
<React.Fragment>
{global.versions?.drafts && (
<React.Fragment>
{!global.versions.drafts.autosave && (
<SaveDraft />
<SaveDraft
CustomComponent={global?.admin?.components?.elements?.SaveDraftButton}
/>
)}
<Publish />
<Publish
CustomComponent={global?.admin?.components?.elements?.PublishButton}
/>
</React.Fragment>
)}
{!global.versions?.drafts && (
<FormSubmit buttonId="action-save">{t('save')}</FormSubmit>
<Save
CustomComponent={global?.admin?.components?.elements?.SaveButton}
/>
)}
</React.Fragment>
)}
@@ -128,6 +137,7 @@ const DefaultGlobalView: React.FC<Props> = (props) => {
{(preview && (global.versions?.drafts && !global.versions?.drafts?.autosave)) && (
<PreviewButton
generatePreviewURL={preview}
CustomComponent={global?.admin?.components?.elements?.PreviewButton}
/>
)}
{global.versions?.drafts && (

View File

@@ -5,7 +5,6 @@ import { useConfig } from '../../../utilities/Config';
import Eyebrow from '../../../elements/Eyebrow';
import Form from '../../../forms/Form';
import PreviewButton from '../../../elements/PreviewButton';
import FormSubmit from '../../../forms/Submit';
import RenderFields from '../../../forms/RenderFields';
import CopyToClipboard from '../../../elements/CopyToClipboard';
import DuplicateDocument from '../../../elements/DuplicateDocument';
@@ -20,8 +19,9 @@ import Upload from './Upload';
import { Props } from './types';
import Autosave from '../../../elements/Autosave';
import Status from '../../../elements/Status';
import Publish from '../../../elements/Publish';
import SaveDraft from '../../../elements/SaveDraft';
import { Publish } from '../../../elements/Publish';
import { SaveDraft } from '../../../elements/SaveDraft';
import { Save } from '../../../elements/Save';
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
import { OperationContext } from '../../../utilities/OperationProvider';
import { Gutter } from '../../../elements/Gutter';
@@ -109,6 +109,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
id={data?.id}
/>
)}
<div className={`${baseClass}__main`}>
<Meta
title={`${isEditing ? t('editing') : t('creating')} - ${getTranslation(collection.labels.singular, i18n)}`}
@@ -118,9 +119,11 @@ const DefaultEditView: React.FC<Props> = (props) => {
{!disableEyebrow && (
<Eyebrow />
)}
{(!(collection.versions?.drafts && collection.versions?.drafts?.autosave) && !disableLeaveWithoutSaving) && (
<LeaveWithoutSaving />
)}
<Gutter className={`${baseClass}__edit`}>
<header className={`${baseClass}__header`}>
{customHeader && customHeader}
@@ -135,6 +138,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
</h1>
)}
</header>
{auth && (
<Auth
useAPIKey={auth.useAPIKey}
@@ -145,6 +149,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
operation={operation}
/>
)}
{upload && (
<Upload
data={data}
@@ -152,6 +157,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
internalState={internalState}
/>
)}
<RenderFields
readOnly={!hasSavePermission}
permissions={permissions.fields}
@@ -176,6 +182,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
{t('createNew')}
</Link>
</li>
{!disableDuplicate && isEditing && (
<li>
<DuplicateDocument
@@ -187,6 +194,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
)}
</React.Fragment>
)}
{permissions?.delete?.permission && (
<li>
<DeleteDocument
@@ -198,6 +206,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
)}
</ul>
)}
<div
className={[
`${baseClass}__document-actions`,
@@ -207,30 +216,39 @@ const DefaultEditView: React.FC<Props> = (props) => {
{(isEditing && preview && (!collection.versions?.drafts || collection.versions?.drafts?.autosave)) && (
<PreviewButton
generatePreviewURL={preview}
CustomComponent={collection?.admin?.components?.elements?.PreviewButton}
/>
)}
{hasSavePermission && (
<React.Fragment>
{collection.versions?.drafts && (
{collection.versions?.drafts ? (
<React.Fragment>
{!collection.versions.drafts.autosave && (
<SaveDraft />
<SaveDraft CustomComponent={collection?.admin?.components?.elements?.SaveDraftButton} />
)}
<Publish />
<Publish
CustomComponent={collection?.admin?.components?.elements?.PublishButton}
/>
</React.Fragment>
)}
{!collection.versions?.drafts && (
<FormSubmit buttonId="action-save">{t('save')}</FormSubmit>
) : (
<Save
CustomComponent={collection?.admin?.components?.elements?.SaveButton}
/>
)}
</React.Fragment>
)}
</div>
<div className={`${baseClass}__sidebar-fields`}>
{(isEditing && preview && (collection.versions?.drafts && !collection.versions?.drafts?.autosave)) && (
<PreviewButton
generatePreviewURL={preview}
CustomComponent={collection?.admin?.components?.elements?.PreviewButton}
/>
)}
{collection.versions?.drafts && (
<React.Fragment>
<Status />
@@ -243,6 +261,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
)}
</React.Fragment>
)}
<RenderFields
readOnly={!hasSavePermission}
permissions={permissions.fields}
@@ -251,6 +270,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
fieldSchema={fields}
/>
</div>
{
isEditing && (
<ul className={`${baseClass}__meta`}>
@@ -270,6 +290,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
</a>
</li>
)}
{versions && (
<li>
<div className={`${baseClass}__label`}>{t('version:versions')}</div>
@@ -279,6 +300,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
/>
</li>
)}
{timestamps && (
<React.Fragment>
{updatedAt && (

View File

@@ -62,6 +62,12 @@ const collectionSchema = joi.object().keys({
List: componentSchema,
Edit: componentSchema,
}),
elements: joi.object({
SaveButton: componentSchema,
PublishButton: componentSchema,
SaveDraftButton: componentSchema,
PreviewButton: componentSchema,
}),
}),
pagination: joi.object({
defaultLimit: joi.number(),

View File

@@ -11,6 +11,7 @@ import { Auth, IncomingAuthType, User } from '../../auth/types';
import { IncomingUploadType, Upload } from '../../uploads/types';
import { IncomingCollectionVersions, SanitizedCollectionVersions } from '../../versions/types';
import { BuildQueryArgs } from '../../mongoose/buildQuery';
import { CustomPreviewButtonProps, CustomPublishButtonProps, CustomSaveButtonProps, CustomSaveDraftButtonProps } from '../../admin/components/elements/types';
type Register<T = any> = (doc: T, password: string) => T;
@@ -193,6 +194,28 @@ export type CollectionAdminOptions = {
* Custom admin components
*/
components?: {
elements?: {
/**
* Replaces the "Save" button
* + drafts must be disabled
*/
SaveButton?: CustomSaveButtonProps
/**
* Replaces the "Publish" button
* + drafts must be enabled
*/
PublishButton?: CustomPublishButtonProps
/**
* Replaces the "Save Draft" button
* + drafts must be enabled
* + autosave must be disabled
*/
SaveDraftButton?: CustomSaveDraftButtonProps
/**
* Replaces the "Preview" button
*/
PreviewButton?: CustomPreviewButtonProps
},
views?: {
Edit?: React.ComponentType<any>
List?: React.ComponentType<any>
@@ -312,7 +335,7 @@ export type CollectionConfig = {
custom?: Record<string, any>;
};
export interface SanitizedCollectionConfig extends Omit<DeepRequired<CollectionConfig>, 'auth' | 'upload' | 'fields' | 'versions'| 'endpoints'> {
export interface SanitizedCollectionConfig extends Omit<DeepRequired<CollectionConfig>, 'auth' | 'upload' | 'fields' | 'versions' | 'endpoints'> {
auth: Auth;
upload: Upload;
fields: Field[];

View File

@@ -26,6 +26,12 @@ const globalSchema = joi.object().keys({
views: joi.object({
Edit: componentSchema,
}),
elements: joi.object({
SaveButton: componentSchema,
PublishButton: componentSchema,
SaveDraftButton: componentSchema,
PreviewButton: componentSchema,
}),
}),
preview: joi.func(),
}),

View File

@@ -7,6 +7,7 @@ import { PayloadRequest } from '../../express/types';
import { Access, Endpoint, EntityDescription, GeneratePreviewURL } from '../../config/types';
import { Field } from '../../fields/config/types';
import { IncomingGlobalVersions, SanitizedGlobalVersions } from '../../versions/types';
import { CustomSaveButtonProps, CustomSaveDraftButtonProps, CustomPublishButtonProps, CustomPreviewButtonProps } from '../../admin/components/elements/types';
export type TypeWithID = {
id: string | number
@@ -70,6 +71,28 @@ export type GlobalAdminOptions = {
views?: {
Edit?: React.ComponentType<any>
}
elements?: {
/**
* Replaces the "Save" button
* + drafts must be disabled
*/
SaveButton?: CustomSaveButtonProps
/**
* Replaces the "Publish" button
* + drafts must be enabled
*/
PublishButton?: CustomPublishButtonProps
/**
* Replaces the "Save Draft" button
* + drafts must be enabled
* + autosave must be disabled
*/
SaveDraftButton?: CustomSaveDraftButtonProps
/**
* Replaces the "Preview" button
*/
PreviewButton?: CustomPreviewButtonProps
},
};
/**
* Function to generate custom preview URL

View File

@@ -1,4 +1,5 @@
import type { CollectionConfig } from '../../../src/collections/config/types';
import { CustomPublishButton } from '../elements/CustomSaveButton';
import { draftSlug } from '../shared';
const DraftPosts: CollectionConfig = {
@@ -7,6 +8,11 @@ const DraftPosts: CollectionConfig = {
useAsTitle: 'title',
defaultColumns: ['title', 'description', 'createdAt', '_status'],
preview: () => 'https://payloadcms.com',
components: {
elements: {
PublishButton: CustomPublishButton,
},
},
},
versions: {
maxPerDoc: 35,

View File

@@ -0,0 +1,16 @@
.customButton {
:global(.btn) {
background-color: var(--theme-success-500);
color: var(--theme-success-900);
&:hover {
background-color: var(--theme-success-550) !important;
color: var(--theme-success-900) !important;
}
&:disabled {
background-color: var(--theme-success-750);
color: var(--theme-success-500);
}
}
}

View File

@@ -0,0 +1,15 @@
import * as React from 'react';
import { CustomPublishButtonProps } from '../../../../src/admin/components/elements/types';
// In your projects, you can import as follows:
// import { CustomPublishButtonProps } from 'payload/types';
import classes from './index.module.scss';
export const CustomPublishButton: CustomPublishButtonProps = ({ DefaultButton, ...rest }) => {
return (
<div className={classes.customButton}>
<DefaultButton {...rest} />
</div>
);
};

10
types.d.ts vendored
View File

@@ -62,3 +62,13 @@ export {
UIField,
Validate,
} from './dist/fields/config/types';
export {
RowLabel,
} from './dist/admin/components/forms/RowLabel/types';
export {
CustomSaveButtonProps,
CustomSaveDraftButtonProps,
CustomPublishButtonProps,
} from './dist/admin/components/elements/types';