diff --git a/package.json b/package.json index f94e6d89ffb..9391d89fdf2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@instructure/ui-core": "^4.8.0", "@instructure/ui-menu": "^5.0.1", "@instructure/ui-overlays": "^5.0.1", - "@instructure/ui-themeable": "^4.8.0", + "@instructure/ui-themeable": "^5.4.0", "@instructure/ui-themes": "^5.0.1", "apollo-boost": "^0.1.4", "axios": "^0.16.0", @@ -193,6 +193,7 @@ "upgrade-and-dedupe": "rm -rf yarn.lock node_modules && yes 1 | yarn install --flat --production && git checkout package.json && yarn install && git add yarn.lock && git commit -m 'update npm deps'" }, "resolutions": { - "jquery": "https://github.com/ryankshaw/jquery.git#a755a3e9c99d5a70d8ea570836f94ae1ba56046d" + "jquery": "https://github.com/ryankshaw/jquery.git#a755a3e9c99d5a70d8ea570836f94ae1ba56046d", + "moment": "2.10.6" } } \ No newline at end of file diff --git a/packages/canvas-planner/package.json b/packages/canvas-planner/package.json index 8b9e89fbcae..9e4e00ea7b7 100644 --- a/packages/canvas-planner/package.json +++ b/packages/canvas-planner/package.json @@ -41,8 +41,9 @@ "license": "AGPL-3.0", "dependencies": { "@instructure/ui-core": "^4.8.0", + "@instructure/ui-forms": "5.4.1-dev.0", + "@instructure/ui-themeable": "^5.4.0", "@instructure/ui-toggle-details": "^5.0.0", - "@instructure/ui-forms": "^5.2.0", "axios": "^0.16.0", "babel-plugin-inline-react-svg": "^0.4.0", "change-case": "^3.0.1", @@ -54,7 +55,6 @@ "instructure-icons": "^4.3.0", "keycode": "^2.1.9", "lodash": "^4", - "moment": "^2.22.0", "moment-timezone": "^0.5.13", "parse-link-header": "^1.0.1", "prop-types": "^15.5.9", @@ -100,6 +100,9 @@ "npm-run-all": "^4", "webpack": "^3" }, + "resolutions": { + "moment": "2.10.6" + }, "lint-staged": { "*.js": "eslint" } diff --git a/packages/canvas-planner/src/actions/__tests__/actions.spec.js b/packages/canvas-planner/src/actions/__tests__/actions.spec.js index ed6a9d6e8e7..2c046802515 100644 --- a/packages/canvas-planner/src/actions/__tests__/actions.spec.js +++ b/packages/canvas-planner/src/actions/__tests__/actions.spec.js @@ -28,6 +28,8 @@ jest.mock('../../utilities/apiUtils', () => ({ transformPlannerNoteApiToInternalItem: jest.fn(response => ({...response, transformedToInternal: true})) })); +const simpleItem = opts => Object.assign({some: 'data', date: moment('2018-03-28T13:14:00-04:00')}, opts); + const getBasicState = () => ({ courses: [], groups: [], @@ -241,7 +243,7 @@ describe('api actions', () => { describe('savePlannerItem', () => { it('dispatches saving and saved actions', () => { const mockDispatch = jest.fn(); - const plannerItem = {some: 'data'}; + const plannerItem = simpleItem(); const savePromise = Actions.savePlannerItem(plannerItem)(mockDispatch, getBasicState); expect(isPromise(savePromise)).toBe(true); expect(mockDispatch).toHaveBeenCalledWith({type: 'SAVING_PLANNER_ITEM', payload: {item: plannerItem, isNewItem: true}}); @@ -250,7 +252,7 @@ describe('api actions', () => { it('sets isNewItem to false if the item id exists', () => { const mockDispatch = jest.fn(); - const plannerItem = {some: 'data', id: '42'}; + const plannerItem = simpleItem({id: '42'}); const savePromise = Actions.savePlannerItem(plannerItem)(mockDispatch, getBasicState); expect(isPromise(savePromise)).toBe(true); expect(mockDispatch).toHaveBeenCalledWith({type: 'SAVING_PLANNER_ITEM', payload: {item: plannerItem, isNewItem: false}}); @@ -259,7 +261,7 @@ describe('api actions', () => { it('sends transformed data in the request', () => { const mockDispatch = jest.fn(); - const plannerItem = {some: 'data'}; + const plannerItem = simpleItem(); Actions.savePlannerItem(plannerItem)(mockDispatch, getBasicState); return moxiosWait(request => { expect(JSON.parse(request.config.data)).toMatchObject({some: 'data', transformedToApi: true}); @@ -268,7 +270,7 @@ describe('api actions', () => { it('resolves the promise with transformed response data', () => { const mockDispatch = jest.fn(); - const plannerItem = {some: 'data'}; + const plannerItem = simpleItem(); const savePromise = Actions.savePlannerItem(plannerItem)(mockDispatch, getBasicState); return moxiosRespond( { some: 'response data' }, @@ -282,8 +284,8 @@ describe('api actions', () => { }); it('does a post if the planner item is new (no id)', () => { - const plannerItem = {some: 'data'}; - Actions.savePlannerItem(plannerItem)(() => {}); + const plannerItem = simpleItem(); + Actions.savePlannerItem(plannerItem)(() => {}, () => {return {timeZone: 'America/Halifax'};}); return moxiosWait((request) => { expect(request.config.method).toBe('post'); expect(request.url).toBe('api/v1/planner_notes'); @@ -291,20 +293,24 @@ describe('api actions', () => { }); }); - it('does set default time of 11:59 pm for planner date', () => { - const plannerItem = {date: moment('2017-06-22T10:05:54').tz("Atlantic/Azores").toISOString()}; - Actions.savePlannerItem(plannerItem)(() => {}); + it('does set default time of 11:59 pm for planner date at midnight', () => { + const TZ = "Atlantic/Azores"; + const plannerItem = simpleItem({date: moment.tz(TZ).startOf('day').toISOString()}); + Actions.savePlannerItem(plannerItem)(() => {}, () => {return {timeZone: TZ};}); return moxiosWait((request) => { expect(request.config.method).toBe('post'); expect(request.url).toBe('api/v1/planner_notes'); expect(JSON.parse(request.config.data).transformedToApi).toBeTruthy(); - expect(moment(JSON.parse(request.config.data).date).tz("Atlantic/Azores").toISOString()).toBe(moment('2017-06-22T23:59:59').tz("Atlantic/Azores").toISOString()); + const result = moment(JSON.parse(request.config.data).date).tz(TZ); + expect(result.hours()).toEqual(23); + expect(result.minutes()).toEqual(59); + expect(result.seconds()).toEqual(59); }); }); it('does a put if the planner item exists (has id)', () => { - const plannerItem = {id: '42', some: 'data'}; - Actions.savePlannerItem(plannerItem, )(() => {}); + const plannerItem = simpleItem({id: '42'}); + Actions.savePlannerItem(plannerItem, )(() => {}, () => {return {timeZone: 'America/Halifax'};}); return moxiosWait((request) => { expect(request.config.method).toBe('put'); expect(request.url).toBe('api/v1/planner_notes/42'); @@ -319,7 +325,7 @@ describe('api actions', () => { visualErrorCallback: fakeAlert }); - const plannerItem = {some: 'data'}; + const plannerItem = simpleItem(); const savePromise = Actions.savePlannerItem(plannerItem)(mockDispatch, getBasicState); return moxiosRespond( { some: 'response data' }, @@ -333,7 +339,7 @@ describe('api actions', () => { it('saves and restores the override data', () => { const mockDispatch = jest.fn(); // a planner item with override data - const plannerItem = {some: 'data', id: '42', overrideId: '17', completed: true}; + const plannerItem = simpleItem({id: '42', overrideId: '17', completed: true}); const savePromise = Actions.savePlannerItem(plannerItem)(mockDispatch, getBasicState); return moxiosRespond( {some: 'data', id: '42'}, // notice the response has no override data @@ -358,7 +364,7 @@ describe('api actions', () => { describe('deletePlannerItem', () => { it('dispatches deleting and deleted actions', () => { const mockDispatch = jest.fn(); - const plannerItem = {some: 'data'}; + const plannerItem = simpleItem(); const deletePromise = Actions.deletePlannerItem(plannerItem)(mockDispatch, getBasicState); expect(isPromise(deletePromise)).toBe(true); expect(mockDispatch).toHaveBeenCalledWith({type: 'DELETING_PLANNER_ITEM', payload: plannerItem}); @@ -366,7 +372,7 @@ describe('api actions', () => { }); it('sends a delete request for the item id', () => { - const plannerItem = {id: '42', some: 'data'}; + const plannerItem = simpleItem({id: '42'}); Actions.deletePlannerItem(plannerItem, )(() => {}); return moxiosWait((request) => { expect(request.config.method).toBe('delete'); @@ -376,7 +382,7 @@ describe('api actions', () => { it('resolves the promise with transformed response data', () => { const mockDispatch = jest.fn(); - const plannerItem = {some: 'data'}; + const plannerItem = simpleItem(); const deletePromise = Actions.deletePlannerItem(plannerItem)(mockDispatch, getBasicState); return moxiosRespond( { some: 'response data' }, @@ -393,7 +399,7 @@ describe('api actions', () => { visualErrorCallback: fakeAlert }); - const plannerItem = { some: 'data' }; + const plannerItem = simpleItem(); const deletePromise = Actions.deletePlannerItem(plannerItem)(mockDispatch, getBasicState); return moxiosRespond( { some: 'response data' }, @@ -408,7 +414,7 @@ describe('api actions', () => { describe('togglePlannerItemCompletion', () => { it('dispatches saving and saved actions', () => { const mockDispatch = jest.fn(); - const plannerItem = {some: 'data'}; + const plannerItem = simpleItem(); const savingItem = {...plannerItem, show: true, toggleAPIPending: true}; const savePromise = Actions.togglePlannerItemCompletion(plannerItem)(mockDispatch, getBasicState); expect(isPromise(savePromise)).toBe(true); @@ -418,7 +424,7 @@ describe('api actions', () => { it ('updates marked_complete and sends override data in the request', () => { const mockDispatch = jest.fn(); - const plannerItem = {some: 'data', marked_complete: null}; + const plannerItem = simpleItem({marked_complete: null}); Actions.togglePlannerItemCompletion(plannerItem)(mockDispatch, getBasicState); return moxiosWait(request => { expect(JSON.parse(request.config.data)).toMatchObject({marked_complete: true, transformedToApiOverride: true}); @@ -427,7 +433,7 @@ describe('api actions', () => { it('does a post if the planner override is new (no id)', () => { const mockDispatch = jest.fn(); - const plannerItem = {id: '42', some: 'data'}; + const plannerItem = simpleItem({id: '42'}); Actions.togglePlannerItemCompletion(plannerItem)(mockDispatch, getBasicState); return moxiosWait((request) => { expect(request.config.method).toBe('post'); @@ -438,7 +444,7 @@ describe('api actions', () => { it('does a put if the planner override exists (has id)', () => { const mockDispatch = jest.fn(); - const plannerItem = {id: '42', some: 'data', planner_override: {id: '5', marked_complete: true}}; + const plannerItem = simpleItem({id: '42', planner_override: {id: '5', marked_complete: true}}); Actions.togglePlannerItemCompletion(plannerItem)(mockDispatch, getBasicState); return moxiosWait((request) => { expect(request.config.method).toBe('put'); @@ -449,7 +455,7 @@ describe('api actions', () => { it ('resolves the promise with override response data in the item', () => { const mockDispatch = jest.fn(); - const plannerItem = {some: 'data', planner_override: {id: 'override_id', marked_complete: true}}; + const plannerItem = simpleItem({planner_override: {id: 'override_id', marked_complete: true}}); const togglePromise = Actions.togglePlannerItemCompletion(plannerItem)(mockDispatch, getBasicState); return moxiosRespond( {some: 'response data', id: 'override_id', marked_complete: false }, diff --git a/packages/canvas-planner/src/actions/index.js b/packages/canvas-planner/src/actions/index.js index 5e774badaf3..a642778a251 100644 --- a/packages/canvas-planner/src/actions/index.js +++ b/packages/canvas-planner/src/actions/index.js @@ -17,11 +17,11 @@ */ import { createActions } from 'redux-actions'; import axios from 'axios'; -import moment from 'moment-timezone'; import configureAxios from '../utilities/configureAxios'; import { alert } from '../utilities/alertUtils'; import formatMessage from '../format-message'; import parseLinkHeader from 'parse-link-header'; +import { makeEndOfDayIfMidnight } from '../utilities/dateUtils'; import { transformInternalToApiItem, @@ -146,9 +146,10 @@ export const dismissOpportunity = (id, plannerOverride) => { }; export const savePlannerItem = (plannerItem) => { - plannerItem.date = moment(plannerItem.date).endOf('day').format('YYYY-MM-DDTHH:mm:ssZ'); - return (dispatch, getState) => { + plannerItem.date = makeEndOfDayIfMidnight(plannerItem.date, getState().timeZone); + plannerItem.date = plannerItem.date.toISOString(); + const isNewItem = !plannerItem.id; const overrideData = getOverrideDataOnItem(plannerItem); dispatch(savingPlannerItem({item: plannerItem, isNewItem})); diff --git a/packages/canvas-planner/src/components/PlannerItem/index.js b/packages/canvas-planner/src/components/PlannerItem/index.js index b69f27aa60c..917604b283c 100644 --- a/packages/canvas-planner/src/components/PlannerItem/index.js +++ b/packages/canvas-planner/src/components/PlannerItem/index.js @@ -125,8 +125,7 @@ export class PlannerItem extends Component { } renderDateField = () => { - if (this.props.date && - this.props.associated_item !== "To Do") { + if (this.props.date) { if (this.props.associated_item === "Announcement") { return this.props.date.format("LT"); diff --git a/packages/canvas-planner/src/components/UpdateItemTray/__tests__/UpdateItemTray.spec.js b/packages/canvas-planner/src/components/UpdateItemTray/__tests__/UpdateItemTray.spec.js index 3a290ff07be..defff656e30 100644 --- a/packages/canvas-planner/src/components/UpdateItemTray/__tests__/UpdateItemTray.spec.js +++ b/packages/canvas-planner/src/components/UpdateItemTray/__tests__/UpdateItemTray.spec.js @@ -18,7 +18,7 @@ import moment from 'moment-timezone'; import React from 'react'; import { shallow, mount } from 'enzyme'; -import UpdateItemTray from '../index'; +import { UpdateItemTray } from '../index'; const defaultProps = { onSavePlannerItem: () => {}, @@ -28,10 +28,16 @@ const defaultProps = { courses: [], }; +const simpleItem = (opts = {}) => Object.assign({title: '', date: moment('2017-04-28T11:00:00Z')}, opts); + +afterEach(()=> { + jest.restoreAllMocks(); +}); + it('renders the item to update if provided', () => { const noteItem = { title: 'Planner Item', - date: '2017-04-25 01:49:00-0700', + date: moment('2017-04-25 01:49:00-0700'), context: {id: '1'}, details: "You made this item to remind you of something, but you forgot what." }; @@ -44,11 +50,11 @@ it('renders the item to update if provided', () => { }); it("doesn't re-render unless new item is provided", () => { - const wrapper = shallow() - const newProps = {...defaultProps, locale: 'fr'} - wrapper.setProps(newProps) - expect(wrapper.find('DateInput').props()['messages'].length).toBe(0) -}) + const wrapper = shallow(); + const newProps = {...defaultProps, locale: 'fr'}; + wrapper.setProps(newProps); + expect(wrapper.find('DateTimeInput').props()['messages'].length).toBe(0); +}); it('renders Add To Do header when creating a new to do', () => { const wrapper = mount( @@ -78,43 +84,44 @@ it('shows details inputs', () => { }); it('disables the save button when title is empty', () => { - const item = { title: '', date: '2017-04-28' }; + const item = simpleItem(); const wrapper = shallow(); const button = wrapper.find('Button[variant="primary"]'); expect(button.props().disabled).toBe(true); }); it('handles courseid being none', () => { - const item = { title: '', date: '2017-04-28' }; + const item = simpleItem(); const wrapper = shallow(); wrapper.instance().handleCourseIdChange({target: {value: 'none'}}); expect(wrapper.instance().state.updates.courseId).toBe(undefined); }); it('correctly updates id to null when courseid is none', () => { - const item = { title: '', date: '2017-04-28' }; + const item = simpleItem(); const mockCallback = jest.fn(); const wrapper = shallow(); wrapper.instance().handleCourseIdChange({target: {value: 'none'}}); wrapper.instance().handleSave(); expect(mockCallback).toHaveBeenCalledWith({ title: item.title, - date: item.date, + date: item.date.toISOString(), context: { id: null } }); }); -it('sets default date when no date is provided', () => { +it('sets default datetime to 11:50pm today when no date is provided', () => { + const now = moment.tz(defaultProps.timeZone).endOf('day'); const item = { title: 'an item', date: '' }; const wrapper = shallow(); - const datePicker = wrapper.find('DateInput'); - expect(!datePicker.props().dateValue.length).toBe(false); + const datePicker = wrapper.find('DateTimeInput'); + expect(datePicker.props().value).toEqual(now.toISOString()); }); it('enables the save button when title and date are present', () => { - const item = { title: 'an item', date: '2017-04-28' }; + const item = simpleItem({ title: 'an item' }); const wrapper = shallow(); const button = wrapper.find('Button[variant="primary"]'); expect(button.props().disabled).toBe(false); @@ -151,7 +158,7 @@ xit('does not set an initial error message on date', () => { }); xit('sets error message on date field when date is set to blank', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.instance().handleDateChange({target: {value: ''}}); const dateInput = wrapper.find('TextInput').at(1); const messages = dateInput.props().messages; @@ -160,7 +167,7 @@ xit('sets error message on date field when date is set to blank', () => { }); xit('clears the error message when a date is typed in', () => { - const wrapper = shallow(); + const wrapper = shallow(); wrapper.instance().handleTitleChange({target: {value: ''}}); wrapper.instance().handleTitleChange({target: {value: '2'}}); const dateInput = wrapper.find('TextInput').at(1); @@ -168,50 +175,43 @@ xit('clears the error message when a date is typed in', () => { }); it('respects the provided timezone', () => { - const item = { title: '', date: '2017-04-25 12:00:00-0300' }; + const item = simpleItem({date: moment('2017-04-25 12:00:00-0300')}); const wrapper = mount(); const d = wrapper.find('DateInput').find('TextInput').props().value; expect(d).toEqual('April 26, 2017'); // timezone shift from -3 to +9 pushes it to the next day }); it('changes state when new date is typed in', () => { - const noteItem = { - title: 'Planner Item', - date: '2017-04-25', - }; + const noteItem = simpleItem({title: 'Planner Item'}); const mockCallback = jest.fn(); const wrapper = mount(); - const newDate = moment('2017-10-16'); + const newDate = moment('2017-10-16T13:30:00'); wrapper.instance().handleDateChange({}, newDate.toISOString()); wrapper.instance().handleSave(); expect(mockCallback).toHaveBeenCalledWith({ title: noteItem.title, date: newDate.toISOString(), - context: { - id: null - } + context: {id: null} }); }); it('updates state when new note is passed in', () => { - const noteItem1 = { + const noteItem1 = simpleItem({ title: 'Planner Item 1', - date: '2017-04-25', context: {id: '1'}, details: "You made this item to remind you of something, but you forgot what." - }; + }); const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); - const noteItem2 = { + const noteItem2 = simpleItem({ title: 'Planner Item 2', - date: '2017-12-25', context: {id: '2'}, details: "This is another reminder" - }; + }); wrapper.setProps({noteItem: noteItem2}); expect(wrapper).toMatchSnapshot(); }); @@ -247,29 +247,38 @@ it('invokes save callback with updated data', () => { const saveMock = jest.fn(); const wrapper = shallow(); wrapper.instance().handleTitleChange({target: {value: 'new title'}}); - wrapper.instance().handleDateChange({}, '2017-05-01'); + wrapper.instance().handleDateChange({}, '2017-05-01T14:00:00Z'); wrapper.instance().handleCourseIdChange({target: {value: '43'}}); wrapper.instance().handleChange('details', 'new details'); wrapper.instance().handleSave(); expect(saveMock).toHaveBeenCalledWith({ - title: 'new title', date: moment('2017-05-01').toISOString(), context: {id: '43'}, details: 'new details', + title: 'new title', date: moment('2017-05-01T14:00:00Z').toISOString(), context: {id: '43'}, details: 'new details', }); }); it('invokes the delete callback', () => { + const item = simpleItem({title: 'a title'}); const mockDelete = jest.fn(); const wrapper = shallow(); const confirmSpy = jest.spyOn(window, 'confirm').mockReturnValue(true); wrapper.instance().handleDeleteClick(); expect(confirmSpy).toHaveBeenCalled(); - expect(mockDelete).toHaveBeenCalledWith({title: 'a title'}); - confirmSpy.mockRestore(); + expect(mockDelete).toHaveBeenCalledWith(item); +}); + +it('invokes invalidDateTimeMessage when an invalid date is entered', () => { + const invalidCallbackSpy = jest.spyOn(UpdateItemTray.prototype, 'invalidDateTimeMessage'); + const wrapper = mount(); + const dateInput = wrapper.find('DateInput').find('input'); + dateInput.simulate('change', {target: {value: 'xxxxx'}}); + dateInput.simulate('blur'); + expect(invalidCallbackSpy).toHaveBeenCalled(); }); diff --git a/packages/canvas-planner/src/components/UpdateItemTray/__tests__/__snapshots__/UpdateItemTray.spec.js.snap b/packages/canvas-planner/src/components/UpdateItemTray/__tests__/__snapshots__/UpdateItemTray.spec.js.snap index 87f08f3e635..de2b5b9bedc 100644 --- a/packages/canvas-planner/src/components/UpdateItemTray/__tests__/__snapshots__/UpdateItemTray.spec.js.snap +++ b/packages/canvas-planner/src/components/UpdateItemTray/__tests__/__snapshots__/UpdateItemTray.spec.js.snap @@ -36,27 +36,30 @@ exports[`renders the item to update if provided 1`] = ` type="text" value="Planner Item" /> - + The date and time this to do is due + + } disabled={false} - format="LL" - inline={false} - inputRef={[Function]} - invalidDateMessage={[Function]} - label="Date" + invalidDateTimeMessage={[Function]} layout="stacked" locale="en" + messageFormat="LLL" messages={Array []} - nextLabel="Next Month" - onDateChange={[Function]} - placement="start" - previousLabel="Previous Month" + onChange={[Function]} readOnly={false} required={true} - size="medium" + timeLabel="Time" + timeStep={30} timezone="Asia/Tokyo" - validationFeedback={true} + value="2017-04-25T08:49:00.000Z" /> - + The date and time this to do is due + + } disabled={false} - format="LL" - inline={false} - inputRef={[Function]} - invalidDateMessage={[Function]} - label="Date" + invalidDateTimeMessage={[Function]} layout="stacked" locale="en" + messageFormat="LLL" messages={Array []} - nextLabel="Next Month" - onDateChange={[Function]} - placement="start" - previousLabel="Previous Month" + onChange={[Function]} readOnly={false} required={true} - size="medium" + timeLabel="Time" + timeStep={30} timezone="Asia/Tokyo" - validationFeedback={true} + value="2017-04-28T11:00:00.000Z" />