mirror of
https://github.com/strapi/strapi.git
synced 2025-11-24 14:11:05 +00:00
Add creator fields to filters and list view
This commit is contained in:
parent
c020a6c78d
commit
3b7d23f061
@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Combobox, ComboboxOption } from '@strapi/design-system';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { useAdminUsers } from '../../../hooks/useAdminUsers';
|
||||||
|
import { getDisplayName } from '../../utils';
|
||||||
|
|
||||||
|
const AdminUsersFilter = ({ value, onChange }) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const { users, isLoading } = useAdminUsers({});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Combobox
|
||||||
|
value={value}
|
||||||
|
aria-label={formatMessage({
|
||||||
|
id: 'content-manager.components.Filters.usersSelect.label',
|
||||||
|
defaultMessage: 'Search and select an user to filter',
|
||||||
|
})}
|
||||||
|
onChange={onChange}
|
||||||
|
loading={isLoading}
|
||||||
|
>
|
||||||
|
{users.map((user) => {
|
||||||
|
return (
|
||||||
|
<ComboboxOption key={user.id} value={user.id.toString()}>
|
||||||
|
{getDisplayName(user, formatMessage)}
|
||||||
|
</ComboboxOption>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Combobox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AdminUsersFilter.propTypes = {
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
value: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
AdminUsersFilter.defaultProps = {
|
||||||
|
value: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export { AdminUsersFilter };
|
||||||
@ -1,11 +1,17 @@
|
|||||||
import { findMatchingPermissions, useRBACProvider } from '@strapi/helper-plugin';
|
import { findMatchingPermissions, useRBACProvider, useCollator } from '@strapi/helper-plugin';
|
||||||
import get from 'lodash/get';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
const NOT_ALLOWED_FILTERS = ['json', 'component', 'media', 'richtext', 'dynamiczone', 'password'];
|
const NOT_ALLOWED_FILTERS = ['json', 'component', 'media', 'richtext', 'dynamiczone', 'password'];
|
||||||
const TIMESTAMPS = ['createdAt', 'updatedAt'];
|
const TIMESTAMPS = ['createdAt', 'updatedAt'];
|
||||||
|
const CREATOR_ATTRIBUTES = ['createdBy', 'updatedBy'];
|
||||||
|
|
||||||
const useAllowedAttributes = (contentType, slug) => {
|
const useAllowedAttributes = (contentType, slug) => {
|
||||||
const { allPermissions } = useRBACProvider();
|
const { allPermissions } = useRBACProvider();
|
||||||
|
const { locale } = useIntl();
|
||||||
|
|
||||||
|
const formatter = useCollator(locale, {
|
||||||
|
sensitivity: 'base',
|
||||||
|
});
|
||||||
|
|
||||||
const readPermissionsForSlug = findMatchingPermissions(allPermissions, [
|
const readPermissionsForSlug = findMatchingPermissions(allPermissions, [
|
||||||
{
|
{
|
||||||
@ -14,11 +20,18 @@ const useAllowedAttributes = (contentType, slug) => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const readPermissionForAttr = get(readPermissionsForSlug, ['0', 'properties', 'fields'], []);
|
const canReadAdminUsers =
|
||||||
const attributesArray = Object.keys(get(contentType, ['attributes']), {});
|
findMatchingPermissions(allPermissions, [
|
||||||
const allowedAttributes = attributesArray
|
{
|
||||||
.filter((attr) => {
|
action: 'admin::users.read',
|
||||||
const current = get(contentType, ['attributes', attr], {});
|
subject: null,
|
||||||
|
},
|
||||||
|
]).length > 0;
|
||||||
|
|
||||||
|
const attributesWithReadPermissions = readPermissionsForSlug?.[0]?.properties?.fields ?? [];
|
||||||
|
|
||||||
|
const allowedAttributes = attributesWithReadPermissions.filter((attr) => {
|
||||||
|
const current = contentType?.attributes?.[attr] ?? {};
|
||||||
|
|
||||||
if (!current.type) {
|
if (!current.type) {
|
||||||
return false;
|
return false;
|
||||||
@ -28,15 +41,16 @@ const useAllowedAttributes = (contentType, slug) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!readPermissionForAttr.includes(attr) && attr !== 'id' && !TIMESTAMPS.includes(attr)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
})
|
});
|
||||||
.sort();
|
const allowedAndDefaultAttributes = [
|
||||||
|
'id',
|
||||||
|
...allowedAttributes,
|
||||||
|
...TIMESTAMPS,
|
||||||
|
...(canReadAdminUsers ? CREATOR_ATTRIBUTES : []),
|
||||||
|
];
|
||||||
|
|
||||||
return allowedAttributes;
|
return allowedAndDefaultAttributes.sort((a, b) => formatter.compare(a, b));
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useAllowedAttributes;
|
export default useAllowedAttributes;
|
||||||
|
|||||||
@ -1,13 +1,41 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useQueryParams } from '@strapi/helper-plugin';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { useAdminUsers } from '../../../hooks/useAdminUsers';
|
||||||
|
import { getDisplayName } from '../../utils';
|
||||||
|
|
||||||
|
import { AdminUsersFilter } from './AdminUsersFilter';
|
||||||
import Filters from './Filters';
|
import Filters from './Filters';
|
||||||
import useAllowedAttributes from './hooks/useAllowedAttributes';
|
import useAllowedAttributes from './hooks/useAllowedAttributes';
|
||||||
|
|
||||||
|
const CREATOR_ATTRIBUTES = ['createdBy', 'updatedBy'];
|
||||||
|
|
||||||
const AttributeFilter = ({ contentType, slug, metadatas }) => {
|
const AttributeFilter = ({ contentType, slug, metadatas }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
const [{ query }] = useQueryParams();
|
||||||
|
// We get the users selected' ids
|
||||||
|
const selectedUsers =
|
||||||
|
query?.filters?.$and?.reduce((acc, filter) => {
|
||||||
|
const [key, value] = Object.entries(filter)[0];
|
||||||
|
const id = value.id?.$eq || value.id?.$ne;
|
||||||
|
|
||||||
|
if (CREATOR_ATTRIBUTES.includes(key) && !acc.includes(id)) {
|
||||||
|
acc.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []) ?? [];
|
||||||
|
const { users, isLoading } = useAdminUsers(
|
||||||
|
{ filter: { id: { in: selectedUsers } } },
|
||||||
|
{
|
||||||
|
enabled: selectedUsers.length > 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const allowedAttributes = useAllowedAttributes(contentType, slug);
|
const allowedAttributes = useAllowedAttributes(contentType, slug);
|
||||||
const displayedFilters = allowedAttributes.map((name) => {
|
const displayedFilters = allowedAttributes.map((name) => {
|
||||||
const attribute = contentType.attributes[name];
|
const attribute = contentType.attributes[name];
|
||||||
@ -20,14 +48,47 @@ const AttributeFilter = ({ contentType, slug, metadatas }) => {
|
|||||||
|
|
||||||
const { mainField, label } = metadatas[name].list;
|
const { mainField, label } = metadatas[name].list;
|
||||||
|
|
||||||
return {
|
const filter = {
|
||||||
name,
|
name,
|
||||||
metadatas: { label: formatMessage({ id: label, defaultMessage: label }) },
|
metadatas: { label: formatMessage({ id: label, defaultMessage: label }) },
|
||||||
fieldSchema: { type, options, mainField },
|
fieldSchema: { type, options, mainField },
|
||||||
trackedEvent,
|
trackedEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (attribute.type === 'relation' && attribute.target === 'admin::user') {
|
||||||
|
filter.metadatas = {
|
||||||
|
...filter.metadatas,
|
||||||
|
customOperators: [
|
||||||
|
{
|
||||||
|
intlLabel: { id: 'components.FilterOptions.FILTER_TYPES.$eq', defaultMessage: 'is' },
|
||||||
|
value: '$eq',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
intlLabel: {
|
||||||
|
id: 'components.FilterOptions.FILTER_TYPES.$ne',
|
||||||
|
defaultMessage: 'is not',
|
||||||
|
},
|
||||||
|
value: '$ne',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
customInput: AdminUsersFilter,
|
||||||
|
options: users.map((user) => ({
|
||||||
|
label: getDisplayName(user, formatMessage),
|
||||||
|
customValue: user.id.toString(),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
filter.fieldSchema.mainField = {
|
||||||
|
name: 'id',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return <Filters displayedFilters={displayedFilters} />;
|
return <Filters displayedFilters={displayedFilters} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,84 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||||
|
import { render, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { rest } from 'msw';
|
||||||
|
import { setupServer } from 'msw/node';
|
||||||
|
import { IntlProvider } from 'react-intl';
|
||||||
|
import { QueryClientProvider, QueryClient } from 'react-query';
|
||||||
|
|
||||||
|
import { AdminUsersFilter } from '../AdminUsersFilter';
|
||||||
|
|
||||||
|
const server = setupServer(
|
||||||
|
rest.get('*/admin/users', (req, res, ctx) => {
|
||||||
|
const mockUsers = [
|
||||||
|
{ id: 1, firstname: 'John', lastname: 'Doe' },
|
||||||
|
{ id: 2, firstname: 'Kai', lastname: 'Doe' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return res(
|
||||||
|
ctx.json({
|
||||||
|
data: {
|
||||||
|
results: mockUsers,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
const setup = (props) => {
|
||||||
|
return {
|
||||||
|
...render(<AdminUsersFilter {...props} />, {
|
||||||
|
wrapper: ({ children }) => (
|
||||||
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<IntlProvider locale="en" messages={{}} defaultLocale="en">
|
||||||
|
{children}
|
||||||
|
</IntlProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
user: userEvent.setup(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('AdminUsersFilter', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
server.listen();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render all the options fetched from the API', async () => {
|
||||||
|
const mockOnChange = jest.fn();
|
||||||
|
const { getByText, user, getByRole } = setup({ onChange: mockOnChange });
|
||||||
|
|
||||||
|
await user.click(getByRole('combobox'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByText('John Doe')).toBeInTheDocument();
|
||||||
|
expect(getByText('Kai Doe')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call the onChange function with the selected value', async () => {
|
||||||
|
const mockOnChange = jest.fn();
|
||||||
|
const { getByText, user, getByRole } = setup({ onChange: mockOnChange });
|
||||||
|
|
||||||
|
await user.click(getByRole('combobox'));
|
||||||
|
|
||||||
|
await waitFor(() => expect(getByText('John Doe')).toBeInTheDocument());
|
||||||
|
|
||||||
|
const option = getByText('John Doe');
|
||||||
|
|
||||||
|
await user.click(option);
|
||||||
|
|
||||||
|
expect(mockOnChange).toHaveBeenCalledWith('1');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -239,7 +239,14 @@ const reducer = (state, action) =>
|
|||||||
(value) => {
|
(value) => {
|
||||||
return value.type === 'relation';
|
return value.type === 'relation';
|
||||||
},
|
},
|
||||||
(_, { path }) => {
|
(value, { path }) => {
|
||||||
|
const relationFieldName = path[path.length - 1];
|
||||||
|
|
||||||
|
// When editing, we don't want to fetch the relations with creator fields because we already have it
|
||||||
|
if (value && (relationFieldName === 'createdBy' || relationFieldName === 'updatedBy')) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
if (state.modifiedData?.id === data.id && get(state.modifiedData, path)) {
|
if (state.modifiedData?.id === data.id && get(state.modifiedData, path)) {
|
||||||
return get(state.modifiedData, path);
|
return get(state.modifiedData, path);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,7 @@ import { useCMEditViewDataManager } from '@strapi/helper-plugin';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { getFullName } from '../../../../utils';
|
import { getTrad, getDisplayName } from '../../../utils';
|
||||||
import { getTrad } from '../../../utils';
|
|
||||||
|
|
||||||
import getUnits from './utils/getUnits';
|
import getUnits from './utils/getUnits';
|
||||||
|
|
||||||
@ -42,9 +41,13 @@ const KeyValuePair = ({ label, value }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
KeyValuePair.defaultProps = {
|
||||||
|
value: '-',
|
||||||
|
};
|
||||||
|
|
||||||
KeyValuePair.propTypes = {
|
KeyValuePair.propTypes = {
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Body = () => {
|
const Body = () => {
|
||||||
@ -53,18 +56,16 @@ const Body = () => {
|
|||||||
const currentTime = useRef(Date.now());
|
const currentTime = useRef(Date.now());
|
||||||
|
|
||||||
const getFieldInfo = (atField, byField) => {
|
const getFieldInfo = (atField, byField) => {
|
||||||
const { firstname, lastname, username } = initialData[byField] ?? {};
|
const user = initialData[byField] ?? {};
|
||||||
|
|
||||||
const userFirstname = firstname ?? '';
|
const displayName = getDisplayName(user, formatMessage);
|
||||||
const userLastname = lastname ?? '';
|
|
||||||
const user = username ?? getFullName(userFirstname, userLastname);
|
|
||||||
const timestamp = initialData[atField] ? new Date(initialData[atField]).getTime() : Date.now();
|
const timestamp = initialData[atField] ? new Date(initialData[atField]).getTime() : Date.now();
|
||||||
const elapsed = timestamp - currentTime.current;
|
const elapsed = timestamp - currentTime.current;
|
||||||
const { unit, value } = getUnits(-elapsed);
|
const { unit, value } = getUnits(-elapsed);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
at: formatRelativeTime(value, unit, { numeric: 'auto' }),
|
at: formatRelativeTime(value, unit, { numeric: 'auto' }),
|
||||||
by: isCreatingEntry ? '-' : user,
|
by: isCreatingEntry ? '-' : displayName,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -50,7 +50,7 @@ import { useEnterprise } from '../../../hooks/useEnterprise';
|
|||||||
import { selectAdminPermissions } from '../../../pages/App/selectors';
|
import { selectAdminPermissions } from '../../../pages/App/selectors';
|
||||||
import { InjectionZone } from '../../../shared/components';
|
import { InjectionZone } from '../../../shared/components';
|
||||||
import AttributeFilter from '../../components/AttributeFilter';
|
import AttributeFilter from '../../components/AttributeFilter';
|
||||||
import { getTrad } from '../../utils';
|
import { getTrad, getDisplayName } from '../../utils';
|
||||||
|
|
||||||
import { getData, getDataSucceeded, onChangeListHeaders, onResetListHeaders } from './actions';
|
import { getData, getDataSucceeded, onChangeListHeaders, onResetListHeaders } from './actions';
|
||||||
import { Body } from './components/Body';
|
import { Body } from './components/Body';
|
||||||
@ -648,6 +648,17 @@ function ListView({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (['createdBy', 'updatedBy'].includes(name.split('.')[0])) {
|
||||||
|
// Display the users full name
|
||||||
|
return (
|
||||||
|
<Td key={key}>
|
||||||
|
<Typography textColor="neutral800">
|
||||||
|
{getDisplayName(rowData[name.split('.')[0]], formatMessage)}
|
||||||
|
</Typography>
|
||||||
|
</Td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof cellFormatter === 'function') {
|
if (typeof cellFormatter === 'function') {
|
||||||
return (
|
return (
|
||||||
<Td key={key}>{cellFormatter(rowData, { key, name, ...rest })}</Td>
|
<Td key={key}>{cellFormatter(rowData, { key, name, ...rest })}</Td>
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Retrieves the display name of an admin panel user
|
||||||
|
* @typedef AdminUserNamesAttributes
|
||||||
|
* @property {string} firstname
|
||||||
|
* @property {string} lastname
|
||||||
|
* @property {string} username
|
||||||
|
* @property {string} email
|
||||||
|
*
|
||||||
|
* @type {(user: AdminUserNamesAttributes, formatMessage: import('react-intl').formatMessage) => string}
|
||||||
|
*/
|
||||||
|
const getDisplayName = ({ firstname, lastname, username, email }, formatMessage) => {
|
||||||
|
if (username) {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstname is not required if the user is created with a username
|
||||||
|
if (firstname) {
|
||||||
|
return formatMessage(
|
||||||
|
{
|
||||||
|
id: 'global.fullname',
|
||||||
|
defaultMessage: '{firstname} {lastname}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
firstname,
|
||||||
|
lastname,
|
||||||
|
}
|
||||||
|
).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return email;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getDisplayName };
|
||||||
@ -12,3 +12,4 @@ export { default as mergeMetasWithSchema } from './mergeMetasWithSchema';
|
|||||||
export { default as removeKeyInObject } from './removeKeyInObject';
|
export { default as removeKeyInObject } from './removeKeyInObject';
|
||||||
export { default as removePasswordFieldsFromData } from './removePasswordFieldsFromData';
|
export { default as removePasswordFieldsFromData } from './removePasswordFieldsFromData';
|
||||||
export { default as createYupSchema } from './schema';
|
export { default as createYupSchema } from './schema';
|
||||||
|
export { getDisplayName } from './getDisplayName';
|
||||||
|
|||||||
@ -671,6 +671,7 @@
|
|||||||
"content-manager.components.FiltersPickWrapper.PluginHeader.description": "Set the conditions to apply to filter the entries",
|
"content-manager.components.FiltersPickWrapper.PluginHeader.description": "Set the conditions to apply to filter the entries",
|
||||||
"content-manager.components.FiltersPickWrapper.PluginHeader.title.filter": "Filters",
|
"content-manager.components.FiltersPickWrapper.PluginHeader.title.filter": "Filters",
|
||||||
"content-manager.components.FiltersPickWrapper.hide": "Hide",
|
"content-manager.components.FiltersPickWrapper.hide": "Hide",
|
||||||
|
"content-manager.components.Filters.usersSelect.label": "Search and select a user to filter by",
|
||||||
"content-manager.components.LeftMenu.Search.label": "Search for a content type",
|
"content-manager.components.LeftMenu.Search.label": "Search for a content type",
|
||||||
"content-manager.components.LeftMenu.collection-types": "Collection Types",
|
"content-manager.components.LeftMenu.collection-types": "Collection Types",
|
||||||
"content-manager.components.LeftMenu.single-types": "Single Types",
|
"content-manager.components.LeftMenu.single-types": "Single Types",
|
||||||
@ -914,6 +915,7 @@
|
|||||||
"global.settings": "Settings",
|
"global.settings": "Settings",
|
||||||
"global.type": "Type",
|
"global.type": "Type",
|
||||||
"global.users": "Users",
|
"global.users": "Users",
|
||||||
|
"global.fullname": "{firstname} {lastname}",
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"notification.contentType.relations.conflict": "Content type has conflicting relations",
|
"notification.contentType.relations.conflict": "Content type has conflicting relations",
|
||||||
"notification.default.title": "Information:",
|
"notification.default.title": "Information:",
|
||||||
|
|||||||
@ -97,4 +97,21 @@ describe('Permissions Manager - Sanitize', () => {
|
|||||||
expect(result).toEqual({ c: 'Bar' });
|
expect(result).toEqual({ c: 'Bar' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Sanitize Query', () => {
|
||||||
|
it('Removes hidden fields on filters, sort, populate and fields', async () => {
|
||||||
|
const data = {
|
||||||
|
filters: { a: 'Foo', c: 'Bar' },
|
||||||
|
sort: { a: 'asc', c: 'desc' },
|
||||||
|
populate: { a: true, c: true },
|
||||||
|
fields: ['a', 'c'],
|
||||||
|
};
|
||||||
|
const result = await sanitizeHelpers.sanitizeQuery(data, { subject: fooModel.uid });
|
||||||
|
|
||||||
|
expect(result.filters).toEqual({ c: 'Bar' });
|
||||||
|
expect(result.sort).toEqual({ c: 'desc' });
|
||||||
|
expect(result.populate).toEqual({ c: true });
|
||||||
|
expect(result.fields).toEqual([undefined, 'c']);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -56,6 +56,7 @@ module.exports = ({ action, ability, model }) => {
|
|||||||
const sanitizeFilters = pipeAsync(
|
const sanitizeFilters = pipeAsync(
|
||||||
traverse.traverseQueryFilters(allowedFields(permittedFields), { schema }),
|
traverse.traverseQueryFilters(allowedFields(permittedFields), { schema }),
|
||||||
traverse.traverseQueryFilters(omitDisallowedAdminUserFields, { schema }),
|
traverse.traverseQueryFilters(omitDisallowedAdminUserFields, { schema }),
|
||||||
|
traverse.traverseQueryFilters(omitHiddenFields, { schema }),
|
||||||
traverse.traverseQueryFilters(removePassword, { schema }),
|
traverse.traverseQueryFilters(removePassword, { schema }),
|
||||||
traverse.traverseQueryFilters(
|
traverse.traverseQueryFilters(
|
||||||
({ key, value }, { remove }) => {
|
({ key, value }, { remove }) => {
|
||||||
@ -70,6 +71,7 @@ module.exports = ({ action, ability, model }) => {
|
|||||||
const sanitizeSort = pipeAsync(
|
const sanitizeSort = pipeAsync(
|
||||||
traverse.traverseQuerySort(allowedFields(permittedFields), { schema }),
|
traverse.traverseQuerySort(allowedFields(permittedFields), { schema }),
|
||||||
traverse.traverseQuerySort(omitDisallowedAdminUserFields, { schema }),
|
traverse.traverseQuerySort(omitDisallowedAdminUserFields, { schema }),
|
||||||
|
traverse.traverseQuerySort(omitHiddenFields, { schema }),
|
||||||
traverse.traverseQuerySort(removePassword, { schema }),
|
traverse.traverseQuerySort(removePassword, { schema }),
|
||||||
traverse.traverseQuerySort(
|
traverse.traverseQuerySort(
|
||||||
({ key, attribute, value }, { remove }) => {
|
({ key, attribute, value }, { remove }) => {
|
||||||
@ -84,11 +86,13 @@ module.exports = ({ action, ability, model }) => {
|
|||||||
const sanitizePopulate = pipeAsync(
|
const sanitizePopulate = pipeAsync(
|
||||||
traverse.traverseQueryPopulate(allowedFields(permittedFields), { schema }),
|
traverse.traverseQueryPopulate(allowedFields(permittedFields), { schema }),
|
||||||
traverse.traverseQueryPopulate(omitDisallowedAdminUserFields, { schema }),
|
traverse.traverseQueryPopulate(omitDisallowedAdminUserFields, { schema }),
|
||||||
|
traverse.traverseQueryPopulate(omitHiddenFields, { schema }),
|
||||||
traverse.traverseQueryPopulate(removePassword, { schema })
|
traverse.traverseQueryPopulate(removePassword, { schema })
|
||||||
);
|
);
|
||||||
|
|
||||||
const sanitizeFields = pipeAsync(
|
const sanitizeFields = pipeAsync(
|
||||||
traverse.traverseQueryFields(allowedFields(permittedFields), { schema }),
|
traverse.traverseQueryFields(allowedFields(permittedFields), { schema }),
|
||||||
|
traverse.traverseQueryFields(omitHiddenFields, { schema }),
|
||||||
traverse.traverseQueryFields(removePassword, { schema })
|
traverse.traverseQueryFields(removePassword, { schema })
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -256,13 +260,21 @@ module.exports = ({ action, ability, model }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getQueryFields = (fields = []) => {
|
const getQueryFields = (fields = []) => {
|
||||||
|
const nonVisibleAttributes = getNonVisibleAttributes(schema);
|
||||||
|
const writableAttributes = getWritableAttributes(schema);
|
||||||
|
|
||||||
|
const nonVisibleWritableAttributes = intersection(nonVisibleAttributes, writableAttributes);
|
||||||
|
|
||||||
return uniq([
|
return uniq([
|
||||||
...fields,
|
...fields,
|
||||||
...STATIC_FIELDS,
|
...STATIC_FIELDS,
|
||||||
...COMPONENT_FIELDS,
|
...COMPONENT_FIELDS,
|
||||||
|
...nonVisibleWritableAttributes,
|
||||||
CREATED_AT_ATTRIBUTE,
|
CREATED_AT_ATTRIBUTE,
|
||||||
UPDATED_AT_ATTRIBUTE,
|
UPDATED_AT_ATTRIBUTE,
|
||||||
PUBLISHED_AT_ATTRIBUTE,
|
PUBLISHED_AT_ATTRIBUTE,
|
||||||
|
CREATED_BY_ATTRIBUTE,
|
||||||
|
UPDATED_BY_ATTRIBUTE,
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -35,11 +35,12 @@ module.exports = () => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const formatAttributes = (contentType) => {
|
const formatAttributes = (contentType) => {
|
||||||
const { getVisibleAttributes, getTimestamps } = contentTypesUtils;
|
const { getVisibleAttributes, getTimestamps, getCreatorFields } = contentTypesUtils;
|
||||||
|
|
||||||
// only get attributes that can be seen in the auto generated Edit view or List view
|
// only get attributes that can be seen in the auto generated Edit view or List view
|
||||||
return getVisibleAttributes(contentType)
|
return getVisibleAttributes(contentType)
|
||||||
.concat(getTimestamps(contentType))
|
.concat(getTimestamps(contentType))
|
||||||
|
.concat(getCreatorFields(contentType))
|
||||||
.reduce((acc, key) => {
|
.reduce((acc, key) => {
|
||||||
const attribute = contentType.attributes[key];
|
const attribute = contentType.attributes[key];
|
||||||
|
|
||||||
|
|||||||
@ -86,6 +86,10 @@ const isVisible = (schema, name) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isCreatorField(schema, name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -108,6 +112,21 @@ const isTimestamp = (schema, name) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isCreatorField = (schema, name) => {
|
||||||
|
if (!_.has(schema.attributes, name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const creatorFields = contentTypesUtils.getCreatorFields(schema);
|
||||||
|
if (!creatorFields || !Array.isArray(creatorFields)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (creatorFields.includes(name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const isRelation = (attribute) => attribute.type === 'relation';
|
const isRelation = (attribute) => attribute.type === 'relation';
|
||||||
|
|
||||||
const hasRelationAttribute = (schema, name) => {
|
const hasRelationAttribute = (schema, name) => {
|
||||||
|
|||||||
@ -52,6 +52,20 @@ const getTimestamps = (model: Model) => {
|
|||||||
return attributes;
|
return attributes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCreatorFields = (model: Model) => {
|
||||||
|
const attributes = [];
|
||||||
|
|
||||||
|
if (has(CREATED_BY_ATTRIBUTE, model.attributes)) {
|
||||||
|
attributes.push(CREATED_BY_ATTRIBUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has(UPDATED_BY_ATTRIBUTE, model.attributes)) {
|
||||||
|
attributes.push(UPDATED_BY_ATTRIBUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
|
};
|
||||||
|
|
||||||
const getNonWritableAttributes = (model: Model) => {
|
const getNonWritableAttributes = (model: Model) => {
|
||||||
if (!model) return [];
|
if (!model) return [];
|
||||||
|
|
||||||
@ -202,6 +216,7 @@ export {
|
|||||||
getNonVisibleAttributes,
|
getNonVisibleAttributes,
|
||||||
getVisibleAttributes,
|
getVisibleAttributes,
|
||||||
getTimestamps,
|
getTimestamps,
|
||||||
|
getCreatorFields,
|
||||||
isVisibleAttribute,
|
isVisibleAttribute,
|
||||||
hasDraftAndPublish,
|
hasDraftAndPublish,
|
||||||
getOptions,
|
getOptions,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user