mirror of
https://github.com/strapi/strapi.git
synced 2025-11-02 02:44:55 +00:00
Features/i18n locale in filters (#9611)
This commit is contained in:
parent
068a31c3e5
commit
32a5e2becf
@ -9,7 +9,7 @@ import { useFetchContentTypeLayout } from '../../hooks';
|
||||
import { formatLayoutToApi } from '../../utils';
|
||||
import EditView from '../EditView';
|
||||
import EditSettingsView from '../EditSettingsView';
|
||||
import ListView from '../ListView';
|
||||
import ListViewLayout from '../ListView/ListViewLayout';
|
||||
import ListSettingsView from '../ListSettingsView';
|
||||
|
||||
const CollectionTypeRecursivePath = ({
|
||||
@ -75,7 +75,7 @@ const CollectionTypeRecursivePath = ({
|
||||
const routes = [
|
||||
{ path: ':id/clone/:origin', comp: EditView },
|
||||
{ path: ':id', comp: EditView },
|
||||
{ path: '', comp: ListView },
|
||||
{ path: '', comp: ListViewLayout },
|
||||
].map(({ path, comp }) => (
|
||||
<Route key={path} path={`${url}/${path}`} render={props => renderRoute(props, comp)} />
|
||||
));
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { setLayout } from './actions';
|
||||
import ListView from '.';
|
||||
import { useQueryParams } from '../../hooks';
|
||||
|
||||
const ListViewLayout = ({ layout, ...props }) => {
|
||||
const dispatch = useDispatch();
|
||||
const initialParams = useSelector(state => state.get('content-manager_listView').initialParams);
|
||||
const [, setQuery] = useQueryParams(initialParams);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setLayout(layout.contentType));
|
||||
}, [dispatch, layout]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialParams) {
|
||||
setQuery(initialParams);
|
||||
}
|
||||
}, [initialParams, setQuery]);
|
||||
|
||||
if (!initialParams) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <ListView {...props} layout={layout} />;
|
||||
};
|
||||
|
||||
ListViewLayout.propTypes = {
|
||||
layout: PropTypes.exact({
|
||||
components: PropTypes.object.isRequired,
|
||||
contentType: PropTypes.shape({
|
||||
attributes: PropTypes.object.isRequired,
|
||||
metadatas: PropTypes.object.isRequired,
|
||||
info: PropTypes.shape({ label: PropTypes.string.isRequired }).isRequired,
|
||||
layouts: PropTypes.shape({
|
||||
list: PropTypes.array.isRequired,
|
||||
editRelations: PropTypes.array,
|
||||
}).isRequired,
|
||||
options: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
pluginOptions: PropTypes.object,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default ListViewLayout;
|
||||
@ -78,6 +78,23 @@ export function toggleModalDelete() {
|
||||
};
|
||||
}
|
||||
|
||||
export const setLayout = layout => ({ layout, type: SET_LIST_LAYOUT });
|
||||
export const setLayout = contentType => {
|
||||
const { layouts, settings } = contentType;
|
||||
const defaultSort = `${settings.defaultSortBy}:${settings.defaultSortOrder}`;
|
||||
|
||||
return {
|
||||
contentType,
|
||||
displayedHeaders: layouts.list,
|
||||
type: SET_LIST_LAYOUT,
|
||||
// initParams needs to explicitly set in the action so that external
|
||||
// plugin can override this one.
|
||||
// For instance, the i18n plugin will catch this action in a middleware and enhance it with a "locale" key
|
||||
initialParams: {
|
||||
page: 1,
|
||||
pageSize: settings.pageSize,
|
||||
_sort: defaultSort,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const onChangeListHeaders = target => ({ type: ON_CHANGE_LIST_HEADERS, target });
|
||||
|
||||
@ -8,7 +8,6 @@ import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Header } from '@buffetjs/custom';
|
||||
import { Flex, Padded } from '@buffetjs/core';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { stringify } from 'qs';
|
||||
import {
|
||||
PopUpWarning,
|
||||
request,
|
||||
@ -53,7 +52,7 @@ import {
|
||||
} from './actions';
|
||||
import makeSelectListView from './selectors';
|
||||
|
||||
import { getAllAllowedHeaders, getFirstSortableHeader } from './utils';
|
||||
import { getAllAllowedHeaders, getFirstSortableHeader, buildQueryString } from './utils';
|
||||
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
function ListView({
|
||||
@ -80,21 +79,14 @@ function ListView({
|
||||
onResetListHeaders,
|
||||
pagination: { total },
|
||||
resetProps,
|
||||
setLayout,
|
||||
slug,
|
||||
initialParams,
|
||||
}) {
|
||||
const {
|
||||
contentType: {
|
||||
attributes,
|
||||
metadatas,
|
||||
settings: {
|
||||
defaultSortBy,
|
||||
defaultSortOrder,
|
||||
bulkable: isBulkable,
|
||||
filterable: isFilterable,
|
||||
searchable: isSearchable,
|
||||
pageSize: defaultPageSize,
|
||||
},
|
||||
settings: { bulkable: isBulkable, filterable: isFilterable, searchable: isSearchable },
|
||||
},
|
||||
} = layout;
|
||||
|
||||
@ -105,12 +97,9 @@ function ListView({
|
||||
isLoading: isLoadingForPermissions,
|
||||
allowedActions: { canCreate, canRead, canUpdate, canDelete },
|
||||
} = useUserPermissions(viewPermissions);
|
||||
const defaultSort = `${defaultSortBy}:${defaultSortOrder}`;
|
||||
const initParams = useMemo(() => ({ page: 1, pageSize: defaultPageSize, _sort: defaultSort }), [
|
||||
defaultPageSize,
|
||||
defaultSort,
|
||||
]);
|
||||
const [{ query, rawQuery }, setQuery] = useQueryParams(initParams);
|
||||
|
||||
const [{ query }, setQuery] = useQueryParams(initialParams);
|
||||
const params = buildQueryString(query);
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const { push } = useHistory();
|
||||
@ -131,22 +120,17 @@ function ListView({
|
||||
|
||||
const label = contentType.info.label;
|
||||
|
||||
const params = useMemo(() => {
|
||||
return rawQuery || `?${stringify(initParams, { encode: false })}`;
|
||||
}, [initParams, rawQuery]);
|
||||
|
||||
const firstSortableHeader = useMemo(() => getFirstSortableHeader(displayedHeaders), [
|
||||
displayedHeaders,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setLayout(layout);
|
||||
setFilterPickerState(false);
|
||||
|
||||
return () => {
|
||||
resetProps();
|
||||
};
|
||||
}, [layout, setLayout, resetProps]);
|
||||
}, [resetProps]);
|
||||
|
||||
// Using a ref to avoid requests being fired multiple times on slug on change
|
||||
// We need it because the hook as mulitple dependencies so it may run before the permissions have checked
|
||||
@ -528,6 +512,7 @@ ListView.propTypes = {
|
||||
toggleModalDelete: PropTypes.func.isRequired,
|
||||
toggleModalDeleteAll: PropTypes.func.isRequired,
|
||||
setLayout: PropTypes.func.isRequired,
|
||||
initialParams: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = makeSelectListView();
|
||||
|
||||
@ -35,6 +35,7 @@ export const initialState = {
|
||||
pagination: {
|
||||
total: 0,
|
||||
},
|
||||
initialParams: null,
|
||||
};
|
||||
|
||||
const listViewReducer = (state = initialState, action) =>
|
||||
@ -47,6 +48,7 @@ const listViewReducer = (state = initialState, action) =>
|
||||
contentType: state.contentType,
|
||||
initialDisplayedHeaders: state.initialDisplayedHeaders,
|
||||
displayedHeaders: state.displayedHeaders,
|
||||
initialParams: state.initialParams,
|
||||
};
|
||||
}
|
||||
|
||||
@ -166,11 +168,12 @@ const listViewReducer = (state = initialState, action) =>
|
||||
break;
|
||||
}
|
||||
case SET_LIST_LAYOUT: {
|
||||
const { contentType } = action.layout;
|
||||
const { contentType, displayedHeaders, initialParams } = action;
|
||||
|
||||
drafState.contentType = contentType;
|
||||
drafState.displayedHeaders = contentType.layouts.list;
|
||||
drafState.initialDisplayedHeaders = contentType.layouts.list;
|
||||
drafState.displayedHeaders = displayedHeaders;
|
||||
drafState.initialDisplayedHeaders = displayedHeaders;
|
||||
drafState.initialParams = initialParams;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ describe('CONTENT MANAGER | CONTAINERS | ListView | reducer', () => {
|
||||
contentType: {},
|
||||
initialDisplayedHeaders: [],
|
||||
displayedHeaders: [],
|
||||
initialParams: null,
|
||||
pagination: {
|
||||
total: 0,
|
||||
},
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Accepts an object of plugins options like: { i18n: { locale: 'en' }, other: {some: "value"} }
|
||||
* and transform it into an array looking like: [{ i18n: { locale: 'en' } }, { other: {some: "value"} }]
|
||||
*/
|
||||
const arrayOfPluginOptions = (pluginOptions = {}) =>
|
||||
Object.keys(pluginOptions || {}).map(key => ({ [key]: pluginOptions[key] }));
|
||||
|
||||
export default arrayOfPluginOptions;
|
||||
@ -0,0 +1,26 @@
|
||||
import { stringify } from 'qs';
|
||||
import arrayOfPluginOptions from './arrayOfPluginOptions';
|
||||
|
||||
/**
|
||||
* Creates a valid query string from an object of queryParams
|
||||
* This includes:
|
||||
* - a _where clause
|
||||
* - plugin options
|
||||
*/
|
||||
const buildQueryString = (queryParams = {}) => {
|
||||
const pluginOptionArray = arrayOfPluginOptions(queryParams.pluginOptions);
|
||||
const _where = queryParams._where || [];
|
||||
|
||||
/**
|
||||
* Extracting pluginOptions from the query since we don't want them to be part
|
||||
* of the url
|
||||
*/
|
||||
const { pluginOptions: _, ...otherQueryParams } = {
|
||||
...queryParams,
|
||||
_where: _where.concat(pluginOptionArray),
|
||||
};
|
||||
|
||||
return `?${stringify(otherQueryParams, { encode: false })}`;
|
||||
};
|
||||
|
||||
export default buildQueryString;
|
||||
@ -1,2 +1,4 @@
|
||||
export { default as getAllAllowedHeaders } from './getAllAllowedHeaders';
|
||||
export { default as getFirstSortableHeader } from './getFirstSortableHeader';
|
||||
export { default as arrayOfPluginOptions } from './arrayOfPluginOptions';
|
||||
export { default as buildQueryString } from './buildQueryString';
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
import buildQueryString from '../buildQueryString';
|
||||
|
||||
describe('buildQueryString', () => {
|
||||
it('creates a valid query string with default params', () => {
|
||||
const queryParams = {
|
||||
page: '1',
|
||||
pageSize: '10',
|
||||
_sort: 'name:ASC',
|
||||
};
|
||||
|
||||
const queryString = buildQueryString(queryParams);
|
||||
|
||||
expect(queryString).toBe('?page=1&pageSize=10&_sort=name:ASC');
|
||||
});
|
||||
|
||||
it('creates a valid query string with default params & plugin options', () => {
|
||||
const queryParams = {
|
||||
page: '1',
|
||||
pageSize: '10',
|
||||
_sort: 'name:ASC',
|
||||
pluginOptions: {
|
||||
i18n: { locale: 'en' },
|
||||
},
|
||||
};
|
||||
|
||||
const queryString = buildQueryString(queryParams);
|
||||
|
||||
expect(queryString).toBe('?page=1&pageSize=10&_sort=name:ASC&_where[0][i18n][locale]=en');
|
||||
});
|
||||
|
||||
it('creates a valid query string with a _where clause', () => {
|
||||
const queryParams = {
|
||||
page: '1',
|
||||
pageSize: '10',
|
||||
_sort: 'name:ASC',
|
||||
_where: [{ name: 'hello world' }],
|
||||
};
|
||||
|
||||
const queryString = buildQueryString(queryParams);
|
||||
|
||||
expect(queryString).toBe('?page=1&pageSize=10&_sort=name:ASC&_where[0][name]=hello world');
|
||||
});
|
||||
|
||||
it('creates a valid query string with a _where and plugin options', () => {
|
||||
const queryParams = {
|
||||
page: '1',
|
||||
pageSize: '10',
|
||||
_sort: 'name:ASC',
|
||||
_where: [{ name: 'hello world' }],
|
||||
pluginOptions: {
|
||||
i18n: { locale: 'en' },
|
||||
},
|
||||
};
|
||||
|
||||
const queryString = buildQueryString(queryParams);
|
||||
|
||||
expect(queryString).toBe(
|
||||
'?page=1&pageSize=10&_sort=name:ASC&_where[0][name]=hello world&_where[1][i18n][locale]=en'
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -14,6 +14,7 @@ const dtoFields = [
|
||||
'options',
|
||||
'pluginOptions',
|
||||
'attributes',
|
||||
'pluginOptions',
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
||||
@ -3,6 +3,7 @@ import { useSelector } from 'react-redux';
|
||||
import { Picker, Padded, Text, Flex } from '@buffetjs/core';
|
||||
import { Carret } from 'strapi-helper-plugin';
|
||||
import styled from 'styled-components';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const List = styled.ul`
|
||||
list-style-type: none;
|
||||
@ -33,10 +34,22 @@ const EllipsisParagraph = styled(Text)`
|
||||
text-align: left;
|
||||
`;
|
||||
|
||||
const selectContentManagerListViewPluginOptions = state =>
|
||||
state.get('content-manager_listView').contentType.pluginOptions;
|
||||
|
||||
const selectI18NLocales = state => state.get('i18n_locales').locales;
|
||||
|
||||
const LocalePicker = () => {
|
||||
const locales = useSelector(state => state.get('i18n_locales').locales);
|
||||
const pluginOptions = useSelector(selectContentManagerListViewPluginOptions);
|
||||
const locales = useSelector(selectI18NLocales);
|
||||
const [selected, setSelected] = useState(locales && locales[0]);
|
||||
|
||||
const isFieldLocalized = get(pluginOptions, 'i18n.localized', false);
|
||||
|
||||
if (!isFieldLocalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!locales || locales.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import extendCTBInitialDataMiddleware from './extendCTBInitialDataMiddleware';
|
||||
import extendCTBAttributeInitialDataMiddleware from './extendCTBAttributeInitialDataMiddleware';
|
||||
import localeQueryParamsMiddleware from './localeQueryParamsMiddleware';
|
||||
|
||||
const middlewares = [extendCTBInitialDataMiddleware, extendCTBAttributeInitialDataMiddleware];
|
||||
const middlewares = [
|
||||
extendCTBInitialDataMiddleware,
|
||||
extendCTBAttributeInitialDataMiddleware,
|
||||
localeQueryParamsMiddleware,
|
||||
];
|
||||
|
||||
export default middlewares;
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
import get from 'lodash/get';
|
||||
|
||||
const localeQueryParamsMiddleware = () => () => next => action => {
|
||||
if (action.type !== 'ContentManager/ListView/SET_LIST_LAYOUT ') {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
const isFieldLocalized = get(action, 'contentType.pluginOptions.i18n.localized', false);
|
||||
|
||||
if (!isFieldLocalized) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
if (action.initialParams.pluginOptions) {
|
||||
action.initialParams.pluginOptions.locale = 'en';
|
||||
} else {
|
||||
action.initialParams.pluginOptions = {
|
||||
locale: 'en',
|
||||
};
|
||||
}
|
||||
|
||||
return next(action);
|
||||
};
|
||||
|
||||
export default localeQueryParamsMiddleware;
|
||||
@ -0,0 +1,86 @@
|
||||
import localeQueryParamsMiddleware from '../localeQueryParamsMiddleware';
|
||||
|
||||
describe('localeQueryParamsMiddleware', () => {
|
||||
it('does nothing on unknown actions', () => {
|
||||
const middleware = localeQueryParamsMiddleware()();
|
||||
const nextFn = jest.fn();
|
||||
const action = { type: 'UNKNOWN' };
|
||||
|
||||
middleware(nextFn)(action);
|
||||
|
||||
expect(nextFn).toBeCalledWith(action);
|
||||
expect(action).toEqual({
|
||||
type: 'UNKNOWN',
|
||||
});
|
||||
});
|
||||
|
||||
it('does nothing when there s no i18n.localized key in the action', () => {
|
||||
const middleware = localeQueryParamsMiddleware()();
|
||||
const nextFn = jest.fn();
|
||||
const action = {
|
||||
type: 'ContentManager/ListView/SET_LIST_LAYOUT ',
|
||||
contentType: { pluginOptions: {} },
|
||||
|
||||
initialParams: {},
|
||||
};
|
||||
|
||||
middleware(nextFn)(action);
|
||||
|
||||
expect(nextFn).toBeCalledWith(action);
|
||||
expect(action).toEqual({
|
||||
contentType: { pluginOptions: {} },
|
||||
initialParams: {},
|
||||
type: 'ContentManager/ListView/SET_LIST_LAYOUT ',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a pluginOptions key with a locale when initialParams does not have a pluginOptions key and the field is localized', () => {
|
||||
const middleware = localeQueryParamsMiddleware()();
|
||||
const nextFn = jest.fn();
|
||||
const action = {
|
||||
type: 'ContentManager/ListView/SET_LIST_LAYOUT ',
|
||||
contentType: {
|
||||
pluginOptions: {
|
||||
i18n: { localized: true },
|
||||
},
|
||||
},
|
||||
initialParams: {},
|
||||
};
|
||||
|
||||
middleware(nextFn)(action);
|
||||
|
||||
expect(nextFn).toBeCalledWith(action);
|
||||
expect(action).toEqual({
|
||||
contentType: { pluginOptions: { i18n: { localized: true } } },
|
||||
initialParams: { pluginOptions: { locale: 'en' } },
|
||||
type: 'ContentManager/ListView/SET_LIST_LAYOUT ',
|
||||
});
|
||||
});
|
||||
|
||||
it('adds a key to pluginOptions with a locale when initialParams has a pluginOptions key and the field is localized', () => {
|
||||
const middleware = localeQueryParamsMiddleware()();
|
||||
const nextFn = jest.fn();
|
||||
const action = {
|
||||
type: 'ContentManager/ListView/SET_LIST_LAYOUT ',
|
||||
contentType: {
|
||||
pluginOptions: {
|
||||
i18n: { localized: true },
|
||||
},
|
||||
},
|
||||
initialParams: {
|
||||
pluginOptions: {
|
||||
hello: 'world',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
middleware(nextFn)(action);
|
||||
|
||||
expect(nextFn).toBeCalledWith(action);
|
||||
expect(action).toEqual({
|
||||
contentType: { pluginOptions: { i18n: { localized: true } } },
|
||||
initialParams: { pluginOptions: { locale: 'en', hello: 'world' } },
|
||||
type: 'ContentManager/ListView/SET_LIST_LAYOUT ',
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user