Revert "List view: new cog button icon with the view settings in the list view page (#17551)" (#17601)

This reverts commit e786f44833aad311e0ef669a7e98aa21ced2bd0a.
This commit is contained in:
Simone 2023-08-08 12:05:25 +02:00 committed by GitHub
parent a92367510f
commit 3e0f6d3514
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 127 additions and 517 deletions

View File

@ -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,6 +21,7 @@ const Filters = ({ displayedFilters }) => {
return (
<>
<Box paddingTop={1} paddingBottom={1}>
<Button
variant="tertiary"
ref={buttonRef}
@ -38,6 +39,7 @@ const Filters = ({ displayedFilters }) => {
source={buttonRef}
/>
)}
</Box>
<FilterListURLQuery filtersSchema={displayedFilters} />
</>
);

View File

@ -233,9 +233,8 @@ describe('Bulk publish selected entries modal', () => {
await user.click(publishDialogButton);
await waitFor(() => {
expect(publishDialog).not.toBeInTheDocument();
await waitFor(async () => {
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();

View File

@ -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 (
<Flex as="fieldset" direction="column" alignItems="stretch" gap={3}>
<Flex justifyContent="space-between">
<Typography as="legend" variant="pi" fontWeight="bold">
{formatMessage({
id: 'containers.ListPage.displayedFields',
defaultMessage: 'Displayed fields',
})}
</Typography>
<TextButton onClick={handleReset}>
{formatMessage({
id: 'app.components.Button.reset',
defaultMessage: 'Reset',
})}
</TextButton>
</Flex>
<Flex direction="column" alignItems="stretch">
{columns.map((header) => {
const isActive = displayedHeaderKeys.includes(header.name);
return (
<ChackboxWrapper
wrap="wrap"
gap={2}
as="label"
background={isActive ? 'primary100' : 'transparent'}
hasRadius
padding={2}
key={header.name}
<Box paddingTop={1} paddingBottom={1}>
<Select
aria-label="change displayed fields"
value={values}
onChange={handleChange}
customizeContent={(values) =>
formatMessage(
{
id: getTrad('select.currently.selected'),
defaultMessage: '{count} currently selected',
},
{ count: values.length }
)
}
multi
size="S"
>
<BaseCheckbox
onChange={() => handleChange(header.name)}
value={isActive}
name={header.name}
/>
<Typography fontSize={1}>{header.label}</Typography>
</ChackboxWrapper>
{allAllowedHeaders.map((header) => {
return (
<Option key={header.name} value={header.name}>
{formatMessage({
id: header.intlLabel.id || header.name,
defaultMessage: header.intlLabel.defaultMessage || header.name,
})}
</Option>
);
})}
</Flex>
</Flex>
</Select>
</Box>
);
};
@ -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();
};

View File

@ -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(<FieldPicker layout={layout} />, {
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 (
<IntlProvider messages={{}} textComponent="span" locale="en">
<ThemeProvider theme={lightTheme}>
<Provider store={store}>{children}</Provider>
</ThemeProvider>
</IntlProvider>
);
},
}),
});
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();
});
});

View File

@ -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 (
<>
<IconButton
icon={<Cog />}
label={formatMessage({
id: 'components.ViewSettings.tooltip',
defaultMessage: 'View Settings',
})}
ref={cogButtonRef}
onClick={handleToggle}
/>
{isVisible && (
<Popover placement="bottom-end" source={cogButtonRef} onDismiss={handleToggle} padding={2}>
<Flex alignItems="stretch" direction="column" gap={3}>
<CheckPermissions
permissions={permissions.contentManager.collectionTypesConfigurations}
>
<LinkButton
size="S"
startIcon={<Layer />}
to={`${slug}/configurations/list`}
variant="secondary"
>
{formatMessage({
id: 'app.links.configure-view',
defaultMessage: 'Configure the view',
})}
</LinkButton>
</CheckPermissions>
<FieldPicker layout={layout} />
</Flex>
</Popover>
)}
</>
);
};
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,
};

View File

@ -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 }) => <div>{children}</div>,
}));
const history = createMemoryHistory();
const render = () => ({
...renderRTL(<ViewSettingsMenu layout={layout} slug="api::temp.temp" />, {
wrapper({ children }) {
const rootReducer = combineReducers(reducers);
const store = createStore(rootReducer, {
'content-manager_listView': {
displayedHeaders: [],
},
admin_app: {
permissions: {
contentManager: {},
},
},
});
return (
<Router history={history}>
<IntlProvider messages={{}} textComponent="span" locale="en">
<ThemeProvider theme={lightTheme}>
<Provider store={store}>{children}</Provider>
</ThemeProvider>
</IntlProvider>
</Router>
);
},
}),
});
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();
});
});

View File

@ -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={
<>
<InjectionZone area="contentManager.listView.actions" />
<ViewSettingsMenu slug={slug} layout={layout} />
<FieldPicker layout={layout} />
<CheckPermissions
permissions={permissions.contentManager.collectionTypesConfigurations}
>
<ConfigureLayoutBox paddingTop={1} paddingBottom={1}>
<IconButton
onClick={() => {
trackUsage('willEditListLayout');
}}
forwardedAs={ReactRouterLink}
to={{ pathname: `${slug}/configurations/list`, search: pluginsQueryParams }}
icon={<Cog />}
label={formatMessage({
id: 'app.links.configure-view',
defaultMessage: 'Configure the view',
})}
/>
</ConfigureLayoutBox>
</CheckPermissions>
</>
}
startActions={

View File

@ -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",