diff --git a/src/admin/components/elements/PerPage/index.scss b/src/admin/components/elements/PerPage/index.scss new file mode 100644 index 0000000000..dcfc7961f5 --- /dev/null +++ b/src/admin/components/elements/PerPage/index.scss @@ -0,0 +1,48 @@ +@import '../../../scss/styles.scss'; + +.per-page { + display: flex; + margin-bottom: $baseline; + margin-right: 0; + margin-left: auto; + + button { + padding: base(.25) 0; + line-height: base(1); + background: transparent; + border: 0; + font-weight: 600; + cursor: pointer; + + &:hover { + text-decoration: underline; + } + + &:active, + &:focus { + outline: none; + } + } + + span { + color: $color-gray; + } + + ul { + list-style: none; + padding: 0; + text-align: left; + margin: 0; + + li { + text-decoration: none; + } + + a { + color: $color-gray; + &:visited, &:link, &:active { + text-decoration: none; + } + } + } +} diff --git a/src/admin/components/elements/PerPage/index.tsx b/src/admin/components/elements/PerPage/index.tsx new file mode 100644 index 0000000000..0590352591 --- /dev/null +++ b/src/admin/components/elements/PerPage/index.tsx @@ -0,0 +1,61 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import qs from 'qs'; + +import { Link } from 'react-router-dom'; +import { useConfig } from '@payloadcms/config-provider'; +import { usePreferences } from '../../utilities/Preferences'; +import { useSearchParams } from '../../utilities/SearchParams'; +import Popup from '../Popup'; +import Chevron from '../../icons/Chevron'; + +import './index.scss'; + +const baseClass = 'per-page'; +type Props = { + setLimit: (limit: number) => void; + limit: number; +} + +const PerPage: React.FC = ({ setLimit }) => { + const { admin: { pagination: { options } } } = useConfig(); + + return ( +
+ + Per Page: + +
+ )} + backgroundColor="#333333" + render={({ close }) => ( +
+ +
+ )} + /> + + ); +}; + +export default PerPage; diff --git a/src/admin/components/elements/Popup/index.tsx b/src/admin/components/elements/Popup/index.tsx index 4353e62640..caabeb0ae9 100644 --- a/src/admin/components/elements/Popup/index.tsx +++ b/src/admin/components/elements/Popup/index.tsx @@ -23,6 +23,8 @@ const Popup: React.FC = (props) => { horizontalAlign = 'left', initActive = false, onToggleOpen, + backgroundColor, + padding, } = props; const buttonRef = useRef(null); @@ -98,7 +100,9 @@ const Popup: React.FC = (props) => { ].filter(Boolean).join(' '); return ( -
+
= (props) => {
-
-
+
+ +
{render && render({ close: () => setActive(false) })} {children && children}
diff --git a/src/admin/components/elements/Popup/types.ts b/src/admin/components/elements/Popup/types.ts index e851f4f639..0f17634514 100644 --- a/src/admin/components/elements/Popup/types.ts +++ b/src/admin/components/elements/Popup/types.ts @@ -1,3 +1,5 @@ +import { CSSProperties } from 'react'; + export type Props = { className?: string render?: (any) => void, @@ -10,4 +12,6 @@ export type Props = { showOnHover?: boolean, initActive?: boolean, onToggleOpen?: () => void, + backgroundColor?: CSSProperties['backgroundColor'], + padding?: CSSProperties['padding'], } diff --git a/src/admin/components/views/collections/List/Default.tsx b/src/admin/components/views/collections/List/Default.tsx index 4d943695ed..6904eec1df 100644 --- a/src/admin/components/views/collections/List/Default.tsx +++ b/src/admin/components/views/collections/List/Default.tsx @@ -13,6 +13,7 @@ import { Props } from './types'; import './index.scss'; import ViewDescription from '../../../elements/ViewDescription'; +import PerPage from '../../../elements/PerPage'; const baseClass = 'collection-list'; @@ -34,6 +35,8 @@ const DefaultList: React.FC = (props) => { newDocumentURL, setListControls, setSort, + limit, + setLimit, columns, hasCreatePermission, } = props; @@ -114,6 +117,7 @@ const DefaultList: React.FC = (props) => {
)}
+ = (props) => { nextPage={data.nextPage} numberOfNeighbors={1} /> + {data?.totalDocs > 0 && (
{(data.page * data.limit) - (data.limit - 1)} diff --git a/src/admin/components/views/collections/List/index.tsx b/src/admin/components/views/collections/List/index.tsx index 2360827f05..8d6f8a6733 100644 --- a/src/admin/components/views/collections/List/index.tsx +++ b/src/admin/components/views/collections/List/index.tsx @@ -31,7 +31,7 @@ const ListView: React.FC = (props) => { }, } = props; - const { serverURL, routes: { api, admin } } = useConfig(); + const { serverURL, routes: { api, admin }, admin: { pagination: { defaultLimit } } } = useConfig(); const { permissions } = useAuth(); const location = useLocation(); const { setStepNav } = useStepNav(); @@ -41,6 +41,7 @@ const ListView: React.FC = (props) => { const [listControls, setListControls] = useState({}); const [columns, setColumns] = useState([]); const [sort, setSort] = useState(null); + const [limit, setLimit] = useState(defaultLimit); const collectionPermissions = permissions?.collections?.[slug]; const hasCreatePermission = collectionPermissions?.create?.permission; @@ -54,19 +55,29 @@ const ListView: React.FC = (props) => { const { columns: listControlsColumns } = listControls; useEffect(() => { - const params = { - depth: 1, - page: undefined, - sort: undefined, - where: undefined, - }; + const perPagePrefKey = `${collection.slug}-per-page`; - if (page) params.page = page; - if (sort) params.sort = sort; - if (listControls?.where) params.where = listControls.where; + (async () => { + const currentLimit = await getPreference(perPagePrefKey) || defaultLimit; + setLimit(currentLimit); - setParams(params); - }, [setParams, page, sort, listControls]); + const params = { + depth: 1, + page: undefined, + sort: undefined, + where: undefined, + limit, + }; + + if (page) params.page = page; + if (sort) params.sort = sort; + if (limit && limit !== defaultLimit) params.limit = limit; + if (listControls?.where) params.where = listControls.where; + + console.log(params); + setParams(params); + })(); + }, [setParams, page, sort, listControls, collection, defaultLimit, getPreference, limit]); useEffect(() => { setStepNav([ @@ -96,6 +107,8 @@ const ListView: React.FC = (props) => { listControls, data, columns, + setLimit, + limit, }} /> ); diff --git a/src/admin/components/views/collections/List/types.ts b/src/admin/components/views/collections/List/types.ts index 9efe943efe..72df94823a 100644 --- a/src/admin/components/views/collections/List/types.ts +++ b/src/admin/components/views/collections/List/types.ts @@ -9,6 +9,8 @@ export type Props = { setSort: (sort: string) => void columns: Column[] hasCreatePermission: boolean + setLimit: (limit: number) => void + limit: number } export type ListIndexProps = { diff --git a/src/config/generateTypes.ts b/src/config/generateTypes.ts new file mode 100644 index 0000000000..009b32a448 --- /dev/null +++ b/src/config/generateTypes.ts @@ -0,0 +1,80 @@ +/* eslint-disable no-nested-ternary */ +/* eslint-disable no-use-before-define */ +import { compile } from 'json-schema-to-typescript'; +// import fse from 'fs-extra'; +import { CollectionConfig, SanitizedCollectionConfig } from '../collections/config/types'; +import { SanitizedConfig } from './types'; + +export function generateTypes(sanitizedConfig: SanitizedConfig): void { + console.log('compiling ts types'); + const jsonSchema = configToJsonSchema(sanitizedConfig.collections); + + compile(jsonSchema, 'Config', { + bannerComment: '// auto-generated by payload', + unreachableDefinitions: true, + }).then((compiled) => { + // fse.writeFileSync('generated-types.ts', compiled); + console.log('compiled', compiled); + }); +} + +function collectionToJsonSchema( + collection: CollectionConfig, + slugToLabel: Record, +): any { + return { + title: collection.labels.singular, + type: 'object', + properties: Object.fromEntries( + collection.fields.map((field) => { + const type = field.type === 'number' + ? { type: 'integer' } + : field.type === 'relationship' + ? { + $ref: `#/definitions/${slugToLabel[field.relationTo as string]}`, + } + : { type: 'string' }; + const enum_ = field.type === 'select' ? { enum: field.options } : {}; + const default_ = field.defaultValue ? { default: field.defaultValue } : {}; + + return [ + field.name, + { + ...type, + ...enum_, + ...default_, + }, + ]; + }), + ), + required: collection.fields + .filter((field) => field.required === true) + .map((field) => field.name), + additionalProperties: false, + }; +} + +function configToJsonSchema(collections: SanitizedCollectionConfig[]): any { + const slugToLabel = Object.fromEntries( + collections.map((collection) => [ + collection.slug, + collection.labels.singular, + ]), + ); + return { + definitions: Object.fromEntries( + collections.map((collection) => [ + collection.labels.singular, + collectionToJsonSchema(collection, slugToLabel), + ]), + ), + additionalProperties: false, + }; +} + +// const result = await compile(jsonSchema, 'Config', { +// bannerComment: '// auto-generated by payload', +// unreachableDefinitions: true, +// }); + +// await fse.writeFile('generated-types.ts', result); diff --git a/src/config/schema.ts b/src/config/schema.ts index 8c802a5418..a22ee50d91 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -56,6 +56,11 @@ export default joi.object({ Logo: component, }), }), + pagination: joi.object() + .keys({ + defaultLimit: joi.number(), + options: joi.array().items(joi.number()), + }), webpack: joi.func(), }), defaultDepth: joi.number() diff --git a/src/config/types.ts b/src/config/types.ts index 5f7c7c6f5f..96494d45bc 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -94,6 +94,10 @@ export type Config = { Dashboard?: React.ComponentType } } + pagination?: { + defaultLimit?: number; + options?: number[]; + } webpack?: (config: Configuration) => Configuration; }; collections?: CollectionConfig[];