Skip to content

Commit

Permalink
Merge pull request #1225 from complexdatacollective/fix/cat-ord-sorting
Browse files Browse the repository at this point in the history
Fix ordinal/categorical bin sorting
  • Loading branch information
buckhalt committed Apr 14, 2023
2 parents bdfd4a7 + 0974908 commit ab93de5
Show file tree
Hide file tree
Showing 20 changed files with 233 additions and 171 deletions.
5 changes: 5 additions & 0 deletions src/behaviours/__tests__/withPrompt.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ describe('withPrompt', () => {
},
installedProtocols: {
mockProtocol: {
codebook: {
node: {},
edge: {},
ego: {},
},
stages: [{}, {}],
},
},
Expand Down
38 changes: 35 additions & 3 deletions src/behaviours/withPrompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,43 @@ import { bindActionCreators } from 'redux';
import PropTypes from 'prop-types';
import { actionCreators as sessionsActions } from '../ducks/modules/sessions';
import { getPromptIndexForCurrentSession } from '../selectors/session';
import { getProtocolStages } from '../selectors/protocol';
import { getAllVariableUUIDsByEntity, getProtocolStages } from '../selectors/protocol';
import { get } from '../utils/lodash-replacements';
import { processProtocolSortRule } from '../utils/createSorter';

/**
* Convert sort rules to new format. See `processProtocolSortRule` for details.
* @param {Array} prompts
* @param {Object} codebookVariables
* @returns {Array}
* @private
*/
const processSortRules = (prompts, codebookVariables) => {
const sortProperties = ['bucketSortOrder', 'binSortOrder'];

return prompts.map((prompt) => {
const sortOptions = {};
sortProperties.forEach((property) => {
const sortRules = get(prompt, property, []);
sortOptions[property] = sortRules.map(processProtocolSortRule(codebookVariables));
});
return {
...prompt,
...sortOptions,
};
});
};

export default function withPrompt(WrappedComponent) {
class WithPrompt extends Component {
get prompts() {
return get(this.props, ['stage', 'prompts']);
const {
codebookVariables,
} = this.props;

const prompts = get(this.props, ['stage', 'prompts'], []);
const processedPrompts = processSortRules(prompts, codebookVariables);
return processedPrompts;
}

get promptsCount() {
Expand Down Expand Up @@ -51,7 +81,8 @@ export default function withPrompt(WrappedComponent) {
}

render() {
const { promptIndex, ...rest } = this.props;
const { promptIndex, codebookVariables, ...rest } = this.props;

return (
<WrappedComponent
prompt={this.prompt()}
Expand Down Expand Up @@ -82,6 +113,7 @@ export default function withPrompt(WrappedComponent) {
return {
promptIndex,
stage: ownProps.stage || getProtocolStages(state)[ownProps.stageIndex],
codebookVariables: getAllVariableUUIDsByEntity(state),
};
}

Expand Down
159 changes: 69 additions & 90 deletions src/components/MultiNodeBucket.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { Component } from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { isEqual } from 'lodash';
import { TransitionGroup } from 'react-transition-group';
import { getCSSVariableAsNumber } from '@codaco/ui/lib/utils/CSSVariables';
import { entityPrimaryKeyProperty } from '@codaco/shared-consts';
Expand All @@ -12,106 +11,86 @@ import createSorter from '../utils/createSorter';

const EnhancedNode = DragSource(Node);

/**
* Renders a list of Node.
*/
class MultiNodeBucket extends Component {
constructor(props) {
super(props);
const MultiNodeBucket = (props) => {
const {
nodes,
listId,
sortOrder,
nodeColor,
label,
itemType,
} = props;

const sorter = createSorter(props.sortOrder);
const sortedNodes = sorter(props.nodes);
const [stagger, setStagger] = useState(true);
const [exit, setExit] = useState(true);
const [currentListId, setCurrentListId] = useState(null);
const [sortedNodes, setSortedNodes] = useState([]);

this.state = {
nodes: sortedNodes,
stagger: true,
exit: true,
};

this.refreshTimer = null;
}
useEffect(() => {
const sorter = createSorter(sortOrder); // Uses the new sortOrder via withPrompt
const sorted = sorter(nodes);

// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(newProps) {
const {
nodes,
listId,
} = this.props;
// Don't update if nodes are the same
if (isEqual(newProps.nodes, nodes)) {
// On first run, just set the nodes.
if (!currentListId) {
setSortedNodes(sorted);
setCurrentListId(listId);
return;
}

const sorter = createSorter(newProps.sortOrder);
const sortedNodes = sorter(newProps.nodes);

// if we provided the same id, then just update normally
if (newProps.listId === listId) {
this.setState({ exit: false }, () => {
this.setState({ nodes: sortedNodes, stagger: false });
});
// if we provided the same list id, update immediately without exit or
// stagger animations.
if (listId === currentListId) {
setExit(false);
setStagger(false);
setSortedNodes(sorted);
return;
}

// Otherwise, transition out and in again
this.setState({ exit: true }, () => {
this.setState(
{ nodes: [], stagger: true },
() => {
if (this.refreshTimer) { clearTimeout(this.refreshTimer); }
this.refreshTimer = setTimeout(
() => this.setState({
nodes: sortedNodes,
stagger: true,
}),
getCSSVariableAsNumber('--animation-duration-slow-ms'),
);
},
);
});
}
// Otherwise, enable animations and update after a delay.
setExit(true);
setStagger(true);
setSortedNodes([]);

render() {
const {
nodeColor,
label,
itemType,
} = this.props;
const refreshTimer = setTimeout(() => {
setSortedNodes(sorted);
setCurrentListId(listId);
}, getCSSVariableAsNumber('--animation-duration-slow-ms'));

const {
stagger,
nodes,
exit,
} = this.state;
// eslint-disable-next-line consistent-return
return () => {
if (refreshTimer) {
clearTimeout(refreshTimer);
}
};
}, [nodes, sortOrder, listId]);

return (
<TransitionGroup
className="node-list"
exit={exit}
>
{
nodes.slice(0, 3).map((node, index) => (
<NodeTransition
key={`${node[entityPrimaryKeyProperty]}_${index}`}
index={index}
stagger={stagger}
>
<EnhancedNode
color={nodeColor}
inactive={index !== 0}
allowDrag={index === 0}
label={`${label(node)}`}
meta={() => ({ ...node, itemType })}
scrollDirection={NO_SCROLL}
{...node}
/>
</NodeTransition>
))
}
</TransitionGroup>
);
}
}
return (
<TransitionGroup
className="node-list"
exit={exit}
>
{
sortedNodes.slice(0, 3).map((node, index) => (
<NodeTransition
key={`${node[entityPrimaryKeyProperty]}_${index}`}
index={index}
stagger={stagger}
>
<EnhancedNode
color={nodeColor}
inactive={index !== 0}
allowDrag={index === 0}
label={`${label(node)}`}
meta={() => ({ ...node, itemType })}
scrollDirection={NO_SCROLL}
{...node}
/>
</NodeTransition>
))
}
</TransitionGroup>
);
};

MultiNodeBucket.propTypes = {
nodes: PropTypes.array,
Expand Down
53 changes: 31 additions & 22 deletions src/components/NewFilterableListWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import { entityAttributesProperty } from '@codaco/shared-consts';
import createSorter from '../utils/createSorter';
import { get } from '../utils/lodash-replacements';

export const getFilteredList = (items, filterTerm, propertyPath) => {
export const getFilteredList = (items, filterTerm, searchPropertyPath) => {
if (!filterTerm) { return items; }

const normalizedFilterTerm = filterTerm.toLowerCase();

return items.filter(
(item) => {
const itemAttributes = propertyPath ? Object.values(get(item, propertyPath, {}))
const itemAttributes = searchPropertyPath ? Object.values(get(item, searchPropertyPath, {}))
: Object.values(item);
// Include in filtered list if any of the attribute property values
// include the filter value
Expand Down Expand Up @@ -62,25 +62,32 @@ const itemVariants = {
const NewFilterableListWrapper = (props) => {
const {
items,
propertyPath,
searchPropertyPath,
ItemComponent,
initialSortProperty,
initialSortDirection,
sortableProperties,
loading,
onFilterChange,
} = props;

// Look for the property `default: true` on a sort rule, or use the first
const defaultSortRule = () => {
const defaultSort = sortableProperties.findIndex(
(property) => property.default,
);

return defaultSort > -1 ? defaultSort : 0;
};

const [filterTerm, setFilterTerm] = useState(null);
const [sortProperty, setSortProperty] = useState(initialSortProperty);
const [sortAscending, setSortAscending] = useState(initialSortDirection === 'asc');
const [sortRule, setSortRule] = useState(defaultSortRule());
const [sortDirection, setSortDirection] = useState('asc');

const handleSetSortProperty = (property) => {
if (sortProperty === property) {
setSortAscending(!sortAscending);
const handleSetSortProperty = (index) => {
if (sortRule === index) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortAscending(true);
setSortProperty(property);
setSortRule(index);
setSortDirection('asc');
}
};

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

const filteredItems = onFilterChange ? items : getFilteredList(items, filterTerm, propertyPath);
const filteredItems = onFilterChange
? items : getFilteredList(items, filterTerm, searchPropertyPath);

const sortedItems = createSorter([{
property: sortProperty,
direction: sortAscending ? 'asc' : 'desc',
}], {}, propertyPath)(filteredItems);
property: sortableProperties[sortRule].variable,
type: sortableProperties[sortRule].type,
direction: sortDirection,
}])(filteredItems);

return (
<div className="new-filterable-list">
Expand All @@ -104,19 +113,19 @@ const NewFilterableListWrapper = (props) => {
{(sortableProperties && sortableProperties.length > 0)
&& (
<div className="scroll-container">
{sortableProperties.map((sortField) => (
{sortableProperties.map((sortField, index) => (
<div
tabIndex="0"
role="button"
className={`filter-button ${sortProperty === sortField.variable ? 'filter-button--active' : ''}`}
className={`filter-button ${sortRule === index ? 'filter-button--active' : ''}`}
key={sortField.variable}
onClick={() => handleSetSortProperty(sortField.variable)}
onClick={() => handleSetSortProperty(index)}
>
{
(sortField.label)
}
{
sortProperty === sortField.variable && (sortAscending ? ' \u25B2' : ' \u25BC')
sortRule === index && (sortDirection === 'asc' ? ' \u25B2' : ' \u25BC')
}
</div>
))}
Expand Down Expand Up @@ -177,7 +186,7 @@ const NewFilterableListWrapper = (props) => {
NewFilterableListWrapper.propTypes = {
ItemComponent: PropTypes.elementType.isRequired,
items: PropTypes.array.isRequired,
propertyPath: PropTypes.string,
searchPropertyPath: PropTypes.string,
initialSortProperty: PropTypes.string.isRequired,
initialSortDirection: PropTypes.oneOf(['asc', 'desc']),
sortableProperties: PropTypes.array,
Expand All @@ -188,7 +197,7 @@ NewFilterableListWrapper.propTypes = {

NewFilterableListWrapper.defaultProps = {
initialSortDirection: 'asc',
propertyPath: entityAttributesProperty,
searchPropertyPath: entityAttributesProperty,
sortableProperties: [],
loading: false,
resetFilter: [],
Expand Down
Loading

0 comments on commit ab93de5

Please sign in to comment.