Add dnd sort for list

This commit is contained in:
soupette 2019-07-08 15:40:01 +02:00
parent a481f3a059
commit c088a96560
9 changed files with 287 additions and 135 deletions

View File

@ -1,3 +1,6 @@
export const ON_CLICK_EDIT_FIELD = 'contentManager/SettingPage/ON_CLICK_EDIT_FIELD'; export const ON_CLICK_EDIT_FIELD =
export const ON_CLICK_EDIT_LIST_ITEM = 'contentManager/SettingPage/ON_CLICK_EDIT_LIST_ITEM'; 'contentManager/SettingPage/ON_CLICK_EDIT_FIELD';
export const ON_CLICK_EDIT_RELATION = 'contentManager/SettingPage/ON_CLICK_EDIT_RELATION'; export const ON_CLICK_EDIT_LIST_ITEM =
'contentManager/SettingPage/ON_CLICK_EDIT_LIST_ITEM';
export const ON_CLICK_EDIT_RELATION =
'contentManager/SettingPage/ON_CLICK_EDIT_RELATION';

View File

@ -1,6 +1,7 @@
import React, { memo, useState } from 'react'; import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Field, Wrapper, InfoLabel } from './components'; import { Field, InfoLabel } from './components';
import { DragSource, DropTarget } from 'react-dnd';
import GrabIcon from '../../assets/images/icon_grab.svg'; import GrabIcon from '../../assets/images/icon_grab.svg';
import GrabIconBlue from '../../assets/images/icon_grab_blue.svg'; import GrabIconBlue from '../../assets/images/icon_grab_blue.svg';
@ -8,21 +9,41 @@ import ClickOverHint from '../../components/ClickOverHint';
import RemoveIcon from '../../components/DraggedRemovedIcon'; import RemoveIcon from '../../components/DraggedRemovedIcon';
import EditIcon from '../../components/VariableEditIcon'; import EditIcon from '../../components/VariableEditIcon';
function ListField({ index, isSelected, label, name, onClick, onRemove }) { import ItemTypes from './itemsTypes';
function ListField({
index,
isDragging,
isSelected,
label,
name,
onClick,
onRemove,
connectDragSource,
connectDropTarget,
}) {
const opacity = isDragging ? 0.2 : 1;
const ref = useRef(null);
const [isOver, setIsOver] = useState(false); const [isOver, setIsOver] = useState(false);
const showLabel = const showLabel =
(!isOver || isSelected) && label.toLowerCase() !== name.toLowerCase(); (!isOver || isSelected) && label.toLowerCase() !== name.toLowerCase();
connectDragSource(ref);
connectDropTarget(ref);
return ( return (
<Wrapper <>
onMouseEnter={() => setIsOver(true)} <Field
onMouseLeave={() => setIsOver(false)} onMouseEnter={() => setIsOver(true)}
onClick={() => { onMouseLeave={() => setIsOver(false)}
onClick(index); onClick={() => {
}} onClick(index);
> }}
<div>{index + 1}.</div> ref={ref}
<Field isSelected={isSelected}> isSelected={isSelected}
style={{ opacity }}
>
<img src={isSelected ? GrabIconBlue : GrabIcon} /> <img src={isSelected ? GrabIconBlue : GrabIcon} />
<span>{name}</span> <span>{name}</span>
<ClickOverHint show={isOver && !isSelected} /> <ClickOverHint show={isOver && !isSelected} />
@ -39,23 +60,70 @@ function ListField({ index, isSelected, label, name, onClick, onRemove }) {
/> />
)} )}
</Field> </Field>
</Wrapper> </>
); );
} }
ListField.defaultProps = { ListField.defaultProps = {
label: '', label: '',
move: () => {},
onClick: () => {}, onClick: () => {},
onRemove: () => {}, onRemove: () => {},
}; };
ListField.propTypes = { ListField.propTypes = {
connectDragSource: PropTypes.func.isRequired,
connectDropTarget: PropTypes.func.isRequired,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
isDragging: PropTypes.bool.isRequired,
isSelected: PropTypes.bool.isRequired, isSelected: PropTypes.bool.isRequired,
label: PropTypes.string, label: PropTypes.string,
move: PropTypes.func,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
onClick: PropTypes.func, onClick: PropTypes.func,
onRemove: PropTypes.func, onRemove: PropTypes.func,
}; };
export default memo(ListField); export default DropTarget(
ItemTypes.FIELD,
{
canDrop: () => false,
hover(props, monitor) {
const { id: draggedId } = monitor.getItem();
const { name: overId } = props;
if (draggedId !== overId) {
const { index: overIndex } = props.findField(overId);
props.move(draggedId, overIndex);
}
},
},
connect => ({
connectDropTarget: connect.dropTarget(),
})
)(
DragSource(
ItemTypes.FIELD,
{
beginDrag: props => {
return {
id: props.name,
originalIndex: props.findField(props.name).index,
};
},
endDrag(props, monitor) {
const { id: droppedId, originalIndex } = monitor.getItem();
const didDrop = monitor.didDrop();
if (!didDrop) {
props.move(droppedId, originalIndex);
}
},
},
(connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
})
)(ListField)
);

View File

@ -1,26 +1,32 @@
import React from 'react'; import React, { Fragment, useCallback, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { get } from 'lodash'; import { get } from 'lodash';
import { DropTarget } from 'react-dnd';
import { InputsIndex as Input } from 'strapi-helper-plugin'; import { InputsIndex as Input } from 'strapi-helper-plugin';
import pluginId from '../../pluginId'; import pluginId from '../../pluginId';
import FormWrapper from '../../components/SettingFormWrapper'; import FormWrapper from '../../components/SettingFormWrapper';
import { Wrapper } from './components';
import Add from './Add'; import Add from './Add';
import ListField from './ListField'; import ListField from './ListField';
import ItemTypes from './itemsTypes';
function ListLayout({ function ListLayout({
addField, addField,
availableData, availableData,
connectDropTarget,
displayedData, displayedData,
fieldToEditIndex, fieldToEditIndex,
modifiedData, modifiedData,
moveListField,
onChange, onChange,
onClick, onClick,
onRemove, onRemove,
}) { }) {
const ref = useRef(null);
const handleRemove = index => { const handleRemove = index => {
if (displayedData.length > 1) { if (displayedData.length > 1) {
onRemove(index); onRemove(index);
@ -29,6 +35,7 @@ function ListLayout({
strapi.notification.info(`${pluginId}.notification.info.minimumFields`); strapi.notification.info(`${pluginId}.notification.info.minimumFields`);
}; };
const fieldName = displayedData[fieldToEditIndex]; const fieldName = displayedData[fieldToEditIndex];
const fieldPath = ['metadata', fieldName, 'list']; const fieldPath = ['metadata', fieldName, 'list'];
@ -56,19 +63,54 @@ function ListLayout({
validations: {}, validations: {},
}, },
]; ];
const findField = useCallback(
id => {
const field = displayedData.filter(current => current === id)[0];
return {
field,
index: displayedData.indexOf(field),
};
},
[displayedData]
);
const move = useCallback(
(id, atIndex) => {
const { index } = findField(id);
moveListField(index, atIndex);
},
[displayedData]
);
connectDropTarget(ref);
return ( return (
<> <>
<div className="col-lg-5 col-md-12"> <div className="col-lg-5 col-md-12" ref={ref}>
{displayedData.map((data, index) => ( {displayedData.map((data, index) => (
<ListField <Fragment key={data}>
key={data} <Wrapper style={{ display: 'flex' }}>
index={index} <div>{index + 1}.</div>
isSelected={fieldToEditIndex === index} <ListField
name={data} findField={findField}
label={get(modifiedData, ['metadata', data, 'list', 'label'], '')} index={index}
onClick={onClick} isSelected={fieldToEditIndex === index}
onRemove={handleRemove} move={move}
/> name={data}
label={get(
modifiedData,
['metadata', data, 'list', 'label'],
''
)}
onClick={onClick}
onRemove={handleRemove}
/>
</Wrapper>
<div style={{ marginBottom: '6px' }}></div>
</Fragment>
))} ))}
<Add data={availableData} onClick={addField} /> <Add data={availableData} onClick={addField} />
</div> </div>
@ -104,12 +146,16 @@ ListLayout.defaultProps = {
ListLayout.propTypes = { ListLayout.propTypes = {
addField: PropTypes.func, addField: PropTypes.func,
availableData: PropTypes.array, availableData: PropTypes.array,
connectDropTarget: PropTypes.func.isRequired,
displayedData: PropTypes.array, displayedData: PropTypes.array,
fieldToEditIndex: PropTypes.number, fieldToEditIndex: PropTypes.number,
modifiedData: PropTypes.object, modifiedData: PropTypes.object,
moveListField: PropTypes.func.isRequired,
onChange: PropTypes.func, onChange: PropTypes.func,
onClick: PropTypes.func, onClick: PropTypes.func,
onRemove: PropTypes.func, onRemove: PropTypes.func,
}; };
export default ListLayout; export default DropTarget(ItemTypes.FIELD, {}, connect => ({
connectDropTarget: connect.dropTarget(),
}))(ListLayout);

View File

@ -2,6 +2,7 @@ import {
ADD_FIELD_TO_LIST, ADD_FIELD_TO_LIST,
GET_DATA, GET_DATA,
GET_DATA_SUCCEEDED, GET_DATA_SUCCEEDED,
MOVE_FIELD_LIST,
ON_CHANGE, ON_CHANGE,
ON_REMOVE_LIST_FIELD, ON_REMOVE_LIST_FIELD,
ON_RESET, ON_RESET,
@ -32,6 +33,14 @@ export function getDataSucceeded(layout) {
}; };
} }
export function moveListField(dragIndex, overIndex) {
return {
type: MOVE_FIELD_LIST,
dragIndex,
overIndex,
};
}
export function onChange({ target: { name, value } }) { export function onChange({ target: { name, value } }) {
return { return {
type: ON_CHANGE, type: ON_CHANGE,

View File

@ -2,6 +2,7 @@ import styled, { css } from 'styled-components';
const Wrapper = styled.div` const Wrapper = styled.div`
display: flex; display: flex;
width: 100%;
> div:first-child { > div:first-child {
height: 30px; height: 30px;
width: 20px; width: 20px;
@ -9,13 +10,15 @@ const Wrapper = styled.div`
text-align: right; text-align: right;
line-height: 30px; line-height: 30px;
} }
> div:last-child {
flex-grow: 2;
}
`; `;
const Field = styled.div` const Field = styled.div`
position: relative; position: relative;
height: 30px; height: 30px;
width: 100%; width: 100%;
margin-bottom: 6px;
padding-left: 10px; padding-left: 10px;
justify-content: space-between; justify-content: space-between;
background: #fafafb; background: #fafafb;
@ -49,7 +52,7 @@ const InfoLabel = styled.div`
position: absolute; position: absolute;
top: 0; top: 0;
right: 40px; right: 40px;
color: #858b9a; // color: #858b9a;
font-weight: 400; font-weight: 400;
color: #007eff; color: #007eff;
`; `;

View File

@ -3,6 +3,8 @@ export const ADD_FIELD_TO_LIST =
export const GET_DATA = 'ContentManager/SettingViewModel/GET_DATA'; export const GET_DATA = 'ContentManager/SettingViewModel/GET_DATA';
export const GET_DATA_SUCCEEDED = export const GET_DATA_SUCCEEDED =
'ContentManager/SettingViewModel/GET_DATA_SUCCEEDED'; 'ContentManager/SettingViewModel/GET_DATA_SUCCEEDED';
export const MOVE_FIELD_LIST =
'ContentManager/SettingViewModel/MOVE_FIELD_LIST';
export const ON_CHANGE = 'ContentManager/SettingViewModel/ON_CHANGE'; export const ON_CHANGE = 'ContentManager/SettingViewModel/ON_CHANGE';
export const ON_REMOVE_LIST_FIELD = export const ON_REMOVE_LIST_FIELD =
'ContentManager/SettingViewModel/ON_REMOVE_LIST_FIELD'; 'ContentManager/SettingViewModel/ON_REMOVE_LIST_FIELD';

View File

@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux'; import { bindActionCreators, compose } from 'redux';
import { get, isEqual, isEmpty, upperFirst } from 'lodash'; import { get, isEqual, isEmpty, upperFirst } from 'lodash';
import { DndProvider } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import { import {
BackHeader, BackHeader,
@ -26,6 +28,7 @@ import Separator from './Separator';
import { import {
addFieldToList, addFieldToList,
getData, getData,
moveListField,
onChange, onChange,
onReset, onReset,
onSubmit, onSubmit,
@ -54,6 +57,7 @@ function SettingViewModel({
params: { name, settingType }, params: { name, settingType },
}, },
modifiedData, modifiedData,
moveListField,
onChange, onChange,
onRemoveListField, onRemoveListField,
onReset, onReset,
@ -131,113 +135,118 @@ function SettingViewModel({
return ( return (
<> <>
<BackHeader onClick={() => goBack()} /> <DndProvider backend={HTML5Backend}>
<Container className="container-fluid"> <BackHeader onClick={() => goBack()} />
<PluginHeader <Container className="container-fluid">
actions={getPluginHeaderActions()} <PluginHeader
title={{ actions={getPluginHeaderActions()}
id: `${pluginId}.containers.SettingViewModel.pluginHeader.title`, title={{
values: { name: upperFirst(name) }, id: `${pluginId}.containers.SettingViewModel.pluginHeader.title`,
}} values: { name: upperFirst(name) },
description={{
id:
'content-manager.containers.SettingPage.pluginHeaderDescription',
}}
/>
<HeaderNav
links={[
{
name: 'content-manager.containers.SettingPage.listSettings.title',
to: getUrl(name, 'list-settings'),
},
{
name: 'content-manager.containers.SettingPage.editSettings.title',
to: getUrl(name, 'edit-settings'),
},
]}
/>
<div className="row">
<Block
style={{
marginBottom: '13px',
paddingBottom: '30px',
paddingTop: '30px',
}} }}
> description={{
<SectionTitle isSettings /> id:
<div className="row"> 'content-manager.containers.SettingPage.pluginHeaderDescription',
{forms[settingType].map(input => { }}
return ( />
<Input <HeaderNav
key={input.name} links={[
{...input} {
onChange={onChange} name:
selectOptions={getSelectOptions(input)} 'content-manager.containers.SettingPage.listSettings.title',
value={get(modifiedData, input.name)} to: getUrl(name, 'list-settings'),
/> },
); {
})} name:
<div className="col-12"> 'content-manager.containers.SettingPage.editSettings.title',
<Separator /> to: getUrl(name, 'edit-settings'),
},
]}
/>
<div className="row">
<Block
style={{
marginBottom: '13px',
paddingBottom: '30px',
paddingTop: '30px',
}}
>
<SectionTitle isSettings />
<div className="row">
{forms[settingType].map(input => {
return (
<Input
key={input.name}
{...input}
onChange={onChange}
selectOptions={getSelectOptions(input)}
value={get(modifiedData, input.name)}
/>
);
})}
<div className="col-12">
<Separator />
</div>
</div> </div>
</div> <SectionTitle />
<SectionTitle />
<div className="row"> <div className="row">
<LayoutTitle className="col-12"> <LayoutTitle className="col-12">
<FormTitle <FormTitle
title={`${pluginId}.global.displayedFields`} title={`${pluginId}.global.displayedFields`}
description={`${pluginId}.containers.SettingPage.${ description={`${pluginId}.containers.SettingPage.${
settingType === 'list-settings' settingType === 'list-settings'
? 'attributes' ? 'attributes'
: 'editSettings' : 'editSettings'
}.description`} }.description`}
/> />
</LayoutTitle> </LayoutTitle>
{settingType === 'list-settings' && ( {settingType === 'list-settings' && (
<ListLayout <ListLayout
addField={addFieldToList} addField={addFieldToList}
displayedData={getListDisplayedFields()} displayedData={getListDisplayedFields()}
availableData={getListRemainingFields()} availableData={getListRemainingFields()}
fieldToEditIndex={listFieldToEditIndex} fieldToEditIndex={listFieldToEditIndex}
modifiedData={modifiedData} modifiedData={modifiedData}
onClick={setListFieldToEditIndex} moveListField={moveListField}
onChange={onChange} onClick={setListFieldToEditIndex}
onRemove={onRemoveListField} onChange={onChange}
/> onRemove={onRemoveListField}
)} />
</div> )}
</Block> </div>
</div> </Block>
</Container> </div>
<PopUpWarning </Container>
isOpen={showWarningCancel} <PopUpWarning
toggleModal={toggleWarningCancel} isOpen={showWarningCancel}
content={{ toggleModal={toggleWarningCancel}
title: 'content-manager.popUpWarning.title', content={{
message: 'content-manager.popUpWarning.warning.cancelAllSettings', title: 'content-manager.popUpWarning.title',
cancel: 'content-manager.popUpWarning.button.cancel', message: 'content-manager.popUpWarning.warning.cancelAllSettings',
confirm: 'content-manager.popUpWarning.button.confirm', cancel: 'content-manager.popUpWarning.button.cancel',
}} confirm: 'content-manager.popUpWarning.button.confirm',
popUpWarningType="danger" }}
onConfirm={() => { popUpWarningType="danger"
onReset(); onConfirm={() => {
toggleWarningCancel(); onReset();
}} toggleWarningCancel();
/> }}
<PopUpWarning />
isOpen={showWarningSubmit} <PopUpWarning
toggleModal={toggleWarningSubmit} isOpen={showWarningSubmit}
content={{ toggleModal={toggleWarningSubmit}
title: 'content-manager.popUpWarning.title', content={{
message: 'content-manager.popUpWarning.warning.updateAllSettings', title: 'content-manager.popUpWarning.title',
cancel: 'content-manager.popUpWarning.button.cancel', message: 'content-manager.popUpWarning.warning.updateAllSettings',
confirm: 'content-manager.popUpWarning.button.confirm', cancel: 'content-manager.popUpWarning.button.cancel',
}} confirm: 'content-manager.popUpWarning.button.confirm',
popUpWarningType="danger" }}
onConfirm={() => onSubmit(name, emitEvent)} popUpWarningType="danger"
/> onConfirm={() => onSubmit(name, emitEvent)}
/>
</DndProvider>
</> </>
); );
} }
@ -260,6 +269,7 @@ SettingViewModel.propTypes = {
}).isRequired, }).isRequired,
modifiedData: PropTypes.object.isRequired, modifiedData: PropTypes.object.isRequired,
moveListField: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onRemoveListField: PropTypes.func.isRequired, onRemoveListField: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired, onReset: PropTypes.func.isRequired,
@ -276,6 +286,7 @@ export function mapDispatchToProps(dispatch) {
{ {
addFieldToList, addFieldToList,
getData, getData,
moveListField,
onChange, onChange,
onRemoveListField, onRemoveListField,
onReset, onReset,

View File

@ -0,0 +1,3 @@
export default {
FIELD: 'field',
};

View File

@ -7,6 +7,7 @@ import { fromJS } from 'immutable';
import { import {
ADD_FIELD_TO_LIST, ADD_FIELD_TO_LIST,
GET_DATA_SUCCEEDED, GET_DATA_SUCCEEDED,
MOVE_FIELD_LIST,
ON_CHANGE, ON_CHANGE,
ON_REMOVE_LIST_FIELD, ON_REMOVE_LIST_FIELD,
ON_RESET, ON_RESET,
@ -34,6 +35,12 @@ function settingViewModelReducer(state = initialState, action) {
.update('initialData', () => fromJS(action.layout)) .update('initialData', () => fromJS(action.layout))
.update('isLoading', () => false) .update('isLoading', () => false)
.update('modifiedData', () => fromJS(action.layout)); .update('modifiedData', () => fromJS(action.layout));
case MOVE_FIELD_LIST:
return state.updateIn(['modifiedData', 'layouts', 'list'], list => {
return list
.delete(action.dragIndex)
.insert(action.overIndex, list.get(action.dragIndex));
});
case ON_CHANGE: case ON_CHANGE:
return state.updateIn(action.keys, () => action.value); return state.updateIn(action.keys, () => action.value);
case ON_REMOVE_LIST_FIELD: { case ON_REMOVE_LIST_FIELD: {