Feat/custom admin buttons (#2613)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { GeneratePreviewURL } from '../../../../config/types';
|
||||
|
||||
export type Props = {
|
||||
generatePreviewURL?: GeneratePreviewURL,
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export type Props = {}
|
||||
34
src/admin/components/elements/Save/index.tsx
Normal file
34
src/admin/components/elements/Save/index.tsx
Normal 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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
4
src/admin/components/elements/types.ts
Normal file
4
src/admin/components/elements/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { CustomPublishButtonProps } from './Publish';
|
||||
export { CustomSaveButtonProps } from './Save';
|
||||
export { CustomSaveDraftButtonProps } from './SaveDraft';
|
||||
export { CustomPreviewButtonProps } from './PreviewButton';
|
||||
@@ -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`}>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
16
test/versions/elements/CustomSaveButton/index.module.scss
Normal file
16
test/versions/elements/CustomSaveButton/index.module.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
test/versions/elements/CustomSaveButton/index.tsx
Normal file
15
test/versions/elements/CustomSaveButton/index.tsx
Normal 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
10
types.d.ts
vendored
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user