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 6b00c4c71e..ee2755fe0c 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 { Button } from '@strapi/design-system'; +import { Box, Button } from '@strapi/design-system'; import { FilterListURLQuery, FilterPopoverURLQuery, useTracking } from '@strapi/helper-plugin'; import { Filter } from '@strapi/icons'; import PropTypes from 'prop-types'; @@ -21,23 +21,25 @@ 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 84bbf87d14..fdf186fa87 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 @@ -233,9 +233,8 @@ describe('Bulk publish selected entries modal', () => { await user.click(publishDialogButton); - expect(publishDialog).not.toBeInTheDocument(); - - await waitFor(async () => { + await waitFor(() => { + expect(publishDialog).not.toBeInTheDocument(); 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 f68acfa6e9..70d54cb3dd 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,93 +1,81 @@ import React from 'react'; -import { Flex, BaseCheckbox, TextButton, Typography } from '@strapi/design-system'; -import { useCollator, useTracking } from '@strapi/helper-plugin'; +import { Select, Option, Box } from '@strapi/design-system'; +import { 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 { checkIfAttributeIsDisplayable } from '../../../../utils'; -import { onChangeListHeaders, onResetListHeaders } from '../../actions'; +import { getTrad, checkIfAttributeIsDisplayable } from '../../../../utils'; +import { onChangeListHeaders } 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, locale } = useIntl(); - const formatter = useCollator(locale, { - sensitivity: 'base', + 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 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 values = displayedHeaders.map(({ name }) => name); - const displayedHeaderKeys = displayedHeaders.map(({ name }) => name); - - const handleChange = (name) => { + const handleChange = (updatedValues) => { trackUsage('didChangeDisplayedFields'); - dispatch(onChangeListHeaders({ name, value: displayedHeaderKeys.includes(name) })); - }; - const handleReset = () => { - dispatch(onResetListHeaders()); + // 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 })); + } }; return ( - - - - {formatMessage({ - id: 'containers.ListPage.displayedFields', - defaultMessage: 'Displayed fields', - })} - - - - {formatMessage({ - id: 'app.components.Button.reset', - defaultMessage: 'Reset', - })} - - - - - {columns.map((header) => { - const isActive = displayedHeaderKeys.includes(header.name); - + + + ); }; @@ -104,3 +92,17 @@ 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 deleted file mode 100644 index 6671dc408d..0000000000 --- a/packages/core/admin/admin/src/content-manager/pages/ListView/components/FieldPicker/tests/index.test.js +++ /dev/null @@ -1,195 +0,0 @@ -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 deleted file mode 100644 index 00b9ea12c5..0000000000 --- a/packages/core/admin/admin/src/content-manager/pages/ListView/components/ViewSettingsMenu/index.js +++ /dev/null @@ -1,74 +0,0 @@ -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 deleted file mode 100644 index e83cba708e..0000000000 --- a/packages/core/admin/admin/src/content-manager/pages/ListView/components/ViewSettingsMenu/tests/index.test.js +++ /dev/null @@ -1,156 +0,0 @@ -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 ced9878e09..70d3ce2be7 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,7 +1,9 @@ import * as React from 'react'; import { + IconButton, Main, + Box, ActionLayout, Button, ContentLayout, @@ -16,6 +18,7 @@ import { } from '@strapi/design-system'; import { NoPermissions, + CheckPermissions, SearchURLQuery, useFetchClient, useFocusWhenNavigate, @@ -30,7 +33,7 @@ import { PaginationURLQuery, PageSizeURLQuery, } from '@strapi/helper-plugin'; -import { ArrowLeft, Plus } from '@strapi/icons'; +import { ArrowLeft, Cog, Plus } from '@strapi/icons'; import axios, { AxiosError } from 'axios'; import isEqual from 'lodash/isEqual'; import PropTypes from 'prop-types'; @@ -40,9 +43,11 @@ 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'; @@ -51,10 +56,18 @@ import { getData, getDataSucceeded, onChangeListHeaders, onResetListHeaders } fr import { Body } from './components/Body'; import BulkActionButtons from './components/BulkActionButtons'; import CellContent from './components/CellContent'; -import { ViewSettingsMenu } from './components/ViewSettingsMenu'; +import { FieldPicker } from './components/FieldPicker'; 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; @@ -87,6 +100,7 @@ function ListView({ const fetchPermissionsRef = React.useRef(refetchPermissions); const { notifyStatus } = useNotifyAT(); const { formatAPIError } = useAPIErrorHandler(getTrad); + const permissions = useSelector(selectAdminPermissions); useFocusWhenNavigate(); @@ -468,7 +482,25 @@ 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 6b1b8b91a5..3edb2e26db 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.ViewSettings.tooltip": "View settings", + "components.Search.placeholder": "Search...", "components.TableHeader.sort": "Sort on {label}", "components.Wysiwyg.ToggleMode.markdown-mode": "Markdown mode", "components.Wysiwyg.ToggleMode.preview-mode": "Preview mode",