merge: relationship options filter

This commit is contained in:
James
2022-04-05 14:59:01 -04:00
19 changed files with 368 additions and 71 deletions

View File

@@ -368,6 +368,10 @@ const Form: React.FC<Props> = (props) => {
refreshCookie();
}, 15000, [fields]);
useThrottledEffect(() => {
validateForm();
}, 1000, [validateForm, fields]);
useEffect(() => {
contextRef.current = { ...contextRef.current }; // triggers rerender of all components that subscribe to form
setModified(false);

View File

@@ -1,7 +1,8 @@
import React, {
useCallback, useEffect, useState, useReducer,
} from 'react';
import { useConfig } from '@payloadcms/config-provider';
import equal from 'deep-equal';
import { useAuth, useConfig } from '@payloadcms/config-provider';
import qs from 'qs';
import withCondition from '../../withCondition';
import ReactSelect from '../../../elements/ReactSelect';
@@ -13,11 +14,13 @@ import FieldDescription from '../../FieldDescription';
import { relationship } from '../../../../../fields/validations';
import { Where } from '../../../../../types';
import { PaginatedDocs } from '../../../../../mongoose/types';
import { useFormProcessing } from '../../Form/context';
import { useFormProcessing, useWatchForm } from '../../Form/context';
import optionsReducer from './optionsReducer';
import { Props, Option, ValueWithRelation, GetResults } from './types';
import { createRelationMap } from './createRelationMap';
import { useDebouncedCallback } from '../../../../hooks/useDebouncedCallback';
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
import { getFilterOptionsQuery } from '../getFilterOptionsQuery';
import './index.scss';
@@ -34,6 +37,7 @@ const Relationship: React.FC<Props> = (props) => {
required,
label,
hasMany,
filterOptions,
admin: {
readOnly,
style,
@@ -52,13 +56,16 @@ const Relationship: React.FC<Props> = (props) => {
collections,
} = useConfig();
const { id } = useDocumentInfo();
const { user } = useAuth();
const { getData, getSiblingData } = useWatchForm();
const formProcessing = useFormProcessing();
const hasMultipleRelations = Array.isArray(relationTo);
const [options, dispatchOptions] = useReducer(optionsReducer, required || hasMany ? [] : [{ value: 'null', label: 'None' }]);
const [lastFullyLoadedRelation, setLastFullyLoadedRelation] = useState(-1);
const [lastLoadedPage, setLastLoadedPage] = useState(1);
const [errorLoading, setErrorLoading] = useState('');
const [optionFilters, setOptionFilters] = useState<{[relation: string]: Where}>();
const [hasLoadedValueOptions, setHasLoadedValueOptions] = useState(false);
const [search, setSearch] = useState('');
@@ -107,9 +114,13 @@ const Relationship: React.FC<Props> = (props) => {
where: Where
} = {
where: {
id: {
not_in: relationMap[relation],
},
and: [
{
id: {
not_in: relationMap[relation],
},
},
],
},
limit: maxResultsPerRequest,
page: lastLoadedPageToUse,
@@ -118,9 +129,15 @@ const Relationship: React.FC<Props> = (props) => {
};
if (searchArg) {
query.where[fieldToSearch] = {
like: searchArg,
};
query.where.and.push({
[fieldToSearch]: {
like: searchArg,
},
});
}
if (optionFilters[relation]) {
query.where.and.push(optionFilters[relation]);
}
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`);
@@ -148,7 +165,7 @@ const Relationship: React.FC<Props> = (props) => {
}
}, Promise.resolve());
}
}, [api, collections, serverURL, errorLoading, relationTo, hasMany, hasMultipleRelations]);
}, [relationTo, hasMany, errorLoading, collections, optionFilters, serverURL, api, hasMultipleRelations]);
const findOptionsByValue = useCallback((): Option | Option[] => {
if (value) {
@@ -262,11 +279,29 @@ const Relationship: React.FC<Props> = (props) => {
}, [hasMany, hasMultipleRelations, relationTo, initialValue, hasLoadedValueOptions, errorLoading, collections, api, serverURL]);
useEffect(() => {
setHasLoadedValueOptions(false);
getResults({
value: initialValue,
if (!filterOptions) {
return;
}
const newOptionFilters = getFilterOptionsQuery(filterOptions, {
id,
data: getData(),
relationTo,
siblingData: getSiblingData(path),
user,
});
}, [initialValue, getResults]);
if (!equal(newOptionFilters, optionFilters)) {
setOptionFilters(newOptionFilters);
}
}, [relationTo, filterOptions, optionFilters, id, getData, getSiblingData, path, user]);
useEffect(() => {
if (optionFilters) {
setHasLoadedValueOptions(false);
getResults({
value: initialValue,
});
}
}, [initialValue, getResults, optionFilters]);
const classes = [
'field-type',

View File

@@ -5,7 +5,7 @@ import Label from '../../Label';
import Error from '../../Error';
import FileDetails from '../../../elements/FileDetails';
import FieldDescription from '../../FieldDescription';
import { UploadField } from '../../../../../fields/config/types';
import { FilterOptions, UploadField } from '../../../../../fields/config/types';
import { Description } from '../../FieldDescription/types';
import { FieldTypes } from '..';
import AddModal from './Add';
@@ -33,6 +33,7 @@ export type UploadInputProps = Omit<UploadField, 'type'> & {
collection?: SanitizedCollectionConfig
serverURL?: string
api?: string
filterOptions: FilterOptions
}
const UploadInput: React.FC<UploadInputProps> = (props) => {
@@ -54,6 +55,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
api = '/api',
collection,
errorMessage,
filterOptions,
} = props;
const { toggle } = useModal();
@@ -160,6 +162,8 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
slug: selectExistingModalSlug,
setValue: onChange,
addModalSlug,
filterOptions,
path,
}}
/>
<FieldDescription

View File

@@ -1,6 +1,8 @@
import React, { Fragment, useState, useEffect } from 'react';
import equal from 'deep-equal';
import { Modal, useModal } from '@faceless-ui/modal';
import { useConfig } from '@payloadcms/config-provider';
import { useAuth, useConfig } from '@payloadcms/config-provider';
import { Where } from '../../../../../../types';
import MinimalTemplate from '../../../../templates/Minimal';
import Button from '../../../../elements/Button';
import usePayloadAPI from '../../../../../hooks/usePayloadAPI';
@@ -12,6 +14,9 @@ import PerPage from '../../../../elements/PerPage';
import formatFields from '../../../../views/collections/List/formatFields';
import './index.scss';
import { getFilterOptionsQuery } from '../../getFilterOptionsQuery';
import { useDocumentInfo } from '../../../../utilities/DocumentInfo';
import { useWatchForm } from '../../../Form/context';
const baseClass = 'select-existing-upload-modal';
@@ -29,15 +34,21 @@ const SelectExistingUploadModal: React.FC<Props> = (props) => {
} = {},
} = {},
slug: modalSlug,
path,
filterOptions,
} = props;
const { serverURL, routes: { api } } = useConfig();
const { id } = useDocumentInfo();
const { user } = useAuth();
const { getData, getSiblingData } = useWatchForm();
const { closeAll, currentModal } = useModal();
const [fields] = useState(() => formatFields(collection));
const [limit, setLimit] = useState(defaultLimit);
const [sort, setSort] = useState(null);
const [where, setWhere] = useState(null);
const [page, setPage] = useState(null);
const [optionFilters, setOptionFilters] = useState<Where>();
const classes = [
baseClass,
@@ -58,12 +69,29 @@ const SelectExistingUploadModal: React.FC<Props> = (props) => {
} = {};
if (page) params.page = page;
if (where) params.where = where;
if (where) params.where = { and: [where, optionFilters] };
if (sort) params.sort = sort;
if (limit) params.limit = limit;
setParams(params);
}, [setParams, page, sort, where, limit]);
}, [setParams, page, sort, where, limit, optionFilters]);
useEffect(() => {
if (!filterOptions || !isOpen) {
return;
}
const newOptionFilters = getFilterOptionsQuery(filterOptions, {
id,
relationTo: collectionSlug,
data: getData(),
siblingData: getSiblingData(path),
user,
})[collectionSlug];
if (!equal(newOptionFilters, optionFilters)) {
setOptionFilters(newOptionFilters);
}
}, [collectionSlug, filterOptions, optionFilters, id, getData, getSiblingData, path, user, isOpen]);
return (
<Modal

View File

@@ -1,7 +1,10 @@
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
import { FilterOptions } from '../../../../../../fields/config/types';
export type Props = {
setValue: (val: { id: string } | null) => void
collection: SanitizedCollectionConfig
slug: string
path
filterOptions: FilterOptions
}

View File

@@ -33,6 +33,7 @@ const Upload: React.FC<Props> = (props) => {
validate = upload,
relationTo,
fieldTypes,
filterOptions,
} = props;
const collection = collections.find((coll) => coll.slug === relationTo);
@@ -82,6 +83,7 @@ const Upload: React.FC<Props> = (props) => {
fieldTypes={fieldTypes}
name={name}
relationTo={relationTo}
filterOptions={filterOptions}
/>
);
}

View File

@@ -0,0 +1,14 @@
import { Where } from '../../../../types';
import { FilterOptions, FilterOptionsProps } from '../../../../fields/config/types';
export const getFilterOptionsQuery = (filterOptions: FilterOptions, options: FilterOptionsProps): {[collection: string]: Where } => {
const { relationTo } = options;
const relations = Array.isArray(relationTo) ? relationTo : [relationTo];
const query = {};
if (typeof filterOptions !== 'undefined') {
relations.forEach((relation) => {
query[relation] = typeof filterOptions === 'function' ? filterOptions(options) : filterOptions;
});
}
return query;
};

View File

@@ -1,9 +1,12 @@
import {
useCallback, useEffect, useState,
} from 'react';
import { useAuth } from '@payloadcms/config-provider';
import { useFormProcessing, useFormSubmitted, useFormModified, useForm } from '../Form/context';
import useDebounce from '../../../hooks/useDebounce';
import { Options, FieldType } from './types';
import { useDocumentInfo } from '../../utilities/DocumentInfo';
import { useOperation } from '../../utilities/OperationProvider';
const useField = <T extends unknown>(options: Options): FieldType<T> => {
const {
@@ -19,10 +22,15 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
const submitted = useFormSubmitted();
const processing = useFormProcessing();
const modified = useFormModified();
const { user } = useAuth();
const { id } = useDocumentInfo();
const operation = useOperation();
const {
dispatchFields,
getField,
getData,
getSiblingData,
setModified,
} = formContext || {};
@@ -40,43 +48,6 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
const valid = (field && typeof field.valid === 'boolean') ? field.valid : true;
const showError = valid === false && submitted;
// Method to send update field values from field component(s)
// Should only be used internally
const sendField = useCallback(async (valueToSend) => {
const fieldToDispatch = {
path,
disableFormData,
ignoreWhileFlattening,
initialValue,
validate,
condition,
value: valueToSend,
valid: false,
errorMessage: undefined,
};
const validationResult = typeof validate === 'function' ? await validate(valueToSend) : true;
if (typeof validationResult === 'string') {
fieldToDispatch.errorMessage = validationResult;
fieldToDispatch.valid = false;
} else {
fieldToDispatch.valid = validationResult;
}
if (typeof dispatchFields === 'function') {
dispatchFields(fieldToDispatch);
}
}, [
path,
dispatchFields,
validate,
disableFormData,
ignoreWhileFlattening,
initialValue,
condition,
]);
// 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
@@ -106,13 +77,38 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
const valueToSend = enableDebouncedValue ? debouncedValue : internalValue;
useEffect(() => {
if (field?.value !== valueToSend && valueToSend !== undefined) {
sendField(valueToSend);
}
const sendField = async () => {
if (field?.value !== valueToSend && valueToSend !== undefined) {
if (typeof dispatchFields === 'function') {
dispatchFields({
...field,
path,
disableFormData,
ignoreWhileFlattening,
initialValue,
validate,
condition,
value: valueToSend,
});
}
}
};
sendField();
}, [
condition,
disableFormData,
dispatchFields,
getData,
getSiblingData,
id,
ignoreWhileFlattening,
initialValue,
operation,
path,
user,
validate,
valueToSend,
sendField,
field,
]);

View File

@@ -85,7 +85,6 @@ const DefaultEditView: React.FC<Props> = (props) => {
onSuccess={onSave}
disabled={!hasSavePermission}
initialState={initialState}
validationOperation={isEditing ? 'update' : 'create'}
>
<div className={`${baseClass}__main`}>
<Meta