diff --git a/packages/core/admin/admin/src/content-manager/components/AttributeFilter/Filters.js b/packages/core/admin/admin/src/content-manager/components/AttributeFilter/Filters.js
index ee2755fe0c..6b00c4c71e 100644
--- a/packages/core/admin/admin/src/content-manager/components/AttributeFilter/Filters.js
+++ b/packages/core/admin/admin/src/content-manager/components/AttributeFilter/Filters.js
@@ -1,6 +1,6 @@
import React, { useRef, useState } from 'react';
-import { Box, Button } from '@strapi/design-system';
+import { Button } from '@strapi/design-system';
import { FilterListURLQuery, FilterPopoverURLQuery, useTracking } from '@strapi/helper-plugin';
import { Filter } from '@strapi/icons';
import PropTypes from 'prop-types';
@@ -21,25 +21,23 @@ const Filters = ({ displayedFilters }) => {
return (
<>
-
- }
- onClick={handleToggle}
- size="S"
- >
- {formatMessage({ id: 'app.utils.filters', defaultMessage: 'Filters' })}
-
- {isVisible && (
-
- )}
-
+ }
+ onClick={handleToggle}
+ size="S"
+ >
+ {formatMessage({ id: 'app.utils.filters', defaultMessage: 'Filters' })}
+
+ {isVisible && (
+
+ )}
>
);
diff --git a/packages/core/admin/admin/src/content-manager/pages/ListView/components/BulkActionButtons/SelectedEntriesModal/tests/index.test.js b/packages/core/admin/admin/src/content-manager/pages/ListView/components/BulkActionButtons/SelectedEntriesModal/tests/index.test.js
index fdf186fa87..f221fab7a9 100644
--- a/packages/core/admin/admin/src/content-manager/pages/ListView/components/BulkActionButtons/SelectedEntriesModal/tests/index.test.js
+++ b/packages/core/admin/admin/src/content-manager/pages/ListView/components/BulkActionButtons/SelectedEntriesModal/tests/index.test.js
@@ -235,6 +235,9 @@ describe('Bulk publish selected entries modal', () => {
await waitFor(() => {
expect(publishDialog).not.toBeInTheDocument();
+ });
+
+ await waitFor(() => {
expect(screen.queryByRole('gridcell', { name: 'Entry 1' })).not.toBeInTheDocument();
expect(screen.queryByRole('gridcell', { name: 'Entry 2' })).not.toBeInTheDocument();
expect(screen.queryByRole('gridcell', { name: 'Entry 3' })).not.toBeInTheDocument();
diff --git a/packages/core/admin/admin/src/content-manager/pages/ListView/components/FieldPicker/index.js b/packages/core/admin/admin/src/content-manager/pages/ListView/components/FieldPicker/index.js
index 70d54cb3dd..f68acfa6e9 100644
--- a/packages/core/admin/admin/src/content-manager/pages/ListView/components/FieldPicker/index.js
+++ b/packages/core/admin/admin/src/content-manager/pages/ListView/components/FieldPicker/index.js
@@ -1,81 +1,93 @@
import React from 'react';
-import { Select, Option, Box } from '@strapi/design-system';
-import { useTracking } from '@strapi/helper-plugin';
+import { Flex, BaseCheckbox, TextButton, Typography } from '@strapi/design-system';
+import { useCollator, useTracking } from '@strapi/helper-plugin';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { useDispatch, useSelector } from 'react-redux';
+import styled from 'styled-components';
-import { getTrad, checkIfAttributeIsDisplayable } from '../../../../utils';
-import { onChangeListHeaders } from '../../actions';
+import { checkIfAttributeIsDisplayable } from '../../../../utils';
+import { onChangeListHeaders, onResetListHeaders } from '../../actions';
import { selectDisplayedHeaders } from '../../selectors';
+const ChackboxWrapper = styled(Flex)`
+ :hover {
+ background-color: ${(props) => props.theme.colors.primary100};
+ }
+`;
+
export const FieldPicker = ({ layout }) => {
const dispatch = useDispatch();
const displayedHeaders = useSelector(selectDisplayedHeaders);
const { trackUsage } = useTracking();
- const { formatMessage } = useIntl();
-
- const allAllowedHeaders = getAllAllowedHeaders(layout.contentType.attributes).map((attrName) => {
- const metadatas = layout.contentType.metadatas[attrName].list;
-
- return {
- name: attrName,
- intlLabel: { id: metadatas.label, defaultMessage: metadatas.label },
- };
+ const { formatMessage, locale } = useIntl();
+ const formatter = useCollator(locale, {
+ sensitivity: 'base',
});
- const values = displayedHeaders.map(({ name }) => name);
+ const columns = Object.keys(layout.contentType.attributes)
+ .filter((name) => checkIfAttributeIsDisplayable(layout.contentType.attributes[name]))
+ .map((name) => ({
+ name,
+ label: layout.contentType.metadatas[name].list.label,
+ }))
+ .sort((a, b) => formatter.compare(a.label, b.label));
- const handleChange = (updatedValues) => {
+ const displayedHeaderKeys = displayedHeaders.map(({ name }) => name);
+
+ const handleChange = (name) => {
trackUsage('didChangeDisplayedFields');
+ dispatch(onChangeListHeaders({ name, value: displayedHeaderKeys.includes(name) }));
+ };
- // removing a header
- if (updatedValues.length < values.length) {
- const removedHeader = values.filter((value) => {
- return updatedValues.indexOf(value) === -1;
- });
-
- dispatch(onChangeListHeaders({ name: removedHeader[0], value: true }));
- } else {
- const addedHeader = updatedValues.filter((value) => {
- return values.indexOf(value) === -1;
- });
-
- dispatch(onChangeListHeaders({ name: addedHeader[0], value: false }));
- }
+ const handleReset = () => {
+ dispatch(onResetListHeaders());
};
return (
-
-
-
+
+
);
};
@@ -92,17 +104,3 @@ FieldPicker.propTypes = {
}).isRequired,
}).isRequired,
};
-
-const getAllAllowedHeaders = (attributes) => {
- const allowedAttributes = Object.keys(attributes).reduce((acc, current) => {
- const attribute = attributes[current];
-
- if (checkIfAttributeIsDisplayable(attribute)) {
- acc.push(current);
- }
-
- return acc;
- }, []);
-
- return allowedAttributes.sort();
-};
diff --git a/packages/core/admin/admin/src/content-manager/pages/ListView/components/FieldPicker/tests/index.test.js b/packages/core/admin/admin/src/content-manager/pages/ListView/components/FieldPicker/tests/index.test.js
new file mode 100644
index 0000000000..6671dc408d
--- /dev/null
+++ b/packages/core/admin/admin/src/content-manager/pages/ListView/components/FieldPicker/tests/index.test.js
@@ -0,0 +1,195 @@
+import React from 'react';
+
+import { lightTheme, ThemeProvider } from '@strapi/design-system';
+import { render as renderRTL, fireEvent } from '@testing-library/react';
+import { IntlProvider } from 'react-intl';
+import { Provider } from 'react-redux';
+import { combineReducers, createStore } from 'redux';
+
+import reducers from '../../../../../../reducers';
+import { FieldPicker } from '../index';
+
+const layout = {
+ contentType: {
+ attributes: {
+ id: { type: 'integer' },
+ name: { type: 'string' },
+ createdAt: { type: 'datetime' },
+ updatedAt: { type: 'datetime' },
+ },
+ metadatas: {
+ id: {
+ list: { label: 'id', searchable: true, sortable: true },
+ },
+ name: {
+ list: { label: 'name', searchable: true, sortable: true },
+ },
+ createdAt: {
+ list: { label: 'createdAt', searchable: true, sortable: true },
+ },
+ updatedAt: {
+ list: { label: 'updatedAt', searchable: true, sortable: true },
+ },
+ },
+ layouts: {
+ list: [],
+ },
+ options: {},
+ settings: {},
+ },
+};
+
+const render = () => ({
+ ...renderRTL(, {
+ wrapper({ children }) {
+ const rootReducer = combineReducers(reducers);
+
+ const store = createStore(rootReducer, {
+ 'content-manager_listView': {
+ contentType: {
+ attributes: {
+ id: { type: 'integer' },
+ name: { type: 'string' },
+ createdAt: { type: 'datetime' },
+ updatedAt: { type: 'datetime' },
+ },
+ metadatas: {
+ id: {
+ edit: {},
+ list: { label: 'id', searchable: true, sortable: true },
+ },
+ name: {
+ edit: {
+ label: 'name',
+ description: '',
+ placeholder: '',
+ visible: true,
+ editable: true,
+ },
+ list: { label: 'name', searchable: true, sortable: true },
+ },
+ createdAt: {
+ edit: {
+ label: 'createdAt',
+ description: '',
+ placeholder: '',
+ visible: false,
+ editable: true,
+ },
+ list: { label: 'createdAt', searchable: true, sortable: true },
+ },
+ updatedAt: {
+ edit: {
+ label: 'updatedAt',
+ description: '',
+ placeholder: '',
+ visible: false,
+ editable: true,
+ },
+ list: { label: 'updatedAt', searchable: true, sortable: true },
+ },
+ },
+ },
+ displayedHeaders: [
+ {
+ key: '__id_key__',
+ name: 'id',
+ fieldSchema: { type: 'integer' },
+ metadatas: { label: 'id', searchable: true, sortable: true },
+ },
+ ],
+ initialDisplayedHeaders: [
+ {
+ key: '__id_key__',
+ name: 'id',
+ fieldSchema: { type: 'integer' },
+ metadatas: { label: 'id', searchable: true, sortable: true },
+ },
+ ],
+ },
+ });
+
+ return (
+
+
+ {children}
+
+
+ );
+ },
+ }),
+});
+
+describe('FieldPicker', () => {
+ it('should contains all the headers', () => {
+ const { getAllByRole, getByRole } = render();
+
+ const checkboxes = getAllByRole('checkbox');
+ const { attributes } = layout.contentType;
+ const attributesKeys = Object.keys(attributes);
+
+ expect(checkboxes.length).toBe(attributesKeys.length);
+
+ // eslint-disable-next-line no-restricted-syntax
+ for (let attributeKey of attributesKeys) {
+ // for each attribute make sure you have a checkbox
+ const checkbox = getByRole('checkbox', {
+ name: attributeKey,
+ });
+
+ expect(checkbox).toBeInTheDocument();
+ }
+ });
+
+ it('should contains the initially selected headers', () => {
+ const { getByRole } = render();
+
+ const checkboxSelected = getByRole('checkbox', {
+ name: 'id',
+ });
+ const checkboxNotSelected = getByRole('checkbox', {
+ name: 'name',
+ });
+
+ expect(checkboxSelected).toHaveAttribute('checked');
+ expect(checkboxNotSelected).not.toHaveAttribute('checked');
+ });
+
+ it('should select an header', async () => {
+ const { getByRole } = render();
+
+ // User can toggle selected headers
+ const checkboxIdHeader = getByRole('checkbox', { name: 'id' });
+ const checkboxNameHeader = getByRole('checkbox', { name: 'name' });
+ expect(checkboxIdHeader).toBeChecked();
+
+ // User can unselect headers
+ fireEvent.click(checkboxIdHeader);
+
+ expect(checkboxIdHeader).not.toBeChecked();
+
+ // User can select headers
+ expect(checkboxNameHeader).not.toBeChecked();
+
+ fireEvent.click(checkboxNameHeader);
+
+ expect(checkboxNameHeader).toBeChecked();
+ });
+
+ it('should show inside the Popover the reset button and when clicked select the initial headers selected', async () => {
+ const { getByRole } = render();
+
+ // select a new header
+ const checkboxNameHeader = getByRole('checkbox', { name: 'name' });
+ fireEvent.click(checkboxNameHeader);
+
+ expect(checkboxNameHeader).toBeChecked();
+
+ const resetBtn = getByRole('button', {
+ name: 'Reset',
+ });
+ fireEvent.click(resetBtn);
+
+ expect(checkboxNameHeader).not.toBeChecked();
+ });
+});
diff --git a/packages/core/admin/admin/src/content-manager/pages/ListView/components/ViewSettingsMenu/index.js b/packages/core/admin/admin/src/content-manager/pages/ListView/components/ViewSettingsMenu/index.js
new file mode 100644
index 0000000000..00b9ea12c5
--- /dev/null
+++ b/packages/core/admin/admin/src/content-manager/pages/ListView/components/ViewSettingsMenu/index.js
@@ -0,0 +1,74 @@
+import React from 'react';
+
+import { Flex, IconButton, Popover } from '@strapi/design-system';
+import { CheckPermissions, LinkButton } from '@strapi/helper-plugin';
+import { Cog, Layer } from '@strapi/icons';
+import PropTypes from 'prop-types';
+import { useIntl } from 'react-intl';
+import { useSelector } from 'react-redux';
+
+import { selectAdminPermissions } from '../../../../../pages/App/selectors';
+import { FieldPicker } from '../FieldPicker';
+
+export const ViewSettingsMenu = ({ slug, layout }) => {
+ const [isVisible, setIsVisible] = React.useState(false);
+ const cogButtonRef = React.useRef();
+ const permissions = useSelector(selectAdminPermissions);
+ const { formatMessage } = useIntl();
+
+ const handleToggle = () => {
+ setIsVisible((prev) => !prev);
+ };
+
+ return (
+ <>
+ }
+ label={formatMessage({
+ id: 'components.ViewSettings.tooltip',
+ defaultMessage: 'View Settings',
+ })}
+ ref={cogButtonRef}
+ onClick={handleToggle}
+ />
+ {isVisible && (
+
+
+
+ }
+ to={`${slug}/configurations/list`}
+ variant="secondary"
+ >
+ {formatMessage({
+ id: 'app.links.configure-view',
+ defaultMessage: 'Configure the view',
+ })}
+
+
+
+
+
+
+ )}
+ >
+ );
+};
+
+ViewSettingsMenu.propTypes = {
+ slug: PropTypes.string.isRequired,
+ layout: PropTypes.shape({
+ contentType: PropTypes.shape({
+ attributes: PropTypes.object.isRequired,
+ metadatas: PropTypes.object.isRequired,
+ layouts: PropTypes.shape({
+ list: PropTypes.array.isRequired,
+ }).isRequired,
+ options: PropTypes.object.isRequired,
+ settings: PropTypes.object.isRequired,
+ }).isRequired,
+ }).isRequired,
+};
diff --git a/packages/core/admin/admin/src/content-manager/pages/ListView/components/ViewSettingsMenu/tests/index.test.js b/packages/core/admin/admin/src/content-manager/pages/ListView/components/ViewSettingsMenu/tests/index.test.js
new file mode 100644
index 0000000000..e83cba708e
--- /dev/null
+++ b/packages/core/admin/admin/src/content-manager/pages/ListView/components/ViewSettingsMenu/tests/index.test.js
@@ -0,0 +1,156 @@
+import React from 'react';
+
+import { lightTheme, ThemeProvider } from '@strapi/design-system';
+import { fireEvent, render as renderRTL, waitFor } from '@testing-library/react';
+import { createMemoryHistory } from 'history';
+import { IntlProvider } from 'react-intl';
+import { Provider } from 'react-redux';
+import { Router } from 'react-router-dom';
+import { combineReducers, createStore } from 'redux';
+
+import reducers from '../../../../../../reducers';
+import { ViewSettingsMenu } from '../index';
+
+const layout = {
+ contentType: {
+ attributes: {
+ id: { type: 'integer' },
+ name: { type: 'string' },
+ createdAt: { type: 'datetime' },
+ updatedAt: { type: 'datetime' },
+ },
+ metadatas: {
+ id: {
+ list: { label: 'id', searchable: true, sortable: true },
+ },
+ name: {
+ list: { label: 'name', searchable: true, sortable: true },
+ },
+ createdAt: {
+ list: { label: 'createdAt', searchable: true, sortable: true },
+ },
+ updatedAt: {
+ list: { label: 'updatedAt', searchable: true, sortable: true },
+ },
+ },
+ layouts: {
+ list: [],
+ },
+ options: {},
+ settings: {},
+ },
+};
+
+jest.mock('@strapi/helper-plugin', () => ({
+ ...jest.requireActual('@strapi/helper-plugin'),
+ // eslint-disable-next-line react/prop-types
+ CheckPermissions: ({ children }) =>
{children}
,
+}));
+
+const history = createMemoryHistory();
+
+const render = () => ({
+ ...renderRTL(, {
+ wrapper({ children }) {
+ const rootReducer = combineReducers(reducers);
+
+ const store = createStore(rootReducer, {
+ 'content-manager_listView': {
+ displayedHeaders: [],
+ },
+ admin_app: {
+ permissions: {
+ contentManager: {},
+ },
+ },
+ });
+
+ return (
+
+
+
+ {children}
+
+
+
+ );
+ },
+ }),
+});
+
+describe('Content Manager | List view | ViewSettingsMenu', () => {
+ it('should show the Cog Button', () => {
+ const { getByRole } = render();
+
+ const cogBtn = getByRole('button', {
+ name: 'View Settings',
+ });
+
+ expect(cogBtn).toBeInTheDocument();
+ });
+
+ it('should open the Popover when you click on the Cog Button', () => {
+ const { getByRole } = render();
+
+ const cogBtn = getByRole('button', {
+ name: 'View Settings',
+ });
+
+ fireEvent.click(cogBtn);
+
+ const configureViewLink = getByRole('link', {
+ name: 'Configure the view',
+ });
+
+ expect(configureViewLink).toBeInTheDocument();
+ });
+
+ it('should show inside the Popover the Configure the view link button', async () => {
+ const { getByRole } = render();
+
+ const cogBtn = getByRole('button', {
+ name: 'View Settings',
+ });
+
+ fireEvent.click(cogBtn);
+
+ const configureViewLink = getByRole('link', {
+ name: 'Configure the view',
+ });
+
+ expect(configureViewLink).toBeInTheDocument();
+
+ fireEvent.click(configureViewLink);
+ await waitFor(() => {
+ expect(history.location.pathname).toBe('/api::temp.temp/configurations/list');
+ });
+ });
+
+ it('should show inside the Popover the title Dysplayed fields title', async () => {
+ const { getByText, getByRole } = render();
+
+ const cogBtn = getByRole('button', {
+ name: 'View Settings',
+ });
+
+ fireEvent.click(cogBtn);
+
+ expect(getByText('Displayed fields')).toBeInTheDocument();
+ });
+
+ it('should show inside the Popover the reset button', () => {
+ const { getByRole } = render();
+
+ const cogBtn = getByRole('button', {
+ name: 'View Settings',
+ });
+
+ fireEvent.click(cogBtn);
+
+ const resetBtn = getByRole('button', {
+ name: 'Reset',
+ });
+
+ expect(resetBtn).toBeInTheDocument();
+ });
+});
diff --git a/packages/core/admin/admin/src/content-manager/pages/ListView/index.js b/packages/core/admin/admin/src/content-manager/pages/ListView/index.js
index 2910d78436..a305910704 100644
--- a/packages/core/admin/admin/src/content-manager/pages/ListView/index.js
+++ b/packages/core/admin/admin/src/content-manager/pages/ListView/index.js
@@ -1,9 +1,7 @@
import * as React from 'react';
import {
- IconButton,
Main,
- Box,
ActionLayout,
Button,
ContentLayout,
@@ -18,7 +16,6 @@ import {
} from '@strapi/design-system';
import {
NoPermissions,
- CheckPermissions,
SearchURLQuery,
useFetchClient,
useFocusWhenNavigate,
@@ -33,7 +30,7 @@ import {
PaginationURLQuery,
PageSizeURLQuery,
} from '@strapi/helper-plugin';
-import { ArrowLeft, Cog, Plus } from '@strapi/icons';
+import { ArrowLeft, Plus } from '@strapi/icons';
import axios, { AxiosError } from 'axios';
import isEqual from 'lodash/isEqual';
import PropTypes from 'prop-types';
@@ -43,11 +40,9 @@ import { useMutation } from 'react-query';
import { connect, useSelector } from 'react-redux';
import { useHistory, useLocation, Link as ReactRouterLink } from 'react-router-dom';
import { bindActionCreators, compose } from 'redux';
-import styled from 'styled-components';
import { INJECT_COLUMN_IN_TABLE } from '../../../exposedHooks';
import { useEnterprise } from '../../../hooks/useEnterprise';
-import { selectAdminPermissions } from '../../../pages/App/selectors';
import { InjectionZone } from '../../../shared/components';
import AttributeFilter from '../../components/AttributeFilter';
import { getTrad } from '../../utils';
@@ -56,18 +51,10 @@ import { getData, getDataSucceeded, onChangeListHeaders, onResetListHeaders } fr
import { Body } from './components/Body';
import BulkActionButtons from './components/BulkActionButtons';
import CellContent from './components/CellContent';
-import { FieldPicker } from './components/FieldPicker';
+import { ViewSettingsMenu } from './components/ViewSettingsMenu';
import makeSelectListView, { selectDisplayedHeaders } from './selectors';
import { buildValidGetParams } from './utils';
-const ConfigureLayoutBox = styled(Box)`
- svg {
- path {
- fill: ${({ theme }) => theme.colors.neutral900};
- }
- }
-`;
-
const REVIEW_WORKFLOW_COLUMNS_CE = null;
const REVIEW_WORKFLOW_COLUMNS_CELL_CE = () => null;
@@ -100,7 +87,6 @@ function ListView({
const fetchPermissionsRef = React.useRef(refetchPermissions);
const { notifyStatus } = useNotifyAT();
const { formatAPIError } = useAPIErrorHandler(getTrad);
- const permissions = useSelector(selectAdminPermissions);
useFocusWhenNavigate();
@@ -514,25 +500,7 @@ function ListView({
endActions={
<>
-
-
-
- {
- trackUsage('willEditListLayout');
- }}
- forwardedAs={ReactRouterLink}
- to={{ pathname: `${slug}/configurations/list`, search: pluginsQueryParams }}
- icon={}
- label={formatMessage({
- id: 'app.links.configure-view',
- defaultMessage: 'Configure the view',
- })}
- />
-
-
+
>
}
startActions={
diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json
index 3edb2e26db..6b1b8b91a5 100644
--- a/packages/core/admin/admin/src/translations/en.json
+++ b/packages/core/admin/admin/src/translations/en.json
@@ -612,7 +612,7 @@
"components.PageFooter.select": "Entries per page",
"components.ProductionBlocker.description": "For safety purposes we have to disable this plugin in other environments.",
"components.ProductionBlocker.header": "This plugin is only available in development.",
- "components.Search.placeholder": "Search...",
+ "components.ViewSettings.tooltip": "View settings",
"components.TableHeader.sort": "Sort on {label}",
"components.Wysiwyg.ToggleMode.markdown-mode": "Markdown mode",
"components.Wysiwyg.ToggleMode.preview-mode": "Preview mode",