fix: relationship field useAsTitle #2333 (#2350)

This commit is contained in:
Jacob Fletcher
2023-03-20 22:21:49 -04:00
committed by GitHub
parent c8594a7e7a
commit 10dd819863
16 changed files with 172 additions and 50 deletions

View File

@@ -23,9 +23,6 @@ const DeleteDocument: React.FC<Props> = (props) => {
buttonId,
collection,
collection: {
admin: {
useAsTitle,
},
slug,
labels: {
singular,
@@ -39,7 +36,7 @@ const DeleteDocument: React.FC<Props> = (props) => {
const { toggleModal } = useModal();
const history = useHistory();
const { t, i18n } = useTranslation('general');
const title = useTitle(useAsTitle, collection.slug) || id;
const title = useTitle(collection);
const titleToRender = titleFromProps || title;
const modalSlug = `delete-${id}`;

View File

@@ -7,13 +7,12 @@ const baseClass = 'render-title';
const RenderTitle: React.FC<Props> = (props) => {
const {
useAsTitle,
collection,
title: titleFromProps,
data,
fallback = '[untitled]',
} = props;
const titleFromForm = useTitle(useAsTitle, collection);
const titleFromForm = useTitle(collection);
let title = titleFromForm;
if (!title) title = data?.id;

View File

@@ -1,3 +1,5 @@
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
export type Props = {
useAsTitle?: string
data?: {
@@ -5,5 +7,5 @@ export type Props = {
}
title?: string
fallback?: string
collection?: string
collection?: SanitizedCollectionConfig
}

View File

@@ -10,6 +10,7 @@ import Plus from '../../../../icons/Plus';
import { getTranslation } from '../../../../../../utilities/getTranslation';
import Tooltip from '../../../../elements/Tooltip';
import { useDocumentDrawer } from '../../../../elements/DocumentDrawer';
import { useConfig } from '../../../../utilities/Config';
import './index.scss';
@@ -25,6 +26,8 @@ export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, val
const [popupOpen, setPopupOpen] = useState(false);
const { t, i18n } = useTranslation('fields');
const [showTooltip, setShowTooltip] = useState(false);
const config = useConfig();
const [
DocumentDrawer,
DocumentDrawerToggler,
@@ -47,6 +50,7 @@ export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, val
],
sort: true,
i18n,
config,
});
if (hasMany) {
@@ -56,7 +60,7 @@ export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, val
}
setSelectedCollection(undefined);
}, [relationTo, collectionConfig, dispatchOptions, i18n, hasMany, setValue, value]);
}, [relationTo, collectionConfig, dispatchOptions, i18n, hasMany, setValue, value, config]);
const onPopopToggle = useCallback((state) => {
setPopupOpen(state);

View File

@@ -1,7 +1,7 @@
import { Value } from './types';
type RelationMap = {
[relation: string]: unknown[]
[relation: string]: (string | number)[]
}
type CreateRelationMap = (args: {

View File

@@ -56,13 +56,15 @@ const Relationship: React.FC<Props> = (props) => {
} = {},
} = props;
const config = useConfig();
const {
serverURL,
routes: {
api,
},
collections,
} = useConfig();
} = config;
const { t, i18n } = useTranslation('fields');
const { permissions } = useAuth();
@@ -172,9 +174,19 @@ const Relationship: React.FC<Props> = (props) => {
if (response.ok) {
const data: PaginatedDocs<unknown> = await response.json();
if (data.docs.length > 0) {
resultsFetched += data.docs.length;
dispatchOptions({ type: 'ADD', docs: data.docs, collection, sort, i18n });
dispatchOptions({
type: 'ADD',
docs: data.docs,
collection,
sort,
i18n,
config,
});
setLastLoadedPage(data.page);
if (!data.nextPage) {
@@ -190,7 +202,15 @@ const Relationship: React.FC<Props> = (props) => {
} else if (response.status === 403) {
setLastFullyLoadedRelation(relations.indexOf(relation));
lastLoadedPageToUse = 1;
dispatchOptions({ type: 'ADD', docs: [], collection, sort, ids: relationMap[relation], i18n });
dispatchOptions({
type: 'ADD',
docs: [],
collection,
sort,
ids: relationMap[relation],
i18n,
config,
});
} else {
setErrorLoading(t('error:unspecific'));
}
@@ -211,6 +231,7 @@ const Relationship: React.FC<Props> = (props) => {
t,
i18n,
locale,
config,
]);
const updateSearch = useDebouncedCallback((searchArg: string, valueArg: Value | Value[]) => {
@@ -261,13 +282,24 @@ const Relationship: React.FC<Props> = (props) => {
'Accept-Language': i18n.language,
},
});
const collection = collections.find((coll) => coll.slug === relation);
let docs = [];
if (response.ok) {
const data = await response.json();
dispatchOptions({ type: 'ADD', docs: data.docs, collection, sort: true, ids: idsToLoad, i18n });
} else if (response.status === 403) {
dispatchOptions({ type: 'ADD', docs: [], collection, sort: true, ids: idsToLoad, i18n });
docs = data.docs;
}
dispatchOptions({
type: 'ADD',
docs,
collection,
sort: true,
ids: idsToLoad,
i18n,
config,
});
}
}
}, Promise.resolve());
@@ -283,6 +315,7 @@ const Relationship: React.FC<Props> = (props) => {
i18n,
relationTo,
locale,
config,
]);
// Determine if we should switch to word boundary search
@@ -311,8 +344,8 @@ const Relationship: React.FC<Props> = (props) => {
}, [relationTo, filterOptionsResult, locale]);
const onSave = useCallback<DocumentDrawerProps['onSave']>((args) => {
dispatchOptions({ type: 'UPDATE', doc: args.doc, collection: args.collectionConfig, i18n });
}, [i18n]);
dispatchOptions({ type: 'UPDATE', doc: args.doc, collection: args.collectionConfig, i18n, config });
}, [i18n, config]);
const classes = [
'field-type',

View File

@@ -1,5 +1,6 @@
import { Option, Action, OptionGroup } from './types';
import { getTranslation } from '../../../../../utilities/getTranslation';
import { formatUseAsTitle } from '../../../../hooks/useTitle';
const reduceToIDs = (options) => options.reduce((ids, option) => {
if (option.options) {
@@ -30,15 +31,22 @@ const optionsReducer = (state: OptionGroup[], action: Action): OptionGroup[] =>
}
case 'UPDATE': {
const { collection, doc, i18n } = action;
const { collection, doc, i18n, config } = action;
const relation = collection.slug;
const newOptions = [...state];
const labelKey = collection.admin.useAsTitle || 'id';
const docTitle = formatUseAsTitle({
doc,
collection,
i18n,
config,
});
const foundOptionGroup = newOptions.find((optionGroup) => optionGroup.label === collection.labels.plural);
const foundOption = foundOptionGroup?.options?.find((option) => option.value === doc.id);
if (foundOption) {
foundOption.label = doc[labelKey] || `${i18n.t('general:untitled')} - ID: ${doc.id}`;
foundOption.label = docTitle || `${i18n.t('general:untitled')} - ID: ${doc.id}`;
foundOption.relationTo = relation;
}
@@ -46,9 +54,8 @@ const optionsReducer = (state: OptionGroup[], action: Action): OptionGroup[] =>
}
case 'ADD': {
const { collection, docs, sort, ids = [], i18n } = action;
const { collection, docs, sort, ids = [], i18n, config } = action;
const relation = collection.slug;
const labelKey = collection.admin.useAsTitle || 'id';
const loadedIDs = reduceToIDs(state);
const newOptions = [...state];
const optionsToAddTo = newOptions.find((optionGroup) => optionGroup.label === collection.labels.plural);
@@ -57,10 +64,17 @@ const optionsReducer = (state: OptionGroup[], action: Action): OptionGroup[] =>
if (loadedIDs.indexOf(doc.id) === -1) {
loadedIDs.push(doc.id);
const docTitle = formatUseAsTitle({
doc,
collection,
i18n,
config,
});
return [
...docSubOptions,
{
label: doc[labelKey] || `${i18n.t('general:untitled')} - ID: ${doc.id}`,
label: docTitle || `${i18n.t('general:untitled')} - ID: ${doc.id}`,
relationTo: relation,
value: doc.id,
},
@@ -74,7 +88,7 @@ const optionsReducer = (state: OptionGroup[], action: Action): OptionGroup[] =>
if (!loadedIDs.includes(id)) {
newSubOptions.push({
relationTo: relation,
label: labelKey === 'id' ? id : `${i18n.t('general:untitled')} - ID: ${id}`,
label: `${i18n.t('general:untitled')} - ID: ${id}`,
value: id,
});
}

View File

@@ -2,6 +2,7 @@ import i18n from 'i18next';
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
import { RelationshipField } from '../../../../../fields/config/types';
import { Where } from '../../../../../types';
import { SanitizedConfig } from '../../../../../config/types';
export type Props = Omit<RelationshipField, 'type'> & {
path?: string
@@ -35,6 +36,7 @@ type UPDATE = {
doc: any
collection: SanitizedCollectionConfig
i18n: typeof i18n
config: SanitizedConfig
}
type ADD = {
@@ -42,8 +44,9 @@ type ADD = {
docs: any[]
collection: SanitizedCollectionConfig
sort?: boolean
ids?: unknown[]
ids?: (string | number)[]
i18n: typeof i18n
config: SanitizedConfig
}
export type Action = CLEAR | ADD | UPDATE

View File

@@ -96,7 +96,7 @@ const DefaultAccount: React.FC<Props> = (props) => {
<h1>
<RenderTitle
data={data}
collection={collection.slug}
collection={collection}
useAsTitle={useAsTitle}
fallback={`[${t('general:untitled')}]`}
/>

View File

@@ -128,7 +128,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
<h1>
<RenderTitle
data={data}
collection={collection.slug}
collection={collection}
useAsTitle={useAsTitle}
fallback={`[${t('untitled')}]`}
/>

View File

@@ -26,7 +26,7 @@ export const SetStepNav: React.FC<{
const { t, i18n } = useTranslation('general');
const { routes: { admin } } = useConfig();
const title = useTitle(useAsTitle, collection.slug);
const title = useTitle(collection);
useEffect(() => {
const nav: StepNavItem[] = [{

View File

@@ -4,6 +4,8 @@ import { useConfig } from '../../../../../../utilities/Config';
import useIntersect from '../../../../../../../hooks/useIntersect';
import { useListRelationships } from '../../../RelationshipProvider';
import { getTranslation } from '../../../../../../../../utilities/getTranslation';
import { formatUseAsTitle } from '../../../../../../../hooks/useTitle';
import { Props as DefaultCellProps } from '../../types';
import './index.scss';
@@ -11,9 +13,13 @@ type Value = { relationTo: string, value: number | string };
const baseClass = 'relationship-cell';
const totalToShow = 3;
const RelationshipCell = (props) => {
const RelationshipCell: React.FC<{
field: DefaultCellProps['field']
data: DefaultCellProps['cellData']
}> = (props) => {
const { field, data: cellData } = props;
const { collections, routes } = useConfig();
const config = useConfig();
const { collections, routes } = config;
const [intersectionRef, entry] = useIntersect();
const [values, setValues] = useState<Value[]>([]);
const { getRelationships, documents } = useListRelationships();
@@ -31,7 +37,7 @@ const RelationshipCell = (props) => {
if (typeof cell === 'object' && 'relationTo' in cell && 'value' in cell) {
formattedValues.push(cell);
}
if ((typeof cell === 'number' || typeof cell === 'string') && typeof field.relationTo === 'string') {
if ((typeof cell === 'number' || typeof cell === 'string') && 'relationTo' in field && typeof field.relationTo === 'string') {
formattedValues.push({
value: cell,
relationTo: field.relationTo,
@@ -52,13 +58,19 @@ const RelationshipCell = (props) => {
{values.map(({ relationTo, value }, i) => {
const document = documents[relationTo][value];
const relatedCollection = collections.find(({ slug }) => slug === relationTo);
const label = document?.[relatedCollection.admin.useAsTitle] ? document[relatedCollection.admin.useAsTitle] : `${t('untitled')} - ID: ${value}`;
const label = formatUseAsTitle({
doc: document === false ? null : document,
collection: relatedCollection,
i18n,
config,
});
return (
<React.Fragment key={i}>
{document === false && `${t('untitled')} - ID: ${value}`}
{document === null && `${t('loading')}...`}
{document && label}
{document && (label || `${t('untitled')} - ID: ${value}`)}
{values.length > i + 1 && ', '}
</React.Fragment>
);
@@ -67,7 +79,7 @@ const RelationshipCell = (props) => {
Array.isArray(cellData) && cellData.length > totalToShow
&& t('fields:itemsAndMore', { items: '', count: cellData.length - totalToShow })
}
{values.length === 0 && t('noLabel', { label: getTranslation(field.label, i18n) })}
{values.length === 0 && t('noLabel', { label: getTranslation(field?.label || '', i18n) })}
</div>
);
};

View File

@@ -1,29 +1,61 @@
import i18next from 'i18next';
import { useTranslation } from 'react-i18next';
import { useRelatedCollections } from '../components/forms/field-types/Relationship/AddNew/useRelatedCollections';
import { SanitizedConfig } from '../../config/types';
import { SanitizedCollectionConfig } from '../../collections/config/types';
import { useFormFields } from '../components/forms/Form/context';
import { Field } from '../components/forms/Form/types';
import { useConfig } from '../components/utilities/Config';
import { formatDate } from '../utilities/formatDate';
import { getObjectDotNotation } from '../../utilities/getObjectDotNotation';
const useTitle = (useAsTitle: string, collection: string): string => {
const titleField = useFormFields(([fields]) => fields[useAsTitle]);
const value: string = titleField?.value as string || '';
// either send a `field` or an entire `doc`
export const formatUseAsTitle = (args: {
field?: Field
doc?: Record<string, any>
collection: SanitizedCollectionConfig
i18n: typeof i18next
config: SanitizedConfig
}): string => {
const {
field: fieldFromProps,
doc,
collection,
collection: {
admin: { useAsTitle },
},
i18n,
config: {
admin: {
dateFormat: dateFormatFromConfig,
},
},
} = args;
const { admin: { dateFormat: dateFormatFromConfig } } = useConfig();
const collectionConfig = useRelatedCollections(collection)?.[0];
const fieldConfig = collectionConfig?.fields?.find((field) => 'name' in field && field?.name === useAsTitle);
if (!fieldFromProps && !doc) {
return '';
}
const { i18n } = useTranslation();
const field = fieldFromProps || getObjectDotNotation<Field>(doc, collection.admin.useAsTitle);
let title = typeof field === 'string' ? field : field?.value as string;
const fieldConfig = collection?.fields?.find((f) => 'name' in f && f?.name === useAsTitle);
const isDate = fieldConfig?.type === 'date';
let title = value;
if (isDate && value) {
if (title && isDate) {
const dateFormat = fieldConfig?.admin?.date?.displayFormat || dateFormatFromConfig;
title = formatDate(value, dateFormat, i18n?.language);
title = formatDate(title, dateFormat, i18n?.language);
}
return title;
};
const useTitle = (collection: SanitizedCollectionConfig): string => {
const { i18n } = useTranslation();
const field = useFormFields(([formFields]) => formFields[collection?.admin?.useAsTitle]);
const config = useConfig();
return formatUseAsTitle({ field, collection, i18n, config });
};
export default useTitle;

View File

@@ -0,0 +1,5 @@
export const getObjectDotNotation = <T>(obj: Record<string, unknown>, path: string, defaultValue?: T): T => {
if (!path || !obj) return defaultValue;
const result = path.split('.').reduce((o, i) => o?.[i], obj);
return result === undefined ? defaultValue : result as T;
};

View File

@@ -142,9 +142,22 @@ export default buildConfig({
{
slug: relationWithTitleSlug,
admin: {
useAsTitle: 'name',
useAsTitle: 'meta.title',
},
fields: baseRelationshipFields,
fields: [
...baseRelationshipFields,
{
name: 'meta',
type: 'group',
fields: [
{
name: 'title',
label: 'Meta Title',
type: 'text',
},
],
},
],
},
{
slug: relationUpdatedExternallySlug,
@@ -297,6 +310,9 @@ export default buildConfig({
collection: relationWithTitleSlug,
data: {
name: title,
meta: {
title,
},
},
});
relationsWithTitle.push(id);

View File

@@ -17,7 +17,6 @@ import wait from '../../src/utilities/wait';
const { beforeAll, beforeEach, describe } = test;
describe('fields - relationship', () => {
let url: AdminUrlUtil;
let page: Page;
@@ -80,6 +79,9 @@ describe('fields - relationship', () => {
collection: relationWithTitleSlug,
data: {
name: 'relation-title',
meta: {
title: 'relation-title',
},
},
});
@@ -88,6 +90,9 @@ describe('fields - relationship', () => {
collection: relationWithTitleSlug,
data: {
name: 'word boundary search',
meta: {
title: 'word boundary search',
},
},
});
@@ -309,7 +314,7 @@ describe('fields - relationship', () => {
const options = page.locator('#field-relationshipWithTitle .rs__menu .rs__option');
await expect(options).toHaveCount(1);
await input.fill('non-occuring-string');
await input.fill('non-occurring-string');
await expect(options).toHaveCount(0);
});