chore(i18n): convert CM List view components to TS (#18838)

This commit is contained in:
Josh 2023-11-20 09:38:50 +00:00 committed by GitHub
parent 306b218011
commit a1c8cbb8a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 870 additions and 811 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,41 +0,0 @@
import * as React from 'react';
import { Typography } from '@strapi/design-system';
import { useIntl } from 'react-intl';
import useHasI18n from '../../../hooks/useHasI18n';
import { getTranslation } from '../../../utils/getTranslation';
const Emphasis = (chunks: React.ReactNode) => {
return (
<Typography fontWeight="semiBold" textColor="danger500">
{chunks}
</Typography>
);
};
const DeleteModalAdditionalInfos = () => {
const hasI18nEnabled = useHasI18n();
const { formatMessage } = useIntl();
if (!hasI18nEnabled) {
return null;
}
return (
<Typography textColor="danger500">
{formatMessage(
{
id: getTranslation('Settings.list.actions.deleteAdditionalInfos'),
defaultMessage:
'This will delete the active locale versions <em>(from Internationalization)</em>',
},
{
em: Emphasis,
}
)}
</Typography>
);
};
export default DeleteModalAdditionalInfos;

View File

@ -1,41 +0,0 @@
import * as React from 'react';
import { Typography } from '@strapi/design-system';
import { useIntl } from 'react-intl';
import useHasI18n from '../../../hooks/useHasI18n';
import { getTranslation } from '../../../utils/getTranslation';
const Emphasis = (chunks: React.ReactNode) => {
return (
<Typography fontWeight="semiBold" textColor="danger500">
{chunks}
</Typography>
);
};
const PublishModalAdditionalInfos = () => {
const hasI18nEnabled = useHasI18n();
const { formatMessage } = useIntl();
if (!hasI18nEnabled) {
return null;
}
return (
<Typography textColor="danger500">
{formatMessage(
{
id: getTranslation('Settings.list.actions.publishAdditionalInfos'),
defaultMessage:
'This will publish the active locale versions <em>(from Internationalization)</em>',
},
{
em: Emphasis,
}
)}
</Typography>
);
};
export default PublishModalAdditionalInfos;

View File

@ -1,41 +0,0 @@
import * as React from 'react';
import { Typography } from '@strapi/design-system';
import { useIntl } from 'react-intl';
import useHasI18n from '../../../hooks/useHasI18n';
import { getTranslation } from '../../../utils/getTranslation';
const Emphasis = (chunks: React.ReactNode) => {
return (
<Typography fontWeight="semiBold" textColor="danger500">
{chunks}
</Typography>
);
};
const UnpublishModalAdditionalInfos = () => {
const hasI18nEnabled = useHasI18n();
const { formatMessage } = useIntl();
if (!hasI18nEnabled) {
return null;
}
return (
<Typography textColor="danger500">
{formatMessage(
{
id: getTranslation('Settings.list.actions.unpublishAdditionalInfos'),
defaultMessage:
'This will unpublish the active locale versions <em>(from Internationalization)</em>',
},
{
em: Emphasis,
}
)}
</Typography>
);
};
export default UnpublishModalAdditionalInfos;

View File

@ -0,0 +1,89 @@
import * as React from 'react';
import { Typography } from '@strapi/design-system';
import { useIntl } from 'react-intl';
import { useContentTypeHasI18n } from '../hooks/useContentTypeHasI18n';
import { getTranslation } from '../utils/getTranslation';
const Emphasis = (chunks: React.ReactNode) => {
return (
<Typography fontWeight="semiBold" textColor="danger500">
{chunks}
</Typography>
);
};
const DeleteModalAdditionalInfo = () => {
const hasI18nEnabled = useContentTypeHasI18n();
const { formatMessage } = useIntl();
if (!hasI18nEnabled) {
return null;
}
return (
<Typography textColor="danger500">
{formatMessage(
{
id: getTranslation('Settings.list.actions.deleteAdditionalInfos'),
defaultMessage:
'This will delete the active locale versions <em>(from Internationalization)</em>',
},
{
em: Emphasis,
}
)}
</Typography>
);
};
const PublishModalAdditionalInfo = () => {
const hasI18nEnabled = useContentTypeHasI18n();
const { formatMessage } = useIntl();
if (!hasI18nEnabled) {
return null;
}
return (
<Typography textColor="danger500">
{formatMessage(
{
id: getTranslation('Settings.list.actions.publishAdditionalInfos'),
defaultMessage:
'This will publish the active locale versions <em>(from Internationalization)</em>',
},
{
em: Emphasis,
}
)}
</Typography>
);
};
const UnpublishModalAdditionalInfo = () => {
const hasI18nEnabled = useContentTypeHasI18n();
const { formatMessage } = useIntl();
if (!hasI18nEnabled) {
return null;
}
return (
<Typography textColor="danger500">
{formatMessage(
{
id: getTranslation('Settings.list.actions.unpublishAdditionalInfos'),
defaultMessage:
'This will unpublish the active locale versions <em>(from Internationalization)</em>',
},
{
em: Emphasis,
}
)}
</Typography>
);
};
export { DeleteModalAdditionalInfo, PublishModalAdditionalInfo, UnpublishModalAdditionalInfo };

View File

@ -0,0 +1,84 @@
import { useState } from 'react';
import { SingleSelect, SingleSelectOption, SingleSelectProps } from '@strapi/design-system';
import { useQueryParams } from '@strapi/helper-plugin';
import { useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import { useRouteMatch } from 'react-router-dom';
import { useContentTypeHasI18n } from '../hooks/useContentTypeHasI18n';
import { useContentTypePermissions } from '../hooks/useContentTypePermissions';
import { useTypedSelector } from '../store/hooks';
import { getTranslation } from '../utils/getTranslation';
import { getInitialLocale } from '../utils/locales';
const LocalePicker = () => {
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const locales = useTypedSelector((state) => state.i18n_locales.locales);
const [{ query }, setQuery] = useQueryParams<{
page: number;
plugins: { i18n: { locale: string } };
}>();
const match = useRouteMatch<{ slug: string }>('/content-manager/collectionType/:slug');
const isContentTypeLocalized = useContentTypeHasI18n();
const { createPermissions, readPermissions } = useContentTypePermissions(match?.params.slug);
const initialLocale = getInitialLocale(query, locales);
const [selected, setSelected] = useState(initialLocale?.code || '');
if (!isContentTypeLocalized) {
return null;
}
if (!locales || locales.length === 0) {
return null;
}
const displayedLocales = locales.filter((locale) => {
const canCreate = createPermissions.some(({ properties }) =>
(properties?.locales ?? []).includes(locale.code)
);
const canRead = readPermissions.some(({ properties }) =>
(properties?.locales ?? []).includes(locale.code)
);
return canCreate || canRead;
});
// @ts-expect-error This can be removed in V2 of the DS.
const handleChange: SingleSelectProps['onChange'] = (code: string) => {
if (code === selected) {
return;
}
setSelected(code);
dispatch({ type: 'ContentManager/RBACManager/RESET_PERMISSIONS' });
setQuery({
page: 1,
plugins: { ...query.plugins, i18n: { locale: code } },
});
};
return (
<SingleSelect
size="S"
aria-label={formatMessage({
id: getTranslation('actions.select-locale'),
defaultMessage: 'Select locale',
})}
value={selected}
onChange={handleChange}
>
{displayedLocales.map((locale) => (
<SingleSelectOption key={locale.id} value={locale.code}>
{locale.name}
</SingleSelectOption>
))}
</SingleSelect>
);
};
export { LocalePicker };

View File

@ -1,93 +0,0 @@
import { useState } from 'react';
import { Option, Select } from '@strapi/design-system';
import { useQueryParams } from '@strapi/helper-plugin';
import get from 'lodash/get';
import { useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import { useRouteMatch } from 'react-router-dom';
import { useContentTypePermissions } from '../../hooks/useContentTypePermissions';
import useHasI18n from '../../hooks/useHasI18n';
import { useTypedSelector } from '../../store/hooks';
import getInitialLocale from '../../utils/getInitialLocale';
import { getTranslation } from '../../utils/getTranslation';
const LocalePicker = () => {
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const locales = useTypedSelector((state) => state.i18n_locales.locales);
const [{ query }, setQuery] = useQueryParams<any>();
const {
params: { slug },
} = useRouteMatch('/content-manager/collectionType/:slug') as any;
const isFieldLocalized = useHasI18n();
const { createPermissions, readPermissions } = useContentTypePermissions(slug);
const initialLocale = getInitialLocale(query, locales);
const [selected, setSelected] = useState(initialLocale?.code || '');
if (!isFieldLocalized) {
return null;
}
if (!locales || locales.length === 0) {
return null;
}
const displayedLocales = locales.filter((locale: any) => {
const canCreate = createPermissions.find(({ properties }: any) => {
return get(properties, 'locales', []).includes(locale.code);
});
const canRead = readPermissions.find(({ properties }: any) =>
get(properties, 'locales', []).includes(locale.code)
);
return canCreate || canRead;
});
const handleClick = (code: string) => {
if (code === selected) {
return;
}
setSelected(code);
/**
* if the selected value is set at the same time as the dispatcher
* is run, react might not have enough time to re-render the Select
* component, which leads to the `source` ref, which is passed to
* Popout, not being defined.
*
* By pushing the dispatcher to the end of the current execution
* context, we can guarantee the rendering can finish before.
*/
setTimeout(() => {
dispatch({ type: 'ContentManager/RBACManager/RESET_PERMISSIONS' });
setQuery({
page: 1,
plugins: { ...query.plugins, i18n: { locale: code } },
});
});
};
return (
<Select
size="S"
aria-label={formatMessage({
id: getTranslation('actions.select-locale'),
defaultMessage: '',
})}
value={selected}
onChange={handleClick as any}
>
{displayedLocales.map((locale) => (
<Option key={locale.id} id={`menu-item${locale.name || locale.code}`} value={locale.code}>
{locale.name}
</Option>
))}
</Select>
);
};
export default LocalePicker;

View File

@ -1,7 +1,7 @@
import get from 'lodash/get';
import { parse, stringify } from 'qs';
import getDefaultLocale from '../../utils/getDefaultLocale';
import { getDefaultLocale } from '../../utils/locales';
const addLocaleToLinksSearch = (
links: any[],

View File

@ -0,0 +1 @@
export const useContentTypeHasI18n = jest.fn().mockReturnValue(true);

View File

@ -0,0 +1,12 @@
import { useTypedSelector } from '../store/hooks';
const useContentTypeHasI18n = (): boolean => {
const pluginOptions = useTypedSelector(
// @ts-expect-error we've not typed the CM ListView yet.
(state) => state['content-manager_listView'].contentType.pluginOptions
);
return pluginOptions?.i18n?.localized ?? false;
};
export { useContentTypeHasI18n };

View File

@ -1,24 +1,37 @@
import { useMemo } from 'react';
import { createSelector } from '@reduxjs/toolkit';
import { Permission } from '@strapi/helper-plugin';
import { useTypedSelector } from '../store/hooks';
import { RootState } from '../store/reducers';
const selectContentTypePermissions = createSelector(
(state: RootState) => state.rbacProvider.collectionTypesRelatedPermissions,
(_, slug: string) => slug,
(state, slug) => {
// @ts-expect-error Selectors are weird, why don't they work with TS?
const currentCTRelatedPermissions = state[slug];
const readPermissions =
currentCTRelatedPermissions['plugin::content-manager.explorer.read'] || [];
const createPermissions =
currentCTRelatedPermissions['plugin::content-manager.explorer.create'] || [];
const makeSelectContentTypePermissions = () =>
// @ts-expect-error I have no idea why this fails like this.
createSelector(
(state: RootState) => state.rbacProvider.collectionTypesRelatedPermissions,
(_, slug: string) => slug,
(state: RootState['rbacProvider']['collectionTypesRelatedPermissions'], slug: string) => {
const currentCTRelatedPermissions = slug ? state[slug] : {};
return { createPermissions, readPermissions };
}
);
if (!currentCTRelatedPermissions) {
return { createPermissions: [], readPermissions: [] };
}
const useContentTypePermissions = (slug: string) =>
useTypedSelector((state) => selectContentTypePermissions(state, slug));
const readPermissions =
currentCTRelatedPermissions['plugin::content-manager.explorer.read'] || [];
const createPermissions =
currentCTRelatedPermissions['plugin::content-manager.explorer.create'] || [];
return { createPermissions, readPermissions };
}
);
const useContentTypePermissions = (
slug?: string
): { createPermissions: Permission[]; readPermissions: Permission[] } => {
const selectContentTypePermissions = useMemo(makeSelectContentTypePermissions, []);
return useTypedSelector((state) => selectContentTypePermissions(state, slug));
};
export { useContentTypePermissions };

View File

@ -1,13 +0,0 @@
import get from 'lodash/get';
import { useSelector } from 'react-redux';
const selectContentManagerListViewPluginOptions = (state: any) =>
state['content-manager_listView'].contentType.pluginOptions;
const useHasI18n = () => {
const pluginOptions = useSelector(selectContentManagerListViewPluginOptions);
return get(pluginOptions, 'i18n.localized', false);
};
export default useHasI18n;

View File

@ -4,11 +4,13 @@ import * as yup from 'yup';
import CheckboxConfirmation from './components/CheckboxConfirmation';
import { CMEditViewInjectedComponents } from './components/CMEditViewInjectedComponents';
import DeleteModalAdditionalInfos from './components/CMListViewInjectedComponents/DeleteModalAdditionalInfos';
import PublishModalAdditionalInfos from './components/CMListViewInjectedComponents/PublishModalAdditionalInfos';
import UnpublishModalAdditionalInfos from './components/CMListViewInjectedComponents/UnpublishModalAdditionalInfos';
import {
DeleteModalAdditionalInfo,
PublishModalAdditionalInfo,
UnpublishModalAdditionalInfo,
} from './components/CMListViewModalsAdditionalInformation';
import Initializer from './components/Initializer';
import LocalePicker from './components/LocalePicker';
import { LocalePicker } from './components/LocalePicker';
import { PERMISSIONS } from './constants';
import addColumnToTableHook from './contentManagerHooks/addColumnToTable';
import addLocaleToCollectionTypesLinksHook from './contentManagerHooks/addLocaleToCollectionTypesLinks';
@ -81,17 +83,17 @@ export default {
app.injectContentManagerComponent('listView', 'deleteModalAdditionalInfos', {
name: 'i18n-delete-bullets-in-modal',
Component: DeleteModalAdditionalInfos,
Component: DeleteModalAdditionalInfo,
});
app.injectContentManagerComponent('listView', 'publishModalAdditionalInfos', {
name: 'i18n-publish-bullets-in-modal',
Component: PublishModalAdditionalInfos,
Component: PublishModalAdditionalInfo,
});
app.injectContentManagerComponent('listView', 'unpublishModalAdditionalInfos', {
name: 'i18n-unpublish-bullets-in-modal',
Component: UnpublishModalAdditionalInfos,
Component: UnpublishModalAdditionalInfo,
});
const ctbPlugin = app.getPlugin('content-type-builder');

View File

@ -1,4 +1,4 @@
import { createSelector, Dispatch, Selector } from '@reduxjs/toolkit';
import { Dispatch } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { Action, RootState } from './reducers';
@ -8,9 +8,4 @@ type AppDispatch = Dispatch<Action>;
const useTypedDispatch: () => AppDispatch = useDispatch;
const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
const createTypedSelector = <TResult, TSelector extends Selector<RootState, TResult>>(
selector: TSelector
// @ts-expect-error TODO: this is needed to avoid TS2742. But it's not quite right.
): ReturnType<TSelector> => createSelector((state: RootState) => state, selector);
export { useTypedSelector, createTypedSelector, useTypedDispatch };
export { useTypedSelector, useTypedDispatch };

View File

@ -1,60 +0,0 @@
import get from 'lodash/get';
const hasLocalePermission = (permissions: any, localeCode: any) => {
if (permissions) {
const hasPermission = permissions.some((permission: any) =>
get(permission, 'properties.locales', []).includes(localeCode)
);
if (hasPermission) {
return true;
}
}
return false;
};
const getFirstLocale = (permissions: any) => {
if (permissions && permissions.length > 0) {
const firstAuthorizedNonDefaultLocale = get(permissions, [0, 'properties', 'locales', 0], null);
if (firstAuthorizedNonDefaultLocale) {
return firstAuthorizedNonDefaultLocale;
}
}
return null;
};
/**
* Entry point of the module
*/
const getDefaultLocale = (ctPermissions: any, locales: any = []) => {
const defaultLocale = locales.find((locale: any) => locale.isDefault);
if (!defaultLocale) {
return null;
}
const readPermissions = ctPermissions['plugin::content-manager.explorer.read'];
const createPermissions = ctPermissions['plugin::content-manager.explorer.create'];
if (hasLocalePermission(readPermissions, defaultLocale.code)) {
return defaultLocale.code;
}
if (hasLocalePermission(createPermissions, defaultLocale.code)) {
return defaultLocale.code;
}
// When the default locale is not authorized, we return the first authorized locale
const firstAuthorizedForReadNonDefaultLocale = getFirstLocale(readPermissions);
if (firstAuthorizedForReadNonDefaultLocale) {
return firstAuthorizedForReadNonDefaultLocale;
}
return getFirstLocale(createPermissions);
};
export default getDefaultLocale;

View File

@ -1,14 +0,0 @@
import getLocaleFromQuery from './getLocaleFromQuery';
const getInitialLocale = (query: any, locales: any = []) => {
const localeFromQuery = getLocaleFromQuery(query);
if (localeFromQuery) {
return locales.find((locale: any) => locale.code === localeFromQuery);
}
// Returns the default locale when nothing is in the query
return locales.find((locale: any) => locale.isDefault);
};
export default getInitialLocale;

View File

@ -1,7 +0,0 @@
import get from 'lodash/get';
const getLocaleFromQuery = (query: any) => {
return get(query, 'plugins.i18n.locale', undefined);
};
export default getLocaleFromQuery;

View File

@ -0,0 +1,76 @@
import { Locale, RootState } from '../store/reducers';
interface PotentialQueryWithLocale {
plugins?: { i18n?: { locale?: string; [key: string]: unknown }; [key: string]: unknown };
}
/**
* Returns the locale from the passed query.
* If a default value is passed, it will return it if the locale does not exist.
*/
function getLocaleFromQuery(query: PotentialQueryWithLocale): string | undefined;
function getLocaleFromQuery(query: PotentialQueryWithLocale, defaultValue: string): string;
function getLocaleFromQuery(
query: PotentialQueryWithLocale,
defaultValue?: string
): string | undefined {
const locale = query?.plugins?.i18n?.locale;
if (!locale && defaultValue) {
return defaultValue;
}
return locale;
}
/**
* Returns the initial locale from the query falling back to the default locale
* listed in the collection of locales provided.
*/
const getInitialLocale = (
query: PotentialQueryWithLocale,
locales: Locale[] = []
): Locale | undefined => {
const localeFromQuery = getLocaleFromQuery(query);
if (localeFromQuery) {
return locales.find((locale) => locale.code === localeFromQuery);
}
// Returns the default locale when nothing is in the query
return locales.find((locale) => locale.isDefault);
};
const getDefaultLocale = (
ctPermissions: RootState['rbacProvider']['collectionTypesRelatedPermissions'][string],
locales: Locale[] = []
) => {
const defaultLocale = locales.find((locale) => locale.isDefault);
if (!defaultLocale) {
return null;
}
const readPermissions = ctPermissions['plugin::content-manager.explorer.read'] ?? [];
const createPermissions = ctPermissions['plugin::content-manager.explorer.create'] ?? [];
if (
readPermissions.some(({ properties }) =>
(properties?.locales ?? []).includes(defaultLocale.code)
) ||
createPermissions.some(({ properties }) =>
(properties?.locales ?? []).includes(defaultLocale.code)
)
) {
return defaultLocale.code;
}
// When the default locale is not authorized, we return the first authorized locale
return (
(readPermissions[0]?.properties?.locales?.[0] ||
createPermissions[0]?.properties?.locales?.[0]) ??
null
);
};
export { getLocaleFromQuery, getInitialLocale, getDefaultLocale };

View File

@ -1,337 +0,0 @@
import getDefaultLocale from '../getDefaultLocale';
describe('getDefaultLocale', () => {
it('gives fr-FR when it s the default locale and that it has read access to it', () => {
const locales = [
{
id: 1,
name: 'English',
code: 'en',
createdAt: '2021-03-09T14:57:03.016Z',
updatedAt: '2021-03-09T14:57:03.016Z',
isDefault: false,
},
{
id: 2,
name: 'French (France) (fr-FR)',
code: 'fr-FR',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-17T13:01:03.569Z',
isDefault: true,
},
];
const ctPermissions = {
'plugin::content-manager.explorer.create': [
{
id: 1325,
action: 'plugin::content-manager.explorer.create',
subject: 'api::address.address',
properties: {
fields: [
'postal_coder',
'categories',
'cover',
'images',
'city',
'likes',
'json',
'slug',
],
locales: [],
},
conditions: [],
},
],
'plugin::content-manager.explorer.read': [
{
id: 1326,
action: 'plugin::content-manager.explorer.read',
subject: 'api::address.address',
properties: {
fields: [],
locales: ['en', 'fr-FR'],
},
conditions: [],
},
],
};
const expected = 'fr-FR';
const actual = getDefaultLocale(ctPermissions, locales);
expect(actual).toEqual(expected);
});
it('gives fr-FR when it s the default locale and that it has create access to it', () => {
const locales = [
{
id: 1,
name: 'English',
code: 'en',
createdAt: '2021-03-09T14:57:03.016Z',
updatedAt: '2021-03-09T14:57:03.016Z',
isDefault: false,
},
{
id: 2,
name: 'French (France) (fr-FR)',
code: 'fr-FR',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-17T13:01:03.569Z',
isDefault: true,
},
];
const ctPermissions = {
'plugin::content-manager.explorer.create': [
{
id: 1325,
action: 'plugin::content-manager.explorer.create',
subject: 'api::address.address',
properties: {
fields: [
'postal_coder',
'categories',
'cover',
'images',
'city',
'likes',
'json',
'slug',
],
locales: ['fr-FR'],
},
conditions: [],
},
],
'plugin::content-manager.explorer.read': [
{
id: 1326,
action: 'plugin::content-manager.explorer.read',
subject: 'api::address.address',
properties: {
fields: [],
locales: ['en'],
},
conditions: [],
},
],
};
const expected = 'fr-FR';
const actual = getDefaultLocale(ctPermissions, locales);
expect(actual).toEqual(expected);
});
it('gives gives the first locale with read permission ("en") when the locale is allowed', () => {
const locales = [
{
id: 1,
name: 'English',
code: 'en',
createdAt: '2021-03-09T14:57:03.016Z',
updatedAt: '2021-03-09T14:57:03.016Z',
isDefault: false,
},
{
id: 2,
name: 'French (France) (fr-FR)',
code: 'fr-FR',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-17T13:01:03.569Z',
isDefault: true,
},
{
id: 3,
name: 'Another lang',
code: 'de',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-17T13:01:03.569Z',
isDefault: false,
},
];
const ctPermissions = {
'plugin::content-manager.explorer.create': [
{
id: 1325,
action: 'plugin::content-manager.explorer.create',
subject: 'api::address.address',
properties: {
fields: [
'postal_coder',
'categories',
'cover',
'images',
'city',
'likes',
'json',
'slug',
],
locales: [],
},
conditions: [],
},
],
'plugin::content-manager.explorer.read': [
{
id: 1326,
action: 'plugin::content-manager.explorer.read',
subject: 'api::address.address',
properties: {
fields: [],
locales: ['en', 'de'],
},
conditions: [],
},
],
};
const expected = 'en';
const actual = getDefaultLocale(ctPermissions, locales);
expect(actual).toEqual(expected);
});
it('gives gives the first locale with create permission ("en") when the locale is allowed', () => {
const locales = [
{
id: 1,
name: 'English',
code: 'en',
createdAt: '2021-03-09T14:57:03.016Z',
updatedAt: '2021-03-09T14:57:03.016Z',
isDefault: false,
},
{
id: 2,
name: 'French (France) (fr-FR)',
code: 'fr-FR',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-17T13:01:03.569Z',
isDefault: true,
},
{
id: 3,
name: 'Another lang',
code: 'de',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-17T13:01:03.569Z',
isDefault: false,
},
];
const ctPermissions = {
'plugin::content-manager.explorer.create': [
{
id: 1325,
action: 'plugin::content-manager.explorer.create',
subject: 'api::address.address',
properties: {
fields: [
'postal_coder',
'categories',
'cover',
'images',
'city',
'likes',
'json',
'slug',
],
locales: ['en', 'de'],
},
conditions: [],
},
],
'plugin::content-manager.explorer.read': [
{
id: 1326,
action: 'plugin::content-manager.explorer.read',
subject: 'api::address.address',
properties: {
fields: [],
locales: [],
},
conditions: [],
},
],
};
const expected = 'en';
const actual = getDefaultLocale(ctPermissions, locales);
expect(actual).toEqual(expected);
});
it('gives null when the user has no permission on any locale', () => {
const locales = [
{
id: 1,
name: 'English',
code: 'en',
createdAt: '2021-03-09T14:57:03.016Z',
updatedAt: '2021-03-09T14:57:03.016Z',
isDefault: false,
},
{
id: 2,
name: 'French (France) (fr-FR)',
code: 'fr-FR',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-17T13:01:03.569Z',
isDefault: true,
},
{
id: 3,
name: 'Another lang',
code: 'de',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-17T13:01:03.569Z',
isDefault: false,
},
];
const ctPermissions = {
'plugin::content-manager.explorer.create': [
{
id: 1325,
action: 'plugin::content-manager.explorer.create',
subject: 'api::address.address',
properties: {
fields: [
'postal_coder',
'categories',
'cover',
'images',
'city',
'likes',
'json',
'slug',
],
locales: [],
},
conditions: [],
},
],
'plugin::content-manager.explorer.read': [
{
id: 1326,
action: 'plugin::content-manager.explorer.read',
subject: 'api::address.address',
properties: {
fields: [],
locales: [],
},
conditions: [],
},
],
};
const expected = null;
const actual = getDefaultLocale(ctPermissions, locales);
expect(actual).toEqual(expected);
});
});

View File

@ -1,106 +0,0 @@
import getInitialLocale from '../getInitialLocale';
describe('getInitialLocale', () => {
it('gives "fr-FR" when the query.plugins.locale is "fr-FR"', () => {
const query = {
page: '1',
pageSize: '10',
sort: 'Name:ASC',
plugins: {
i18n: { locale: 'fr-FR' },
},
};
const locales = [
{
id: 1,
name: 'English',
code: 'en',
createdAt: '2021-03-09T14:57:03.016Z',
updatedAt: '2021-03-09T14:57:03.016Z',
isDefault: true,
},
{
id: 2,
name: 'French (France) (fr-FR)',
code: 'fr-FR',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-09T15:03:06.996Z',
isDefault: false,
},
];
const expected = {
id: 2,
name: 'French (France) (fr-FR)',
code: 'fr-FR',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-09T15:03:06.996Z',
isDefault: false,
};
const actual = getInitialLocale(query, locales);
expect(actual).toEqual(expected);
});
it('gives the default locale ("en") when there s no locale in the query', () => {
const query = {
page: '1',
pageSize: '10',
sort: 'Name:ASC',
plugins: {
something: 'great',
},
};
const locales = [
{
id: 2,
name: 'French (France) (fr-FR)',
code: 'fr-FR',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-09T15:03:06.996Z',
isDefault: false,
},
{
id: 1,
name: 'English',
code: 'en',
createdAt: '2021-03-09T14:57:03.016Z',
updatedAt: '2021-03-09T14:57:03.016Z',
isDefault: true,
},
];
const expected = {
id: 1,
name: 'English',
code: 'en',
createdAt: '2021-03-09T14:57:03.016Z',
updatedAt: '2021-03-09T14:57:03.016Z',
isDefault: true,
};
const actual = getInitialLocale(query, locales);
expect(actual).toEqual(expected);
});
it('gives "undefined" when theres no locale. IMPORTANT: such case should not exist since at least one locale is created on the backend when plug-in i18n', () => {
const query = {
page: '1',
pageSize: '10',
sort: 'Name:ASC',
plugins: {
something: 'great',
},
};
const locales: any = [];
const expected = undefined;
const actual = getInitialLocale(query, locales);
expect(actual).toEqual(expected);
});
});

View File

@ -0,0 +1,502 @@
import { Locale } from '../../store/reducers';
import { getInitialLocale, getLocaleFromQuery, getDefaultLocale } from '../locales';
describe('locales', () => {
describe('getLocaleFromQuery', () => {
it('returns the locale from the query', () => {
const query = {
plugins: {
i18n: {
locale: 'en-GB',
},
},
};
expect(getLocaleFromQuery(query)).toBe('en-GB');
});
it("should return undefined if the locale doesn't exist", () => {
expect(
getLocaleFromQuery({
plugins: {
i18n: {},
},
})
).toBe(undefined);
expect(
getLocaleFromQuery({
plugins: {},
})
).toBe(undefined);
expect(getLocaleFromQuery({})).toBe(undefined);
});
it('should return the default value if the locale does not exist', () => {
expect(
getLocaleFromQuery(
{
plugins: {
i18n: {},
},
},
'en-GB'
)
).toBe('en-GB');
expect(
getLocaleFromQuery(
{
plugins: {},
},
'en-GB'
)
).toBe('en-GB');
expect(getLocaleFromQuery({}, 'en-GB')).toBe('en-GB');
});
});
describe('getInitialLocale', () => {
it('gives "fr-FR" when the query.plugins.locale is "fr-FR"', () => {
const query = {
page: '1',
pageSize: '10',
sort: 'Name:ASC',
plugins: {
i18n: { locale: 'en-GB' },
},
};
const locales: Locale[] = [
{
id: 1,
name: 'English',
code: 'en-GB',
createdAt: '2021-03-09T14:57:03.016Z',
updatedAt: '2021-03-09T14:57:03.016Z',
isDefault: true,
},
{
id: 2,
name: 'French (France) (fr-FR)',
code: 'fr-FR',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-09T15:03:06.996Z',
isDefault: false,
},
];
const actual = getInitialLocale(query, locales);
expect(actual).toMatchInlineSnapshot(`
{
"code": "en-GB",
"createdAt": "2021-03-09T14:57:03.016Z",
"id": 1,
"isDefault": true,
"name": "English",
"updatedAt": "2021-03-09T14:57:03.016Z",
}
`);
});
it('gives the default locale ("en") when there s no locale in the query', () => {
const query = {
page: '1',
pageSize: '10',
sort: 'Name:ASC',
plugins: {
something: 'great',
},
};
const locales: Locale[] = [
{
id: 2,
name: 'French (France) (fr-FR)',
code: 'fr-FR',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-09T15:03:06.996Z',
isDefault: false,
},
{
id: 1,
name: 'English',
code: 'en-GB',
createdAt: '2021-03-09T14:57:03.016Z',
updatedAt: '2021-03-09T14:57:03.016Z',
isDefault: true,
},
];
const actual = getInitialLocale(query, locales);
expect(actual).toMatchInlineSnapshot(`
{
"code": "en-GB",
"createdAt": "2021-03-09T14:57:03.016Z",
"id": 1,
"isDefault": true,
"name": "English",
"updatedAt": "2021-03-09T14:57:03.016Z",
}
`);
});
it('gives "undefined" when theres no locale', () => {
/**
* @note this case should not exist since at least one locale
* is created on the backend when plug-in i18n. But you can
* never trust the server.
*/
const query = {
page: '1',
pageSize: '10',
sort: 'Name:ASC',
plugins: {
something: 'great',
},
};
const locales: Locale[] = [];
const actual = getInitialLocale(query, locales);
expect(actual).toMatchInlineSnapshot(`undefined`);
});
});
describe('getDefaultLocale', () => {
it('gives fr-FR when it s the default locale and that it has read access to it', () => {
const locales = [
{
id: 1,
name: 'English',
code: 'en',
createdAt: '2021-03-09T14:57:03.016Z',
updatedAt: '2021-03-09T14:57:03.016Z',
isDefault: false,
},
{
id: 2,
name: 'French (France) (fr-FR)',
code: 'fr-FR',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-17T13:01:03.569Z',
isDefault: true,
},
];
const ctPermissions = {
'plugin::content-manager.explorer.create': [
{
id: 1325,
action: 'plugin::content-manager.explorer.create',
subject: 'api::address.address',
properties: {
fields: [
'postal_coder',
'categories',
'cover',
'images',
'city',
'likes',
'json',
'slug',
],
locales: [],
},
conditions: [],
},
],
'plugin::content-manager.explorer.read': [
{
id: 1326,
action: 'plugin::content-manager.explorer.read',
subject: 'api::address.address',
properties: {
fields: [],
locales: ['en', 'fr-FR'],
},
conditions: [],
},
],
};
const expected = 'fr-FR';
const actual = getDefaultLocale(ctPermissions, locales);
expect(actual).toEqual(expected);
});
it('gives fr-FR when it s the default locale and that it has create access to it', () => {
const locales = [
{
id: 1,
name: 'English',
code: 'en',
createdAt: '2021-03-09T14:57:03.016Z',
updatedAt: '2021-03-09T14:57:03.016Z',
isDefault: false,
},
{
id: 2,
name: 'French (France) (fr-FR)',
code: 'fr-FR',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-17T13:01:03.569Z',
isDefault: true,
},
];
const ctPermissions = {
'plugin::content-manager.explorer.create': [
{
id: 1325,
action: 'plugin::content-manager.explorer.create',
subject: 'api::address.address',
properties: {
fields: [
'postal_coder',
'categories',
'cover',
'images',
'city',
'likes',
'json',
'slug',
],
locales: ['fr-FR'],
},
conditions: [],
},
],
'plugin::content-manager.explorer.read': [
{
id: 1326,
action: 'plugin::content-manager.explorer.read',
subject: 'api::address.address',
properties: {
fields: [],
locales: ['en'],
},
conditions: [],
},
],
};
const expected = 'fr-FR';
const actual = getDefaultLocale(ctPermissions, locales);
expect(actual).toEqual(expected);
});
it('gives gives the first locale with read permission ("en") when the locale is allowed', () => {
const locales = [
{
id: 1,
name: 'English',
code: 'en',
createdAt: '2021-03-09T14:57:03.016Z',
updatedAt: '2021-03-09T14:57:03.016Z',
isDefault: false,
},
{
id: 2,
name: 'French (France) (fr-FR)',
code: 'fr-FR',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-17T13:01:03.569Z',
isDefault: true,
},
{
id: 3,
name: 'Another lang',
code: 'de',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-17T13:01:03.569Z',
isDefault: false,
},
];
const ctPermissions = {
'plugin::content-manager.explorer.create': [
{
id: 1325,
action: 'plugin::content-manager.explorer.create',
subject: 'api::address.address',
properties: {
fields: [
'postal_coder',
'categories',
'cover',
'images',
'city',
'likes',
'json',
'slug',
],
locales: [],
},
conditions: [],
},
],
'plugin::content-manager.explorer.read': [
{
id: 1326,
action: 'plugin::content-manager.explorer.read',
subject: 'api::address.address',
properties: {
fields: [],
locales: ['en', 'de'],
},
conditions: [],
},
],
};
const expected = 'en';
const actual = getDefaultLocale(ctPermissions, locales);
expect(actual).toEqual(expected);
});
it('gives gives the first locale with create permission ("en") when the locale is allowed', () => {
const locales = [
{
id: 1,
name: 'English',
code: 'en',
createdAt: '2021-03-09T14:57:03.016Z',
updatedAt: '2021-03-09T14:57:03.016Z',
isDefault: false,
},
{
id: 2,
name: 'French (France) (fr-FR)',
code: 'fr-FR',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-17T13:01:03.569Z',
isDefault: true,
},
{
id: 3,
name: 'Another lang',
code: 'de',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-17T13:01:03.569Z',
isDefault: false,
},
];
const ctPermissions = {
'plugin::content-manager.explorer.create': [
{
id: 1325,
action: 'plugin::content-manager.explorer.create',
subject: 'api::address.address',
properties: {
fields: [
'postal_coder',
'categories',
'cover',
'images',
'city',
'likes',
'json',
'slug',
],
locales: ['en', 'de'],
},
conditions: [],
},
],
'plugin::content-manager.explorer.read': [
{
id: 1326,
action: 'plugin::content-manager.explorer.read',
subject: 'api::address.address',
properties: {
fields: [],
locales: [],
},
conditions: [],
},
],
};
const expected = 'en';
const actual = getDefaultLocale(ctPermissions, locales);
expect(actual).toEqual(expected);
});
it('gives null when the user has no permission on any locale', () => {
const locales = [
{
id: 1,
name: 'English',
code: 'en',
createdAt: '2021-03-09T14:57:03.016Z',
updatedAt: '2021-03-09T14:57:03.016Z',
isDefault: false,
},
{
id: 2,
name: 'French (France) (fr-FR)',
code: 'fr-FR',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-17T13:01:03.569Z',
isDefault: true,
},
{
id: 3,
name: 'Another lang',
code: 'de',
createdAt: '2021-03-09T15:03:06.992Z',
updatedAt: '2021-03-17T13:01:03.569Z',
isDefault: false,
},
];
const ctPermissions = {
'plugin::content-manager.explorer.create': [
{
id: 1325,
action: 'plugin::content-manager.explorer.create',
subject: 'api::address.address',
properties: {
fields: [
'postal_coder',
'categories',
'cover',
'images',
'city',
'likes',
'json',
'slug',
],
locales: [],
},
conditions: [],
},
],
'plugin::content-manager.explorer.read': [
{
id: 1326,
action: 'plugin::content-manager.explorer.read',
subject: 'api::address.address',
properties: {
fields: [],
locales: [],
},
conditions: [],
},
],
};
const expected = null;
const actual = getDefaultLocale(ctPermissions, locales);
expect(actual).toEqual(expected);
});
});
});

View File

@ -219,7 +219,7 @@ const getNestedPopulateOfNonLocalizedAttributes = (modelUID: any) => {
return attributesToPopulate;
};
export default () => ({
const contentTypes = () => ({
isLocalizedContentType,
getValidLocale,
getNewLocalizationsFrom,
@ -230,3 +230,8 @@ export default () => ({
fillNonLocalizedAttributes,
getNestedPopulateOfNonLocalizedAttributes,
});
type ContentTypesService = typeof contentTypes;
export default contentTypes;
export { ContentTypesService };

View File

@ -277,9 +277,14 @@ const addGraphqlLocalizationAction = (contentType: any) => {
});
};
export default () => ({
const coreApi = () => ({
addCreateLocalizationAction,
addGraphqlLocalizationAction,
createSanitizer,
createCreateLocalizationHandler,
});
type CoreApiService = typeof coreApi;
export default coreApi;
export { CoreApiService };

View File

@ -196,7 +196,12 @@ const decorator = (service: any) => ({
},
});
export default () => ({
const entityServiceDecorator = () => ({
decorator,
wrapParams,
});
type EntityServiceDecoratorService = typeof entityServiceDecorator;
export default entityServiceDecorator;
export type { EntityServiceDecoratorService };

View File

@ -2,6 +2,11 @@ import { isoLocales } from '../constants';
const getIsoLocales = () => isoLocales;
export default () => ({
const isoLocalesService = () => ({
getIsoLocales,
});
type ISOLocalesService = typeof isoLocalesService;
export default isoLocalesService;
export type { ISOLocalesService };

View File

@ -79,7 +79,7 @@ const deleteAllLocalizedEntriesFor = async ({ locale }: any) => {
}
};
export default () => ({
const locales = () => ({
find,
findById,
findByCode,
@ -92,3 +92,8 @@ export default () => ({
delete: deleteFn,
initDefaultLocale,
});
type LocaleService = typeof locales;
export default locales;
export type { LocaleService };

View File

@ -82,8 +82,13 @@ const syncNonLocalizedAttributes = async (entry: any, { model }: any) => {
}
};
export default () => ({
const localizations = () => ({
assignDefaultLocaleToEntries,
syncLocalizations,
syncNonLocalizedAttributes,
});
type LocalizationsService = typeof localizations;
export default localizations;
export type { LocalizationsService };

View File

@ -21,7 +21,12 @@ const sendDidUpdateI18nLocalesEvent = async () => {
});
};
export default () => ({
const metrics = () => ({
sendDidInitializeEvent,
sendDidUpdateI18nLocalesEvent,
});
type MetricsService = typeof metrics;
export default metrics;
export type { MetricsService };

View File

@ -2,8 +2,13 @@ import i18nActionsService from './permissions/actions';
import sectionsBuilderService from './permissions/sections-builder';
import engineService from './permissions/engine';
export default () => ({
const permissions = () => ({
actions: i18nActionsService,
sectionsBuilder: sectionsBuilderService,
engine: engineService,
});
type PermissionsService = typeof permissions;
export default permissions;
export type { PermissionsService };

View File

@ -1,21 +1,21 @@
import locales from '../services/locales';
import permissions from '../services/permissions';
import contentTypes from '../services/content-types';
import metrics from '../services/metrics';
import entityServiceDecorator from '../services/entity-service-decorator';
import coreAPI from '../services/core-api';
import ISOLocales from '../services/iso-locales';
import localizations from '../services/localizations';
import type { LocaleService } from '../services/locales';
import type { PermissionsService } from '../services/permissions';
import type { ContentTypesService } from '../services/content-types';
import type { MetricsService } from '../services/metrics';
import type { EntityServiceDecoratorService } from '../services/entity-service-decorator';
import type { CoreApiService } from '../services/core-api';
import type { ISOLocalesService } from '../services/iso-locales';
import type { LocalizationsService } from '../services/localizations';
type S = {
permissions: typeof permissions;
metrics: typeof metrics;
locales: typeof locales;
localizations: typeof localizations;
['iso-locales']: typeof ISOLocales;
['content-types']: typeof contentTypes;
['entity-service-decorator']: typeof entityServiceDecorator;
['core-api']: typeof coreAPI;
permissions: PermissionsService;
metrics: MetricsService;
locales: LocaleService;
localizations: LocalizationsService;
['iso-locales']: ISOLocalesService;
['content-types']: ContentTypesService;
['entity-service-decorator']: EntityServiceDecoratorService;
['core-api']: CoreApiService;
};
const getCoreStore = () => {