diff --git a/package.json b/package.json index 4bb629ae18..7f04159c46 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@typescript-eslint/parser": "4.0.1", "@udecode/slate-plugins": "^0.64.3", "ajv": "^6.12.6", + "asap": "^2.0.6", "autoprefixer": "^9.7.4", "babel-jest": "^26.3.0", "babel-loader": "^8.1.0", @@ -133,12 +134,14 @@ "webpack-bundle-analyzer": "^3.8.0", "webpack-cli": "^4.2.0", "webpack-dev-middleware": "^4.0.2", - "webpack-hot-middleware": "^2.25.0" + "webpack-hot-middleware": "^2.25.0", + "wrappy": "^1.0.2" }, "devDependencies": { "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.0.4", "@trbl/eslint-config": "^1.2.4", + "@types/asap": "^2.0.0", "@types/autoprefixer": "^9.7.2", "@types/babel__core": "^7.1.12", "@types/babel__plugin-transform-runtime": "^7.9.1", diff --git a/src/admin/components/elements/ReactSelect/index.tsx b/src/admin/components/elements/ReactSelect/index.tsx index b7e992d05f..51e6f282f9 100644 --- a/src/admin/components/elements/ReactSelect/index.tsx +++ b/src/admin/components/elements/ReactSelect/index.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import Select from 'react-select'; import { Props } from './types'; import Chevron from '../../icons/Chevron'; diff --git a/src/admin/components/forms/Form/index.tsx b/src/admin/components/forms/Form/index.tsx index be1e74a48f..9d86291c4d 100644 --- a/src/admin/components/forms/Form/index.tsx +++ b/src/admin/components/forms/Form/index.tsx @@ -5,10 +5,10 @@ import { objectToFormData } from 'object-to-formdata'; import { useHistory } from 'react-router-dom'; import PropTypes from 'prop-types'; import { toast } from 'react-toastify'; +import { useAuth } from '@payloadcms/config-provider'; import { useLocale } from '../../utilities/Locale'; import { requests } from '../../../api'; import useThrottledEffect from '../../../hooks/useThrottledEffect'; -import { useAuth } from '@payloadcms/config-provider'; import fieldReducer from './fieldReducer'; import initContextState from './initContextState'; import reduceFieldsToValues from './reduceFieldsToValues'; @@ -24,7 +24,7 @@ import './index.scss'; const baseClass = 'form'; -const Form = (props) => { +const Form: React.FC = (props) => { const { disabled, onSubmit, diff --git a/src/admin/components/forms/RenderFields/index.tsx b/src/admin/components/forms/RenderFields/index.tsx index b817b4e04e..9b3f0fc67f 100644 --- a/src/admin/components/forms/RenderFields/index.tsx +++ b/src/admin/components/forms/RenderFields/index.tsx @@ -1,7 +1,7 @@ import React, { createContext, useEffect, useContext, useState } from 'react'; -import PropTypes from 'prop-types'; import RenderCustomComponent from '../../utilities/RenderCustomComponent'; import useIntersect from '../../../hooks/useIntersect'; +import { Props, Context } from './types'; const baseClass = 'render-fields'; @@ -9,11 +9,11 @@ const intersectionObserverOptions = { rootMargin: '1000px', }; -const RenderedFieldContext = createContext({}); +const RenderedFieldContext = createContext({} as Context); -export const useRenderedFields = () => useContext(RenderedFieldContext); +export const useRenderedFields = (): Context => useContext(RenderedFieldContext); -const RenderFields: React.FC = (props) => { +const RenderFields: React.FC = (props) => { const { fieldSchema, fieldTypes, @@ -135,26 +135,4 @@ const RenderFields: React.FC = (props) => { return null; }; -RenderFields.defaultProps = { - filter: null, - readOnly: false, - permissions: {}, - operation: undefined, - className: undefined, -}; - -RenderFields.propTypes = { - fieldSchema: PropTypes.arrayOf( - PropTypes.shape({}), - ).isRequired, - fieldTypes: PropTypes.shape({ - hidden: PropTypes.function, - }).isRequired, - filter: PropTypes.func, - permissions: PropTypes.shape({}), - readOnly: PropTypes.bool, - operation: PropTypes.string, - className: PropTypes.string, -}; - export default RenderFields; diff --git a/src/admin/components/forms/RenderFields/types.ts b/src/admin/components/forms/RenderFields/types.ts new file mode 100644 index 0000000000..524b544336 --- /dev/null +++ b/src/admin/components/forms/RenderFields/types.ts @@ -0,0 +1,17 @@ +import { CollectionPermission, GlobalPermission } from '../../../../auth/types'; +import { Field } from '../../../../fields/config/types'; + +export type Operation = 'create' | 'update' + +export type Context = { + operation: Operation +} + +export type Props = { + className?: string + operation: Operation + readOnly: boolean + permissions: CollectionPermission | GlobalPermission + filter: (field: Field) => boolean + fieldSchema: Field[] +} diff --git a/src/admin/components/forms/field-types/Relationship/index.tsx b/src/admin/components/forms/field-types/Relationship/index.tsx index fa2b8da413..a64223764e 100644 --- a/src/admin/components/forms/field-types/Relationship/index.tsx +++ b/src/admin/components/forms/field-types/Relationship/index.tsx @@ -10,6 +10,8 @@ import useFieldType from '../../useFieldType'; import Label from '../../Label'; import Error from '../../Error'; import { relationship } from '../../../../../fields/validations'; +import { PaginatedDocs } from '../../../../../collections/config/types'; +import { RelationshipProps, OptionsPage } from './types'; import './index.scss'; @@ -17,7 +19,7 @@ const maxResultsPerRequest = 10; const baseClass = 'relationship'; -class Relationship extends Component { +class Relationship extends Component { constructor(props) { super(props); @@ -77,7 +79,7 @@ class Relationship extends Component { const fieldToSearch = collection?.admin?.useAsTitle || 'id'; const searchParam = search ? `&where[${fieldToSearch}][like]=${search}` : ''; const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPage}${searchParam}`); - const data = await response.json(); + const data: PaginatedDocs = await response.json(); if (response.ok) { if (data.hasNextPage) { @@ -87,7 +89,7 @@ class Relationship extends Component { }); } - return callback({ relation, data }); + return callback(true, { relation, data }); } let error = 'There was a problem loading options for this field.'; @@ -99,7 +101,7 @@ class Relationship extends Component { return this.setState({ errorLoading: error, }); - }, (lastPage, nextPage) => { + }, (lastPage: OptionsPage, nextPage: OptionsPage) => { if (nextPage) { const { data, relation } = nextPage; this.addOptions(data, relation); diff --git a/src/admin/components/forms/field-types/Relationship/types.ts b/src/admin/components/forms/field-types/Relationship/types.ts new file mode 100644 index 0000000000..3302039a3b --- /dev/null +++ b/src/admin/components/forms/field-types/Relationship/types.ts @@ -0,0 +1,25 @@ +import React from 'react'; +import { PaginatedDocs } from '../../../../../collections/config/types'; +import { Config } from '../../../../../config/types'; + +export type OptionsPage = { + relation: string + data: PaginatedDocs +} + +export type RelationshipProps = { + required: boolean + errorMessage: string + hasMany: boolean + showError: boolean + value: unknown + path: string + formProcessing: boolean + admin: { + readOnly: boolean + style: React.CSSProperties + width: string + } + relationTo: string | string[] + config: Config +} diff --git a/src/collections/config/types.ts b/src/collections/config/types.ts index 4be03c4640..6f2786b230 100644 --- a/src/collections/config/types.ts +++ b/src/collections/config/types.ts @@ -144,3 +144,16 @@ export type AfterLoginHook = (args?: { export type AfterForgotPasswordHook = (args?: { args?: any; }) => any; + +export type PaginatedDocs = { + docs: unknown[] + totalDocs: number + limit: number + totalPages: number + page: number + pagingCounter: number + hasPrevPage: boolean + hasNextPage: boolean + prevPage: number | null + nextPage: number | null +} diff --git a/src/fields/config/types.ts b/src/fields/config/types.ts index b3e605e6ef..1f25e7d8f9 100644 --- a/src/fields/config/types.ts +++ b/src/fields/config/types.ts @@ -39,7 +39,7 @@ type FieldBase = { readOnly?: boolean; disabled?: boolean; condition?: (...args: any[]) => any | void; - components?: { [key: string]: JSX.Element | (() => JSX.Element) }; + components?: { [key: string]: React.ComponentType }; }; access?: { create?: Access; diff --git a/src/utilities/asyncSome.ts b/src/utilities/asyncSome.ts index 124ccdf70a..9deb2e73e3 100644 --- a/src/utilities/asyncSome.ts +++ b/src/utilities/asyncSome.ts @@ -1,10 +1,56 @@ -const asyncSome = async (arr: unknown[], predicate: (event: unknown) => Promise): Promise => { - // eslint-disable-next-line no-restricted-syntax - for (const e of arr) { - // eslint-disable-next-line no-await-in-loop - if (await predicate(e)) return true; - } - return false; -}; +/* eslint-disable no-use-before-define */ +/* eslint-disable consistent-return */ +import wrappy from 'wrappy'; +import asap from 'asap'; -export default asyncSome; +type Reduce = (er: boolean, result: unknown) => void; +type Callback = (last: unknown, next: unknown) => void; + +const ensureFutureTick = wrappy((cb: Callback) => { + let sync = true; + asap(() => { + sync = false; + }); + + return function safe(...args: unknown[]) { + if (sync) { + asap(() => { + cb.apply(this, args); + }); + } else { cb.apply(this, args); } + }; +}); + +function some(list: unknown[], predicate: (item: unknown, reduce: Reduce) => void, cb: Callback): void { + const array = slice(list); + let index = 0; + const { length } = array; + const hecomes = ensureFutureTick(cb); + + + const reduce: Reduce = (er, result) => { + if (er) return hecomes(er, false); + if (result) return hecomes(null, result); + + index += 1; + map(); + }; + + map(); + + function map() { + if (index >= length) return hecomes(null, false); + + predicate(array[index], reduce); + } +} + +function slice(args: unknown[]) { + const l = args.length; + const a = []; + let i: number; + for (i = 0; i < l; i += 1) a[i] = args[i]; + return a; +} + +export default some; diff --git a/yarn.lock b/yarn.lock index 7ed1ced001..b7e2e09e62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1446,6 +1446,11 @@ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.0.tgz#14264692a9d6e2fa4db3df5e56e94b5e25647ac0" integrity sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A== +"@types/asap@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/asap/-/asap-2.0.0.tgz#d529e9608c83499a62ae08c871c5e62271aa2963" + integrity sha512-upIS0Gt9Mc8eEpCbYMZ1K8rhNosfKUtimNcINce+zLwJF5UpM3Vv7yz3S5l/1IX+DxTa8lTkUjqynvjRXyJzsg== + "@types/autoprefixer@^9.7.2": version "9.7.2" resolved "https://registry.yarnpkg.com/@types/autoprefixer/-/autoprefixer-9.7.2.tgz#64b3251c9675feef5a631b7dd34cfea50a8fdbcc" @@ -2840,6 +2845,11 @@ array.prototype.flatmap@^1.2.3: es-abstract "^1.18.0-next.1" function-bind "^1.1.1" +asap@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" @@ -12421,7 +12431,7 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrappy@1: +wrappy@1, wrappy@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=