diff --git a/src/components/UploadsGridView/UploadsGridView.tsx b/src/components/UploadsGridView/UploadsGridView.tsx deleted file mode 100644 index 59382d1..0000000 --- a/src/components/UploadsGridView/UploadsGridView.tsx +++ /dev/null @@ -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) => { - 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 ( -
- {Array.isArray(BeforeList) && - BeforeList.map((Component, i) => )} - - - - {!disableEyebrow && } - -
- {customHeader && customHeader} - {!customHeader && ( - -

{getTranslation(pluralLabel, i18n)}

- {hasCreatePermission && ( - - {t("createNew")} - - )} - {!smallBreak && ( - - )} - {description && ( -
- -
- )} -
- )} -
- - {Array.isArray(BeforeListTable) && - BeforeListTable.map((Component, i) => )} - {!data.docs && ( - - )} - {data.docs && data.docs.length > 0 && ( - - - - )} - {data.docs && data.docs.length === 0 && ( -
-

{t("noResults", { label: getTranslation(pluralLabel, i18n) })}

- {hasCreatePermission && newDocumentURL && ( - - )} -
- )} - {Array.isArray(AfterListTable) && - AfterListTable.map((Component, i) => )} - -
- - {data?.totalDocs > 0 && ( - -
- {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} -
- -
- {smallBreak && ( - - -
- - - - -
-
- )} -
-
- )} -
-
-
- {Array.isArray(AfterList) && - AfterList.map((Component, i) => )} -
- ); -}; diff --git a/src/payload.config.ts b/src/payload.config.ts index 9652f6f..e914a20 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -25,6 +25,7 @@ import { WeaponsThumbnails } from "./collections/WeaponsThumbnails/WeaponsThumbn import { Icon } from "./components/Icon"; import { Logo } from "./components/Logo"; import { Collections } from "./constants"; +import { gridViewPlugin } from "./plugins/payload-grid-view"; export default buildConfig({ serverURL: process.env.PAYLOAD_URI, @@ -70,4 +71,5 @@ export default buildConfig({ graphQL: { disable: true, }, + plugins: [gridViewPlugin], }); diff --git a/src/components/UploadsGridView/Grid/index.scss b/src/plugins/payload-grid-view/components/UploadsGridView/Grid/index.scss similarity index 100% rename from src/components/UploadsGridView/Grid/index.scss rename to src/plugins/payload-grid-view/components/UploadsGridView/Grid/index.scss diff --git a/src/components/UploadsGridView/Grid/index.tsx b/src/plugins/payload-grid-view/components/UploadsGridView/Grid/index.tsx similarity index 100% rename from src/components/UploadsGridView/Grid/index.tsx rename to src/plugins/payload-grid-view/components/UploadsGridView/Grid/index.tsx diff --git a/src/plugins/payload-grid-view/components/UploadsGridView/ListControls.tsx b/src/plugins/payload-grid-view/components/UploadsGridView/ListControls.tsx new file mode 100644 index 0000000..fef71c3 --- /dev/null +++ b/src/plugins/payload-grid-view/components/UploadsGridView/ListControls.tsx @@ -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 ( +
+
+ +
+
+ {!smallBreak && ( + + + + + + + )} + {enableColumns && ( + + setVisibleDrawer(visibleDrawer !== "columns" ? "columns" : undefined) + } + aria-expanded={visibleDrawer === "columns"} + aria-controls={`${baseClass}-columns`} + icon={}> + {t("columns")} + + )} + + setVisibleDrawer(visibleDrawer !== "where" ? "where" : undefined)} + aria-expanded={visibleDrawer === "where"} + aria-controls={`${baseClass}-where`} + icon={}> + {t("filters")} + + {enableSort && ( + + )} + {showViewModeToggle && ( +
+ 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"> + + + 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"> + + +
+ )} +
+
+
+ {enableColumns && ( + + + + )} + + + + {enableSort && ( + + + + )} +
+ ); +}; + +export default ListControls; diff --git a/src/plugins/payload-grid-view/components/UploadsGridView/UploadsGridView.tsx b/src/plugins/payload-grid-view/components/UploadsGridView/UploadsGridView.tsx new file mode 100644 index 0000000..0b122b7 --- /dev/null +++ b/src/plugins/payload-grid-view/components/UploadsGridView/UploadsGridView.tsx @@ -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(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 ( +
+ {Array.isArray(BeforeList) && + BeforeList.map((Component, i) => )} + + + + {!disableEyebrow && } + +
+ {customHeader && customHeader} + {!customHeader && ( + +

{getTranslation(pluralLabel, i18n)}

+ {hasCreatePermission && ( + + {t("createNew")} + + )} + {!smallBreak && ( + + )} + {description && ( +
+ +
+ )} +
+ )} +
+ setViewMode(newViewMode)} + resetParams={resetParams} + viewMode={viewMode} + showViewModeToggle={options.list === true && options.grid === true} + /> + {Array.isArray(BeforeListTable) && + BeforeListTable.map((Component, i) => )} + {!data.docs && ( + + )} + {data.docs && data.docs.length > 0 && ( + + {viewMode === "grid" ? ( + + ) : ( + + )} + + )} + {data.docs && data.docs.length === 0 && ( +
+

{t("noResults", { label: getTranslation(pluralLabel, i18n) })}

+ {hasCreatePermission && newDocumentURL && ( + + )} +
+ )} + {Array.isArray(AfterListTable) && + AfterListTable.map((Component, i) => )} + +
+ + {data?.totalDocs > 0 && ( + +
+ {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} +
+ +
+ {smallBreak && ( + + +
+ + + + +
+
+ )} +
+
+ )} +
+ + + {Array.isArray(AfterList) && + AfterList.map((Component, i) => )} + + ); + }; diff --git a/src/plugins/payload-grid-view/index.ts b/src/plugins/payload-grid-view/index.ts new file mode 100644 index 0000000..a1f3101 --- /dev/null +++ b/src/plugins/payload-grid-view/index.ts @@ -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["components"]; +type ViewsComponents = Required["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, +}); diff --git a/src/utils/collectionConfig.ts b/src/utils/collectionConfig.ts index 756da4b..53e04aa 100644 --- a/src/utils/collectionConfig.ts +++ b/src/utils/collectionConfig.ts @@ -1,7 +1,13 @@ import { CollectionConfig } from "payload/types"; import { Collections } from "../constants"; +import { CollectionConfigWithGridView } from "../plugins/payload-grid-view"; -export type BuildCollectionConfig = Omit & { +type CollectionConfigWithPlugins = CollectionConfig & CollectionConfigWithGridView; + +export type BuildCollectionConfig = Omit< + CollectionConfigWithPlugins, + "slug" | "typescript" | "labels" +> & { slug: Collections; labels: { singular: string; plural: string }; }; diff --git a/src/utils/imageCollectionConfig.ts b/src/utils/imageCollectionConfig.ts index 4c1f447..4652dad 100644 --- a/src/utils/imageCollectionConfig.ts +++ b/src/utils/imageCollectionConfig.ts @@ -1,6 +1,5 @@ import { ImageSize } from "payload/dist/uploads/types"; import { CollectionConfig } from "payload/types"; -import { UploadsGridView } from "../components/UploadsGridView/UploadsGridView"; import { CollectionGroups } from "../constants"; import { createImageRegenerationEndpoint } from "../endpoints/createImageRegenerationEndpoint"; import { BuildCollectionConfig, buildCollectionConfig } from "./collectionConfig"; @@ -21,7 +20,6 @@ export const buildImageCollectionConfig = ({ disableDuplicate: true, useAsTitle: "filename", group: CollectionGroups.Media, - components: { views: { List: UploadsGridView } }, ...admin, }, endpoints: [createImageRegenerationEndpoint(otherConfig.slug)],