chore: merge master

This commit is contained in:
James
2021-11-26 18:00:59 -05:00
72 changed files with 1034 additions and 230 deletions

View File

@@ -63,7 +63,7 @@ const RelationshipField: React.FC<Props> = (props) => {
const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPageToUse}&depth=0${searchParam}`);
if (response.ok) {
const data: PaginatedDocs = await response.json();
const data: PaginatedDocs<any> = await response.json();
if (data.docs.length > 0) {
resultsFetched += data.docs.length;
addOptions(data, relation);

View File

@@ -20,7 +20,7 @@ type CLEAR = {
type ADD = {
type: 'ADD'
data: PaginatedDocs
data: PaginatedDocs<any>
relation: string
hasMultipleRelations: boolean
collection: SanitizedCollectionConfig

View File

@@ -1,5 +1,5 @@
import React from 'react';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import Pill from '../../../elements/Pill';
import { Props } from './types';
@@ -10,7 +10,7 @@ const baseClass = 'section-title';
const SectionTitle: React.FC<Props> = (props) => {
const { label, path, readOnly } = props;
const { value, setValue } = useFieldType({ path });
const { value, setValue } = useField({ path });
const classes = [
baseClass,

View File

@@ -7,7 +7,7 @@ import DraggableSection from '../../DraggableSection';
import reducer from '../rowReducer';
import { useForm } from '../../Form/context';
import buildStateFromSchema from '../../Form/buildStateFromSchema';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import Error from '../../Error';
import { array } from '../../../../../fields/validations';
import Banner from '../../../elements/Banner';
@@ -66,7 +66,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
errorMessage,
value,
setValue,
} = useFieldType({
} = useField({
path,
validate: memoizedValidate,
disableFormData,

View File

@@ -12,7 +12,7 @@ import { useForm } from '../../Form/context';
import buildStateFromSchema from '../../Form/buildStateFromSchema';
import DraggableSection from '../../DraggableSection';
import Error from '../../Error';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import Popup from '../../../elements/Popup';
import BlockSelector from './BlockSelector';
import { blocks as blocksValidator } from '../../../../../fields/validations';
@@ -76,7 +76,7 @@ const Blocks: React.FC<Props> = (props) => {
errorMessage,
value,
setValue,
} = useFieldType({
} = useField({
path,
validate: memoizedValidate,
disableFormData,
@@ -110,7 +110,7 @@ const Blocks: React.FC<Props> = (props) => {
if (preferencesKey) {
const preferences: DocumentPreferences = await getPreference(preferencesKey);
const preferencesToSet = preferences || { fields: { } };
const preferencesToSet = preferences || { fields: {} };
let newCollapsedState = preferencesToSet?.fields?.[path]?.collapsed
.filter((filterID) => (rows.find((row) => row.id === filterID)))
|| [];

View File

@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import withCondition from '../../withCondition';
import Error from '../../Error';
import { checkbox } from '../../../../../fields/validations';
@@ -41,7 +41,7 @@ const Checkbox: React.FC<Props> = (props) => {
showError,
errorMessage,
setValue,
} = useFieldType({
} = useField({
path,
validate: memoizedValidate,
disableFormData,

View File

@@ -3,7 +3,7 @@ import Editor from 'react-simple-code-editor';
import { highlight, languages } from 'prismjs/components/prism-core';
import 'prismjs/components/prism-clike';
import 'prismjs/components/prism-javascript';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import withCondition from '../../withCondition';
import Label from '../../Label';
import Error from '../../Error';
@@ -52,7 +52,7 @@ const Code: React.FC<Props> = (props) => {
showError,
setValue,
errorMessage,
} = useFieldType({
} = useField({
path,
validate: memoizedValidate,
enableDebouncedValue: true,

View File

@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import Label from '../../Label';
import Error from '../../Error';
import { useWatchForm } from '../../Form/context';
@@ -23,7 +23,7 @@ const ConfirmPassword: React.FC = () => {
showError,
setValue,
errorMessage,
} = useFieldType({
} = useField({
path: 'confirm-password',
disableFormData: true,
validate,

View File

@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
import DatePicker from '../../../elements/DatePicker';
import withCondition from '../../withCondition';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import Label from '../../Label';
import Error from '../../Error';
import FieldDescription from '../../FieldDescription';
@@ -43,7 +43,7 @@ const DateTime: React.FC<Props> = (props) => {
showError,
errorMessage,
setValue,
} = useFieldType({
} = useField({
path,
validate: memoizedValidate,
condition,

View File

@@ -1,6 +1,6 @@
import React, { useCallback } from 'react';
import withCondition from '../../withCondition';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import Label from '../../Label';
import Error from '../../Error';
import FieldDescription from '../../FieldDescription';
@@ -34,7 +34,7 @@ const Email: React.FC<Props> = (props) => {
return validationResult;
}, [validate, required]);
const fieldType = useFieldType({
const fieldType = useField({
path,
validate: memoizedValidate,
enableDebouncedValue: true,

View File

@@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import withCondition from '../../withCondition';
import { Props } from './types';
@@ -13,7 +13,7 @@ const HiddenInput: React.FC<Props> = (props) => {
const path = pathFromProps || name;
const { value, setValue } = useFieldType({
const { value, setValue } = useField({
path,
});

View File

@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import Label from '../../Label';
import Error from '../../Error';
import FieldDescription from '../../FieldDescription';
@@ -41,7 +41,7 @@ const NumberField: React.FC<Props> = (props) => {
showError,
setValue,
errorMessage,
} = useFieldType({
} = useField({
path,
validate: memoizedValidate,
enableDebouncedValue: true,

View File

@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import Label from '../../Label';
import Error from '../../Error';
import withCondition from '../../withCondition';
@@ -33,7 +33,7 @@ const Password: React.FC<Props> = (props) => {
formProcessing,
setValue,
errorMessage,
} = useFieldType({
} = useField({
path,
validate: memoizedValidate,
enableDebouncedValue: true,

View File

@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import Label from '../../Label';
import Error from '../../Error';
import FieldDescription from '../../FieldDescription';
@@ -41,7 +41,7 @@ const PointField: React.FC<Props> = (props) => {
showError,
setValue,
errorMessage,
} = useFieldType<[number, number]>({
} = useField<[number, number]>({
path,
validate: memoizedValidate,
enableDebouncedValue: true,

View File

@@ -1,6 +1,6 @@
import React, { useCallback } from 'react';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import withCondition from '../../withCondition';
import Error from '../../Error';
import Label from '../../Label';
@@ -44,7 +44,7 @@ const RadioGroup: React.FC<Props> = (props) => {
showError,
errorMessage,
setValue,
} = useFieldType({
} = useField({
path,
validate: memoizedValidate,
condition,

View File

@@ -5,7 +5,7 @@ import { useConfig } from '@payloadcms/config-provider';
import withCondition from '../../withCondition';
import ReactSelect from '../../../elements/ReactSelect';
import { Value } from '../../../elements/ReactSelect/types';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import Label from '../../Label';
import Error from '../../Error';
import FieldDescription from '../../FieldDescription';
@@ -70,7 +70,7 @@ const Relationship: React.FC<Props> = (props) => {
showError,
errorMessage,
setValue,
} = useFieldType({
} = useField({
path: path || name,
validate: memoizedValidate,
condition,
@@ -106,7 +106,7 @@ const Relationship: React.FC<Props> = (props) => {
const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPageToUse}&depth=0${searchParam}`);
if (response.ok) {
const data: PaginatedDocs = await response.json();
const data: PaginatedDocs<any> = await response.json();
if (data.docs.length > 0) {
resultsFetched += data.docs.length;
addOptions(data, relation);

View File

@@ -19,7 +19,7 @@ type CLEAR = {
type ADD = {
type: 'ADD'
data: PaginatedDocs
data: PaginatedDocs<any>
relation: string
hasMultipleRelations: boolean
collection: SanitizedCollectionConfig

View File

@@ -4,7 +4,7 @@ import { createEditor, Transforms, Node, Element as SlateElement, Text, BaseEdit
import { ReactEditor, Editable, withReact, Slate } from 'slate-react';
import { HistoryEditor, withHistory } from 'slate-history';
import { richText } from '../../../../../fields/validations';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import withCondition from '../../withCondition';
import Label from '../../Label';
import Error from '../../Error';
@@ -29,7 +29,7 @@ const defaultElements: RichTextElement[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6',
const defaultLeaves: RichTextLeaf[] = ['bold', 'italic', 'underline', 'strikethrough', 'code'];
const baseClass = 'rich-text';
type CustomText = { text: string; [x: string]: unknown }
type CustomText = { text: string;[x: string]: unknown }
type CustomElement = { type: string; children: CustomText[] }
@@ -115,7 +115,7 @@ const RichText: React.FC<Props> = (props) => {
return validationResult;
}, [validate, required]);
const fieldType = useFieldType({
const fieldType = useField({
path,
validate: memoizedValidate,
stringify: true,
@@ -194,7 +194,7 @@ const RichText: React.FC<Props> = (props) => {
}}
>
<div className={`${baseClass}__wrap`}>
{ !hideGutter && (<FieldTypeGutter />) }
{!hideGutter && (<FieldTypeGutter />)}
<Error
showError={showError}
message={errorMessage}

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback, useState, useEffect } from 'react';
import withCondition from '../../withCondition';
import ReactSelect from '../../../elements/ReactSelect';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import Label from '../../Label';
import Error from '../../Error';
import FieldDescription from '../../FieldDescription';
@@ -40,6 +40,8 @@ const Select: React.FC<Props> = (props) => {
description,
condition,
} = {},
value: valueFromProps,
onChange: onChangeFromProps
} = props;
const path = pathFromProps || name;
@@ -52,16 +54,42 @@ const Select: React.FC<Props> = (props) => {
}, [validate, required, options]);
const {
value,
value: valueFromContext,
showError,
setValue,
errorMessage,
} = useFieldType({
} = useField({
path,
validate: memoizedValidate,
condition,
});
const onChange = useCallback((selectedOption) => {
if (!readOnly) {
let newValue;
if (hasMany) {
if (Array.isArray(selectedOption)) {
newValue = selectedOption.map((option) => option.value);
} else {
newValue = [];
}
} else {
newValue = selectedOption.value;
}
if (typeof onChangeFromProps === 'function') {
onChangeFromProps(newValue);
} else {
setValue(newValue);
}
}
}, [
readOnly,
hasMany,
onChangeFromProps,
setValue
])
const classes = [
'field-type',
baseClass,
@@ -71,6 +99,8 @@ const Select: React.FC<Props> = (props) => {
let valueToRender;
const value = valueFromProps || valueFromContext || '';
if (hasMany && Array.isArray(value)) {
valueToRender = value.map((val) => options.find((option) => option.value === val));
} else {
@@ -95,17 +125,7 @@ const Select: React.FC<Props> = (props) => {
required={required}
/>
<ReactSelect
onChange={!readOnly ? (selectedOption) => {
if (hasMany) {
if (Array.isArray(selectedOption)) {
setValue(selectedOption.map((option) => option.value));
} else {
setValue([]);
}
} else {
setValue(selectedOption.value);
}
} : undefined}
onChange={onChange}
value={valueToRender}
showError={showError}
isDisabled={readOnly}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import useFieldType from '../../useFieldType';
import React, { useCallback, useEffect } from 'react';
import useField from '../../useField';
import withCondition from '../../withCondition';
import Label from '../../Label';
import Error from '../../Error';
@@ -24,11 +24,13 @@ const Text: React.FC<Props> = (props) => {
description,
condition,
} = {},
value: valueFromProps,
onChange: onChangeFromProps,
} = props;
const path = pathFromProps || name;
const fieldType = useFieldType<string>({
const fieldType = useField<string>({
path,
validate,
enableDebouncedValue: true,
@@ -36,12 +38,24 @@ const Text: React.FC<Props> = (props) => {
});
const {
value,
value: valueFromContext,
showError,
setValue,
errorMessage,
} = fieldType;
const onChange = useCallback((e) => {
const { value: incomingValue } = e.target;
if (typeof onChangeFromProps === 'function') {
onChangeFromProps(incomingValue);
} else {
setValue(e);
}
}, [
onChangeFromProps,
setValue,
]);
const classes = [
'field-type',
'text',
@@ -49,6 +63,8 @@ const Text: React.FC<Props> = (props) => {
readOnly && 'read-only',
].filter(Boolean).join(' ');
const value = valueFromProps || valueFromContext || '';
return (
<div
className={classes}
@@ -67,8 +83,8 @@ const Text: React.FC<Props> = (props) => {
required={required}
/>
<input
value={value || ''}
onChange={setValue}
value={value}
onChange={onChange}
disabled={readOnly}
placeholder={placeholder}
type="text"

View File

@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import withCondition from '../../withCondition';
import Label from '../../Label';
import Error from '../../Error';
@@ -41,7 +41,7 @@ const Textarea: React.FC<Props> = (props) => {
showError,
setValue,
errorMessage,
} = useFieldType({
} = useField({
path,
validate: memoizedValidate,
enableDebouncedValue: true,

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useModal } from '@faceless-ui/modal';
import { useConfig } from '@payloadcms/config-provider';
import useFieldType from '../../useFieldType';
import useField from '../../useField';
import withCondition from '../../withCondition';
import Button from '../../../elements/Button';
import Label from '../../Label';
@@ -38,6 +38,8 @@ const Upload: React.FC<Props> = (props) => {
validate = upload,
relationTo,
fieldTypes,
value: valueFromProps,
onChange: onChangeFromProps,
} = props;
const collection = collections.find((coll) => coll.slug === relationTo);
@@ -51,19 +53,21 @@ const Upload: React.FC<Props> = (props) => {
return validationResult;
}, [validate, required]);
const fieldType = useFieldType({
const fieldType = useField({
path,
validate: memoizedValidate,
condition,
});
const {
value,
value: valueFromContext,
showError,
setValue,
errorMessage,
} = fieldType;
const value = valueFromProps || valueFromContext || '';
const classes = [
'field-type',
baseClass,
@@ -81,14 +85,28 @@ const Upload: React.FC<Props> = (props) => {
setInternalValue(json);
} else {
setInternalValue(undefined);
setValue(null);
setMissingFile(true);
}
};
fetchFile();
}
}, [value, setInternalValue, relationTo, api, serverURL, setValue]);
}, [
value,
relationTo,
api,
serverURL,
setValue
]);
useEffect(() => {
const { id: incomingID } = internalValue || {};
if (typeof onChangeFromProps === 'function') {
onChangeFromProps(incomingID)
} else {
setValue(incomingID);
}
}, [internalValue]);
return (
<div
@@ -147,7 +165,6 @@ const Upload: React.FC<Props> = (props) => {
fieldTypes,
setValue: (val) => {
setMissingFile(false);
setValue(val.id);
setInternalValue(val);
},
}}
@@ -157,7 +174,6 @@ const Upload: React.FC<Props> = (props) => {
slug: selectExistingModalSlug,
setValue: (val) => {
setMissingFile(false);
setValue(val.id);
setInternalValue(val);
},
addModalSlug,

View File

@@ -5,7 +5,7 @@ import { useFormProcessing, useFormSubmitted, useFormModified, useForm } from '.
import useDebounce from '../../../hooks/useDebounce';
import { Options, FieldType } from './types';
const useFieldType = <T extends unknown>(options: Options): FieldType<T> => {
const useField = <T extends unknown>(options: Options): FieldType<T> => {
const {
path,
validate,
@@ -22,8 +22,10 @@ const useFieldType = <T extends unknown>(options: Options): FieldType<T> => {
const modified = useFormModified();
const {
dispatchFields, getField, setModified,
} = formContext;
dispatchFields,
getField,
setModified,
} = formContext || {};
const [internalValue, setInternalValue] = useState(undefined);
@@ -64,21 +66,38 @@ const useFieldType = <T extends unknown>(options: Options): FieldType<T> => {
fieldToDispatch.valid = validationResult;
}
dispatchFields(fieldToDispatch);
}, [path, dispatchFields, validate, disableFormData, ignoreWhileFlattening, initialValue, stringify, condition]);
if (typeof dispatchFields === 'function') {
dispatchFields(fieldToDispatch);
}
}, [
path,
dispatchFields,
validate,
disableFormData,
ignoreWhileFlattening,
initialValue,
stringify,
condition
]);
// Method to return from `useFieldType`, used to
// Method to return from `useField`, used to
// update internal field values from field component(s)
// as fast as they arrive. NOTE - this method is NOT debounced
const setValue = useCallback((e, modifyForm = true) => {
const val = (e && e.target) ? e.target.value : e;
if ((!ignoreWhileFlattening && !modified) && modifyForm) {
setModified(true);
if (typeof setModified === 'function') {
setModified(true);
}
}
setInternalValue(val);
}, [setModified, modified, ignoreWhileFlattening]);
}, [
setModified,
modified,
ignoreWhileFlattening
]);
useEffect(() => {
setInternalValue(initialValue);
@@ -94,7 +113,11 @@ const useFieldType = <T extends unknown>(options: Options): FieldType<T> => {
if (field?.value !== valueToSend && valueToSend !== undefined) {
sendField(valueToSend);
}
}, [valueToSend, sendField, field]);
}, [
valueToSend,
sendField,
field
]);
return {
...options,
@@ -107,4 +130,4 @@ const useFieldType = <T extends unknown>(options: Options): FieldType<T> => {
};
};
export default useFieldType;
export default useField;

View File

@@ -1,6 +1,6 @@
import React, { useMemo, useState, useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
import useFieldType from '../../../../forms/useFieldType';
import useField from '../../../../forms/useField';
import Label from '../../../../forms/Label';
import CopyToClipboard from '../../../../elements/CopyToClipboard';
import { text } from '../../../../../../fields/validations';
@@ -31,7 +31,7 @@ const APIKey: React.FC = () => {
</div>
), [apiKeyValue]);
const fieldType = useFieldType({
const fieldType = useField({
path: 'apiKey',
validate,
});

View File

@@ -1,7 +1,7 @@
import React, {
useState, useRef, useEffect, useCallback,
} from 'react';
import useFieldType from '../../../../forms/useFieldType';
import useField from '../../../../forms/useField';
import Button from '../../../../elements/Button';
import FileDetails from '../../../../elements/FileDetails';
import Error from '../../../../forms/Error';
@@ -45,7 +45,7 @@ const Upload: React.FC<Props> = (props) => {
setValue,
showError,
errorMessage,
} = useFieldType<{name: string}>({
} = useField<{ name: string }>({
path: 'file',
validate,
});

View File

@@ -3,7 +3,7 @@ import { Column } from '../../../elements/Table/types';
export type Props = {
collection: SanitizedCollectionConfig
data: PaginatedDocs
data: PaginatedDocs<any>
newDocumentURL: string
setListControls: (controls: unknown) => void
setSort: (sort: string) => void

View File

@@ -363,6 +363,10 @@ export function generateTypes(): void {
compile(jsonSchema, 'Config', {
unreachableDefinitions: true,
bannerComment: '/* tslint:disable */\n/**\n* This file was automatically generated by Payload CMS.\n* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,\n* and re-run `payload generate:types` to regenerate this file.\n*/',
style: {
singleQuote: true,
},
}).then((compiled) => {
fs.writeFileSync(config.typescript.outputFile, compiled);
payload.logger.info(`Types written to ${config.typescript.outputFile}`);

View File

@@ -18,47 +18,71 @@ export interface AuthCollectionModel extends CollectionModel {
}
export type HookOperationType =
| 'create'
| 'read'
| 'update'
| 'delete'
| 'refresh'
| 'login'
| 'forgotPassword';
| 'create'
| 'read'
| 'update'
| 'delete'
| 'refresh'
| 'login'
| 'forgotPassword';
type CreateOrUpdateOperation = Extract<HookOperationType, 'create' | 'update'>;
export type BeforeOperationHook = (args: {
args?: any;
/**
* Hook operation being performed
*/
operation: HookOperationType;
}) => any;
export type BeforeValidateHook = (args: {
data?: any;
export type BeforeValidateHook<T extends TypeWithID = any> = (args: {
data?: Partial<T>;
req?: PayloadRequest;
operation: 'create' | 'update';
originalDoc?: any; // undefined on 'create' operation
/**
* Hook operation being performed
*/
operation: CreateOrUpdateOperation;
/**
* Original document before change
*
* `undefined` on 'create' operation
*/
originalDoc?: T;
}) => any;
export type BeforeChangeHook = (args: {
data: any;
export type BeforeChangeHook<T extends TypeWithID = any> = (args: {
data: Partial<T>;
req: PayloadRequest;
operation: 'create' | 'update'
originalDoc?: any; // undefined on 'create' operation
/**
* Hook operation being performed
*/
operation: CreateOrUpdateOperation;
/**
* Original document before change
*
* `undefined` on 'create' operation
*/
originalDoc?: T;
}) => any;
export type AfterChangeHook = (args: {
doc: any;
export type AfterChangeHook<T extends TypeWithID = any> = (args: {
doc: T;
req: PayloadRequest;
operation: 'create' | 'update';
/**
* Hook operation being performed
*/
operation: CreateOrUpdateOperation;
}) => any;
export type BeforeReadHook = (args: {
doc: any;
export type BeforeReadHook<T extends TypeWithID = any> = (args: {
doc: T;
req: PayloadRequest;
query: { [key: string]: any };
}) => any;
export type AfterReadHook = (args: {
doc: any;
export type AfterReadHook<T extends TypeWithID = any> = (args: {
doc: T;
req: PayloadRequest;
query?: { [key: string]: any };
}) => any;
@@ -68,10 +92,10 @@ export type BeforeDeleteHook = (args: {
id: string;
}) => any;
export type AfterDeleteHook = (args: {
export type AfterDeleteHook<T extends TypeWithID = any> = (args: {
doc: T;
req: PayloadRequest;
id: string;
doc: any;
}) => any;
export type AfterErrorHook = (err: Error, res: unknown) => { response: any, status: number } | void;
@@ -80,9 +104,9 @@ export type BeforeLoginHook = (args: {
req: PayloadRequest;
}) => any;
export type AfterLoginHook = (args: {
export type AfterLoginHook<T extends TypeWithID = any> = (args: {
req: PayloadRequest;
doc: any;
doc: T;
token: string;
}) => any;
@@ -90,31 +114,57 @@ export type AfterForgotPasswordHook = (args: {
args?: any;
}) => any;
export type CollectionAdminOptions = {
/**
* Field to use as title in Edit view and first column in List view
*/
useAsTitle?: string;
/**
* Default columns to show in list view
*/
defaultColumns?: string[];
/**
* Custom description for collection
*/
description?: string | (() => string) | React.FC;
disableDuplicate?: boolean;
/**
* Custom admin components
*/
components?: {
views?: {
Edit?: React.ComponentType
List?: React.ComponentType
}
};
pagination?: {
defaultLimit?: number
limits?: number[]
}
enableRichTextRelationship?: boolean
/**
* Function to generate custom preview URL
*/
preview?: GeneratePreviewURL
}
export type CollectionConfig = {
slug: string;
/**
* Label configuration
*/
labels?: {
singular?: string;
plural?: string;
};
fields: Field[];
admin?: {
useAsTitle?: string;
defaultColumns?: string[];
description?: string | (() => string);
disableDuplicate?: boolean;
components?: {
views?: {
Edit?: React.ComponentType
List?: React.ComponentType
}
};
pagination?: {
defaultLimit?: number
limits?: number[]
}
enableRichTextRelationship?: boolean
preview?: GeneratePreviewURL
};
/**
* Collection admin options
*/
admin?: CollectionAdminOptions;
/**
* Hooks to modify Payload functionality
*/
hooks?: {
beforeOperation?: BeforeOperationHook[];
beforeValidate?: BeforeValidateHook[];
@@ -129,6 +179,9 @@ export type CollectionConfig = {
afterLogin?: AfterLoginHook[];
afterForgotPassword?: AfterForgotPasswordHook[];
};
/**
* Access control
*/
access?: {
create?: Access;
read?: Access;
@@ -138,7 +191,15 @@ export type CollectionConfig = {
unlock?: Access;
readRevisions?: Access;
};
/**
* Collection login options
*
* Use `true` to enable with default options
*/
auth?: IncomingAuthType | boolean;
/**
* Upload options
*/
upload?: IncomingUploadType | boolean;
revisions?: IncomingRevisionsType | boolean;
timestamps?: boolean
@@ -160,8 +221,8 @@ export type AuthCollection = {
config: SanitizedCollectionConfig;
}
export type PaginatedDocs = {
docs: any[]
export type PaginatedDocs<T extends TypeWithID = any> = {
docs: T[]
totalDocs: number
limit: number
totalPages: number
@@ -172,3 +233,7 @@ export type PaginatedDocs = {
prevPage: number | null
nextPage: number | null
}
export type TypeWithID = {
id: string | number
}

View File

@@ -29,6 +29,7 @@ export type Arguments = {
overrideAccess?: boolean
showHiddenFields?: boolean
data: Record<string, unknown>
overwriteExistingFiles?: boolean
}
async function create(this: Payload, incomingArgs: Arguments): Promise<Document> {
@@ -59,6 +60,7 @@ async function create(this: Payload, incomingArgs: Arguments): Promise<Document>
depth,
overrideAccess,
showHiddenFields,
overwriteExistingFiles = false,
} = args;
let { data } = args;
@@ -108,7 +110,7 @@ async function create(this: Payload, incomingArgs: Arguments): Promise<Document>
mkdirp.sync(staticPath);
}
const fsSafeName = await getSafeFilename(staticPath, file.name);
const fsSafeName = !overwriteExistingFiles ? await getSafeFilename(Model, staticPath, file.name) : file.name;
try {
if (!disableLocalStorage) {
@@ -122,7 +124,15 @@ async function create(this: Payload, incomingArgs: Arguments): Promise<Document>
if (Array.isArray(imageSizes) && file.mimetype !== 'image/svg+xml') {
req.payloadUploadSizes = {};
fileData.sizes = await resizeAndSave(req, file.data, dimensions, staticPath, collectionConfig, fsSafeName, fileData.mimeType);
fileData.sizes = await resizeAndSave({
req,
file: file.data,
dimensions,
staticPath,
config: collectionConfig,
savedFilename: fsSafeName,
mimeType: fileData.mimeType,
});
}
}
} catch (err) {

View File

@@ -5,7 +5,7 @@ import { PayloadRequest } from '../../express/types';
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
import { NotFound, Forbidden, ErrorDeletingFile } from '../../errors';
import executeAccess from '../../auth/executeAccess';
import fileExists from '../../uploads/fileExists';
import fileOrDocExists from '../../uploads/fileOrDocExists';
import { BeforeOperationHook, Collection } from '../config/types';
import { Document, Where } from '../../types';
import { hasWhereAccessResult } from '../../auth/types';
@@ -46,7 +46,6 @@ async function deleteQuery(incomingArgs: Arguments): Promise<Document> {
req,
req: {
locale,
fallbackLocale,
},
overrideAccess,
showHiddenFields,
@@ -106,7 +105,8 @@ async function deleteQuery(incomingArgs: Arguments): Promise<Document> {
const staticPath = path.resolve(this.config.paths.configDir, staticDir);
const fileToDelete = `${staticPath}/${resultToDelete.filename}`;
if (await fileExists(fileToDelete)) {
if (await fileOrDocExists(Model, staticPath, resultToDelete.filename)) {
fs.unlink(fileToDelete, (err) => {
if (err) {
throw new ErrorDeletingFile();
@@ -116,7 +116,7 @@ async function deleteQuery(incomingArgs: Arguments): Promise<Document> {
if (resultToDelete.sizes) {
Object.values(resultToDelete.sizes).forEach(async (size: FileData) => {
if (await fileExists(`${staticPath}/${size.filename}`)) {
if (await fileOrDocExists(Model, staticPath, size.filename)) {
fs.unlink(`${staticPath}/${size.filename}`, (err) => {
if (err) {
throw new ErrorDeletingFile();

View File

@@ -2,7 +2,7 @@ import { Where } from '../../types';
import { PayloadRequest } from '../../express/types';
import executeAccess from '../../auth/executeAccess';
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
import { Collection, PaginatedDocs } from '../config/types';
import { Collection, TypeWithID, PaginatedDocs } from '../config/types';
import { hasWhereAccessResult } from '../../auth/types';
import flattenWhereConstraints from '../../utilities/flattenWhereConstraints';
@@ -18,7 +18,7 @@ export type Arguments = {
showHiddenFields?: boolean
}
async function find(incomingArgs: Arguments): Promise<PaginatedDocs> {
async function find<T extends TypeWithID = any>(incomingArgs: Arguments): Promise<PaginatedDocs<T>> {
let args = incomingArgs;
// /////////////////////////////////////
@@ -145,7 +145,7 @@ async function find(incomingArgs: Arguments): Promise<PaginatedDocs> {
return docRef;
})),
} as PaginatedDocs;
} as PaginatedDocs<T>;
// /////////////////////////////////////
// afterRead - Fields
@@ -195,7 +195,7 @@ async function find(incomingArgs: Arguments): Promise<PaginatedDocs> {
result = {
...result,
docs: result.docs.map((doc) => sanitizeInternalFields(doc)),
docs: result.docs.map((doc) => sanitizeInternalFields<T>(doc)),
};
return result;

View File

@@ -1,11 +1,11 @@
/* eslint-disable no-underscore-dangle */
import memoize from 'micro-memoize';
import { PayloadRequest } from '../../express/types';
import { Collection } from '../config/types';
import { Collection, TypeWithID } from '../config/types';
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields';
import { Forbidden, NotFound } from '../../errors';
import executeAccess from '../../auth/executeAccess';
import { Document, Where } from '../../types';
import { Where } from '../../types';
import { hasWhereAccessResult } from '../../auth/types';
export type Arguments = {
@@ -19,7 +19,7 @@ export type Arguments = {
depth?: number
}
async function findByID(incomingArgs: Arguments): Promise<Document> {
async function findByID<T extends TypeWithID = any>(incomingArgs: Arguments): Promise<T> {
let args = incomingArgs;
// /////////////////////////////////////

View File

@@ -12,6 +12,7 @@ export type Options = {
disableVerificationEmail?: boolean
showHiddenFields?: boolean
filePath?: string
overwriteExistingFiles?: boolean
}
export default async function create(options: Options): Promise<Document> {
const {
@@ -25,6 +26,7 @@ export default async function create(options: Options): Promise<Document> {
disableVerificationEmail,
showHiddenFields,
filePath,
overwriteExistingFiles = false,
} = options;
const collection = this.collections[collectionSlug];
@@ -36,6 +38,7 @@ export default async function create(options: Options): Promise<Document> {
overrideAccess,
disableVerificationEmail,
showHiddenFields,
overwriteExistingFiles,
req: {
user,
payloadAPI: 'local',

View File

@@ -1,3 +1,4 @@
import { TypeWithID } from '../../config/types';
import { Document } from '../../../types';
export type Options = {
@@ -11,7 +12,7 @@ export type Options = {
showHiddenFields?: boolean
}
export default async function localDelete(options: Options): Promise<Document> {
export default async function localDelete<T extends TypeWithID = any>(options: Options): Promise<T> {
const {
collection: collectionSlug,
depth,

View File

@@ -1,4 +1,4 @@
import { PaginatedDocs } from '../../config/types';
import { PaginatedDocs, TypeWithID } from '../../config/types';
import { Document, Where } from '../../../types';
export type Options = {
@@ -15,7 +15,7 @@ export type Options = {
where?: Where
}
export default async function find(options: Options): Promise<PaginatedDocs> {
export default async function find<T extends TypeWithID = any>(options: Options): Promise<PaginatedDocs<T>> {
const {
collection: collectionSlug,
depth,

View File

@@ -1,3 +1,4 @@
import { TypeWithID } from '../../config/types';
import { PayloadRequest } from '../../../express/types';
import { Document } from '../../../types';
@@ -14,7 +15,7 @@ export type Options = {
req?: PayloadRequest
}
export default async function findByID(options: Options): Promise<Document> {
export default async function findByID<T extends TypeWithID = any>(options: Options): Promise<T> {
const {
collection: collectionSlug,
depth,

View File

@@ -1,3 +1,4 @@
import { TypeWithID } from '../../config/types';
import { Document } from '../../../types';
import getFileByPath from '../../../uploads/getFileByPath';
@@ -15,7 +16,7 @@ export type Options = {
overwriteExistingFiles?: boolean
}
export default async function update(options: Options): Promise<Document> {
export default async function update<T extends TypeWithID = any>(options: Options): Promise<T> {
const {
collection: collectionSlug,
depth,

View File

@@ -138,7 +138,7 @@ async function update(incomingArgs: Arguments): Promise<Document> {
const file = ((req.files && req.files.file) ? req.files.file : req.file) as UploadedFile;
if (file) {
const fsSafeName = !overwriteExistingFiles ? await getSafeFilename(staticPath, file.name) : file.name;
const fsSafeName = !overwriteExistingFiles ? await getSafeFilename(Model, staticPath, file.name) : file.name;
try {
if (!disableLocalStorage) {
@@ -156,7 +156,15 @@ async function update(incomingArgs: Arguments): Promise<Document> {
if (Array.isArray(imageSizes) && file.mimetype !== 'image/svg+xml') {
req.payloadUploadSizes = {};
fileData.sizes = await resizeAndSave(req, file.data, dimensions, staticPath, collectionConfig, fsSafeName, fileData.mimeType);
fileData.sizes = await resizeAndSave({
req,
file: file.data,
dimensions,
staticPath,
config: collectionConfig,
savedFilename: fsSafeName,
mimeType: fileData.mimeType,
});
}
}
} catch (err) {

View File

@@ -1,9 +1,9 @@
import { Response, NextFunction } from 'express';
import httpStatus from 'http-status';
import { PayloadRequest } from '../../express/types';
import { PaginatedDocs } from '../config/types';
import { PaginatedDocs, TypeWithID } from '../config/types';
export default async function find(req: PayloadRequest, res: Response, next: NextFunction): Promise<Response<PaginatedDocs> | void> {
export default async function find<T extends TypeWithID = any>(req: PayloadRequest, res: Response, next: NextFunction): Promise<Response<PaginatedDocs<T>> | void> {
try {
let page;

View File

@@ -68,6 +68,10 @@ export type InitOptions = {
};
export type AccessResult = boolean | Where;
/**
* Access function
*/
export type Access = (args?: any) => AccessResult;
export type Config = {

View File

@@ -1,32 +1,32 @@
/* eslint-disable no-use-before-define */
import { CSSProperties } from 'react';
import { Editor } from 'slate';
import { TypeWithID } from '../../collections/config/types';
import { PayloadRequest } from '../../express/types';
import { Document } from '../../types';
import { ConditionalDateProps } from '../../admin/components/elements/DatePicker/types';
import { Description } from '../../admin/components/forms/FieldDescription/types';
export type FieldHook = (args: {
value?: unknown,
originalDoc?: Document,
data?: {
[key: string]: unknown
},
export type FieldHookArgs<T extends TypeWithID = any, P = any> = {
value?: P,
originalDoc?: T,
data?: Partial<T>,
operation?: 'create' | 'read' | 'update' | 'delete',
req: PayloadRequest
}) => Promise<unknown> | unknown;
}
export type FieldAccess = (args: {
export type FieldHook<T extends TypeWithID = any, P = any> = (args: FieldHookArgs<T, P>) => Promise<P> | P;
export type FieldAccess<T extends TypeWithID = any, P = any> = (args: {
req: PayloadRequest
id?: string
data: Record<string, unknown>
siblingData: Record<string, unknown>
data: Partial<T>
siblingData: Partial<P>
}) => Promise<boolean> | boolean;
export type Condition = (data: Record<string, unknown>, siblingData: Record<string, unknown>) => boolean;
export type Condition<T extends TypeWithID = any, P = any> = (data: Partial<T>, siblingData: Partial<P>) => boolean;
type Admin = {
position?: string;
position?: 'sidebar';
width?: string;
style?: CSSProperties;
readOnly?: boolean;
@@ -46,7 +46,7 @@ export type Labels = {
plural: string;
};
export type Validate = (value: unknown, options?: any) => string | true | Promise<string | true>;
export type Validate<T = any> = (value?: T, options?: any) => string | true | Promise<string | true>;
export type OptionObject = {
label: string
@@ -99,6 +99,8 @@ export type TextField = FieldBase & {
placeholder?: string
autoComplete?: string
}
value?: string
onChange?: (value: string) => void
}
export type EmailField = FieldBase & {
@@ -167,9 +169,11 @@ export type UIField = {
}
export type UploadField = FieldBase & {
type: 'upload';
relationTo: string;
maxDepth?: number;
type: 'upload'
relationTo: string
maxDepth?: number
value?: string
onChange?: (value: string) => void
}
type CodeAdmin = Admin & {
@@ -184,9 +188,11 @@ export type CodeField = Omit<FieldBase, 'admin'> & {
}
export type SelectField = FieldBase & {
type: 'select';
options: Option[];
hasMany?: boolean;
type: 'select'
options: Option[]
hasMany?: boolean
value?: string
onChange?: (value: string) => void
}
export type RelationshipField = FieldBase & {
@@ -378,4 +384,4 @@ export function fieldAffectsData(field: Field): field is FieldAffectingData {
return 'name' in field && !fieldIsPresentationalOnly(field);
}
export type HookName = 'beforeChange' | 'beforeValidate' | 'afterChange' | 'afterRead';
export type HookName = 'beforeRead' | 'beforeChange' | 'beforeValidate' | 'afterChange' | 'afterRead';

View File

@@ -1,14 +1,15 @@
import express, { Express, Router } from 'express';
import crypto from 'crypto';
import { Document } from 'mongoose';
import {
TypeWithID,
Collection, CollectionModel, PaginatedDocs,
} from './collections/config/types';
import {
SanitizedConfig,
EmailOptions,
InitOptions,
} from './config/types';
import {
Collection, CollectionModel, PaginatedDocs,
} from './collections/config/types';
import Logger from './utilities/logger';
import bindOperations from './init/bindOperations';
import bindRequestHandlers, { RequestHandlers } from './init/bindRequestHandlers';
@@ -210,7 +211,7 @@ export class Payload {
* @param options
* @returns created document
*/
create = async (options: CreateOptions): Promise<Document> => {
create = async <T>(options: CreateOptions): Promise<T> => {
let { create } = localOperations;
create = create.bind(this);
return create(options);
@@ -221,19 +222,19 @@ export class Payload {
* @param options
* @returns documents satisfying query
*/
find = async (options: FindOptions): Promise<PaginatedDocs> => {
find = async <T extends TypeWithID = any>(options: FindOptions): Promise<PaginatedDocs<T>> => {
let { find } = localOperations;
find = find.bind(this);
return find(options);
}
findGlobal = async (options): Promise<any> => {
findGlobal = async <T>(options): Promise<T> => {
let { findOne } = localGlobalOperations;
findOne = findOne.bind(this);
return findOne(options);
}
updateGlobal = async (options): Promise<any> => {
updateGlobal = async <T>(options): Promise<T> => {
let { update } = localGlobalOperations;
update = update.bind(this);
return update(options);
@@ -244,10 +245,10 @@ export class Payload {
* @param options
* @returns document with specified ID
*/
findByID = async (options: FindByIDOptions): Promise<Document> => {
findByID = async <T extends TypeWithID = any>(options: FindByIDOptions): Promise<T> => {
let { findByID } = localOperations;
findByID = findByID.bind(this);
return findByID(options);
return findByID<T>(options);
}
/**
@@ -255,16 +256,16 @@ export class Payload {
* @param options
* @returns Updated document
*/
update = async (options: UpdateOptions): Promise<Document> => {
update = async <T extends TypeWithID = any>(options: UpdateOptions): Promise<T> => {
let { update } = localOperations;
update = update.bind(this);
return update(options);
return update<T>(options);
}
delete = async (options: DeleteOptions): Promise<Document> => {
delete = async <T extends TypeWithID = any>(options: DeleteOptions): Promise<T> => {
let { localDelete: deleteOperation } = localOperations;
deleteOperation = deleteOperation.bind(this);
return deleteOperation(options);
return deleteOperation<T>(options);
}
login = async (options): Promise<any> => {

View File

@@ -49,7 +49,6 @@ describe('Revisions - REST', () => {
}).then((res) => res.json());
expect(typeof revision.doc.id).toBe('string');
expect(revision.doc._status).toBe('draft');
});
});
});

View File

@@ -3,11 +3,14 @@ import { promisify } from 'util';
const stat = promisify(fs.stat);
export default async (fileName: string): Promise<boolean> => {
const fileExists = async (filename: string): Promise<boolean> => {
try {
await stat(fileName);
await stat(filename);
return true;
} catch (err) {
return false;
}
};
export default fileExists;

View File

@@ -0,0 +1,20 @@
import fs from 'fs';
import { promisify } from 'util';
import { CollectionModel } from '../collections/config/types';
const stat = promisify(fs.stat);
const fileOrDocExists = async (Model: CollectionModel, path: string, filename: string): Promise<boolean> => {
try {
const doc = await Model.findOne({ filename });
if (doc) return true;
await stat(`${path}/${filename}`);
return true;
} catch (err) {
return false;
}
};
export default fileOrDocExists;

View File

@@ -67,6 +67,7 @@ const getBaseUploadFields = ({ config, collection }: Options): Field[] => {
label: 'File Name',
type: 'text',
index: true,
unique: true,
admin: {
readOnly: true,
disabled: true,

View File

@@ -1,5 +1,6 @@
import sanitize from 'sanitize-filename';
import fileExists from './fileExists';
import { CollectionModel } from '../collections/config/types';
import fileOrDocExists from './fileOrDocExists';
const incrementName = (name: string) => {
const extension = name.split('.').pop();
@@ -19,11 +20,11 @@ const incrementName = (name: string) => {
return `${incrementedName}.${extension}`;
};
async function getSafeFileName(staticPath: string, desiredFilename: string): Promise<string> {
async function getSafeFileName(Model: CollectionModel, staticPath: string, desiredFilename: string): Promise<string> {
let modifiedFilename = desiredFilename;
// eslint-disable-next-line no-await-in-loop
while (await fileExists(`${staticPath}/${modifiedFilename}`)) {
while (await fileOrDocExists(Model, staticPath, modifiedFilename)) {
modifiedFilename = incrementName(modifiedFilename);
}
return modifiedFilename;

View File

@@ -7,6 +7,16 @@ import { SanitizedCollectionConfig } from '../collections/config/types';
import { FileSizes, ImageSize } from './types';
import { PayloadRequest } from '../express/types';
type Args = {
req: PayloadRequest,
file: Buffer,
dimensions: ProbedImageSize,
staticPath: string,
config: SanitizedCollectionConfig,
savedFilename: string,
mimeType: string,
}
function getOutputImage(sourceImage: string, size: ImageSize) {
const extension = sourceImage.split('.').pop();
const name = sanitize(sourceImage.substr(0, sourceImage.lastIndexOf('.')) || sourceImage);
@@ -27,15 +37,15 @@ function getOutputImage(sourceImage: string, size: ImageSize) {
* @param mimeType
* @returns image sizes keyed to strings
*/
export default async function resizeAndSave(
req: PayloadRequest,
file: Buffer,
dimensions: ProbedImageSize,
staticPath: string,
config: SanitizedCollectionConfig,
savedFilename: string,
mimeType: string,
): Promise<FileSizes> {
export default async function resizeAndSave({
req,
file,
dimensions,
staticPath,
config,
savedFilename,
mimeType,
}: Args): Promise<FileSizes> {
const { imageSizes, disableLocalStorage } = config.upload;
const sizes = imageSizes

View File

@@ -1,6 +1,8 @@
import { TypeWithID } from '../collections/config/types';
const internalFields = ['__v', 'salt', 'hash'];
const sanitizeInternalFields = (incomingDoc) => Object.entries(incomingDoc).reduce((newDoc, [key, val]) => {
const sanitizeInternalFields = <T extends TypeWithID = any>(incomingDoc): T => Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => {
if (key === '_id') {
return {
...newDoc,
@@ -16,6 +18,6 @@ const sanitizeInternalFields = (incomingDoc) => Object.entries(incomingDoc).redu
...newDoc,
[key]: val,
};
}, {});
}, {} as T);
export default sanitizeInternalFields;