From c225720261d4ae3ff9e0347dc681f262ce6e55a4 Mon Sep 17 00:00:00 2001 From: Joe Ipson Date: Fri, 3 Apr 2020 16:57:27 -0600 Subject: [PATCH 1/2] Added some tests for opening a new entry on the timeline --- .../timeline/c-timeline/c-timeline.test.ts | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/src/components/timeline/c-timeline/c-timeline.test.ts b/src/components/timeline/c-timeline/c-timeline.test.ts index 6b4d0990..75d581e1 100644 --- a/src/components/timeline/c-timeline/c-timeline.test.ts +++ b/src/components/timeline/c-timeline/c-timeline.test.ts @@ -4,6 +4,7 @@ Licensed under the terms of the MIT license. See the LICENSE file in the project */ import {TaskQueue} from 'aurelia-framework'; +import * as moment from 'moment'; import {instance, mock} from 'ts-mockito'; import {CTimeline, ZOOM_LEVELS} from './c-timeline'; @@ -13,6 +14,42 @@ import {CToastsService} from '../../toasts/c-toasts/c-toasts-service'; const taskQueue = mock(TaskQueue); const toastsService = mock(CToastsService); +// Mock _.debounce +// @ts-ignore +jest.spyOn(_, 'debounce').mockImplementation(fn => fn); + +const now = moment('12/12/2020', 'MM/DD/YYYY') + .startOf('day') + .add(12, 'hours'); +const startDay = moment(now) + .startOf('day') + .toISOString(); + +const sortedEntries: any[] = [ + { + duration: 240, + start: now.toISOString(), + }, + { + duration: 120, + start: moment(now) + .add(1, 'hour') + .toISOString(), + }, + { + duration: 120, + start: moment(now) + .add(1, 'hour') + .toISOString(), + }, + { + duration: 120, + start: moment(now) + .add(1, 'day') + .toISOString(), + }, +]; + describe('c-timeline-block element', () => { let component; @@ -30,7 +67,26 @@ describe('c-timeline-block element', () => { describe('Unit', () => { beforeEach(() => { + jest.useFakeTimers(); component = new CTimeline(instance(taskQueue), instance(toastsService)); + + component.date = startDay; + component.entries = sortedEntries; + component.placeholderEntry = { + openPopover: jest.fn(), + }; + + component.attached(); + + component.parentScrollElem = { + offset: () => ({top: 0}), + scrollTop: () => 0, + }; + }); + + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); }); describe('#mapAllowedTimes', () => { @@ -73,6 +129,57 @@ describe('c-timeline-block element', () => { }); }); + describe('#togglePopover', () => { + test('regular interval positioning', () => { + const ev = { + layerY: 10, + pageY: 10, + }; + + component.togglePopover(ev); + expect(component.newItem.title).toBe('00:00 (New Item)'); + + ev.layerY = 30; + component.togglePopover(ev); + expect(component.newItem.title).toBe('00:15 (New Item)'); + + ev.pageY = 60; + component.togglePopover(ev); + expect(component.newItem.title).toBe('00:45 (New Item)'); + }); + + test('snap add positioning', () => { + const ev = { + layerY: 10, + pageY: 1600, + }; + + component.snapAdd = true; + + // No snapping + component.togglePopover(ev); + expect(component.newItem.title).toBe('16:00 (New Item)'); + + // Snapping + ev.pageY = 1200; + component.togglePopover(ev); + expect(component.newItem.title).toBe('12:04 (New Item)'); + }); + + test('opening popover', () => { + const ev = { + layerY: 10, + pageY: 10, + }; + + component.togglePopover(ev); + + jest.runOnlyPendingTimers(); + + expect(component.placeholderEntry.openPopover).toHaveBeenCalled(); + }); + }); + describe('#getHoursMinutes', () => { let data; From a5b34eaae79e47d076c43db36539a17563abb5c7 Mon Sep 17 00:00:00 2001 From: Joe Ipson Date: Fri, 10 Apr 2020 12:54:13 -0600 Subject: [PATCH 2/2] Fixed a few issues with the timeline: changing timezone didn't change the time on the entries, changing timezone could mess with the current day selected, entries on highest zoom weren't showing seconds. Fixed an issue where new users couldn't install bindable without a global jquery variable in webpack. --- dev-app/app.html | 2 +- .../components/timeline/demo/index.html | 2 +- .../routes/components/timeline/demo/index.ts | 4 +- package-lock.json | 2 +- package.json | 2 +- .../forms/date/c-form-date/c-form-date.ts | 12 +--- .../timeline/c-timeline/c-timeline.ts | 56 +++++++++++++++---- .../timeline/c-timeline/workers.test.ts | 8 +-- src/components/timeline/c-timeline/workers.ts | 31 ++++++++-- src/index.ts | 8 +++ 10 files changed, 92 insertions(+), 35 deletions(-) diff --git a/dev-app/app.html b/dev-app/app.html index 47ec0e33..d7558d78 100644 --- a/dev-app/app.html +++ b/dev-app/app.html @@ -24,7 +24,7 @@ state="${link.isActive ? 'active' : ''}" > - + diff --git a/dev-app/routes/components/timeline/demo/index.html b/dev-app/routes/components/timeline/demo/index.html index 759c873d..56ce6a1e 100644 --- a/dev-app/routes/components/timeline/demo/index.html +++ b/dev-app/routes/components/timeline/demo/index.html @@ -13,7 +13,7 @@ snap-add.bind="true" zoom-level.bind="zoomLevel" is-loading.bind="loading" - prevent-create.call="preventCreate(isoTime)" + prevent-create.bind="preventCreate" snap-add.bind="true" > diff --git a/dev-app/routes/components/timeline/demo/index.ts b/dev-app/routes/components/timeline/demo/index.ts index b5da718c..238cd175 100644 --- a/dev-app/routes/components/timeline/demo/index.ts +++ b/dev-app/routes/components/timeline/demo/index.ts @@ -168,10 +168,10 @@ export class TimelineExample { // }, ]; - public zoomLevel = 5; + public zoomLevel = 2; public displayView = 'three-day'; public loading = false; - public preventCreate = _isoTime => false; + public preventCreate = false; // constructor() { // const genRandom = (min, max) => Math.random() * (max - min + 1) + min; diff --git a/package-lock.json b/package-lock.json index f23b1e44..71bf8b61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@bindable-ui/bindable", - "version": "1.0.23", + "version": "1.0.24", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a5e2d408..eea377d2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@bindable-ui/bindable", "description": "An Aurelia component library", - "version": "1.0.23", + "version": "1.0.24", "repository": { "type": "git", "url": "https://github.com/bindable-ui/bindable" diff --git a/src/components/forms/date/c-form-date/c-form-date.ts b/src/components/forms/date/c-form-date/c-form-date.ts index ac028cdd..ea6a8e4d 100644 --- a/src/components/forms/date/c-form-date/c-form-date.ts +++ b/src/components/forms/date/c-form-date/c-form-date.ts @@ -3,24 +3,14 @@ Copyright 2020, Verizon Media Licensed under the terms of the MIT license. See the LICENSE file in the project root for license terms. */ -import {bindable, bindingMode, computedFrom, containerless} from 'aurelia-framework'; -import * as datetimepicker from 'eonasdan-bootstrap-datetimepicker'; +import {bindable, bindingMode, containerless} from 'aurelia-framework'; import * as moment from 'moment'; import {authState} from '../../../../decorators/auth-state'; import * as styles from './c-form-date.css.json'; -declare const window: any; declare let $: any; -if (window.$) { - $ = window.$; -} - -$.fn.extend({ - datetimepicker, -}); - /** * @param id {String} - Element ID. * @param timestamp {Number} - Unix timestamp. diff --git a/src/components/timeline/c-timeline/c-timeline.ts b/src/components/timeline/c-timeline/c-timeline.ts index 722e8b74..adc79b74 100644 --- a/src/components/timeline/c-timeline/c-timeline.ts +++ b/src/components/timeline/c-timeline/c-timeline.ts @@ -209,6 +209,8 @@ export class CTimeline { this.timeView, this.editEntryViewModel, this.date, + this.getTzOffet(), + this.zoomLevel, ); } @@ -227,6 +229,8 @@ export class CTimeline { this.timeView, this.editEntryViewModel, this.date, + this.getTzOffet(), + this.zoomLevel, ); startTime.add(1, 'day'); @@ -438,6 +442,11 @@ export class CTimeline { } public attached() { + // Trigger the default TZ stuff + if (!this.timezone) { + this.setupTimezone(); + } + this.getParentScrollElem(); this.buildTimeline(); @@ -496,16 +505,7 @@ export class CTimeline { } public timezoneChanged() { - const tzNames = moment.tz.names(); - const tzIndex = tzNames.findIndex(name => name === this.timezone); - - if (tzIndex > -1) { - moment.tz.setDefault(this.timezone); - } else { - moment.tz.setDefault(); - } - - this.renderTimeline(); + this.setupTimezone(true); } public scrollTimeChanged(_new, old) { @@ -535,6 +535,42 @@ export class CTimeline { // Private methods + /** + * Return the offset in minutes from the selected timezone to the browser timezone + */ + private getTzOffet(): number { + const browserOffset = moment.tz.zone(moment.tz.guess()).utcOffset(moment()); + const offset = moment().utcOffset(); + + return browserOffset + offset; + } + + /** + * Sets up the timezone data + * + * @param updateDate Boolean value to determine whether or not to update `this.date` + */ + private setupTimezone(updateDate?: boolean) { + const tzNames = moment.tz.names(); + const tzIndex = tzNames.findIndex(name => name === this.timezone); + + if (tzIndex > -1) { + moment.tz.setDefault(this.timezone); + } else { + moment.tz.setDefault(); + } + + if (updateDate) { + // This prevents issues where the date will look in the past and throw off the three-day view + const offset = this.getTzOffet(); + this.date = moment(this.date) + .add(offset * -1, 'minutes') + .toISOString(); + } + + this.renderTimeline(); + } + /** * Get the closest parent element that scrolls */ diff --git a/src/components/timeline/c-timeline/workers.test.ts b/src/components/timeline/c-timeline/workers.test.ts index f37e39bf..1d5a442f 100644 --- a/src/components/timeline/c-timeline/workers.test.ts +++ b/src/components/timeline/c-timeline/workers.test.ts @@ -55,21 +55,21 @@ describe('Web worker functions', () => { describe('#mapEntries', () => { it('tests formatting', async () => { - const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString()); + const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString(), 0, 2); expect(data[0].startTime).toBe('12:00'); expect(data[0].endTime).toBe('12:04'); }); it('tests positioning', async () => { - const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString()); + const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString(), 0, 2); expect(data[0].top).toBe(1440); expect(data[0].height).toBe(8); }); it('tests same time entries', async () => { - const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString()); + const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString(), 0, 2); expect(data[0].widthCalc).toBeUndefined(); expect(data[2].widthCalc).toBeDefined(); @@ -77,7 +77,7 @@ describe('Web worker functions', () => { }); it('tests nested entries', async () => { - const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString()); + const data = await mapEntries(sortedEntries, 2, startDay, endDay, 'day', '', now.toISOString(), 0, 2); expect(data[0].widthCalc).toBeUndefined(); expect(data[1].widthCalc).toBeDefined(); diff --git a/src/components/timeline/c-timeline/workers.ts b/src/components/timeline/c-timeline/workers.ts index 0972a41c..157a60a1 100644 --- a/src/components/timeline/c-timeline/workers.ts +++ b/src/components/timeline/c-timeline/workers.ts @@ -32,6 +32,8 @@ function mapEntriesFn( timeView: string, editEntryViewModel: string, date: string, + tzOffset: number, + zoomLevel: number, ) { const SECONDS_IN_MINUTE = 60; @@ -44,10 +46,17 @@ function mapEntriesFn( }; const formatHHmm = isoString => { - const dateObj = new Date(isoString); + const dateObj = new Date(new Date(isoString).getTime() + tzOffset * 60 * 1000); return `${appendLeadingZeroes(dateObj.getHours())}:${appendLeadingZeroes(dateObj.getMinutes())}`; }; + const formatHHmmss = isoString => { + const dateObj = new Date(new Date(isoString).getTime() + tzOffset * 60 * 1000); + return `${appendLeadingZeroes(dateObj.getHours())}:${appendLeadingZeroes( + dateObj.getMinutes(), + )}:${appendLeadingZeroes(dateObj.getSeconds())}`; + }; + const upToMm = isoString => { const dateObj = new Date(isoString); const year = dateObj.getFullYear(); @@ -75,13 +84,13 @@ function mapEntriesFn( entry.start = startTime; } - entry.startTime = formatHHmm(entry.start); + entry.startTime = zoomLevel === 5 ? formatHHmmss(entry.start) : formatHHmm(entry.start); if (!entry.end) { entry.end = new Date(new Date(entry.start).getTime() + entry.duration * 1000).toISOString(); } - entry.endTime = formatHHmm(entry.end); + entry.endTime = zoomLevel === 5 ? formatHHmmss(entry.end) : formatHHmm(entry.end); const entryStartDate: any = new Date(entry.start); const startTimeDate: any = new Date(startTime); @@ -225,6 +234,8 @@ export const mapEntries = async ( timeView: string, editEntryViewModel: string, date: string, + tzOffset: number, + zoomLevel: number, ): Promise => { if (window.Worker) { return await worker.postMessage('mapEntries', [ @@ -235,10 +246,22 @@ export const mapEntries = async ( timeView, editEntryViewModel, date, + tzOffset, + zoomLevel, ]); } - return mapEntriesFn(sortedEntries, pxPerMinute, startTime, endTime, timeView, editEntryViewModel, date); + return mapEntriesFn( + sortedEntries, + pxPerMinute, + startTime, + endTime, + timeView, + editEntryViewModel, + date, + tzOffset, + zoomLevel, + ); }; export const filterEntriesDay = async (sortedEntries: any[], startTime: string, endTime: string): Promise => { diff --git a/src/index.ts b/src/index.ts index b3ff2b59..325f8b1e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,14 @@ Copyright 2020, Verizon Media Licensed under the terms of the MIT license. See the LICENSE file in the project root for license terms. */ +// Setup jQuery +import * as datetimepicker from 'eonasdan-bootstrap-datetimepicker'; +import * as $ from 'jquery'; + +$.fn.extend({ + datetimepicker, +}); + import {FrameworkConfiguration} from 'aurelia-framework'; import {PLATFORM} from 'aurelia-pal';