Turn grid view into a plugin
This commit is contained in:
parent
f454b22569
commit
3532dab712
|
@ -1,185 +0,0 @@
|
||||||
import { useWindowInfo } from "@faceless-ui/window-info";
|
|
||||||
import Button from "payload/dist/admin/components/elements/Button";
|
|
||||||
import DeleteMany from "payload/dist/admin/components/elements/DeleteMany";
|
|
||||||
import EditMany from "payload/dist/admin/components/elements/EditMany";
|
|
||||||
import Eyebrow from "payload/dist/admin/components/elements/Eyebrow";
|
|
||||||
import { Gutter } from "payload/dist/admin/components/elements/Gutter";
|
|
||||||
import ListControls from "payload/dist/admin/components/elements/ListControls";
|
|
||||||
import ListSelection from "payload/dist/admin/components/elements/ListSelection";
|
|
||||||
import Paginator from "payload/dist/admin/components/elements/Paginator";
|
|
||||||
import PerPage from "payload/dist/admin/components/elements/PerPage";
|
|
||||||
import Pill from "payload/dist/admin/components/elements/Pill";
|
|
||||||
import PublishMany from "payload/dist/admin/components/elements/PublishMany";
|
|
||||||
import { StaggeredShimmers } from "payload/dist/admin/components/elements/ShimmerEffect";
|
|
||||||
import UnpublishMany from "payload/dist/admin/components/elements/UnpublishMany";
|
|
||||||
import ViewDescription from "payload/dist/admin/components/elements/ViewDescription";
|
|
||||||
import Meta from "payload/dist/admin/components/utilities/Meta";
|
|
||||||
import { RelationshipProvider } from "payload/dist/admin/components/views/collections/List/RelationshipProvider";
|
|
||||||
import { SelectionProvider } from "payload/dist/admin/components/views/collections/List/SelectionProvider";
|
|
||||||
import { Props } from "payload/dist/admin/components/views/collections/List/types";
|
|
||||||
import formatFilesize from "payload/dist/uploads/formatFilesize";
|
|
||||||
import { getTranslation } from "payload/dist/utilities/getTranslation";
|
|
||||||
import React, { Fragment } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import Grid from "./Grid";
|
|
||||||
|
|
||||||
const baseClass = "collection-list";
|
|
||||||
|
|
||||||
export const UploadsGridView: React.ComponentType<Props> = (props) => {
|
|
||||||
const {
|
|
||||||
collection,
|
|
||||||
collection: {
|
|
||||||
labels: { singular: singularLabel, plural: pluralLabel },
|
|
||||||
admin: {
|
|
||||||
description,
|
|
||||||
components: { BeforeList, BeforeListTable, AfterListTable, AfterList } = {},
|
|
||||||
} = {},
|
|
||||||
},
|
|
||||||
data,
|
|
||||||
newDocumentURL,
|
|
||||||
limit,
|
|
||||||
hasCreatePermission,
|
|
||||||
disableEyebrow,
|
|
||||||
modifySearchParams,
|
|
||||||
handleSortChange,
|
|
||||||
handleWhereChange,
|
|
||||||
handlePageChange,
|
|
||||||
handlePerPageChange,
|
|
||||||
customHeader,
|
|
||||||
resetParams,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
breakpoints: { s: smallBreak },
|
|
||||||
} = useWindowInfo();
|
|
||||||
const { t, i18n } = useTranslation("general");
|
|
||||||
let formattedDocs = data.docs || [];
|
|
||||||
|
|
||||||
if (collection.upload) {
|
|
||||||
formattedDocs = formattedDocs?.map((doc) => {
|
|
||||||
return {
|
|
||||||
...doc,
|
|
||||||
filesize: formatFilesize(doc.filesize),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={baseClass}>
|
|
||||||
{Array.isArray(BeforeList) &&
|
|
||||||
BeforeList.map((Component, i) => <Component key={i} {...props} />)}
|
|
||||||
|
|
||||||
<Meta title={getTranslation(collection.labels.plural, i18n)} />
|
|
||||||
<SelectionProvider docs={data.docs} totalDocs={data.totalDocs}>
|
|
||||||
{!disableEyebrow && <Eyebrow />}
|
|
||||||
<Gutter className={`${baseClass}__wrap`}>
|
|
||||||
<header className={`${baseClass}__header`}>
|
|
||||||
{customHeader && customHeader}
|
|
||||||
{!customHeader && (
|
|
||||||
<Fragment>
|
|
||||||
<h1>{getTranslation(pluralLabel, i18n)}</h1>
|
|
||||||
{hasCreatePermission && (
|
|
||||||
<Pill
|
|
||||||
to={newDocumentURL}
|
|
||||||
aria-label={t("createNewLabel", {
|
|
||||||
label: getTranslation(singularLabel, i18n),
|
|
||||||
})}>
|
|
||||||
{t("createNew")}
|
|
||||||
</Pill>
|
|
||||||
)}
|
|
||||||
{!smallBreak && (
|
|
||||||
<ListSelection label={getTranslation(collection.labels.plural, i18n)} />
|
|
||||||
)}
|
|
||||||
{description && (
|
|
||||||
<div className={`${baseClass}__sub-header`}>
|
|
||||||
<ViewDescription description={description} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</header>
|
|
||||||
<ListControls
|
|
||||||
collection={collection}
|
|
||||||
modifySearchQuery={modifySearchParams}
|
|
||||||
handleSortChange={handleSortChange}
|
|
||||||
handleWhereChange={handleWhereChange}
|
|
||||||
resetParams={resetParams}
|
|
||||||
/>
|
|
||||||
{Array.isArray(BeforeListTable) &&
|
|
||||||
BeforeListTable.map((Component, i) => <Component key={i} {...props} />)}
|
|
||||||
{!data.docs && (
|
|
||||||
<StaggeredShimmers
|
|
||||||
className={[`${baseClass}__shimmer`, `${baseClass}__shimmer--rows`].join(" ")}
|
|
||||||
count={6}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{data.docs && data.docs.length > 0 && (
|
|
||||||
<RelationshipProvider>
|
|
||||||
<Grid data={formattedDocs} collection={collection} />
|
|
||||||
</RelationshipProvider>
|
|
||||||
)}
|
|
||||||
{data.docs && data.docs.length === 0 && (
|
|
||||||
<div className={`${baseClass}__no-results`}>
|
|
||||||
<p>{t("noResults", { label: getTranslation(pluralLabel, i18n) })}</p>
|
|
||||||
{hasCreatePermission && newDocumentURL && (
|
|
||||||
<Button el="link" to={newDocumentURL}>
|
|
||||||
{t("createNewLabel", { label: getTranslation(singularLabel, i18n) })}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{Array.isArray(AfterListTable) &&
|
|
||||||
AfterListTable.map((Component, i) => <Component key={i} {...props} />)}
|
|
||||||
|
|
||||||
<div className={`${baseClass}__page-controls`}>
|
|
||||||
<Paginator
|
|
||||||
limit={data.limit}
|
|
||||||
totalPages={data.totalPages}
|
|
||||||
page={data.page}
|
|
||||||
hasPrevPage={data.hasPrevPage}
|
|
||||||
hasNextPage={data.hasNextPage}
|
|
||||||
prevPage={data.prevPage ?? undefined}
|
|
||||||
nextPage={data.nextPage ?? undefined}
|
|
||||||
numberOfNeighbors={1}
|
|
||||||
disableHistoryChange={modifySearchParams === false}
|
|
||||||
onChange={handlePageChange}
|
|
||||||
/>
|
|
||||||
{data?.totalDocs > 0 && (
|
|
||||||
<Fragment>
|
|
||||||
<div className={`${baseClass}__page-info`}>
|
|
||||||
{data.page ?? 1 * data.limit - (data.limit - 1)}-
|
|
||||||
{data.totalPages > 1 && data.totalPages !== data.page
|
|
||||||
? data.limit * (data.page ?? 1)
|
|
||||||
: data.totalDocs}{" "}
|
|
||||||
{t("of")} {data.totalDocs}
|
|
||||||
</div>
|
|
||||||
<PerPage
|
|
||||||
limits={collection?.admin?.pagination?.limits}
|
|
||||||
limit={limit}
|
|
||||||
modifySearchParams={modifySearchParams}
|
|
||||||
handleChange={handlePerPageChange}
|
|
||||||
resetPage={data.totalDocs <= data.pagingCounter}
|
|
||||||
/>
|
|
||||||
<div className={`${baseClass}__list-selection`}>
|
|
||||||
{smallBreak && (
|
|
||||||
<Fragment>
|
|
||||||
<ListSelection label={getTranslation(collection.labels.plural, i18n)} />
|
|
||||||
<div className={`${baseClass}__list-selection-actions`}>
|
|
||||||
<EditMany collection={collection} resetParams={resetParams} />
|
|
||||||
<PublishMany collection={collection} resetParams={resetParams} />
|
|
||||||
<UnpublishMany collection={collection} resetParams={resetParams} />
|
|
||||||
<DeleteMany collection={collection} resetParams={resetParams} />
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Gutter>
|
|
||||||
</SelectionProvider>
|
|
||||||
{Array.isArray(AfterList) &&
|
|
||||||
AfterList.map((Component, i) => <Component key={i} {...props} />)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -25,6 +25,7 @@ import { WeaponsThumbnails } from "./collections/WeaponsThumbnails/WeaponsThumbn
|
||||||
import { Icon } from "./components/Icon";
|
import { Icon } from "./components/Icon";
|
||||||
import { Logo } from "./components/Logo";
|
import { Logo } from "./components/Logo";
|
||||||
import { Collections } from "./constants";
|
import { Collections } from "./constants";
|
||||||
|
import { gridViewPlugin } from "./plugins/payload-grid-view";
|
||||||
|
|
||||||
export default buildConfig({
|
export default buildConfig({
|
||||||
serverURL: process.env.PAYLOAD_URI,
|
serverURL: process.env.PAYLOAD_URI,
|
||||||
|
@ -70,4 +71,5 @@ export default buildConfig({
|
||||||
graphQL: {
|
graphQL: {
|
||||||
disable: true,
|
disable: true,
|
||||||
},
|
},
|
||||||
|
plugins: [gridViewPlugin],
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,228 @@
|
||||||
|
import { useWindowInfo } from "@faceless-ui/window-info";
|
||||||
|
import Button from "payload/dist/admin/components/elements/Button";
|
||||||
|
import ColumnSelector from "payload/dist/admin/components/elements/ColumnSelector";
|
||||||
|
import DeleteMany from "payload/dist/admin/components/elements/DeleteMany";
|
||||||
|
import EditMany from "payload/dist/admin/components/elements/EditMany";
|
||||||
|
import { getTextFieldsToBeSearched } from "payload/dist/admin/components/elements/ListControls/getTextFieldsToBeSearched";
|
||||||
|
import { Props } from "payload/dist/admin/components/elements/ListControls/types";
|
||||||
|
import Pill from "payload/dist/admin/components/elements/Pill";
|
||||||
|
import PublishMany from "payload/dist/admin/components/elements/PublishMany";
|
||||||
|
import SearchFilter from "payload/dist/admin/components/elements/SearchFilter";
|
||||||
|
import SortComplex from "payload/dist/admin/components/elements/SortComplex";
|
||||||
|
import UnpublishMany from "payload/dist/admin/components/elements/UnpublishMany";
|
||||||
|
import WhereBuilder from "payload/dist/admin/components/elements/WhereBuilder";
|
||||||
|
import validateWhereQuery from "payload/dist/admin/components/elements/WhereBuilder/validateWhereQuery";
|
||||||
|
import Chevron from "payload/dist/admin/components/icons/Chevron";
|
||||||
|
import { useSearchParams } from "payload/dist/admin/components/utilities/SearchParams";
|
||||||
|
import { SanitizedCollectionConfig } from "payload/dist/collections/config/types";
|
||||||
|
import { fieldAffectsData } from "payload/dist/fields/config/types";
|
||||||
|
import flattenFields from "payload/dist/utilities/flattenTopLevelFields";
|
||||||
|
import { getTranslation } from "payload/dist/utilities/getTranslation";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import AnimateHeight from "react-animate-height";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
const baseClass = "list-controls";
|
||||||
|
|
||||||
|
export type ViewMode = "grid" | "list";
|
||||||
|
|
||||||
|
const getUseAsTitle = (collection: SanitizedCollectionConfig) => {
|
||||||
|
const {
|
||||||
|
admin: { useAsTitle },
|
||||||
|
fields,
|
||||||
|
} = collection;
|
||||||
|
|
||||||
|
const topLevelFields = flattenFields(fields);
|
||||||
|
return topLevelFields.find((field) => fieldAffectsData(field) && field.name === useAsTitle);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ListControls component is used to render the controls (search, filter, where)
|
||||||
|
* for a collection's list view. You can find those directly above the table which lists
|
||||||
|
* the collection's documents.
|
||||||
|
*/
|
||||||
|
const ListControls: React.FC<
|
||||||
|
Props & {
|
||||||
|
viewMode: ViewMode;
|
||||||
|
handleViewModeChange: (newMode: ViewMode) => void;
|
||||||
|
showViewModeToggle: boolean;
|
||||||
|
}
|
||||||
|
> = (props) => {
|
||||||
|
const {
|
||||||
|
collection,
|
||||||
|
enableColumns = true,
|
||||||
|
enableSort = false,
|
||||||
|
handleSortChange,
|
||||||
|
handleWhereChange,
|
||||||
|
modifySearchQuery = true,
|
||||||
|
resetParams = () => undefined,
|
||||||
|
viewMode,
|
||||||
|
handleViewModeChange,
|
||||||
|
collection: {
|
||||||
|
fields,
|
||||||
|
admin: { listSearchableFields },
|
||||||
|
},
|
||||||
|
showViewModeToggle,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const params = useSearchParams();
|
||||||
|
const shouldInitializeWhereOpened = validateWhereQuery(params?.where);
|
||||||
|
|
||||||
|
const [titleField, setTitleField] = useState(getUseAsTitle(collection));
|
||||||
|
useEffect(() => {
|
||||||
|
setTitleField(getUseAsTitle(collection));
|
||||||
|
}, [collection]);
|
||||||
|
|
||||||
|
const [textFieldsToBeSearched] = useState(
|
||||||
|
getTextFieldsToBeSearched(listSearchableFields, fields)
|
||||||
|
);
|
||||||
|
const [visibleDrawer, setVisibleDrawer] = useState<"where" | "sort" | "columns" | undefined>(
|
||||||
|
shouldInitializeWhereOpened ? "where" : undefined
|
||||||
|
);
|
||||||
|
const { t, i18n } = useTranslation("general");
|
||||||
|
const {
|
||||||
|
breakpoints: { s: smallBreak },
|
||||||
|
} = useWindowInfo();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={baseClass}>
|
||||||
|
<div className={`${baseClass}__wrap`}>
|
||||||
|
<SearchFilter
|
||||||
|
fieldName={titleField && fieldAffectsData(titleField) ? titleField.name : undefined}
|
||||||
|
handleChange={handleWhereChange}
|
||||||
|
modifySearchQuery={modifySearchQuery}
|
||||||
|
fieldLabel={
|
||||||
|
titleField && fieldAffectsData(titleField)
|
||||||
|
? getTranslation(String(titleField.label ?? titleField.name), i18n)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
listSearchableFields={textFieldsToBeSearched}
|
||||||
|
/>
|
||||||
|
<div className={`${baseClass}__buttons`}>
|
||||||
|
<div className={`${baseClass}__buttons-wrap`}>
|
||||||
|
{!smallBreak && (
|
||||||
|
<React.Fragment>
|
||||||
|
<EditMany collection={collection} resetParams={resetParams} />
|
||||||
|
<PublishMany collection={collection} resetParams={resetParams} />
|
||||||
|
<UnpublishMany collection={collection} resetParams={resetParams} />
|
||||||
|
<DeleteMany collection={collection} resetParams={resetParams} />
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
{enableColumns && (
|
||||||
|
<Pill
|
||||||
|
pillStyle="light"
|
||||||
|
className={`${baseClass}__toggle-columns ${
|
||||||
|
visibleDrawer === "columns" ? `${baseClass}__buttons-active` : ""
|
||||||
|
}`}
|
||||||
|
onClick={() =>
|
||||||
|
setVisibleDrawer(visibleDrawer !== "columns" ? "columns" : undefined)
|
||||||
|
}
|
||||||
|
aria-expanded={visibleDrawer === "columns"}
|
||||||
|
aria-controls={`${baseClass}-columns`}
|
||||||
|
icon={<Chevron />}>
|
||||||
|
{t("columns")}
|
||||||
|
</Pill>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Pill
|
||||||
|
pillStyle="light"
|
||||||
|
className={`${baseClass}__toggle-where ${
|
||||||
|
visibleDrawer === "where" ? `${baseClass}__buttons-active` : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setVisibleDrawer(visibleDrawer !== "where" ? "where" : undefined)}
|
||||||
|
aria-expanded={visibleDrawer === "where"}
|
||||||
|
aria-controls={`${baseClass}-where`}
|
||||||
|
icon={<Chevron />}>
|
||||||
|
{t("filters")}
|
||||||
|
</Pill>
|
||||||
|
{enableSort && (
|
||||||
|
<Button
|
||||||
|
className={`${baseClass}__toggle-sort`}
|
||||||
|
buttonStyle={visibleDrawer === "sort" ? undefined : "secondary"}
|
||||||
|
onClick={() => setVisibleDrawer(visibleDrawer !== "sort" ? "sort" : undefined)}
|
||||||
|
aria-expanded={visibleDrawer === "sort"}
|
||||||
|
aria-controls={`${baseClass}-sort`}
|
||||||
|
icon="chevron"
|
||||||
|
iconStyle="none">
|
||||||
|
{t("sort")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{showViewModeToggle && (
|
||||||
|
<div style={{ marginLeft: 10 }}>
|
||||||
|
<svg
|
||||||
|
onClick={() => handleViewModeChange("list")}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
color:
|
||||||
|
viewMode === "list"
|
||||||
|
? "var(--theme-elevation-1000)"
|
||||||
|
: "var(--theme-elevation-500)",
|
||||||
|
}}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
height="32"
|
||||||
|
width="28">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M333-242h432.5q12 0 22-10t10-22v-100.5H333V-242ZM162.5-586h145v-132h-113q-12 0-22 10t-10 22v100Zm0 187h145v-161.5h-145V-399Zm32 157h113v-132.5h-145V-274q0 12 10 22t22 10ZM333-399h464.5v-161.5H333V-399Zm0-187h464.5v-100q0-12-10-22t-22-10H333v132ZM194.28-216.5q-24.218 0-40.749-16.531Q137-249.562 137-273.802v-412.396q0-24.24 16.531-40.771Q170.062-743.5 194.28-743.5h571.44q24.218 0 40.749 16.531Q823-710.438 823-686.198v412.396q0 24.24-16.531 40.771Q789.938-216.5 765.72-216.5H194.28Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
onClick={() => handleViewModeChange("grid")}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
color:
|
||||||
|
viewMode === "grid"
|
||||||
|
? "var(--theme-elevation-1000)"
|
||||||
|
: "var(--theme-elevation-500)",
|
||||||
|
}}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
height="28"
|
||||||
|
width="28">
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M176.5-519v-264.5h265V-519h-265Zm0 342.5v-265h265v265h-265ZM519-519v-264.5h264.5V-519H519Zm0 342.5v-265h264.5v265H519Zm-317-368h214V-758H202v213.5Zm342.5 0H758V-758H544.5v213.5Zm0 342.5H758v-214H544.5v214ZM202-202h214v-214H202v214Zm342.5-342.5Zm0 128.5ZM416-416Zm0-128.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{enableColumns && (
|
||||||
|
<AnimateHeight
|
||||||
|
className={`${baseClass}__columns`}
|
||||||
|
height={visibleDrawer === "columns" ? "auto" : 0}
|
||||||
|
id={`${baseClass}-columns`}>
|
||||||
|
<ColumnSelector collection={collection} />
|
||||||
|
</AnimateHeight>
|
||||||
|
)}
|
||||||
|
<AnimateHeight
|
||||||
|
className={`${baseClass}__where`}
|
||||||
|
height={visibleDrawer === "where" ? "auto" : 0}
|
||||||
|
id={`${baseClass}-where`}>
|
||||||
|
<WhereBuilder
|
||||||
|
collection={collection}
|
||||||
|
modifySearchQuery={modifySearchQuery}
|
||||||
|
handleChange={handleWhereChange}
|
||||||
|
/>
|
||||||
|
</AnimateHeight>
|
||||||
|
{enableSort && (
|
||||||
|
<AnimateHeight
|
||||||
|
className={`${baseClass}__sort`}
|
||||||
|
height={visibleDrawer === "sort" ? "auto" : 0}
|
||||||
|
id={`${baseClass}-sort`}>
|
||||||
|
<SortComplex
|
||||||
|
modifySearchQuery={modifySearchQuery}
|
||||||
|
collection={collection}
|
||||||
|
handleChange={handleSortChange}
|
||||||
|
/>
|
||||||
|
</AnimateHeight>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListControls;
|
|
@ -0,0 +1,202 @@
|
||||||
|
import { useWindowInfo } from "@faceless-ui/window-info";
|
||||||
|
import Button from "payload/dist/admin/components/elements/Button";
|
||||||
|
import DeleteMany from "payload/dist/admin/components/elements/DeleteMany";
|
||||||
|
import EditMany from "payload/dist/admin/components/elements/EditMany";
|
||||||
|
import Eyebrow from "payload/dist/admin/components/elements/Eyebrow";
|
||||||
|
import { Gutter } from "payload/dist/admin/components/elements/Gutter";
|
||||||
|
import ListSelection from "payload/dist/admin/components/elements/ListSelection";
|
||||||
|
import Paginator from "payload/dist/admin/components/elements/Paginator";
|
||||||
|
import PerPage from "payload/dist/admin/components/elements/PerPage";
|
||||||
|
import Pill from "payload/dist/admin/components/elements/Pill";
|
||||||
|
import PublishMany from "payload/dist/admin/components/elements/PublishMany";
|
||||||
|
import { StaggeredShimmers } from "payload/dist/admin/components/elements/ShimmerEffect";
|
||||||
|
import { Table } from "payload/dist/admin/components/elements/Table";
|
||||||
|
import UnpublishMany from "payload/dist/admin/components/elements/UnpublishMany";
|
||||||
|
import ViewDescription from "payload/dist/admin/components/elements/ViewDescription";
|
||||||
|
import Meta from "payload/dist/admin/components/utilities/Meta";
|
||||||
|
import { RelationshipProvider } from "payload/dist/admin/components/views/collections/List/RelationshipProvider";
|
||||||
|
import { SelectionProvider } from "payload/dist/admin/components/views/collections/List/SelectionProvider";
|
||||||
|
import { Props } from "payload/dist/admin/components/views/collections/List/types";
|
||||||
|
import formatFilesize from "payload/dist/uploads/formatFilesize";
|
||||||
|
import { getTranslation } from "payload/dist/utilities/getTranslation";
|
||||||
|
import React, { Fragment, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Grid from "./Grid";
|
||||||
|
import ListControls, { ViewMode } from "./ListControls";
|
||||||
|
|
||||||
|
const baseClass = "collection-list";
|
||||||
|
|
||||||
|
export type UploadsGridViewOptions = {
|
||||||
|
list?: boolean;
|
||||||
|
grid?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UploadsGridView =
|
||||||
|
(options: UploadsGridViewOptions) =>
|
||||||
|
(props: Props): JSX.Element => {
|
||||||
|
const {
|
||||||
|
collection,
|
||||||
|
collection: {
|
||||||
|
labels: { singular: singularLabel, plural: pluralLabel },
|
||||||
|
admin: {
|
||||||
|
description,
|
||||||
|
components: { BeforeList, BeforeListTable, AfterListTable, AfterList } = {},
|
||||||
|
} = {},
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
newDocumentURL,
|
||||||
|
limit,
|
||||||
|
hasCreatePermission,
|
||||||
|
disableEyebrow,
|
||||||
|
modifySearchParams,
|
||||||
|
handleSortChange,
|
||||||
|
handleWhereChange,
|
||||||
|
handlePageChange,
|
||||||
|
handlePerPageChange,
|
||||||
|
customHeader,
|
||||||
|
resetParams,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [viewMode, setViewMode] = useState<ViewMode>(options.grid === true ? "grid" : "list");
|
||||||
|
|
||||||
|
const {
|
||||||
|
breakpoints: { s: smallBreak },
|
||||||
|
} = useWindowInfo();
|
||||||
|
const { t, i18n } = useTranslation("general");
|
||||||
|
let formattedDocs = data.docs || [];
|
||||||
|
|
||||||
|
if (collection.upload) {
|
||||||
|
formattedDocs = formattedDocs?.map((doc) => {
|
||||||
|
return {
|
||||||
|
...doc,
|
||||||
|
filesize: formatFilesize(doc.filesize),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={baseClass}>
|
||||||
|
{Array.isArray(BeforeList) &&
|
||||||
|
BeforeList.map((Component, i) => <Component key={i} {...props} />)}
|
||||||
|
|
||||||
|
<Meta title={getTranslation(collection.labels.plural, i18n)} />
|
||||||
|
<SelectionProvider docs={data.docs} totalDocs={data.totalDocs}>
|
||||||
|
{!disableEyebrow && <Eyebrow />}
|
||||||
|
<Gutter className={`${baseClass}__wrap`}>
|
||||||
|
<header className={`${baseClass}__header`}>
|
||||||
|
{customHeader && customHeader}
|
||||||
|
{!customHeader && (
|
||||||
|
<Fragment>
|
||||||
|
<h1>{getTranslation(pluralLabel, i18n)}</h1>
|
||||||
|
{hasCreatePermission && (
|
||||||
|
<Pill
|
||||||
|
to={newDocumentURL}
|
||||||
|
aria-label={t("createNewLabel", {
|
||||||
|
label: getTranslation(singularLabel, i18n),
|
||||||
|
})}>
|
||||||
|
{t("createNew")}
|
||||||
|
</Pill>
|
||||||
|
)}
|
||||||
|
{!smallBreak && (
|
||||||
|
<ListSelection label={getTranslation(collection.labels.plural, i18n)} />
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<div className={`${baseClass}__sub-header`}>
|
||||||
|
<ViewDescription description={description} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
<ListControls
|
||||||
|
collection={collection}
|
||||||
|
modifySearchQuery={modifySearchParams}
|
||||||
|
handleSortChange={handleSortChange}
|
||||||
|
handleWhereChange={handleWhereChange}
|
||||||
|
handleViewModeChange={(newViewMode) => setViewMode(newViewMode)}
|
||||||
|
resetParams={resetParams}
|
||||||
|
viewMode={viewMode}
|
||||||
|
showViewModeToggle={options.list === true && options.grid === true}
|
||||||
|
/>
|
||||||
|
{Array.isArray(BeforeListTable) &&
|
||||||
|
BeforeListTable.map((Component, i) => <Component key={i} {...props} />)}
|
||||||
|
{!data.docs && (
|
||||||
|
<StaggeredShimmers
|
||||||
|
className={[`${baseClass}__shimmer`, `${baseClass}__shimmer--rows`].join(" ")}
|
||||||
|
count={6}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{data.docs && data.docs.length > 0 && (
|
||||||
|
<RelationshipProvider>
|
||||||
|
{viewMode === "grid" ? (
|
||||||
|
<Grid data={formattedDocs} collection={collection} />
|
||||||
|
) : (
|
||||||
|
<Table data={formattedDocs} />
|
||||||
|
)}
|
||||||
|
</RelationshipProvider>
|
||||||
|
)}
|
||||||
|
{data.docs && data.docs.length === 0 && (
|
||||||
|
<div className={`${baseClass}__no-results`}>
|
||||||
|
<p>{t("noResults", { label: getTranslation(pluralLabel, i18n) })}</p>
|
||||||
|
{hasCreatePermission && newDocumentURL && (
|
||||||
|
<Button el="link" to={newDocumentURL}>
|
||||||
|
{t("createNewLabel", { label: getTranslation(singularLabel, i18n) })}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{Array.isArray(AfterListTable) &&
|
||||||
|
AfterListTable.map((Component, i) => <Component key={i} {...props} />)}
|
||||||
|
|
||||||
|
<div className={`${baseClass}__page-controls`}>
|
||||||
|
<Paginator
|
||||||
|
limit={data.limit}
|
||||||
|
totalPages={data.totalPages}
|
||||||
|
page={data.page}
|
||||||
|
hasPrevPage={data.hasPrevPage}
|
||||||
|
hasNextPage={data.hasNextPage}
|
||||||
|
prevPage={data.prevPage ?? undefined}
|
||||||
|
nextPage={data.nextPage ?? undefined}
|
||||||
|
numberOfNeighbors={1}
|
||||||
|
disableHistoryChange={modifySearchParams === false}
|
||||||
|
onChange={handlePageChange}
|
||||||
|
/>
|
||||||
|
{data?.totalDocs > 0 && (
|
||||||
|
<Fragment>
|
||||||
|
<div className={`${baseClass}__page-info`}>
|
||||||
|
{data.page ?? 1 * data.limit - (data.limit - 1)}-
|
||||||
|
{data.totalPages > 1 && data.totalPages !== data.page
|
||||||
|
? data.limit * (data.page ?? 1)
|
||||||
|
: data.totalDocs}{" "}
|
||||||
|
{t("of")} {data.totalDocs}
|
||||||
|
</div>
|
||||||
|
<PerPage
|
||||||
|
limits={collection?.admin?.pagination?.limits}
|
||||||
|
limit={limit}
|
||||||
|
modifySearchParams={modifySearchParams}
|
||||||
|
handleChange={handlePerPageChange}
|
||||||
|
resetPage={data.totalDocs <= data.pagingCounter}
|
||||||
|
/>
|
||||||
|
<div className={`${baseClass}__list-selection`}>
|
||||||
|
{smallBreak && (
|
||||||
|
<Fragment>
|
||||||
|
<ListSelection label={getTranslation(collection.labels.plural, i18n)} />
|
||||||
|
<div className={`${baseClass}__list-selection-actions`}>
|
||||||
|
<EditMany collection={collection} resetParams={resetParams} />
|
||||||
|
<PublishMany collection={collection} resetParams={resetParams} />
|
||||||
|
<UnpublishMany collection={collection} resetParams={resetParams} />
|
||||||
|
<DeleteMany collection={collection} resetParams={resetParams} />
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Gutter>
|
||||||
|
</SelectionProvider>
|
||||||
|
{Array.isArray(AfterList) &&
|
||||||
|
AfterList.map((Component, i) => <Component key={i} {...props} />)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Plugin } from "payload/config";
|
||||||
|
import { CollectionAdminOptions } from "payload/dist/collections/config/types";
|
||||||
|
import { CollectionConfig } from "payload/types";
|
||||||
|
import { UploadsGridView, UploadsGridViewOptions } from "./components/UploadsGridView/UploadsGridView";
|
||||||
|
|
||||||
|
type Components = Required<CollectionAdminOptions>["components"];
|
||||||
|
type ViewsComponents = Required<Required<CollectionAdminOptions>["components"]>["views"];
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
isUploadEnabled: boolean;
|
||||||
|
gridView: UploadsGridViewOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CollectionConfigWithGridView = CollectionConfig & {
|
||||||
|
custom?: { gridView?: UploadsGridViewOptions };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const gridViewPlugin: Plugin = ({ collections, ...others }) => ({
|
||||||
|
collections: collections?.map(handleCollection),
|
||||||
|
...others,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCollection = ({
|
||||||
|
admin,
|
||||||
|
...others
|
||||||
|
}: CollectionConfigWithGridView): CollectionConfig => ({
|
||||||
|
...others,
|
||||||
|
admin: handleAdmin(admin, {
|
||||||
|
isUploadEnabled: others.upload !== undefined,
|
||||||
|
gridView: others.custom?.gridView ?? { grid: true, list: true },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAdmin = (
|
||||||
|
{ components, ...others }: CollectionAdminOptions = {},
|
||||||
|
options: Options
|
||||||
|
): CollectionAdminOptions => ({
|
||||||
|
...others,
|
||||||
|
components: handleComponents(components, options),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleComponents = ({ views, ...others }: Components = {}, options: Options): Components => ({
|
||||||
|
...others,
|
||||||
|
views: handleViewsComponents(views, options),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleViewsComponents = (
|
||||||
|
{ List, ...others }: ViewsComponents = {},
|
||||||
|
{ isUploadEnabled, gridView }: Options
|
||||||
|
): ViewsComponents => ({
|
||||||
|
...others,
|
||||||
|
List: isUploadEnabled ? UploadsGridView(gridView) : List,
|
||||||
|
});
|
|
@ -1,7 +1,13 @@
|
||||||
import { CollectionConfig } from "payload/types";
|
import { CollectionConfig } from "payload/types";
|
||||||
import { Collections } from "../constants";
|
import { Collections } from "../constants";
|
||||||
|
import { CollectionConfigWithGridView } from "../plugins/payload-grid-view";
|
||||||
|
|
||||||
export type BuildCollectionConfig = Omit<CollectionConfig, "slug" | "typescript" | "labels"> & {
|
type CollectionConfigWithPlugins = CollectionConfig & CollectionConfigWithGridView;
|
||||||
|
|
||||||
|
export type BuildCollectionConfig = Omit<
|
||||||
|
CollectionConfigWithPlugins,
|
||||||
|
"slug" | "typescript" | "labels"
|
||||||
|
> & {
|
||||||
slug: Collections;
|
slug: Collections;
|
||||||
labels: { singular: string; plural: string };
|
labels: { singular: string; plural: string };
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { ImageSize } from "payload/dist/uploads/types";
|
import { ImageSize } from "payload/dist/uploads/types";
|
||||||
import { CollectionConfig } from "payload/types";
|
import { CollectionConfig } from "payload/types";
|
||||||
import { UploadsGridView } from "../components/UploadsGridView/UploadsGridView";
|
|
||||||
import { CollectionGroups } from "../constants";
|
import { CollectionGroups } from "../constants";
|
||||||
import { createImageRegenerationEndpoint } from "../endpoints/createImageRegenerationEndpoint";
|
import { createImageRegenerationEndpoint } from "../endpoints/createImageRegenerationEndpoint";
|
||||||
import { BuildCollectionConfig, buildCollectionConfig } from "./collectionConfig";
|
import { BuildCollectionConfig, buildCollectionConfig } from "./collectionConfig";
|
||||||
|
@ -21,7 +20,6 @@ export const buildImageCollectionConfig = ({
|
||||||
disableDuplicate: true,
|
disableDuplicate: true,
|
||||||
useAsTitle: "filename",
|
useAsTitle: "filename",
|
||||||
group: CollectionGroups.Media,
|
group: CollectionGroups.Media,
|
||||||
components: { views: { List: UploadsGridView } },
|
|
||||||
...admin,
|
...admin,
|
||||||
},
|
},
|
||||||
endpoints: [createImageRegenerationEndpoint(otherConfig.slug)],
|
endpoints: [createImageRegenerationEndpoint(otherConfig.slug)],
|
||||||
|
|
Loading…
Reference in New Issue