mirror of
				https://github.com/strapi/strapi.git
				synced 2025-11-04 03:43:34 +00:00 
			
		
		
		
	feat(cm): new view settings menu for the CM the list view
This commit is contained in:
		
							parent
							
								
									d25fed47e8
								
							
						
					
					
						commit
						20c49a1e8b
					
				@ -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 (
 | 
			
		||||
    <>
 | 
			
		||||
      <Box paddingTop={1} paddingBottom={1}>
 | 
			
		||||
        <Button
 | 
			
		||||
          variant="tertiary"
 | 
			
		||||
          ref={buttonRef}
 | 
			
		||||
          startIcon={<Filter />}
 | 
			
		||||
          onClick={handleToggle}
 | 
			
		||||
          size="S"
 | 
			
		||||
        >
 | 
			
		||||
          {formatMessage({ id: 'app.utils.filters', defaultMessage: 'Filters' })}
 | 
			
		||||
        </Button>
 | 
			
		||||
        {isVisible && (
 | 
			
		||||
          <FilterPopoverURLQuery
 | 
			
		||||
            displayedFilters={displayedFilters}
 | 
			
		||||
            isVisible={isVisible}
 | 
			
		||||
            onToggle={handleToggle}
 | 
			
		||||
            source={buttonRef}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      </Box>
 | 
			
		||||
      <Button
 | 
			
		||||
        variant="tertiary"
 | 
			
		||||
        ref={buttonRef}
 | 
			
		||||
        startIcon={<Filter />}
 | 
			
		||||
        onClick={handleToggle}
 | 
			
		||||
        size="S"
 | 
			
		||||
      >
 | 
			
		||||
        {formatMessage({ id: 'app.utils.filters', defaultMessage: 'Filters' })}
 | 
			
		||||
      </Button>
 | 
			
		||||
      {isVisible && (
 | 
			
		||||
        <FilterPopoverURLQuery
 | 
			
		||||
          displayedFilters={displayedFilters}
 | 
			
		||||
          isVisible={isVisible}
 | 
			
		||||
          onToggle={handleToggle}
 | 
			
		||||
          source={buttonRef}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      <FilterListURLQuery filtersSchema={displayedFilters} />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
 | 
			
		||||
@ -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 (
 | 
			
		||||
    <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"
 | 
			
		||||
      >
 | 
			
		||||
        {allAllowedHeaders.map((header) => {
 | 
			
		||||
    <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 (
 | 
			
		||||
            <Option key={header.name} value={header.name}>
 | 
			
		||||
              {formatMessage({
 | 
			
		||||
                id: header.intlLabel.id || header.name,
 | 
			
		||||
                defaultMessage: header.intlLabel.defaultMessage || header.name,
 | 
			
		||||
              })}
 | 
			
		||||
            </Option>
 | 
			
		||||
            <ChackboxWrapper
 | 
			
		||||
              wrap="wrap"
 | 
			
		||||
              gap={2}
 | 
			
		||||
              as="label"
 | 
			
		||||
              background={isActive ? 'primary100' : 'transparent'}
 | 
			
		||||
              hasRadius
 | 
			
		||||
              padding={2}
 | 
			
		||||
              key={header.name}
 | 
			
		||||
            >
 | 
			
		||||
              <BaseCheckbox
 | 
			
		||||
                onChange={() => handleChange(header.name)}
 | 
			
		||||
                value={isActive}
 | 
			
		||||
                name={header.name}
 | 
			
		||||
              />
 | 
			
		||||
              <Typography fontSize={1}>{header.label}</Typography>
 | 
			
		||||
            </ChackboxWrapper>
 | 
			
		||||
          );
 | 
			
		||||
        })}
 | 
			
		||||
      </Select>
 | 
			
		||||
    </Box>
 | 
			
		||||
      </Flex>
 | 
			
		||||
    </Flex>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -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(<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();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -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 (
 | 
			
		||||
    <>
 | 
			
		||||
      <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,
 | 
			
		||||
};
 | 
			
		||||
@ -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 }) => <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();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -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={
 | 
			
		||||
            <>
 | 
			
		||||
              <InjectionZone area="contentManager.listView.actions" />
 | 
			
		||||
              <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>
 | 
			
		||||
              <ViewSettingsMenu slug={slug} layout={layout} />
 | 
			
		||||
            </>
 | 
			
		||||
          }
 | 
			
		||||
          startActions={
 | 
			
		||||
 | 
			
		||||
@ -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",
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user