Merge branch 'feature/per-page' of github.com:payloadcms/payload
This commit is contained in:
48
src/admin/components/elements/PerPage/index.scss
Normal file
48
src/admin/components/elements/PerPage/index.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
src/admin/components/elements/PerPage/index.tsx
Normal file
61
src/admin/components/elements/PerPage/index.tsx
Normal file
@@ -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<Props> = ({ setLimit }) => {
|
||||
const { admin: { pagination: { options } } } = useConfig();
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Popup
|
||||
horizontalAlign="center"
|
||||
button={(
|
||||
<div>
|
||||
Per Page:
|
||||
<Chevron />
|
||||
</div>
|
||||
)}
|
||||
backgroundColor="#333333"
|
||||
render={({ close }) => (
|
||||
<div>
|
||||
<ul>
|
||||
{options.map((limitNumber, i) => (
|
||||
<li
|
||||
className={`${baseClass}-item`}
|
||||
key={i}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
close();
|
||||
setLimit(limitNumber);
|
||||
}}
|
||||
>
|
||||
{limitNumber}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
;
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PerPage;
|
||||
@@ -23,6 +23,8 @@ const Popup: React.FC<Props> = (props) => {
|
||||
horizontalAlign = 'left',
|
||||
initActive = false,
|
||||
onToggleOpen,
|
||||
backgroundColor,
|
||||
padding,
|
||||
} = props;
|
||||
|
||||
const buttonRef = useRef(null);
|
||||
@@ -98,7 +100,9 @@ const Popup: React.FC<Props> = (props) => {
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div
|
||||
className={classes}
|
||||
>
|
||||
<div
|
||||
ref={buttonRef}
|
||||
className={`${baseClass}__wrapper`}
|
||||
@@ -133,9 +137,22 @@ const Popup: React.FC<Props> = (props) => {
|
||||
<div
|
||||
className={`${baseClass}__content`}
|
||||
ref={contentRef}
|
||||
style={{
|
||||
backgroundColor,
|
||||
}}
|
||||
|
||||
>
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<div className={`${baseClass}__scroll`}>
|
||||
<div
|
||||
className={`${baseClass}__wrap`}
|
||||
// TODO: color ::after with bg color
|
||||
>
|
||||
|
||||
<div
|
||||
className={`${baseClass}__scroll`}
|
||||
style={{
|
||||
padding,
|
||||
}}
|
||||
>
|
||||
{render && render({ close: () => setActive(false) })}
|
||||
{children && children}
|
||||
</div>
|
||||
|
||||
@@ -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'],
|
||||
}
|
||||
|
||||
@@ -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> = (props) => {
|
||||
newDocumentURL,
|
||||
setListControls,
|
||||
setSort,
|
||||
limit,
|
||||
setLimit,
|
||||
columns,
|
||||
hasCreatePermission,
|
||||
} = props;
|
||||
@@ -114,6 +117,7 @@ const DefaultList: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
<div className={`${baseClass}__page-controls`}>
|
||||
|
||||
<Paginator
|
||||
limit={data.limit}
|
||||
totalPages={data.totalPages}
|
||||
@@ -124,6 +128,10 @@ const DefaultList: React.FC<Props> = (props) => {
|
||||
nextPage={data.nextPage}
|
||||
numberOfNeighbors={1}
|
||||
/>
|
||||
<PerPage
|
||||
limit={limit}
|
||||
setLimit={setLimit}
|
||||
/>
|
||||
{data?.totalDocs > 0 && (
|
||||
<div className={`${baseClass}__page-info`}>
|
||||
{(data.page * data.limit) - (data.limit - 1)}
|
||||
|
||||
@@ -31,7 +31,7 @@ const ListView: React.FC<ListIndexProps> = (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<ListIndexProps> = (props) => {
|
||||
const [listControls, setListControls] = useState<ListControls>({});
|
||||
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<ListIndexProps> = (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<number>(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<ListIndexProps> = (props) => {
|
||||
listControls,
|
||||
data,
|
||||
columns,
|
||||
setLimit,
|
||||
limit,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,8 @@ export type Props = {
|
||||
setSort: (sort: string) => void
|
||||
columns: Column[]
|
||||
hasCreatePermission: boolean
|
||||
setLimit: (limit: number) => void
|
||||
limit: number
|
||||
}
|
||||
|
||||
export type ListIndexProps = {
|
||||
|
||||
80
src/config/generateTypes.ts
Normal file
80
src/config/generateTypes.ts
Normal file
@@ -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<string, string>,
|
||||
): 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);
|
||||
@@ -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()
|
||||
|
||||
@@ -94,6 +94,10 @@ export type Config = {
|
||||
Dashboard?: React.ComponentType
|
||||
}
|
||||
}
|
||||
pagination?: {
|
||||
defaultLimit?: number;
|
||||
options?: number[];
|
||||
}
|
||||
webpack?: (config: Configuration) => Configuration;
|
||||
};
|
||||
collections?: CollectionConfig[];
|
||||
|
||||
Reference in New Issue
Block a user