merge: relationship options filter
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user