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 { 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], | ||||
| }); | ||||
|  | ||||
| @ -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> | ||||
|     ); | ||||
|   }; | ||||
							
								
								
									
										53
									
								
								src/plugins/payload-grid-view/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/plugins/payload-grid-view/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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 { 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; | ||||
|   labels: { singular: string; plural: string }; | ||||
| }; | ||||
|  | ||||
| @ -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)], | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 DrMint
						DrMint