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 ( <> - - - {isVisible && ( - - )} - + + {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",