From 3715e011c97c8e30174c35c502fa7db12bc84e2c Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Wed, 6 Oct 2021 21:38:08 -0400 Subject: [PATCH 1/4] feat(admin): initial per page component --- .../components/elements/PerPage/index.scss | 48 ++++++++++++++++++ .../components/elements/PerPage/index.tsx | 49 +++++++++++++++++++ src/admin/components/elements/Popup/index.tsx | 23 +++++++-- src/admin/components/elements/Popup/types.ts | 4 ++ .../views/collections/List/Default.tsx | 5 ++ 5 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 src/admin/components/elements/PerPage/index.scss create mode 100644 src/admin/components/elements/PerPage/index.tsx 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..1290819b52 --- /dev/null +++ b/src/admin/components/elements/PerPage/index.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import './index.scss'; +import { Link } from 'react-router-dom'; +import Popup from '../Popup'; +import Chevron from '../../icons/Chevron'; + +const baseClass = 'per-page'; +type Props = { + valueOptions: number[]; +} + +const PerPage: React.FC = ({ valueOptions }) => ( +
+ + Per Page: + +
+ )} + backgroundColor="#333333" + render={({ close }) => ( +
+
    + {valueOptions.map((option, i) => ( +
  • + + {option} + + +
  • + ))} + +
+
+ )} + /> + +); + +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..8be1f780cb 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'; @@ -114,6 +115,7 @@ const DefaultList: React.FC = (props) => {
)}
+ = (props) => { nextPage={data.nextPage} numberOfNeighbors={1} /> + {data?.totalDocs > 0 && (
{(data.page * data.limit) - (data.limit - 1)} From c132f2ff10b3efdb3854ec2d5a895120ccf22002 Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Mon, 11 Oct 2021 11:34:25 -0400 Subject: [PATCH 2/4] feat(per-page): add pagination to admin config --- src/config/generateTypes.ts | 80 +++++++++++++++++++++++++++++++++++++ src/config/schema.ts | 5 +++ src/config/types.ts | 4 ++ 3 files changed, 89 insertions(+) create mode 100644 src/config/generateTypes.ts 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..d6bd83a59e 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({ + default: 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..544e2d40ba 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -94,6 +94,10 @@ export type Config = { Dashboard?: React.ComponentType } } + pagination?: { + default?: number; + options?: number[]; + } webpack?: (config: Configuration) => Configuration; }; collections?: CollectionConfig[]; From d88ce2d342b20c1601b1b58470c226a9826758b3 Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Mon, 11 Oct 2021 17:06:35 -0400 Subject: [PATCH 3/4] feat(per-page): set and load from preferences --- .../components/elements/PerPage/index.tsx | 116 ++++++++++++------ .../views/collections/List/Default.tsx | 2 +- .../views/collections/List/index.tsx | 34 +++-- 3 files changed, 103 insertions(+), 49 deletions(-) diff --git a/src/admin/components/elements/PerPage/index.tsx b/src/admin/components/elements/PerPage/index.tsx index 1290819b52..dca6a2ef0e 100644 --- a/src/admin/components/elements/PerPage/index.tsx +++ b/src/admin/components/elements/PerPage/index.tsx @@ -1,49 +1,93 @@ -import React from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; +import qs from 'qs'; -import './index.scss'; 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 = { - valueOptions: number[]; + collectionSlug: string; } -const PerPage: React.FC = ({ valueOptions }) => ( -
- - Per Page: - -
- )} - backgroundColor="#333333" - render={({ close }) => ( -
-
    - {valueOptions.map((option, i) => ( -
  • - - {option} - +const PerPage: React.FC = ({ collectionSlug }) => { + const preferencesKey = `${collectionSlug}-per-page`; + const { admin: { pagination: { default: defaultPagination, options } } } = useConfig(); + const { getPreference, setPreference } = usePreferences(); + const [, setPerPage] = useState(defaultPagination); + const searchParams = useSearchParams(); -
  • - ))} + const updatePerPage = useCallback((perPage) => { + setPerPage(perPage); + setPreference(preferencesKey, perPage); + }, [setPerPage, setPreference, preferencesKey]); -
-
- )} - /> -
-); + useEffect(() => { + const asyncGetPreference = async () => { + const perPageFromPreferences = await getPreference(preferencesKey) || defaultPagination; + setPerPage(perPageFromPreferences); + }; + + asyncGetPreference(); + }, [defaultPagination, preferencesKey, getPreference]); + + const closeAndSet = ({ close, option }) => { + console.log(`Setting option: ${option}`); + updatePerPage(option); + close(); + }; + + return ( +
+ + Per Page: + +
+ )} + backgroundColor="#333333" + render={({ close }) => ( +
+
    + {options.map((option, i) => { + const newParams = { + ...searchParams, + limit: option, + }; + + const search = qs.stringify(newParams); + const linkPath = `${collectionSlug}?${search}`; + + return ( +
  • + closeAndSet({ close, option })} + > + {option} + + +
  • + ); + })} + ; + +
+
+ )} + /> +
+ ); +}; export default PerPage; diff --git a/src/admin/components/views/collections/List/Default.tsx b/src/admin/components/views/collections/List/Default.tsx index 8be1f780cb..508dae602c 100644 --- a/src/admin/components/views/collections/List/Default.tsx +++ b/src/admin/components/views/collections/List/Default.tsx @@ -127,7 +127,7 @@ const DefaultList: React.FC = (props) => { numberOfNeighbors={1} /> {data?.totalDocs > 0 && (
diff --git a/src/admin/components/views/collections/List/index.tsx b/src/admin/components/views/collections/List/index.tsx index 2360827f05..37ee284fa4 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: { default: defaultPagination } } } = 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(defaultPagination); const collectionPermissions = permissions?.collections?.[slug]; const hasCreatePermission = collectionPermissions?.create?.permission; @@ -54,19 +55,28 @@ 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) || defaultPagination; + 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 !== defaultPagination) params.limit = limit; + if (listControls?.where) params.where = listControls.where; + + setParams(params); + })(); + }, [setParams, page, sort, listControls, collection, defaultPagination, getPreference, limit]); useEffect(() => { setStepNav([ From 6807637e25db56d5ccfdccf595e16213c684ab14 Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Mon, 11 Oct 2021 17:42:02 -0400 Subject: [PATCH 4/4] wip(per-page): thread the needle, not working --- .../components/elements/PerPage/index.tsx | 70 +++++-------------- .../views/collections/List/Default.tsx | 5 +- .../views/collections/List/index.tsx | 13 ++-- .../views/collections/List/types.ts | 2 + src/config/schema.ts | 2 +- src/config/types.ts | 2 +- 6 files changed, 35 insertions(+), 59 deletions(-) diff --git a/src/admin/components/elements/PerPage/index.tsx b/src/admin/components/elements/PerPage/index.tsx index dca6a2ef0e..0590352591 100644 --- a/src/admin/components/elements/PerPage/index.tsx +++ b/src/admin/components/elements/PerPage/index.tsx @@ -12,35 +12,12 @@ import './index.scss'; const baseClass = 'per-page'; type Props = { - collectionSlug: string; + setLimit: (limit: number) => void; + limit: number; } -const PerPage: React.FC = ({ collectionSlug }) => { - const preferencesKey = `${collectionSlug}-per-page`; - const { admin: { pagination: { default: defaultPagination, options } } } = useConfig(); - const { getPreference, setPreference } = usePreferences(); - const [, setPerPage] = useState(defaultPagination); - const searchParams = useSearchParams(); - - const updatePerPage = useCallback((perPage) => { - setPerPage(perPage); - setPreference(preferencesKey, perPage); - }, [setPerPage, setPreference, preferencesKey]); - - useEffect(() => { - const asyncGetPreference = async () => { - const perPageFromPreferences = await getPreference(preferencesKey) || defaultPagination; - setPerPage(perPageFromPreferences); - }; - - asyncGetPreference(); - }, [defaultPagination, preferencesKey, getPreference]); - - const closeAndSet = ({ close, option }) => { - console.log(`Setting option: ${option}`); - updatePerPage(option); - close(); - }; +const PerPage: React.FC = ({ setLimit }) => { + const { admin: { pagination: { options } } } = useConfig(); return (
@@ -56,32 +33,23 @@ const PerPage: React.FC = ({ collectionSlug }) => { render={({ close }) => (
    - {options.map((option, i) => { - const newParams = { - ...searchParams, - limit: option, - }; - - const search = qs.stringify(newParams); - const linkPath = `${collectionSlug}?${search}`; - - return ( -
  • ( +
  • +
  • - ); - })} + {limitNumber} + + + ))} ; -
)} diff --git a/src/admin/components/views/collections/List/Default.tsx b/src/admin/components/views/collections/List/Default.tsx index 508dae602c..6904eec1df 100644 --- a/src/admin/components/views/collections/List/Default.tsx +++ b/src/admin/components/views/collections/List/Default.tsx @@ -35,6 +35,8 @@ const DefaultList: React.FC = (props) => { newDocumentURL, setListControls, setSort, + limit, + setLimit, columns, hasCreatePermission, } = props; @@ -127,7 +129,8 @@ const DefaultList: React.FC = (props) => { numberOfNeighbors={1} /> {data?.totalDocs > 0 && (
diff --git a/src/admin/components/views/collections/List/index.tsx b/src/admin/components/views/collections/List/index.tsx index 37ee284fa4..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 }, admin: { pagination: { default: defaultPagination } } } = useConfig(); + const { serverURL, routes: { api, admin }, admin: { pagination: { defaultLimit } } } = useConfig(); const { permissions } = useAuth(); const location = useLocation(); const { setStepNav } = useStepNav(); @@ -41,7 +41,7 @@ const ListView: React.FC = (props) => { const [listControls, setListControls] = useState({}); const [columns, setColumns] = useState([]); const [sort, setSort] = useState(null); - const [limit, setLimit] = useState(defaultPagination); + const [limit, setLimit] = useState(defaultLimit); const collectionPermissions = permissions?.collections?.[slug]; const hasCreatePermission = collectionPermissions?.create?.permission; @@ -58,7 +58,7 @@ const ListView: React.FC = (props) => { const perPagePrefKey = `${collection.slug}-per-page`; (async () => { - const currentLimit = await getPreference(perPagePrefKey) || defaultPagination; + const currentLimit = await getPreference(perPagePrefKey) || defaultLimit; setLimit(currentLimit); const params = { @@ -71,12 +71,13 @@ const ListView: React.FC = (props) => { if (page) params.page = page; if (sort) params.sort = sort; - if (limit && limit !== defaultPagination) params.limit = limit; + if (limit && limit !== defaultLimit) params.limit = limit; if (listControls?.where) params.where = listControls.where; + console.log(params); setParams(params); })(); - }, [setParams, page, sort, listControls, collection, defaultPagination, getPreference, limit]); + }, [setParams, page, sort, listControls, collection, defaultLimit, getPreference, limit]); useEffect(() => { setStepNav([ @@ -106,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/schema.ts b/src/config/schema.ts index d6bd83a59e..a22ee50d91 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -58,7 +58,7 @@ export default joi.object({ }), pagination: joi.object() .keys({ - default: joi.number(), + defaultLimit: joi.number(), options: joi.array().items(joi.number()), }), webpack: joi.func(), diff --git a/src/config/types.ts b/src/config/types.ts index 544e2d40ba..96494d45bc 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -95,7 +95,7 @@ export type Config = { } } pagination?: { - default?: number; + defaultLimit?: number; options?: number[]; } webpack?: (config: Configuration) => Configuration;