Skip to content

Commit

Permalink
Merge pull request #22 from maxime-rainville/pulls/0/react-sortable-hoc
Browse files Browse the repository at this point in the history
Add abaility to sort
  • Loading branch information
Maxime Rainville authored Sep 28, 2023
2 parents 94c787a + a0dae15 commit d33095e
Show file tree
Hide file tree
Showing 21 changed files with 531 additions and 70 deletions.
2 changes: 1 addition & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client/dist/styles/bundle.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion client/src/components/AbstractAnyField/anyFieldHOC.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,16 @@ export const stringifyData = (Component) => (({ data, value, ...props }) => {
if (typeof dataValue === 'string') {
dataValue = JSON.parse(dataValue);
}
return <Component dataStr={JSON.stringify(dataValue)} {...props} data={dataValue} />;

// Rebuild the data string and sort the entries to avoid doing a pointless
// GraphQL request when the sorting changes. Only the ManyAnyField needs this
const dataStr = JSON.stringify(
Array.isArray(dataValue) ?
[...dataValue].sort() :
dataValue
);

return <Component dataStr={dataStr} {...props} data={dataValue} />;
});


Expand Down
2 changes: 1 addition & 1 deletion client/src/components/AnyFieldBox/AnyFieldBox.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.any-field-box {
.form-control.any-field-box {
display: flex;
height: 54px;
background: white;
Expand Down
42 changes: 34 additions & 8 deletions client/src/components/AnyPicker/AnyPicker.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
.any-picker-menu {
.dropdown.any-picker-menu {
width: 100%;
height: 100%;
}

.any-picker-menu {

&.btn {
width: 100%;
height: 100%;
}

&.font-icon-any::before {
margin: $spacer-xs;
Expand All @@ -23,12 +30,14 @@

.any-picker-title {

display: flex;
align-items: center;
width: 100%;
text-align: left;
border: none;
margin-right: 0;
&.btn {
display: flex;
align-items: center;
width: 100%;
text-align: left;
border: none;
margin-right: 0;
}

&:hover, &:focus {
background: $gray-100;
Expand Down Expand Up @@ -80,4 +89,21 @@
&__url {
color: $link-color;
}

&__icon {
position: relative;
margin-right: 6px;
line-height: 0;
vertical-align: middle;
font-size: 1.231rem;
}

&__handle {
position: relative;
margin-right: 6px;
padding-right: 6px;
line-height: 0;
vertical-align: middle;
font-size: 1.231rem;
}
}
14 changes: 10 additions & 4 deletions client/src/components/AnyPicker/AnyPickerTitle.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import AllowedDataObjectClass from 'types/AllowedDataObjectClass';
import { Button } from 'reactstrap';
import AnyPickerTitleHandle from './AnyPickerTitleHandle';
import classnames from 'classnames';

const stopPropagation = (fn) => (e) => {
e.nativeEvent.stopImmediatePropagation();
Expand All @@ -15,13 +17,15 @@ const stopPropagation = (fn) => (e) => {
}
};

const AnyPickerTitle = ({ title, dataObjectClass, description, onClear, onClick, className, id }) => (
const AnyPickerTitle = ({ title, dataObjectClass, description, onClear, onClick, className, id, sortable }) => (
<Button
className={classnames('any-picker-title', `font-icon-${dataObjectClass.icon || 'link'}`, className)}
className={classnames('any-picker-title', className)}
color="secondary"
onClick={stopPropagation(onClick)}
id={id}
>
{sortable && <AnyPickerTitleHandle />}
<span className={`font-icon-${dataObjectClass.icon || 'link'} any-picker-title__icon` } />
<div className="any-picker-title__detail">
<div className="any-picker-title__title">{title}</div>
<small className="any-picker-title__type">
Expand All @@ -45,11 +49,13 @@ AnyPickerTitle.propTypes = {
dataObjectClass: AllowedDataObjectClass.isRequired,
description: PropTypes.string,
onClear: PropTypes.func,
onClick: PropTypes.func
onClick: PropTypes.func,
sortable: PropTypes.bool,
};

AnyPickerTitle.defaultProps = {
dataObjectClass: {}
dataObjectClass: {},
sortable: false,
};

export default AnyPickerTitle;
20 changes: 20 additions & 0 deletions client/src/components/AnyPicker/AnyPickerTitleHandle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* eslint-disable */
import i18n from 'i18n';
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { SortableHandle } from 'react-sortable-hoc';
import classnames from 'classnames';

const AnyPickerTitleHandle = SortableHandle(({ className }) => (
<span
className={classnames('any-picker-title__handle font-icon-drag-handle', className)}
aria-label='Reorder element' />
));

AnyPickerTitleHandle.propTypes = {
className: PropTypes.string,
};

AnyPickerTitleHandle.defaultProps = { };

export default AnyPickerTitleHandle;
55 changes: 42 additions & 13 deletions client/src/components/AnyPicker/tests/AnyPicker-story.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,44 @@ import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import AnyPicker from '../AnyPicker';

const types = [
{ key: 'cms', title: 'Page on this site' },
{ key: 'asset', title: 'File' },
{ key: 'external', title: 'External URL' },
{ key: 'mailto', title: 'Email address' },
];
const allowedDataObjectClasses = [
{
"key": "SilverStripe\\LinkField\\Models\\EmailLink",
"title": "Email Link",
"icon": "p-mail",
"modalHandler": null
},
{
"key": "SilverStripe\\LinkField\\Models\\ExternalLink",
"title": "External Link",
"icon": "external-link",
"modalHandler": null
},
{
"key": "SilverStripe\\LinkField\\Models\\FileLink",
"title": "File Link",
"icon": "menu-files",
"modalHandler": "InsertMediaModal"
},
{
"key": "SilverStripe\\LinkField\\Models\\PhoneLink",
"title": "Phone Link",
"icon": "link",
"modalHandler": null
},
{
"key": "SilverStripe\\LinkField\\Models\\SiteTreeLink",
"title": "Site Tree Link",
"icon": "page",
"modalHandler": null
}
]

const dataobject = {
title: 'Our people',
type: types[0],
description: '/about-us/people'
const dataObjectClass = {
"key": "SilverStripe\\LinkField\\Models\\EmailLink",
"title": "Email Link",
"icon": "p-mail",
"modalHandler": null
};

const onSelect = action('onSelect');
Expand All @@ -29,16 +56,18 @@ const onClear = action('onClear');
onClear.toString = () => 'onClear';

const props = {
types,
allowedDataObjectClasses,
onSelect,
onClear,
onEdit
onEdit,
baseDataObjectName: "Link",
id: '"Form_EditForm_MyTestLink"'
};

storiesOf('AnyField/AnyPicker', module)
.add('Initial', () => (
<AnyPicker {...props} />
))
.add('Selected', () => (
<AnyPicker {...props} dataobject={dataobject} />
<AnyPicker {...props} dataObjectClass={dataObjectClass} title="Contact us" description="hello@world.com" />
));
8 changes: 7 additions & 1 deletion client/src/components/ManyAnyField/ManyAnyField.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { v4 as uuidv4 } from 'uuid';
import AnyFieldData from '../../types/AnyFieldData';
import AbstractAnyField, { anyFieldPropTypes } from '../AbstractAnyField/AbstractAnyField';
import anyFieldHOC from '../AbstractAnyField/anyFieldHOC';
import { arrayMoveImmutable as arrayMove } from 'array-move';

/**
* Helper that matches dataobjects to their descriptions
Expand All @@ -24,10 +25,14 @@ function mergeAnyFieldDataWithDescription(datalist, descriptions, allowedDataObj
/**
* Renders a AnyField allowing the selection of multiple links.
*/
const ManyAnyField = (props) => {
const ManyAnyField = ({ sortable, ...props }) => {
const staticProps = {
buildProps: () => ({
dataobjects: mergeAnyFieldDataWithDescription(props.data, props.anyFieldDescriptions, props.allowedDataObjectClasses),
onSort: ({ oldIndex, newIndex }, event) => {
props.onChange(event, { id: props.id, value: JSON.stringify(arrayMove(props.data, oldIndex, newIndex)) });
},
sortable
}),
clearData: linkId => (
props.data.filter(({ ID }) => ID !== linkId)
Expand All @@ -52,6 +57,7 @@ const ManyAnyField = (props) => {
ManyAnyField.propTypes = {
...anyFieldPropTypes,
data: PropTypes.arrayOf(AnyFieldData),
sortable: PropTypes.bool,
};

export { ManyAnyField as Component };
Expand Down
45 changes: 45 additions & 0 deletions client/src/components/ManyAnyPicker/ManyAnyList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import AnyPickerMenu from '../AnyPicker/AnyPickerMenu';
import AnyPickerTitle from '../AnyPicker/AnyPickerTitle';
import AnyFieldBox from '../AnyFieldBox/AnyFieldBox';
import { SortableContainer, SortableElement } from 'react-sortable-hoc';


const SortablePicker = SortableElement((props) => (
<AnyPickerTitle {...props} sortable />
));

const ManyAnyList = SortableContainer(({
dataobjects, onEdit, onClear, sortable
}) => {
const Picker = sortable ? SortablePicker : AnyPickerTitle;
return (
<AnyFieldBox className="multi-any-picker__list">
{ dataobjects.map(({ ID, ...dataobject }, index) => (
<Picker
{...dataobject}
className="multi-any-picker__dataobject"
key={`${ID} ${dataobject.description}`}
index={index}
onClear={(event) => onClear(event, ID)}
onClick={() => onEdit(ID)}
/>
)) }
</AnyFieldBox>
);
});


ManyAnyList.propTypes = {
...AnyPickerMenu.propTypes,
dataobjects: PropTypes.arrayOf(PropTypes.shape(AnyPickerTitle.propTypes)),
onEdit: PropTypes.func,
onClear: PropTypes.func,
sortable: PropTypes.func
};


export { ManyAnyList as Component };

export default ManyAnyList;
29 changes: 17 additions & 12 deletions client/src/components/ManyAnyPicker/ManyAnyPicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,29 @@ import PropTypes from 'prop-types';
import AnyPickerMenu from '../AnyPicker/AnyPickerMenu';
import AnyPickerTitle from '../AnyPicker/AnyPickerTitle';
import AnyFieldBox from '../AnyFieldBox/AnyFieldBox';
import ManyAnyList from './ManyAnyList';
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';


const ManyAnyPicker = ({
onSelect, allowedDataObjectClasses, dataobjects, onEdit, onClear,
baseDataObjectName, baseDataObjectIcon, id
baseDataObjectName, baseDataObjectIcon, id, onSort, sortable
}) => (
<div className="multi-any-picker" data-manyanyfield-id={id}>
<AnyFieldBox className="multi-any-picker__picker">
<AnyPickerMenu allowedDataObjectClasses={allowedDataObjectClasses} onSelect={onSelect} baseDataObjectName={baseDataObjectName} baseDataObjectIcon={baseDataObjectIcon} />
</AnyFieldBox>
{ dataobjects.length > 0 && <AnyFieldBox className="multi-any-picker__list">
{ dataobjects.map(({ ID, ...dataobject }) => (
<AnyPickerTitle
{...dataobject}
className="multi-any-picker__dataobject"
key={`${ID} ${dataobject.description}`}
onClear={(event) => onClear(event, ID)}
onClick={() => onEdit(ID)}
/>
)) }
</AnyFieldBox> }
{ dataobjects.length > 0 &&
<ManyAnyList
dataobjects={dataobjects}
onClear={onClear}
onEdit={onEdit}
useDragHandle
helperClass="sortableHelper"
onSortEnd={onSort}
sortable={sortable}
/>
}
</div>
);

Expand All @@ -32,7 +35,9 @@ ManyAnyPicker.propTypes = {
dataobjects: PropTypes.arrayOf(PropTypes.shape(AnyPickerTitle.propTypes)),
onEdit: PropTypes.func,
onClear: PropTypes.func,
onSort: PropTypes.func,
id: PropTypes.string.isRequired,
sortable: PropTypes.bool,
};


Expand Down
11 changes: 8 additions & 3 deletions client/src/components/ManyAnyPicker/ManyAnyPicker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@
margin-bottom: $spacer-xs;
}

&__list {
&__list.any-field-box {
flex-wrap: wrap;
height: auto;
}

&__dataobject {
&__dataobject.btn {
width: 100%;
border-top: 1px solid #ced5e1;
border-radius: 0;

&:first-child {
border-top: none;
}
}
}

.sortableHelper {
background-color: white;;
border-radius: .23rem !important;
border: solid 1px #ced5e1 !important;
}
Loading

0 comments on commit d33095e

Please sign in to comment.