Migration/ Configure LV add+remove field (#11247)

* add and delete view field

* removed submit succeded reducer test

* added test

* test add field + feedback fixes

* replaced select add field by simple menu + updated test

* made View its own component

* UI badges feedback fix

* feedback fixes
This commit is contained in:
ronronscelestes 2021-10-14 11:47:08 +02:00 committed by GitHub
parent 44e60f304e
commit 46e01ea7d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 4347 additions and 1415 deletions

View File

@ -0,0 +1,152 @@
import React, { useRef } from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { Row } from '@strapi/parts/Row';
import { Box } from '@strapi/parts/Box';
import { ButtonText } from '@strapi/parts/Text';
import { Stack } from '@strapi/parts/Stack';
import EditIcon from '@strapi/icons/EditIcon';
import CloseAlertIcon from '@strapi/icons/CloseAlertIcon';
import Drag from '@strapi/icons/Drag';
import { getTrad } from '../../../utils';
const ActionButton = styled.button`
display: flex;
align-items: center;
height: ${({ theme }) => theme.spaces[7]};
&:last-child {
padding: 0 ${({ theme }) => theme.spaces[3]};
}
`;
const DragButton = styled(ActionButton)`
padding: 0 ${({ theme }) => theme.spaces[3]};
border-right: 1px solid ${({ theme }) => theme.colors.neutral150};
cursor: all-scroll;
svg {
width: ${12 / 16}rem;
height: ${12 / 16}rem;
}
`;
const FieldContainer = styled(Row)`
max-height: ${32 / 16}rem;
cursor: pointer;
svg {
width: ${10 / 16}rem;
height: ${10 / 16}rem;
path {
fill: ${({ theme }) => theme.colors.neutral600};
}
}
&:hover {
background-color: ${({ theme }) => theme.colors.primary100};
border-color: ${({ theme }) => theme.colors.primary200};
svg {
path {
fill: ${({ theme }) => theme.colors.primary600};
}
}
${ButtonText} {
color: ${({ theme }) => theme.colors.primary600};
}
${DragButton} {
border-right: 1px solid ${({ theme }) => theme.colors.primary200};
}
}
`;
const FieldWrapper = styled(Box)`
&:last-child {
padding-right: ${({ theme }) => theme.spaces[3]};
}
`;
const DraggableCard = ({ title, onRemoveField }) => {
const { formatMessage } = useIntl();
const editButtonRef = useRef();
const cardTitle = title.length > 20 ? `${title.substring(0, 20)}...` : title;
const handleClickEditRow = () => {
if (editButtonRef.current) {
editButtonRef.current.click();
}
};
return (
<FieldWrapper>
<FieldContainer
borderColor="neutral150"
background="neutral100"
hasRadius
justifyContent="space-between"
onClick={handleClickEditRow}
>
<Stack horizontal size={3}>
<DragButton
aria-label={formatMessage(
{
id: getTrad('components.DraggableCard.move.field'),
defaultMessage: 'Move {item}',
},
{ item: title }
)}
type="button"
>
<Drag />
</DragButton>
<ButtonText>{cardTitle}</ButtonText>
</Stack>
<Row paddingLeft={3}>
<ActionButton
ref={editButtonRef}
onClick={e => {
e.stopPropagation();
console.log('edit');
}}
aria-label={formatMessage(
{
id: getTrad('components.DraggableCard.edit.field'),
defaultMessage: 'Edit {item}',
},
{ item: title }
)}
type="button"
>
<EditIcon />
</ActionButton>
<ActionButton
onClick={onRemoveField}
data-testid={`delete-${title}`}
aria-label={formatMessage(
{
id: getTrad('components.DraggableCard.delete.field'),
defaultMessage: 'Delete {item}',
},
{ item: title }
)}
type="button"
>
<CloseAlertIcon />
</ActionButton>
</Row>
</FieldContainer>
</FieldWrapper>
);
};
DraggableCard.propTypes = {
onRemoveField: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
};
export default DraggableCard;

View File

@ -0,0 +1,91 @@
import React from 'react';
import styled from 'styled-components';
import { PropTypes } from 'prop-types';
import { useIntl } from 'react-intl';
import { Box } from '@strapi/parts/Box';
import { Row } from '@strapi/parts/Row';
import { Stack } from '@strapi/parts/Stack';
import { H3 } from '@strapi/parts/Text';
import { SimpleMenu, MenuItem } from '@strapi/parts/SimpleMenu';
import { IconButton } from '@strapi/parts/IconButton';
import AddIcon from '@strapi/icons/AddIcon';
import DraggableCard from './DraggableCard';
import { getTrad } from '../../../utils';
const Flex = styled(Box)`
flex: ${({ size }) => size};
`;
const ScrollableContainer = styled(Flex)`
overflow-x: scroll;
overflow-y: hidden;
`;
const SelectContainer = styled(Flex)`
max-width: ${32 / 16}rem;
`;
const View = ({ listRemainingFields, displayedFields, handleAddField, handleRemoveField }) => {
const { formatMessage } = useIntl();
return (
<>
<Box paddingBottom={4}>
<H3 as="h2">
{formatMessage({
id: getTrad('containers.SettingPage.view'),
defaultMessage: 'View',
})}
</H3>
</Box>
<Row
paddingTop={4}
paddingLeft={4}
paddingRight={4}
borderColor="neutral300"
borderStyle="dashed"
borderWidth="1px"
hasRadius
>
<ScrollableContainer size="1" paddingBottom={4}>
<Stack horizontal size={3}>
{displayedFields.map((field, index) => (
<DraggableCard
onRemoveField={e => handleRemoveField(e, index)}
key={field}
title={field}
/>
))}
</Stack>
</ScrollableContainer>
<SelectContainer size="auto" paddingBottom={4}>
<SimpleMenu
label={formatMessage({
id: getTrad('components.FieldSelect.label'),
defaultMessage: 'Add a field',
})}
as={IconButton}
icon={<AddIcon />}
disabled={listRemainingFields.length <= 0}
data-testid="add-field"
>
{listRemainingFields.map(field => (
<MenuItem key={field} onClick={() => handleAddField(field)}>
{field}
</MenuItem>
))}
</SimpleMenu>
</SelectContainer>
</Row>
</>
);
};
View.propTypes = {
displayedFields: PropTypes.array.isRequired,
handleAddField: PropTypes.func.isRequired,
handleRemoveField: PropTypes.func.isRequired,
listRemainingFields: PropTypes.array.isRequired,
};
export default View;

View File

@ -1,10 +1,4 @@
import React, {
memo,
useContext,
// useMemo,
useReducer,
useState,
} from 'react';
import React, { memo, useContext, useReducer, useState } from 'react';
import PropTypes from 'prop-types';
import { useMutation } from 'react-query';
import isEqual from 'lodash/isEqual';
@ -21,35 +15,39 @@ import { Main } from '@strapi/parts/Main';
import { Button } from '@strapi/parts/Button';
import CheckIcon from '@strapi/icons/CheckIcon';
import BackIcon from '@strapi/icons/BackIcon';
// import LayoutDndProvider from '../../components/LayoutDndProvider';
import { checkIfAttributeIsDisplayable, getTrad } from '../../utils';
import ModelsContext from '../../contexts/ModelsContext';
import { usePluginsQueryParams } from '../../hooks';
import putCMSettingsLV from './utils/api';
import Settings from './components/Settings';
// import LayoutDndProvider from '../../components/LayoutDndProvider';
import View from './components/View';
import init from './init';
import reducer, { initialState } from './reducer';
import { EXCLUDED_SORT_OPTIONS } from './utils/excludedSortOptions';
const ListSettingsView = ({ layout, slug }) => {
const { formatMessage } = useIntl();
const { trackUsage } = useTracking();
const pluginsQueryParams = usePluginsQueryParams();
const toggleNotification = useNotification();
const { refetchData } = useContext(ModelsContext);
const [showWarningSubmit, setWarningSubmit] = useState(false);
const toggleWarningSubmit = () => setWarningSubmit(prevState => !prevState);
const [reducerState, dispatch] = useReducer(reducer, initialState, () =>
init(initialState, layout)
);
// const [isOpen, setIsOpen] = useState(false);
// const [isModalFormOpen, setIsModalFormOpen] = useState(false);
// const [isDraggingSibling, setIsDraggingSibling] = useState(false);
const { formatMessage } = useIntl();
const { trackUsage } = useTracking();
// const toggleModalForm = () => setIsModalFormOpen(prevState => !prevState);
const {
// labelForm,
// labelToEdit,
initialData,
modifiedData,
} = reducerState;
// const metadatas = get(modifiedData, ['metadatas'], {});
// const attributes = useMemo(() => {
// return get(modifiedData, ['attributes'], {});
@ -57,11 +55,7 @@ const ListSettingsView = ({ layout, slug }) => {
const { attributes } = layout;
// const displayedFields = useMemo(() => {
// return get(modifiedData, ['layouts', 'list'], []);
// }, [modifiedData]);
// const excludedSortOptions = ['media', 'richtext', 'dynamiczone', 'relation', 'component', 'json'];
const displayedFields = modifiedData.layouts.list;
const sortOptions = Object.entries(attributes).reduce((acc, cur) => {
const [name, { type }] = cur;
@ -73,19 +67,6 @@ const ListSettingsView = ({ layout, slug }) => {
return acc;
}, []);
// const listRemainingFields = useMemo(() => {
// return Object.keys(metadatas)
// .filter(key => {
// return checkIfAttributeIsDisplayable(get(attributes, key, {}));
// })
// .filter(field => {
// return !displayedFields.includes(field);
// })
// .sort();
// }, [displayedFields, attributes, metadatas]);
// console.log(displayedFields, listRemainingFields);
// const handleClickEditLabel = labelToEdit => {
// dispatch({
// type: 'SET_LABEL_TO_EDIT',
@ -108,15 +89,6 @@ const ListSettingsView = ({ layout, slug }) => {
});
};
const [showWarningSubmit, setWarningSubmit] = useState(false);
const toggleWarningSubmit = () => setWarningSubmit(prevState => !prevState);
const handleSubmit = e => {
e.preventDefault();
toggleWarningSubmit();
trackUsage('willSaveContentTypeLayout');
};
const goBackUrl = () => {
const {
settings: { pageSize, defaultSortBy, defaultSortOrder },
@ -136,24 +108,42 @@ const ListSettingsView = ({ layout, slug }) => {
return `/content-manager/${kind}/${uid}?${goBackSearch}`;
};
// const handleChangeEditLabel = ({ target: { name, value } }) => {
// dispatch({
// type: 'ON_CHANGE_LABEL_METAS',
// name,
// value,
// });
// };
const handleConfirm = async () => {
const body = pick(modifiedData, ['layouts', 'settings', 'metadatas']);
submitMutation.mutateAsync(body);
submitMutation.mutate(body);
};
const handleAddField = item => {
dispatch({
type: 'ADD_FIELD',
item,
});
};
const handleRemoveField = (e, index) => {
e.stopPropagation();
if (displayedFields.length === 1) {
toggleNotification({
type: 'info',
message: { id: getTrad('notification.info.minimumFields') },
});
} else {
dispatch({
type: 'REMOVE_FIELD',
index,
});
}
};
const handleSubmit = e => {
e.preventDefault();
toggleWarningSubmit();
trackUsage('willSaveContentTypeLayout');
};
const submitMutation = useMutation(body => putCMSettingsLV(body, slug), {
onSuccess: async () => {
dispatch({
type: 'SUBMIT_SUCCEEDED',
});
onSuccess: () => {
trackUsage('didEditListSettings');
refetchData();
},
@ -163,11 +153,46 @@ const ListSettingsView = ({ layout, slug }) => {
message: { id: 'notification.error' },
});
},
refetchActive: true,
});
const { isLoading: isSubmittingForm } = submitMutation;
const listRemainingFields = Object.entries(attributes)
.reduce((acc, cur) => {
const [attrName, fieldSchema] = cur;
const isDisplayable = checkIfAttributeIsDisplayable(fieldSchema);
const isAlreadyDisplayed = displayedFields.includes(attrName);
if (isDisplayable && !isAlreadyDisplayed) {
acc.push(attrName);
}
return acc;
}, [])
.sort();
// const handleClickEditLabel = labelToEdit => {
// dispatch({
// type: 'SET_LABEL_TO_EDIT',
// labelToEdit,
// });
// toggleModalForm();
// };
// const handleClosed = () => {
// dispatch({
// type: 'UNSET_LABEL_TO_EDIT',
// });
// };
// const handleChangeEditLabel = ({ target: { name, value } }) => {
// dispatch({
// type: 'ON_CHANGE_LABEL_METAS',
// name,
// value,
// });
// };
// const move = (originalIndex, atIndex) => {
// dispatch({
// type: 'MOVE_FIELD',
@ -238,12 +263,12 @@ const ListSettingsView = ({ layout, slug }) => {
</Button>
}
subtitle={formatMessage({
id: `components.SettingsViewWrapper.pluginHeader.description.list-settings`,
defaultMessage: `Define the settings of the list view.`,
id: getTrad('components.SettingsViewWrapper.pluginHeader.description.list-settings'),
defaultMessage: 'Define the settings of the list view.',
})}
title={formatMessage(
{
id: 'components.SettingsViewWrapper.pluginHeader.title',
id: getTrad('components.SettingsViewWrapper.pluginHeader.title'),
defaultMessage: 'Configure the view - {name}',
},
{ name: upperFirst(modifiedData.info.label) }
@ -264,14 +289,20 @@ const ListSettingsView = ({ layout, slug }) => {
onChange={handleChange}
sortOptions={sortOptions}
/>
<Box padding={6}>
<Box paddingTop={6} paddingBottom={6}>
<Divider />
</Box>
<View
listRemainingFields={listRemainingFields}
displayedFields={displayedFields}
handleAddField={handleAddField}
handleRemoveField={handleRemoveField}
/>
</Box>
</ContentLayout>
<ConfirmDialog
bodyText={{
id: 'content-manager.popUpWarning.warning.updateAllSettings',
id: getTrad('popUpWarning.warning.updateAllSettings'),
defaultMessage: 'This will modify all your settings',
}}
iconRightButton={<CheckIcon />}

View File

@ -1,6 +1,6 @@
import produce from 'immer'; // current
import set from 'lodash/set';
// import get from 'lodash/get';
import get from 'lodash/get';
// import { arrayMoveItem } from '../../utils';
const initialState = {
@ -14,13 +14,13 @@ const initialState = {
const reducer = (state = initialState, action) =>
// eslint-disable-next-line consistent-return
produce(state, draftState => {
// const layoutFieldListPath = ['modifiedData', 'layouts', 'list'];
const layoutFieldListPath = ['modifiedData', 'layouts', 'list'];
switch (action.type) {
// case 'ADD_FIELD': {
// const layoutFieldList = get(state, layoutFieldListPath, []);
// set(draftState, layoutFieldListPath, [...layoutFieldList, action.item]);
// break;
// }
case 'ADD_FIELD': {
const layoutFieldList = get(state, layoutFieldListPath, []);
set(draftState, layoutFieldListPath, [action.item, ...layoutFieldList]);
break;
}
// case 'MOVE_FIELD': {
// const layoutFieldList = get(state, layoutFieldListPath, []);
// const { originalIndex, atIndex } = action;
@ -43,15 +43,15 @@ const reducer = (state = initialState, action) =>
// draftState.modifiedData = state.initialData;
// break;
// }
// case 'REMOVE_FIELD': {
// const layoutFieldList = get(state, layoutFieldListPath, []);
// set(
// draftState,
// layoutFieldListPath,
// layoutFieldList.filter((_, index) => action.index !== index)
// );
// break;
// }
case 'REMOVE_FIELD': {
const layoutFieldList = get(state, layoutFieldListPath, []);
set(
draftState,
layoutFieldListPath,
layoutFieldList.filter((_, index) => action.index !== index)
);
break;
}
// case 'SET_LABEL_TO_EDIT': {
// const { labelToEdit } = action;
// draftState.labelToEdit = labelToEdit;
@ -78,10 +78,6 @@ const reducer = (state = initialState, action) =>
// set(draftState, [...fieldMetadataPath, 'sortable'], state.labelForm.sortable);
// break;
// }
case 'SUBMIT_SUCCEEDED': {
draftState.initialData = state.modifiedData;
break;
}
default:
return draftState;
}

View File

@ -19,21 +19,21 @@ describe('CONTENT MANAGER | CONTAINERS | ListSettingsView | reducer', () => {
expect(reducer(state, {})).toEqual(expected);
});
// describe('ADD_FIELD', () => {
// it('should add a field to the layout correctly', () => {
// const expected = {
// ...state,
// modifiedData: {
// layouts: {
// list: ['title'],
// },
// },
// };
// const action = { type: 'ADD_FIELD', item: 'title' };
describe('ADD_FIELD', () => {
it('should add a field to the layout correctly', () => {
const expected = {
...state,
modifiedData: {
layouts: {
list: ['title'],
},
},
};
const action = { type: 'ADD_FIELD', item: 'title' };
// expect(reducer(state, action)).toEqual(expected);
// });
// });
expect(reducer(state, action)).toEqual(expected);
});
});
// describe('MOVE_FIELD', () => {
// it('should replace the title by the description and vice-versa', () => {
@ -103,32 +103,32 @@ describe('CONTENT MANAGER | CONTAINERS | ListSettingsView | reducer', () => {
// });
// });
// describe('REMOVE_FIELD', () => {
// it('should remove the field', () => {
// state.modifiedData = {
// layouts: {
// list: ['id', 'description', 'title'],
// },
// settings: {
// defaultSortBy: 'id',
// },
// };
// const expected = {
// ...state,
// modifiedData: {
// layouts: {
// list: ['id', 'title'],
// },
// settings: {
// defaultSortBy: 'id',
// },
// },
// };
// const action = { type: 'REMOVE_FIELD', index: 1 };
describe('REMOVE_FIELD', () => {
it('should remove the field', () => {
state.modifiedData = {
layouts: {
list: ['id', 'description', 'title'],
},
settings: {
defaultSortBy: 'id',
},
};
const expected = {
...state,
modifiedData: {
layouts: {
list: ['id', 'title'],
},
settings: {
defaultSortBy: 'id',
},
},
};
const action = { type: 'REMOVE_FIELD', index: 1 };
// expect(reducer(state, action)).toEqual(expected);
// });
// });
expect(reducer(state, action)).toEqual(expected);
});
});
// describe('SET_LABEL_TO_EDIT', () => {
// it('should set the label form data of the field to edit', () => {
@ -250,45 +250,4 @@ describe('CONTENT MANAGER | CONTAINERS | ListSettingsView | reducer', () => {
// expect(reducer(state, action)).toEqual(expected);
// });
// });
describe('SUBMIT_SUCCEEDED', () => {
it('should submit the label and the sortable value of the field to edit', () => {
state.modifiedData = {
metadatas: {
cover: {
list: {
label: 'Cover',
sortable: false,
},
},
},
};
const expected = {
...state,
initialData: {
metadatas: {
cover: {
list: {
label: 'Cover',
sortable: false,
},
},
},
},
modifiedData: {
metadatas: {
cover: {
list: {
label: 'Cover',
sortable: false,
},
},
},
},
};
const action = { type: 'SUBMIT_SUCCEEDED' };
expect(reducer(state, action)).toEqual(expected);
});
});
});

View File

@ -432,6 +432,9 @@
"content-manager.api.id": "API ID",
"content-manager.components.AddFilterCTA.add": "Filters",
"content-manager.components.AddFilterCTA.hide": "Filters",
"content-manager.components.DraggableCard.edit.field": "Edit {item}",
"content-manager.components.DraggableCard.delete.field": "Delete {item}",
"content-manager.components.DraggableCard.move.field": "Move {item}",
"content-manager.components.DragHandle-label": "Drag",
"content-manager.components.DraggableAttr.edit": "Click to edit",
"content-manager.components.DynamicTable.row-line": "item line {number}",
@ -445,6 +448,7 @@
"content-manager.components.DynamicZone.required": "Component is required",
"content-manager.components.EmptyAttributesBlock.button": "Go to settings page",
"content-manager.components.EmptyAttributesBlock.description": "You can change your settings",
"content-manager.components.FieldSelect.label": "Add a field",
"content-manager.components.FieldItem.linkToComponentLayout": "Set the component's layout",
"content-manager.components.FilterOptions.button.apply": "Apply",
"content-manager.components.FiltersPickWrapper.PluginHeader.actions.apply": "Apply",