@@ -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}`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Value } from './types';
|
||||
|
||||
type RelationMap = {
|
||||
[relation: string]: unknown[]
|
||||
[relation: string]: (string | number)[]
|
||||
}
|
||||
|
||||
type CreateRelationMap = (args: {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')}]`}
|
||||
/>
|
||||
|
||||
@@ -128,7 +128,7 @@ const DefaultEditView: React.FC<Props> = (props) => {
|
||||
<h1>
|
||||
<RenderTitle
|
||||
data={data}
|
||||
collection={collection.slug}
|
||||
collection={collection}
|
||||
useAsTitle={useAsTitle}
|
||||
fallback={`[${t('untitled')}]`}
|
||||
/>
|
||||
|
||||
@@ -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[] = [{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
5
src/utilities/getObjectDotNotation.ts
Normal file
5
src/utilities/getObjectDotNotation.ts
Normal 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;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user