Skip to content

Commit ab93de5

Browse files
authored
Merge pull request #1225 from complexdatacollective/fix/cat-ord-sorting
Fix ordinal/categorical bin sorting
2 parents bdfd4a7 + 0974908 commit ab93de5

File tree

20 files changed

+233
-171
lines changed

20 files changed

+233
-171
lines changed

src/behaviours/__tests__/withPrompt.test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ describe('withPrompt', () => {
1818
},
1919
installedProtocols: {
2020
mockProtocol: {
21+
codebook: {
22+
node: {},
23+
edge: {},
24+
ego: {},
25+
},
2126
stages: [{}, {}],
2227
},
2328
},

src/behaviours/withPrompt.js

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,43 @@ import { bindActionCreators } from 'redux';
44
import PropTypes from 'prop-types';
55
import { actionCreators as sessionsActions } from '../ducks/modules/sessions';
66
import { getPromptIndexForCurrentSession } from '../selectors/session';
7-
import { getProtocolStages } from '../selectors/protocol';
7+
import { getAllVariableUUIDsByEntity, getProtocolStages } from '../selectors/protocol';
88
import { get } from '../utils/lodash-replacements';
9+
import { processProtocolSortRule } from '../utils/createSorter';
10+
11+
/**
12+
* Convert sort rules to new format. See `processProtocolSortRule` for details.
13+
* @param {Array} prompts
14+
* @param {Object} codebookVariables
15+
* @returns {Array}
16+
* @private
17+
*/
18+
const processSortRules = (prompts, codebookVariables) => {
19+
const sortProperties = ['bucketSortOrder', 'binSortOrder'];
20+
21+
return prompts.map((prompt) => {
22+
const sortOptions = {};
23+
sortProperties.forEach((property) => {
24+
const sortRules = get(prompt, property, []);
25+
sortOptions[property] = sortRules.map(processProtocolSortRule(codebookVariables));
26+
});
27+
return {
28+
...prompt,
29+
...sortOptions,
30+
};
31+
});
32+
};
933

1034
export default function withPrompt(WrappedComponent) {
1135
class WithPrompt extends Component {
1236
get prompts() {
13-
return get(this.props, ['stage', 'prompts']);
37+
const {
38+
codebookVariables,
39+
} = this.props;
40+
41+
const prompts = get(this.props, ['stage', 'prompts'], []);
42+
const processedPrompts = processSortRules(prompts, codebookVariables);
43+
return processedPrompts;
1444
}
1545

1646
get promptsCount() {
@@ -51,7 +81,8 @@ export default function withPrompt(WrappedComponent) {
5181
}
5282

5383
render() {
54-
const { promptIndex, ...rest } = this.props;
84+
const { promptIndex, codebookVariables, ...rest } = this.props;
85+
5586
return (
5687
<WrappedComponent
5788
prompt={this.prompt()}
@@ -82,6 +113,7 @@ export default function withPrompt(WrappedComponent) {
82113
return {
83114
promptIndex,
84115
stage: ownProps.stage || getProtocolStages(state)[ownProps.stageIndex],
116+
codebookVariables: getAllVariableUUIDsByEntity(state),
85117
};
86118
}
87119

src/components/MultiNodeBucket.js

Lines changed: 69 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import React, { Component } from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import PropTypes from 'prop-types';
3-
import { isEqual } from 'lodash';
43
import { TransitionGroup } from 'react-transition-group';
54
import { getCSSVariableAsNumber } from '@codaco/ui/lib/utils/CSSVariables';
65
import { entityPrimaryKeyProperty } from '@codaco/shared-consts';
@@ -12,106 +11,86 @@ import createSorter from '../utils/createSorter';
1211

1312
const EnhancedNode = DragSource(Node);
1413

15-
/**
16-
* Renders a list of Node.
17-
*/
18-
class MultiNodeBucket extends Component {
19-
constructor(props) {
20-
super(props);
14+
const MultiNodeBucket = (props) => {
15+
const {
16+
nodes,
17+
listId,
18+
sortOrder,
19+
nodeColor,
20+
label,
21+
itemType,
22+
} = props;
2123

22-
const sorter = createSorter(props.sortOrder);
23-
const sortedNodes = sorter(props.nodes);
24+
const [stagger, setStagger] = useState(true);
25+
const [exit, setExit] = useState(true);
26+
const [currentListId, setCurrentListId] = useState(null);
27+
const [sortedNodes, setSortedNodes] = useState([]);
2428

25-
this.state = {
26-
nodes: sortedNodes,
27-
stagger: true,
28-
exit: true,
29-
};
30-
31-
this.refreshTimer = null;
32-
}
29+
useEffect(() => {
30+
const sorter = createSorter(sortOrder); // Uses the new sortOrder via withPrompt
31+
const sorted = sorter(nodes);
3332

34-
// eslint-disable-next-line camelcase
35-
UNSAFE_componentWillReceiveProps(newProps) {
36-
const {
37-
nodes,
38-
listId,
39-
} = this.props;
40-
// Don't update if nodes are the same
41-
if (isEqual(newProps.nodes, nodes)) {
33+
// On first run, just set the nodes.
34+
if (!currentListId) {
35+
setSortedNodes(sorted);
36+
setCurrentListId(listId);
4237
return;
4338
}
4439

45-
const sorter = createSorter(newProps.sortOrder);
46-
const sortedNodes = sorter(newProps.nodes);
47-
48-
// if we provided the same id, then just update normally
49-
if (newProps.listId === listId) {
50-
this.setState({ exit: false }, () => {
51-
this.setState({ nodes: sortedNodes, stagger: false });
52-
});
40+
// if we provided the same list id, update immediately without exit or
41+
// stagger animations.
42+
if (listId === currentListId) {
43+
setExit(false);
44+
setStagger(false);
45+
setSortedNodes(sorted);
5346
return;
5447
}
5548

56-
// Otherwise, transition out and in again
57-
this.setState({ exit: true }, () => {
58-
this.setState(
59-
{ nodes: [], stagger: true },
60-
() => {
61-
if (this.refreshTimer) { clearTimeout(this.refreshTimer); }
62-
this.refreshTimer = setTimeout(
63-
() => this.setState({
64-
nodes: sortedNodes,
65-
stagger: true,
66-
}),
67-
getCSSVariableAsNumber('--animation-duration-slow-ms'),
68-
);
69-
},
70-
);
71-
});
72-
}
49+
// Otherwise, enable animations and update after a delay.
50+
setExit(true);
51+
setStagger(true);
52+
setSortedNodes([]);
7353

74-
render() {
75-
const {
76-
nodeColor,
77-
label,
78-
itemType,
79-
} = this.props;
54+
const refreshTimer = setTimeout(() => {
55+
setSortedNodes(sorted);
56+
setCurrentListId(listId);
57+
}, getCSSVariableAsNumber('--animation-duration-slow-ms'));
8058

81-
const {
82-
stagger,
83-
nodes,
84-
exit,
85-
} = this.state;
59+
// eslint-disable-next-line consistent-return
60+
return () => {
61+
if (refreshTimer) {
62+
clearTimeout(refreshTimer);
63+
}
64+
};
65+
}, [nodes, sortOrder, listId]);
8666

87-
return (
88-
<TransitionGroup
89-
className="node-list"
90-
exit={exit}
91-
>
92-
{
93-
nodes.slice(0, 3).map((node, index) => (
94-
<NodeTransition
95-
key={`${node[entityPrimaryKeyProperty]}_${index}`}
96-
index={index}
97-
stagger={stagger}
98-
>
99-
<EnhancedNode
100-
color={nodeColor}
101-
inactive={index !== 0}
102-
allowDrag={index === 0}
103-
label={`${label(node)}`}
104-
meta={() => ({ ...node, itemType })}
105-
scrollDirection={NO_SCROLL}
106-
{...node}
107-
/>
108-
</NodeTransition>
109-
))
110-
}
111-
</TransitionGroup>
112-
);
113-
}
114-
}
67+
return (
68+
<TransitionGroup
69+
className="node-list"
70+
exit={exit}
71+
>
72+
{
73+
sortedNodes.slice(0, 3).map((node, index) => (
74+
<NodeTransition
75+
key={`${node[entityPrimaryKeyProperty]}_${index}`}
76+
index={index}
77+
stagger={stagger}
78+
>
79+
<EnhancedNode
80+
color={nodeColor}
81+
inactive={index !== 0}
82+
allowDrag={index === 0}
83+
label={`${label(node)}`}
84+
meta={() => ({ ...node, itemType })}
85+
scrollDirection={NO_SCROLL}
86+
{...node}
87+
/>
88+
</NodeTransition>
89+
))
90+
}
91+
</TransitionGroup>
92+
);
93+
};
11594

11695
MultiNodeBucket.propTypes = {
11796
nodes: PropTypes.array,

src/components/NewFilterableListWrapper.js

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ import { entityAttributesProperty } from '@codaco/shared-consts';
88
import createSorter from '../utils/createSorter';
99
import { get } from '../utils/lodash-replacements';
1010

11-
export const getFilteredList = (items, filterTerm, propertyPath) => {
11+
export const getFilteredList = (items, filterTerm, searchPropertyPath) => {
1212
if (!filterTerm) { return items; }
1313

1414
const normalizedFilterTerm = filterTerm.toLowerCase();
1515

1616
return items.filter(
1717
(item) => {
18-
const itemAttributes = propertyPath ? Object.values(get(item, propertyPath, {}))
18+
const itemAttributes = searchPropertyPath ? Object.values(get(item, searchPropertyPath, {}))
1919
: Object.values(item);
2020
// Include in filtered list if any of the attribute property values
2121
// include the filter value
@@ -62,25 +62,32 @@ const itemVariants = {
6262
const NewFilterableListWrapper = (props) => {
6363
const {
6464
items,
65-
propertyPath,
65+
searchPropertyPath,
6666
ItemComponent,
67-
initialSortProperty,
68-
initialSortDirection,
6967
sortableProperties,
7068
loading,
7169
onFilterChange,
7270
} = props;
7371

72+
// Look for the property `default: true` on a sort rule, or use the first
73+
const defaultSortRule = () => {
74+
const defaultSort = sortableProperties.findIndex(
75+
(property) => property.default,
76+
);
77+
78+
return defaultSort > -1 ? defaultSort : 0;
79+
};
80+
7481
const [filterTerm, setFilterTerm] = useState(null);
75-
const [sortProperty, setSortProperty] = useState(initialSortProperty);
76-
const [sortAscending, setSortAscending] = useState(initialSortDirection === 'asc');
82+
const [sortRule, setSortRule] = useState(defaultSortRule());
83+
const [sortDirection, setSortDirection] = useState('asc');
7784

78-
const handleSetSortProperty = (property) => {
79-
if (sortProperty === property) {
80-
setSortAscending(!sortAscending);
85+
const handleSetSortProperty = (index) => {
86+
if (sortRule === index) {
87+
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
8188
} else {
82-
setSortAscending(true);
83-
setSortProperty(property);
89+
setSortRule(index);
90+
setSortDirection('asc');
8491
}
8592
};
8693

@@ -90,12 +97,14 @@ const NewFilterableListWrapper = (props) => {
9097
if (onFilterChange) { onFilterChange(value); }
9198
};
9299

93-
const filteredItems = onFilterChange ? items : getFilteredList(items, filterTerm, propertyPath);
100+
const filteredItems = onFilterChange
101+
? items : getFilteredList(items, filterTerm, searchPropertyPath);
94102

95103
const sortedItems = createSorter([{
96-
property: sortProperty,
97-
direction: sortAscending ? 'asc' : 'desc',
98-
}], {}, propertyPath)(filteredItems);
104+
property: sortableProperties[sortRule].variable,
105+
type: sortableProperties[sortRule].type,
106+
direction: sortDirection,
107+
}])(filteredItems);
99108

100109
return (
101110
<div className="new-filterable-list">
@@ -104,19 +113,19 @@ const NewFilterableListWrapper = (props) => {
104113
{(sortableProperties && sortableProperties.length > 0)
105114
&& (
106115
<div className="scroll-container">
107-
{sortableProperties.map((sortField) => (
116+
{sortableProperties.map((sortField, index) => (
108117
<div
109118
tabIndex="0"
110119
role="button"
111-
className={`filter-button ${sortProperty === sortField.variable ? 'filter-button--active' : ''}`}
120+
className={`filter-button ${sortRule === index ? 'filter-button--active' : ''}`}
112121
key={sortField.variable}
113-
onClick={() => handleSetSortProperty(sortField.variable)}
122+
onClick={() => handleSetSortProperty(index)}
114123
>
115124
{
116125
(sortField.label)
117126
}
118127
{
119-
sortProperty === sortField.variable && (sortAscending ? ' \u25B2' : ' \u25BC')
128+
sortRule === index && (sortDirection === 'asc' ? ' \u25B2' : ' \u25BC')
120129
}
121130
</div>
122131
))}
@@ -177,7 +186,7 @@ const NewFilterableListWrapper = (props) => {
177186
NewFilterableListWrapper.propTypes = {
178187
ItemComponent: PropTypes.elementType.isRequired,
179188
items: PropTypes.array.isRequired,
180-
propertyPath: PropTypes.string,
189+
searchPropertyPath: PropTypes.string,
181190
initialSortProperty: PropTypes.string.isRequired,
182191
initialSortDirection: PropTypes.oneOf(['asc', 'desc']),
183192
sortableProperties: PropTypes.array,
@@ -188,7 +197,7 @@ NewFilterableListWrapper.propTypes = {
188197

189198
NewFilterableListWrapper.defaultProps = {
190199
initialSortDirection: 'asc',
191-
propertyPath: entityAttributesProperty,
200+
searchPropertyPath: entityAttributesProperty,
192201
sortableProperties: [],
193202
loading: false,
194203
resetFilter: [],

0 commit comments

Comments
 (0)