Sort groups and items correctly

fixes ADMIN-994

test plan:
  - fill one day of a planner with stuff. Put items in multiple courses,
    with multiple due dates and times. Include some user todos not in a
    course.
  > expect the groupings and items to be ordered:
    - Groupings are alpha by course/group name
    - Except that To Dos are last
    - Within each grouping,
        - items are ordered by due time, first things first
        - then alpha by item title if matching due times

Change-Id: Idcac554b93f908e5643db87fc913c69238e2cb15
Reviewed-on: https://gerrit.instructure.com/149416
Tested-by: Jenkins
Reviewed-by: Jon Willesen <jonw+gerrit@instructure.com>
QA-Review: Deepeeca Soundarrajan <dsoundarrajan@instructure.com>
Product-Review: Christi Wruck
This commit is contained in:
Ed Schiebel 2018-05-07 16:16:40 -04:00
parent 5441e760e9
commit bf25c7b988
13 changed files with 534 additions and 172 deletions

View File

@ -44,49 +44,11 @@ it('renders the friendlyName in medium text when it is not today', () => {
expect(wrapper.find('Text').first().props().size).toEqual('medium');
});
it('groups itemsForDay based on context type + context id', () => {
const items = [{
title: 'Black Friday',
context: {
type: 'Course',
id: 128,
inform_students_of_overdue_submissions: true
}
}, {
title: 'San Juan',
context: {
type: 'Course',
id: 256,
inform_students_of_overdue_submissions: true
}
}, {
title: 'Roll for the Galaxy',
context: {
type: 'Course',
id: 256,
inform_students_of_overdue_submissions: true
}
}, {
title: 'Same id, different type',
context: {
type: 'Group',
id: 256,
inform_students_of_overdue_submissions: false
}
}];
const wrapper = shallow(
<Day timeZone="America/Denver" day="2017-04-25" itemsForDay={items} />
);
const groupedItems = wrapper.state('groupedItems');
expect(groupedItems['Course128'].length).toEqual(1);
expect(groupedItems['Course256'].length).toEqual(2);
expect(groupedItems['Group256'].length).toEqual(1);
});
it('renders grouping correctly when having itemsForDay', () => {
const TZ = "America/Denver";
const items = [{
title: 'Black Friday',
date: moment.tz('2017-04-25T23:59:00Z', TZ),
context: {
type: 'Course',
id: 128,
@ -95,6 +57,7 @@ it('renders grouping correctly when having itemsForDay', () => {
}
}, {
title: 'San Juan',
date: moment.tz('2017-04-25T23:59:00Z', TZ),
context: {
type: 'Course',
id: 256,
@ -103,6 +66,7 @@ it('renders grouping correctly when having itemsForDay', () => {
}
}, {
title: 'Roll for the Galaxy',
date: moment.tz('2017-04-25T23:59:00Z', TZ),
context: {
type: 'Course',
id: 256,
@ -111,6 +75,7 @@ it('renders grouping correctly when having itemsForDay', () => {
}
}, {
title: 'Same id, different type',
date: moment.tz('2017-04-25T23:59:00Z', TZ),
context: {
type: 'Group',
id: 256,
@ -119,13 +84,15 @@ it('renders grouping correctly when having itemsForDay', () => {
}];
const wrapper = shallow(
<Day timeZone="America/Denver" day="2017-04-25" itemsForDay={items} animatableIndex={1}/>
<Day timeZone={TZ} day="2017-04-25" itemsForDay={items} animatableIndex={1}/>
);
expect(wrapper).toMatchSnapshot();
});
it('groups itemsForDay that have no context into the "Notes" category', () => {
const TZ = "America/Denver";
const items = [{
title: 'Black Friday',
date: moment.tz('2017-04-25T23:59:00Z', TZ),
context: {
type: 'Course',
id: 128,
@ -133,6 +100,7 @@ it('groups itemsForDay that have no context into the "Notes" category', () => {
}
}, {
title: 'San Juan',
date: moment.tz('2017-04-25T23:59:00Z', TZ),
context: {
type: 'Course',
id: 256,
@ -140,6 +108,7 @@ it('groups itemsForDay that have no context into the "Notes" category', () => {
}
}, {
title: 'Roll for the Galaxy',
date: moment.tz('2017-04-25T23:59:00Z', TZ),
context: {
type: 'Course',
id: 256,
@ -150,15 +119,16 @@ it('groups itemsForDay that have no context into the "Notes" category', () => {
}];
const wrapper = shallow(
<Day timeZone="America/Denver" day="2017-04-25" itemsForDay={items} />
<Day timeZone={TZ} day="2017-04-25" itemsForDay={items} />
);
const groupedItems = wrapper.state('groupedItems');
expect(groupedItems.Notes.length).toEqual(1);
expect(wrapper).toMatchSnapshot();
});
it('groups itemsForDay that come in on prop changes', () => {
const TZ = "America/Denver";
const items = [{
title: 'Black Friday',
date: moment.tz('2017-04-25T23:59:00Z', TZ),
context: {
type: 'Course',
id: 128,
@ -166,6 +136,7 @@ it('groups itemsForDay that come in on prop changes', () => {
}
}, {
title: 'San Juan',
date: moment.tz('2017-04-25T23:59:00Z', TZ),
context: {
type: 'Course',
id: 256,
@ -174,13 +145,13 @@ it('groups itemsForDay that come in on prop changes', () => {
}];
const wrapper = shallow(
<Day timeZone="America/Denver" day="2017-04-25" itemsForDay={items} registerAnimatable={() => {}} deregisterAnimatable={() => {}} />
<Day timeZone={TZ} day="2017-04-25" itemsForDay={items} registerAnimatable={() => {}} deregisterAnimatable={() => {}} />
);
let groupedItems = wrapper.state('groupedItems');
expect(Object.keys(groupedItems).length).toEqual(2);
expect(wrapper).toMatchSnapshot();
const newItemsForDay = items.concat([{
title: 'Roll for the Galaxy',
date: moment.tz('2017-04-25T23:59:00Z', TZ),
context: {
type: 'Course',
id: 256,
@ -191,8 +162,7 @@ it('groups itemsForDay that come in on prop changes', () => {
}]);
wrapper.setProps({ itemsForDay: newItemsForDay });
groupedItems = wrapper.state('groupedItems');
expect(Object.keys(groupedItems).length).toEqual(3);
expect(wrapper).toMatchSnapshot();
});
@ -205,14 +175,21 @@ it('renders even when there are no items', () => {
});
it('registers itself as animatable', () => {
const TZ = "Asia/Tokyo";
const fakeRegister = jest.fn();
const fakeDeregister = jest.fn();
const firstItems = [{title: 'asdf', context: {id: 128, inform_students_of_overdue_submissions: true}, id: '1', uniqueId: 'first'}, {title: 'jkl', context: {id: 256, inform_students_of_overdue_submissions: true}, id: '2', uniqueId: 'second'}];
const secondItems = [{title: 'qwer', context: {id: 128, inform_students_of_overdue_submissions: true}, id: '3', uniqueId: 'third'}, {title: 'uiop', context: {id: 256, inform_students_of_overdue_submissions: true}, id: '4', uniqueId: 'fourth'}];
const firstItems = [
{title: 'asdf', date: moment.tz('2017-04-25T23:59:00Z', TZ), context: {id: 128, inform_students_of_overdue_submissions: true}, id: '1', uniqueId: 'first'},
{title: 'jkl', date: moment.tz('2017-04-25T23:59:00Z', TZ), context: {id: 256, inform_students_of_overdue_submissions: true}, id: '2', uniqueId: 'second'}
];
const secondItems = [
{title: 'qwer', date: moment.tz('2017-04-25T23:59:00Z', TZ), context: {id: 128, inform_students_of_overdue_submissions: true}, id: '3', uniqueId: 'third'},
{title: 'uiop', date: moment.tz('2017-04-25T23:59:00Z', TZ), context: {id: 256, inform_students_of_overdue_submissions: true}, id: '4', uniqueId: 'fourth'}
];
const wrapper = mount(
<Day
day={'2017-08-11'}
timeZone="Asia/Tokyo"
timeZone={TZ}
animatableIndex={42}
itemsForDay={firstItems}
registerAnimatable={fakeRegister}

View File

@ -1,5 +1,286 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`groups itemsForDay that come in on prop changes 1`] = `
<div
className="planner-day"
>
<Heading
border="none"
color="inherit"
ellipsis={false}
level="h2"
>
<Text
as="div"
letterSpacing="normal"
lineHeight="condensed"
size="medium"
transform="uppercase"
>
Tuesday
</Text>
<Text
as="div"
letterSpacing="normal"
lineHeight="condensed"
size="medium"
>
April 25, 2017
</Text>
</Heading>
<div>
<Animatable(ResponsiveGrouping)
animatableIndex={1}
items={
Array [
Object {
"context": Object {
"id": 128,
"inform_students_of_overdue_submissions": true,
"type": "Course",
},
"date": "2017-04-25T23:59:00.000Z",
"title": "Black Friday",
},
]
}
theme={
Object {
"titleColor": null,
}
}
timeZone="America/Denver"
/>
<Animatable(ResponsiveGrouping)
animatableIndex={2}
items={
Array [
Object {
"context": Object {
"id": 256,
"inform_students_of_overdue_submissions": true,
"type": "Course",
},
"date": "2017-04-25T23:59:00.000Z",
"title": "San Juan",
},
]
}
theme={
Object {
"titleColor": null,
}
}
timeZone="America/Denver"
/>
</div>
</div>
`;
exports[`groups itemsForDay that come in on prop changes 2`] = `
<div
className="planner-day"
>
<Heading
border="none"
color="inherit"
ellipsis={false}
level="h2"
>
<Text
as="div"
letterSpacing="normal"
lineHeight="condensed"
size="medium"
transform="uppercase"
>
Tuesday
</Text>
<Text
as="div"
letterSpacing="normal"
lineHeight="condensed"
size="medium"
>
April 25, 2017
</Text>
</Heading>
<div>
<Animatable(ResponsiveGrouping)
animatableIndex={1}
items={
Array [
Object {
"context": Object {
"id": 128,
"inform_students_of_overdue_submissions": true,
"type": "Course",
},
"date": "2017-04-25T23:59:00.000Z",
"title": "Black Friday",
},
]
}
theme={
Object {
"titleColor": null,
}
}
timeZone="America/Denver"
/>
<Animatable(ResponsiveGrouping)
animatableIndex={2}
items={
Array [
Object {
"context": Object {
"id": 256,
"inform_students_of_overdue_submissions": true,
"type": "Course",
},
"date": "2017-04-25T23:59:00.000Z",
"title": "San Juan",
},
Object {
"context": Object {
"id": 256,
"inform_students_of_overdue_submissions": true,
"type": "Course",
},
"date": "2017-04-25T23:59:00.000Z",
"title": "Roll for the Galaxy",
},
]
}
theme={
Object {
"titleColor": null,
}
}
timeZone="America/Denver"
/>
<Animatable(ResponsiveGrouping)
animatableIndex={3}
items={
Array [
Object {
"title": "Get work done!",
},
]
}
theme={
Object {
"titleColor": null,
}
}
timeZone="America/Denver"
/>
</div>
</div>
`;
exports[`groups itemsForDay that have no context into the "Notes" category 1`] = `
<div
className="planner-day"
>
<Heading
border="none"
color="inherit"
ellipsis={false}
level="h2"
>
<Text
as="div"
letterSpacing="normal"
lineHeight="condensed"
size="medium"
transform="uppercase"
>
Tuesday
</Text>
<Text
as="div"
letterSpacing="normal"
lineHeight="condensed"
size="medium"
>
April 25, 2017
</Text>
</Heading>
<div>
<Animatable(ResponsiveGrouping)
animatableIndex={1}
items={
Array [
Object {
"context": Object {
"id": 128,
"inform_students_of_overdue_submissions": true,
"type": "Course",
},
"date": "2017-04-25T23:59:00.000Z",
"title": "Black Friday",
},
]
}
theme={
Object {
"titleColor": null,
}
}
timeZone="America/Denver"
/>
<Animatable(ResponsiveGrouping)
animatableIndex={2}
items={
Array [
Object {
"context": Object {
"id": 256,
"inform_students_of_overdue_submissions": true,
"type": "Course",
},
"date": "2017-04-25T23:59:00.000Z",
"title": "San Juan",
},
Object {
"context": Object {
"id": 256,
"inform_students_of_overdue_submissions": true,
"type": "Course",
},
"date": "2017-04-25T23:59:00.000Z",
"title": "Roll for the Galaxy",
},
]
}
theme={
Object {
"titleColor": null,
}
}
timeZone="America/Denver"
/>
<Animatable(ResponsiveGrouping)
animatableIndex={3}
items={
Array [
Object {
"title": "Get work done!",
},
]
}
theme={
Object {
"titleColor": null,
}
}
timeZone="America/Denver"
/>
</div>
</div>
`;
exports[`renders grouping correctly when having itemsForDay 1`] = `
<div
className="planner-day"
@ -40,6 +321,7 @@ exports[`renders grouping correctly when having itemsForDay 1`] = `
"type": "Course",
"url": "http://www.non_default_url.com",
},
"date": "2017-04-25T23:59:00.000Z",
"title": "Black Friday",
},
]
@ -63,6 +345,7 @@ exports[`renders grouping correctly when having itemsForDay 1`] = `
"type": "Course",
"url": "http://www.non_default_url.com",
},
"date": "2017-04-25T23:59:00.000Z",
"title": "San Juan",
},
Object {
@ -72,6 +355,7 @@ exports[`renders grouping correctly when having itemsForDay 1`] = `
"type": "Course",
"url": "http://www.non_default_url.com",
},
"date": "2017-04-25T23:59:00.000Z",
"title": "Roll for the Galaxy",
},
]
@ -94,6 +378,7 @@ exports[`renders grouping correctly when having itemsForDay 1`] = `
"inform_students_of_overdue_submissions": false,
"type": "Group",
},
"date": "2017-04-25T23:59:00.000Z",
"title": "Same id, different type",
},
]

View File

@ -28,7 +28,6 @@ import { userShape, itemShape } from '../plannerPropTypes';
import styles from './styles.css';
import theme from './theme.js';
import { getFriendlyDate, getFullDate, isToday } from '../../utilities/dateUtils';
import { groupBy } from 'lodash';
import Grouping from '../Grouping';
import formatMessage from '../../format-message';
import { animatable } from '../../dynamic-ui';
@ -45,6 +44,9 @@ export class Day extends Component {
deregisterAnimatable: func,
currentUser: shape(userShape),
};
static defaultProps = {
animatableIndex: 0,
};
constructor (props) {
super(props);
@ -52,9 +54,6 @@ export class Day extends Component {
const tzMomentizedDate = moment.tz(props.day, props.timeZone);
this.friendlyName = getFriendlyDate(tzMomentizedDate);
this.fullDate = getFullDate(tzMomentizedDate);
this.state = {
groupedItems: this.groupItems(props.itemsForDay)
};
}
componentDidMount () {
@ -64,12 +63,6 @@ export class Day extends Component {
componentWillReceiveProps (nextProps) {
this.props.deregisterAnimatable('day', this, this.itemUniqueIds());
this.props.registerAnimatable('day', this, nextProps.animatableIndex, this.itemUniqueIds(nextProps));
this.setState((state) => {
return {
groupedItems: this.groupItems(nextProps.itemsForDay)
};
});
}
componentWillUnmount () {
@ -78,12 +71,55 @@ export class Day extends Component {
itemUniqueIds (props = this.props) { return props.itemsForDay.map(item => item.uniqueId); }
groupItems = (items) => groupBy(items, item => {
return (item.context && (item.context.type+item.context.id)) || 'Notes';
})
hasItems () {
return !!Object.keys(this.state.groupedItems).length;
return this.props.itemsForDay && this.props.itemsForDay.length > 0;
}
renderGrouping(groupKey, groupItems, index) {
const courseInfo = groupItems[0].context || {};
return (
<Grouping
title={courseInfo.title}
image_url={courseInfo.image_url}
color={courseInfo.color}
timeZone={this.props.timeZone}
updateTodo={this.props.updateTodo}
items={groupItems}
animatableIndex={this.props.animatableIndex * 100 + index + 1}
url={courseInfo.url}
key={groupKey}
theme={{
titleColor: courseInfo.color || null
}}
toggleCompletion={this.props.toggleCompletion}
currentUser={this.props.currentUser}
/>
);
}
renderGroupings () {
const groupings = [];
let currGroupItems;
let currGroupKey;
const nItems = this.props.itemsForDay.length;
for (let i = 0; i < nItems; ++i) {
let item = this.props.itemsForDay[i];
let groupKey = (item.context && item.context.id) ? `${item.context.type}${item.context.id}` : 'Notes';
if (groupKey !== currGroupKey) {
if (currGroupKey) { // emit the grouping we've been working
groupings.push(this.renderGrouping(currGroupKey, currGroupItems, groupings.length));
}
// start new grouping
currGroupKey = groupKey;
currGroupItems = [item];
} else {
currGroupItems.push(item);
}
}
// the last groupings// emit the grouping we've been working
groupings.push(this.renderGrouping(currGroupKey, currGroupItems, groupings.length));
return groupings;
}
render () {
@ -113,28 +149,7 @@ export class Day extends Component {
<div>
{
(this.hasItems()) ? (
Object.keys(this.state.groupedItems).map((cid, groupIndex) => {
const groupItems = this.state.groupedItems[cid];
const courseInfo = groupItems[0].context || {};
return (
<Grouping
title={courseInfo.title}
image_url={courseInfo.image_url}
color={courseInfo.color}
timeZone={this.props.timeZone}
updateTodo={this.props.updateTodo}
items={groupItems}
animatableIndex={this.props.animatableIndex * 100 + groupIndex + 1}
url={courseInfo.url}
key={cid}
theme={{
titleColor: courseInfo.color || null
}}
toggleCompletion={this.props.toggleCompletion}
currentUser={this.props.currentUser}
/>
);
})
this.renderGroupings()
) : (
<View
textAlign="center"

View File

@ -17,6 +17,7 @@
*/
import MockDate from 'mockdate';
import moment from 'moment-timezone';
import {initialize} from '../../../utilities/alertUtils';
import {ScrollToLoadedToday} from '../scroll-to-loaded-today';
import {createAnimation, mockRegistryEntry} from './test-utils';
import {startLoadingPastUntilTodaySaga, gotDaysSuccess} from '../../../actions/loading-actions';
@ -25,6 +26,11 @@ const TZ = 'Asia/Tokyo';
beforeAll(() => {
MockDate.set('2018-04-15', TZ);
initialize({
visualSuccessCallback: jest.fn(),
visualErrorCallback: jest.fn(),
srAlertCallback: jest.fn()
});
});
afterAll(() => {
MockDate.reset();

View File

@ -18,6 +18,7 @@
import MockDate from 'mockdate';
import moment from 'moment-timezone';
import {initialize} from '../../../utilities/alertUtils';
import {ScrollToToday} from '../scroll-to-today';
import {createAnimation, mockRegistryEntry} from './test-utils';
@ -25,6 +26,11 @@ const TZ = 'Asia/Tokyo';
beforeAll(() => {
MockDate.set('2018-04-15', TZ);
initialize({
visualSuccessCallback: jest.fn(),
visualErrorCallback: jest.fn(),
srAlertCallback: jest.fn()
});
});
afterAll(() => {
MockDate.reset();

View File

@ -58,7 +58,7 @@ export function scrollAndFocusTodayItem (manager, todayElem) {
}
function findTodayOrNext (registry) {
const today = moment();
const today = moment().startOf('day');
const todayOrNextItem = registry.getAllItemsSorted().find(item => {
return item.component.props.date >= today;
});

View File

@ -24,10 +24,10 @@ describe('getting new items', () => {
const initialState = [];
const gotDataAction = gotItemsSuccess([
{ id: 'fourth', dateBucketMoment: moment.tz('2017-04-29', 'UTC') },
{ id: 'second', dateBucketMoment: moment.tz('2017-04-28', 'UTC') },
{ id: 'first', dateBucketMoment: moment.tz('2017-04-27', 'UTC') },
{ id: 'third', dateBucketMoment: moment.tz('2017-04-28', 'UTC') },
{ id: 'fourth', date: moment.tz('2017-04-29', 'UTC'), dateBucketMoment: moment.tz('2017-04-29', 'UTC'), title: 'aaa' },
{ id: 'second', date: moment.tz('2017-04-28', 'UTC'), dateBucketMoment: moment.tz('2017-04-28', 'UTC'), title: 'aaa' },
{ id: 'first', date: moment.tz('2017-04-27', 'UTC'), dateBucketMoment: moment.tz('2017-04-27', 'UTC'), title: 'aaa' },
{ id: 'third', date: moment.tz('2017-04-28', 'UTC'), dateBucketMoment: moment.tz('2017-04-28', 'UTC'), title: 'bbb' },
]);
const newState = daysReducer(initialState, gotDataAction);
@ -38,9 +38,9 @@ describe('getting new items', () => {
]);
const nextGotDataAction = gotItemsSuccess([
{id: 'fifth', dateBucketMoment: moment.tz('2017-04-29', 'UTC')},
{id: 'zeroth', dateBucketMoment: moment.tz('2017-04-26', 'UTC')},
{id: 'second', with: 'new data', dateBucketMoment: moment.tz('2017-04-28', 'UTC')}
{id: 'fifth', date: moment.tz('2017-04-29', 'UTC'), dateBucketMoment: moment.tz('2017-04-29', 'UTC'), title: 'aaa'},
{id: 'zeroth', date: moment.tz('2017-04-26', 'UTC') , dateBucketMoment: moment.tz('2017-04-26', 'UTC'), title: 'aaa'},
{id: 'second', with: 'new data',date: moment.tz('2017-04-28', 'UTC'), dateBucketMoment: moment.tz('2017-04-28', 'UTC'), title: 'aaa'}
]);
const mergedState = daysReducer(newState, nextGotDataAction);
expect(mergedState).toMatchObject([
@ -55,11 +55,11 @@ describe('getting new items', () => {
describe('saving planner items', () => {
it('adds new items to the day', () => {
const initialState = [
['2017-04-27', [{id: '42', dateBucketMoment: moment.tz('2017-04-27', 'UTC')}]],
['2017-04-27', [{id: '42', date: moment.tz('2017-04-27', 'UTC'), dateBucketMoment: moment.tz('2017-04-27', 'UTC'), title: 'aaa'}]],
];
const newState = daysReducer(initialState, {
type: 'SAVED_PLANNER_ITEM',
payload: {item: {id: '43', dateBucketMoment: moment.tz('2017-04-27', 'UTC')}},
payload: {item: {id: '43', date: moment.tz('2017-04-27', 'UTC'), dateBucketMoment: moment.tz('2017-04-27', 'UTC'), title: 'aaa'}},
});
expect(newState).toMatchObject([
['2017-04-27', [
@ -73,19 +73,20 @@ describe('saving planner items', () => {
// more than one item to make sure edited item gets merged, not deleted and re-added
const initialState = [
['2017-04-27', [
{dateBucketMoment: moment.tz('2017-04-27', 'UTC'), id: '42', title: 'an event'},
{dateBucketMoment: moment.tz('2017-04-27', 'UTC'), id: '43', title: 'another event'},
{date: moment.tz('2017-04-27', 'UTC'), dateBucketMoment: moment.tz('2017-04-27', 'UTC'), id: '42', title: 'aaa event'},
{date: moment.tz('2017-04-27', 'UTC'), dateBucketMoment: moment.tz('2017-04-27', 'UTC'), id: '43', title: 'bbb event'},
]],
];
const newState = daysReducer(initialState, {
type: 'SAVED_PLANNER_ITEM',
payload: {item: {dateBucketMoment: moment.tz( '2017-04-27', 'UTC'), id: '42', title: 'renamed event'}},
payload: {item: {date: moment.tz( '2017-04-27', 'UTC'), dateBucketMoment: moment.tz( '2017-04-27', 'UTC'), id: '42', title: 'ccc event'}},
});
expect(newState).toMatchObject([
['2017-04-27', [
{id: '42', title: 'renamed event'},
{id: '43', title: 'another event'},
]],
expect(newState[0][0]).toEqual('2017-04-27');
// new title, and resorted
expect(newState[0][1]).toMatchObject([
{id: '43', title: 'bbb event'},
{id: '42', title: 'ccc event'},
]);
});

View File

@ -19,7 +19,7 @@
import { handleActions } from 'redux-actions';
import { formatDayKey } from '../utilities/dateUtils';
import { findPlannerItemById } from '../utilities/storeUtils';
import { daysToDaysHash, daysHashToDays, mergeDaysIntoDaysHash, itemsToDays } from '../utilities/daysUtils';
import { daysToDaysHash, daysHashToDays, mergeDaysIntoDaysHash, itemsToDays, groupAndSortDayItems } from '../utilities/daysUtils';
function savedPlannerItem (state, action) {
if (action.error) return state;
@ -57,6 +57,12 @@ function _deletePlannerItem(state, doomedPlannerItem) {
function gotDaysSuccess (state, days) {
const oldDaysHash = daysToDaysHash(state);
const mergedDaysHash = mergeDaysIntoDaysHash(oldDaysHash, days);
days.forEach(d => {
const dayKey = d[0];
const dayItems = mergedDaysHash[dayKey].slice(0); // copy items array for this day
groupAndSortDayItems(dayItems);
mergedDaysHash[dayKey] = dayItems;
});
return daysHashToDays(mergedDaysHash);
}

View File

@ -302,38 +302,6 @@ Object {
}
`;
exports[`transformApiToInternalItem extracts and transforms the proper data for an announcement response 1`] = `
Object {
"completed": false,
"context": Object {
"color": "#abffaa",
"id": "1",
"image_url": "blah_url",
"inform_students_of_overdue_submissions": true,
"title": "blah",
"type": "Course",
"url": undefined,
},
"course_id": undefined,
"date": "2017-05-15T16:32:34Z",
"dateBucketMoment": "2018-03-27T00:00:00.000Z",
"html_url": "/courses/1/discussion_topics/10",
"id": "1",
"newActivity": false,
"overrideAssignId": 9,
"overrideId": null,
"points": undefined,
"status": Object {
"unread_count": 1,
},
"title": "",
"toggleAPIPending": false,
"type": "Announcement",
"uniqueId": "announcement-1",
"unread_count": 1,
}
`;
exports[`transformApiToInternalItem extracts and transforms the proper data for an ungraded discussion reponse with an unread count 1`] = `
Object {
"completed": false,
@ -408,7 +376,7 @@ Object {
"url": undefined,
},
"course_id": "1",
"date": "2017-06-21T18:58:51Z",
"date": "2017-06-21T18:58:51.000Z",
"dateBucketMoment": "2017-06-21T18:58:51.000Z",
"details": "asdfasdfasdf",
"id": 14,
@ -424,7 +392,7 @@ Object {
"completed": false,
"context": Object {},
"course_id": null,
"date": "2017-06-21T18:58:51Z",
"date": "2017-06-21T18:58:51.000Z",
"dateBucketMoment": "2017-06-21T18:58:51.000Z",
"details": "asdfasdfasdf",
"id": 14,

View File

@ -403,20 +403,6 @@ describe('transformApiToInternalItem', () => {
expect(result).toMatchSnapshot();
});
it('extracts and transforms the proper data for an announcement response', () => {
const apiResponse = makeApiResponse({
plannable_type: 'announcement',
plannable: makeDiscussionTopic({ // TODO: Discussion topic is probably fine for now to simulate this, but probably should change later
due_at: undefined,
todo_date: undefined,
unread_count: 1
})
});
const result = transformApiToInternalItem(apiResponse, courses, groups, 'UTC');
expect(result).toMatchSnapshot();
});
it('extracts and transforms the ID for a wiki page repsonse', () => {
const apiResponse = makeApiResponse({
plannable_type: 'wiki_page',

View File

@ -19,12 +19,14 @@
import {
mergeNewItemsIntoDays, mergeNewItemsIntoDaysHash, mergeDaysIntoDaysHash, mergeDaysHashes,
itemsToDaysHash, daysToDaysHash, itemsToDays, daysToItems, mergeItems, purgeDuplicateDays,
mergeDays, daysHashToDays,
mergeDays, daysHashToDays, groupAndSortDayItems,
} from '../daysUtils';
function mockItem (date = '2017-12-18', opts = {}) {
return {
date: date,
dateBucketMoment: date,
title: 'aaa',
...opts,
};
}
@ -32,10 +34,10 @@ function mockItem (date = '2017-12-18', opts = {}) {
describe('mergeNewItemsIntoDays', () => {
it('merges', () => {
const newItems = [
mockItem('2017-12-18', {id: 1, name: 'merged item'}),
mockItem('2017-12-19', {id: 2, name: 'new item'}),
mockItem('2017-12-18', {id: 1, title: 'merged item'}),
mockItem('2017-12-19', {id: 2, title: 'new item'}),
];
const oldItems = [mockItem('2017-12-18', {id: 3, name: 'old item'})];
const oldItems = [mockItem('2017-12-18', {id: 3, title: 'old item'})];
const oldDays = [['2017-12-18', oldItems]];
const result = mergeNewItemsIntoDays(oldDays, newItems);
expect(result).toEqual([
@ -86,10 +88,10 @@ describe('mergeDaysIntoDaysHash', () => {
describe('mergeNewItemsIntoDaysHash', () => {
it('merges', () => {
const newItems = [
mockItem('2017-12-18', {id: 1, name: 'merged item'}),
mockItem('2017-12-19', {id: 2, name: 'new item'}),
mockItem('2017-12-18', {id: 1, title: 'merged item'}),
mockItem('2017-12-19', {id: 2, title: 'new item'}),
];
const oldItems = [mockItem('2017-12-18', {id: 3, name: 'old item'})];
const oldItems = [mockItem('2017-12-18', {id: 3, title: 'old item'})];
const oldDaysHash = {'2017-12-18': oldItems};
const result = mergeNewItemsIntoDaysHash(oldDaysHash, newItems);
expect(result).toEqual({
@ -103,10 +105,10 @@ describe('mergeNewItemsIntoDaysHash', () => {
describe('mergeDaysHashes', () => {
it('merges', () => {
const newDaysHash = {
'2017-12-18': [mockItem('2017-12-18', {id: 1, name: 'merged item'})],
'2017-12-19': [mockItem('2017-12-19', {id: 2, name: 'new item'})],
'2017-12-18': [mockItem('2017-12-18', {id: 1, title: 'merged item'})],
'2017-12-19': [mockItem('2017-12-19', {id: 2, title: 'new item'})],
};
const oldItems = [mockItem('2017-12-18', {id: 3, name: 'old item'})];
const oldItems = [mockItem('2017-12-18', {id: 3, title: 'old item'})];
const oldDaysHash = {'2017-12-18': oldItems};
const result = mergeDaysHashes(oldDaysHash, newDaysHash);
expect(result).toEqual({
@ -192,11 +194,11 @@ describe('daysToItems', () => {
describe('mergeItems', () => {
it('merges', () => {
const oldItems = [
mockItem('2017-12-18', {id: 1, name: 'to be replaced'}),
mockItem('2017-12-18', {id: 1, title: 'to be replaced'}),
mockItem('2017-12-18', {id: 2}),
];
const newItems = [
mockItem('2017-12-18', {id: 1, name: 'replacement'}),
mockItem('2017-12-18', {id: 1, title: 'replacement'}),
mockItem('2017-12-19', {id: 3})];
const result = mergeItems(oldItems, newItems);
expect(result).toEqual([newItems[0], oldItems[1], newItems[1]]);
@ -218,3 +220,70 @@ describe('purgeDuplicateDays', () => {
expect(result).not.toBe(oldDays); // no mutation
});
});
describe('groupAndSortDayItems', () => {
it('groups and sorts courses by title with ToDos at end', () => {
const items = [
mockItem('2017-12-05T11:00:00Z', {id: '1'}),
mockItem('2017-12-05T11:00:00Z', {id: '2', context: {type: 'Course', id: '1', title: 'ZZZ Course'}}),
mockItem('2017-12-05T11:00:00Z', {id: '3', context: {type: 'Course', id: '2', title: 'AAA Course'}}),
mockItem('2017-12-05T11:00:00Z', {id: '4', context: {type: 'Course', id: '1', title: 'ZZZ Course'}}),
];
const result = groupAndSortDayItems(items);
expect(result).toMatchObject([
{id: '3'}, {id: '2'}, {id: '4'}, {id: '1'}
]);
});
it('sorts by context type+id if missing title', () => {
const items = [
mockItem('2017-12-05T11:00:00Z', {id: '1'}),
mockItem('2017-12-05T11:00:00Z', {id: '2', context: {type: 'Course', id: '1'}}),
mockItem('2017-12-05T11:00:00Z', {id: '3', context: {type: 'Course', id: '2'}}),
mockItem('2017-12-05T11:00:00Z', {id: '4', context: {type: 'Course', id: '1'}}),
];
const result = groupAndSortDayItems(items);
expect(result).toMatchObject([
{id: '2'}, {id: '4'}, {id: '3'}, {id: '1'}
]);
});
it('sorts items with same time by title', () => {
const items = [
mockItem('2017-12-05T11:00:00Z', {id: '1'}),
mockItem('2017-12-05T11:00:00Z', {id: '2', title: 'zzz', context: {type: 'Course', id: '1', title: 'Math'}}),
mockItem('2017-12-05T11:00:00Z', {id: '3', context: {type: 'Course', id: '2', title: 'English'}}),
mockItem('2017-12-05T11:00:00Z', {id: '4', title: 'aaa', context: {type: 'Course', id: '1', title: 'Math'}}),
];
const result = groupAndSortDayItems(items);
expect(result).toMatchObject([
{id: '3'}, {id: '4'}, {id: '2'}, {id: '1'}
]);
});
it('sorts items with same time by title with numbers', () => {
const items = [
mockItem('2017-12-05T11:00:00Z', {id: '1', title: 'x 1'}),
mockItem('2017-12-05T11:00:00Z', {id: '3', title: 'x 21'}),
mockItem('2017-12-05T11:00:00Z', {id: '2', title: 'x 3'}),
];
const result = groupAndSortDayItems(items);
expect(result).toMatchObject([
{id: '1'}, {id: '2'}, {id: '3'}
]);
});
it('sorts items by time', () => {
const items = [
mockItem('2017-12-05T11:00:00Z', {id: '1'}),
mockItem('2017-12-05T12:00:00Z', {id: '2', context: {type: 'Course', id: '1', title: 'Math'}}),
mockItem('2017-12-05T11:00:00Z', {id: '3', context: {type: 'Course', id: '2', title: 'English'}}),
mockItem('2017-12-05T11:00:00Z', {id: '4', context: {type: 'Course', id: '1', title: 'Math'}}),
];
const result = groupAndSortDayItems(items);
expect(result).toMatchObject([
{id: '3'}, {id: '4'}, {id: '2'}, {id: '1'}
]);
});
});

View File

@ -42,9 +42,6 @@ const getItemDetailsFromPlannable = (apiResponse, timeZone) => {
if (plannable_type === 'discussion_topic' || plannable_type === 'announcement') {
details.unread_count = plannable.unread_count;
}
if (plannable_type === 'announcement' && !details.date) {
details.date = plannable.delayed_post_at || plannable.posted_at;
}
if (plannable_type === 'planner_note') {
details.details = plannable.details;
@ -53,6 +50,7 @@ const getItemDetailsFromPlannable = (apiResponse, timeZone) => {
if (plannable_type === 'calendar_event') {
details.allDay = plannable.all_day
}
return details;
};
@ -137,7 +135,7 @@ export function transformPlannerNoteApiToInternalItem (plannerItemApiResponse, c
course_id: plannerNote.course_id,
context: context,
title: plannerNote.title,
date: plannerNote.todo_date,
date: moment.tz(plannerNote.todo_date, timeZone),
details: plannerNote.details,
completed: false
};

View File

@ -94,3 +94,48 @@ export function purgeDuplicateDays (oldDays, newDays) {
newDays.forEach(day => { delete purgedDaysHash[day[0]]; });
return daysHashToDays(purgedDaysHash);
}
// sort the items:
// First by grouping (alpha by course or group title, followed by the Notes (aka To Dos)
// Then by due-time for each item w/in the grouping.
export function groupAndSortDayItems (items) {
return items.sort(orderItems);
}
// ----- grouping and sorting helpers -----
const cmpopts = {numeric: true};
const locale =(window.ENV && window.ENV.LOCALE) || 'en';
// order items by their grouping
function getItemGroupTitle(item) {
if (item.context && item.context.id) { // edited items have an empty context, so look for the id too
return item.context.title || `${item.context.type}${item.context.id}`;
}
return 'Notes';
}
function orderItemsByGrouping (a, b) {
let namea = getItemGroupTitle(a);
let nameb = getItemGroupTitle(b);
if (namea.localeCompare(nameb, locale, cmpopts) === 0) return 0;
if (namea === 'Notes') return 1;
if (nameb === 'Notes') return -1;
return namea.localeCompare(nameb, locale, cmpopts);
}
// order items by time, then title
function orderItemsByTimeAndTitle (a, b) {
if (a.date.valueOf() === b.date.valueOf()) {
return a.title.localeCompare(b.title, locale, cmpopts);
}
return a.date < b.date ? -1 : 1;
}
// order items
function orderItems (a, b) {
let order = orderItemsByGrouping(a, b);
if (order === 0) {
order = orderItemsByTimeAndTitle(a, b);
}
return order;
}