* chore: ensures relationship fields react to locale changes in the admin panel - fixes #1870

* chore: patches in default values for fields, and localized fields using fallbacks - fixes #1859

* chore: organizes field localization and sanitizing

* Revert "Feat/1180 loading UI enhancements"

* Feat/1180 loading UI enhancements

* chore: safely sets tab if name field, only sets fallback value if it exists

* chore: adds test to ensure text fields use fallback locale value when empty
This commit is contained in:
Jarrod Flesch
2023-01-19 16:55:03 -05:00
committed by GitHub
parent 5d71d4bf6e
commit c0ac155a71
3 changed files with 68 additions and 15 deletions

View File

@@ -26,6 +26,7 @@ import { GetFilterOptions } from '../../../utilities/GetFilterOptions';
import { SingleValue } from './select-components/SingleValue'; import { SingleValue } from './select-components/SingleValue';
import { MultiValueLabel } from './select-components/MultiValueLabel'; import { MultiValueLabel } from './select-components/MultiValueLabel';
import { DocumentDrawerProps } from '../../../elements/DocumentDrawer/types'; import { DocumentDrawerProps } from '../../../elements/DocumentDrawer/types';
import { useLocale } from '../../../utilities/Locale';
import './index.scss'; import './index.scss';
@@ -64,6 +65,7 @@ const Relationship: React.FC<Props> = (props) => {
const { t, i18n } = useTranslation('fields'); const { t, i18n } = useTranslation('fields');
const { permissions } = useAuth(); const { permissions } = useAuth();
const locale = useLocale();
const formProcessing = useFormProcessing(); const formProcessing = useFormProcessing();
const hasMultipleRelations = Array.isArray(relationTo); const hasMultipleRelations = Array.isArray(relationTo);
const [options, dispatchOptions] = useReducer(optionsReducer, []); const [options, dispatchOptions] = useReducer(optionsReducer, []);
@@ -140,7 +142,7 @@ const Relationship: React.FC<Props> = (props) => {
limit: maxResultsPerRequest, limit: maxResultsPerRequest,
page: lastLoadedPageToUse, page: lastLoadedPageToUse,
sort: fieldToSearch, sort: fieldToSearch,
locale: i18n.language, locale,
depth: 0, depth: 0,
}; };
@@ -203,6 +205,7 @@ const Relationship: React.FC<Props> = (props) => {
api, api,
t, t,
i18n, i18n,
locale,
]); ]);
const updateSearch = useDebouncedCallback((searchArg: string, valueArg: unknown) => { const updateSearch = useDebouncedCallback((searchArg: string, valueArg: unknown) => {
@@ -242,7 +245,7 @@ const Relationship: React.FC<Props> = (props) => {
}, },
}, },
depth: 0, depth: 0,
locale: i18n.language, locale,
limit: idsToLoad.length, limit: idsToLoad.length,
}; };
@@ -274,6 +277,7 @@ const Relationship: React.FC<Props> = (props) => {
api, api,
i18n, i18n,
relationTo, relationTo,
locale,
]); ]);
// Determine if we should switch to word boundary search // Determine if we should switch to word boundary search
@@ -287,7 +291,7 @@ const Relationship: React.FC<Props> = (props) => {
setEnableWordBoundarySearch(!isIdOnly); setEnableWordBoundarySearch(!isIdOnly);
}, [relationTo, collections]); }, [relationTo, collections]);
// When relationTo or filterOptionsResult changes, reset component // When (`relationTo` || `filterOptionsResult` || `locale`) changes, reset component
// Note - effect should not run on first run // Note - effect should not run on first run
useEffect(() => { useEffect(() => {
if (firstRun.current) { if (firstRun.current) {
@@ -299,7 +303,7 @@ const Relationship: React.FC<Props> = (props) => {
setLastFullyLoadedRelation(-1); setLastFullyLoadedRelation(-1);
setLastLoadedPage(1); setLastLoadedPage(1);
setHasLoadedFirstPage(false); setHasLoadedFirstPage(false);
}, [relationTo, filterOptionsResult]); }, [relationTo, filterOptionsResult, locale]);
const onSave = useCallback<DocumentDrawerProps['onSave']>((args) => { const onSave = useCallback<DocumentDrawerProps['onSave']>((args) => {
dispatchOptions({ type: 'UPDATE', doc: args.doc, collection: args.collectionConfig, i18n }); dispatchOptions({ type: 'UPDATE', doc: args.doc, collection: args.collectionConfig, i18n });
@@ -373,7 +377,7 @@ const Relationship: React.FC<Props> = (props) => {
sort: false, sort: false,
}); });
}} }}
value={valueToRender} value={valueToRender ?? null}
showError={showError} showError={showError}
disabled={formProcessing} disabled={formProcessing}
options={options} options={options}

View File

@@ -46,22 +46,49 @@ export const promise = async ({
delete siblingDoc[field.name]; delete siblingDoc[field.name];
} }
const hasLocalizedValue = flattenLocales const shouldHoistLocalizedValue = flattenLocales
&& fieldAffectsData(field) && fieldAffectsData(field)
&& (typeof siblingDoc[field.name] === 'object' && siblingDoc[field.name] !== null) && (typeof siblingDoc[field.name] === 'object' && siblingDoc[field.name] !== null)
&& field.localized && field.localized
&& req.locale !== 'all'; && req.locale !== 'all';
if (hasLocalizedValue) { if (shouldHoistLocalizedValue) {
let localizedValue = siblingDoc[field.name][req.locale]; // replace actual value with localized value before sanitizing
if (typeof localizedValue === 'undefined' && req.fallbackLocale) localizedValue = siblingDoc[field.name][req.fallbackLocale]; // { [locale]: fields } -> fields
if (localizedValue === null && (field.type === 'array' || field.type === 'blocks')) localizedValue = siblingDoc[field.name][req.fallbackLocale]; const { locale } = req;
if (typeof localizedValue === 'undefined' && (field.type === 'group' || field.type === 'tab')) localizedValue = {}; const value = siblingDoc[field.name][locale];
if (typeof localizedValue === 'undefined') localizedValue = null; const fallbackLocale = req.payload.config.localization && req.payload.config.localization?.fallback && req.fallbackLocale;
siblingDoc[field.name] = localizedValue;
let hoistedValue = value;
if (fallbackLocale && fallbackLocale !== locale) {
const fallbackValue = siblingDoc[field.name][fallbackLocale];
const isNullOrUndefined = typeof value === 'undefined' || value === null;
if (fallbackValue) {
switch (field.type) {
case 'text':
case 'textarea': {
if (value === '' || isNullOrUndefined) {
hoistedValue = fallbackValue;
}
break;
}
default: {
if (isNullOrUndefined) {
hoistedValue = fallbackValue;
}
break;
}
}
}
}
siblingDoc[field.name] = hoistedValue;
} }
// Sanitize outgoing data // Sanitize outgoing field value
switch (field.type) { switch (field.type) {
case 'group': { case 'group': {
// Fill groups with empty objects so fields with hooks within groups can populate // Fill groups with empty objects so fields with hooks within groups can populate
@@ -74,7 +101,7 @@ export const promise = async ({
} }
case 'tabs': { case 'tabs': {
field.tabs.forEach((tab) => { field.tabs.forEach((tab) => {
if (tabHasName(tab) && typeof siblingDoc[tab.name] === 'undefined') { if (tabHasName(tab) && (typeof siblingDoc[tab.name] === 'undefined' || siblingDoc[tab.name] === null)) {
siblingDoc[tab.name] = {}; siblingDoc[tab.name] = {};
} }
}); });

View File

@@ -98,6 +98,28 @@ describe('Localization', () => {
expect(localized.title.es).toEqual(spanishTitle); expect(localized.title.es).toEqual(spanishTitle);
}); });
it('should fallback to english translation when empty', async () => {
const updated = await payload.update<LocalizedPost>({
collection,
id: post1.id,
locale: spanishLocale,
data: {
title: '',
},
});
expect(updated.title).toEqual(englishTitle);
const localizedFallback = await payload.findByID<LocalizedPostAllLocale>({
collection,
id: post1.id,
locale: 'all',
});
expect(localizedFallback.title.en).toEqual(englishTitle);
expect(localizedFallback.title.es).toEqual('');
});
describe('querying', () => { describe('querying', () => {
let localizedPost: LocalizedPost; let localizedPost: LocalizedPost;
beforeEach(async () => { beforeEach(async () => {