Timelist bug fixes (#6661)

* Ensure timelist scrolling happens correctly for clock as well as fixed time

* If an activity has already started, show the duration as time to/since the end of the activity

* Addresses review comments: Reverse +/- indicators, removes milliseconds from times.

* Fix linting issues

* Add e2e test for timelist display

* Scroll to 'now' if available
This commit is contained in:
Shefali Joshi 2023-06-16 12:45:59 -07:00 committed by GitHub
parent 4c5de37cff
commit 022dffd419
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 184 additions and 22 deletions

View File

@ -0,0 +1,122 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test, expect } = require('../../../pluginFixtures');
const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../../appActions');
const testPlan = {
TEST_GROUP: [
{
name: 'Past event 1',
start: 1660320408000,
end: 1660343797000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
},
{
name: 'Past event 2',
start: 1660406808000,
end: 1660429160000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
},
{
name: 'Past event 3',
start: 1660493208000,
end: 1660503981000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
},
{
name: 'Past event 4',
start: 1660579608000,
end: 1660624108000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
},
{
name: 'Past event 5',
start: 1660666008000,
end: 1660681529000,
type: 'TEST-GROUP',
color: 'orange',
textColor: 'white'
}
]
};
test.describe('Time List', () => {
test('Create a Time List, add a single Plan to it and verify all the activities are displayed with no milliseconds', async ({
page
}) => {
// Goto baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
const timelist = await test.step('Create a Time List', async () => {
const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' });
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
expect(objectName).toBe(createdTimeList.name);
return createdTimeList;
});
await test.step('Create a Plan and add it to the timelist', async () => {
const createdPlan = await createPlanFromJSON(page, {
name: 'Test Plan',
json: testPlan
});
await page.goto(timelist.url);
// Expand the tree to show the plan
await page.click("button[title='Show selected item in tree']");
await page.dragAndDrop(`role=treeitem[name=/${createdPlan.name}/]`, '.c-object-view');
await page.click("button[title='Save']");
await page.click("li[title='Save and Finish Editing']");
const startBound = testPlan.TEST_GROUP[0].start;
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
// Switch to fixed time mode with all plan events within the bounds
await page.goto(
`${timelist.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=timelist.view`
);
// Verify all events are displayed
const eventCount = await page.locator('.js-list-item').count();
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
});
await test.step('Does not show milliseconds in times', async () => {
// Get the first activity
const row = await page.locator('.js-list-item').first();
// Verify that none fo the times have milliseconds displayed.
// Example: 2024-11-17T16:00:00Z is correct and 2024-11-17T16:00:00.000Z is wrong
await expect(row.locator('.--start')).not.toContainText('.');
await expect(row.locator('.--end')).not.toContainText('.');
await expect(row.locator('.--duration')).not.toContainText('.');
});
});
});

View File

@ -41,8 +41,10 @@ import moment from 'moment';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
const SCROLL_TIMEOUT = 10000; const SCROLL_TIMEOUT = 10000;
const ROW_HEIGHT = 30; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss:SSS'; const CURRENT_CSS_SUFFIX = '--is-current';
const PAST_CSS_SUFFIX = '--is-past';
const FUTURE_CSS_SUFFIX = '--is-future';
const headerItems = [ const headerItems = [
{ {
defaultDirection: true, defaultDirection: true,
@ -79,9 +81,9 @@ const headerItems = [
format: function (value) { format: function (value) {
let result; let result;
if (value < 0) { if (value < 0) {
result = `-${getPreciseDuration(Math.abs(value))}`; result = `+${getPreciseDuration(Math.abs(value), true)}`;
} else if (value > 0) { } else if (value > 0) {
result = `+${getPreciseDuration(value)}`; result = `-${getPreciseDuration(value, true)}`;
} else { } else {
result = 'Now'; result = 'Now';
} }
@ -360,11 +362,12 @@ export default {
groups.forEach((key) => { groups.forEach((key) => {
activities = activities.concat(this.planData[key]); activities = activities.concat(this.planData[key]);
}); });
activities = activities.filter(this.filterActivities); // filter activities first, then sort by start time
activities = activities.filter(this.filterActivities).sort(this.sortByStartTime);
activities = this.applyStyles(activities); activities = this.applyStyles(activities);
this.setScrollTop(); this.planActivities = activities;
// sort by start time //We need to wait for the next tick since we need the height of the row from the DOM
this.planActivities = activities.sort(this.sortByStartTime); this.$nextTick(this.setScrollTop);
}, },
updateTimeStampAndListActivities(time) { updateTimeStampAndListActivities(time) {
this.timestamp = time; this.timestamp = time;
@ -410,30 +413,41 @@ export default {
}, },
applyStyles(activities) { applyStyles(activities) {
let firstCurrentActivityIndex = -1; let firstCurrentActivityIndex = -1;
let activityClosestToNowIndex = -1;
let currentActivitiesCount = 0; let currentActivitiesCount = 0;
const styledActivities = activities.map((activity, index) => { const styledActivities = activities.map((activity, index) => {
if (this.timestamp >= activity.start && this.timestamp <= activity.end) { if (this.timestamp >= activity.start && this.timestamp <= activity.end) {
activity.cssClass = '--is-current'; activity.cssClass = CURRENT_CSS_SUFFIX;
if (firstCurrentActivityIndex < 0) { if (firstCurrentActivityIndex < 0) {
firstCurrentActivityIndex = index; firstCurrentActivityIndex = index;
} }
currentActivitiesCount = currentActivitiesCount + 1; currentActivitiesCount = currentActivitiesCount + 1;
} else if (this.timestamp < activity.start) { } else if (this.timestamp < activity.start) {
activity.cssClass = '--is-future'; activity.cssClass = FUTURE_CSS_SUFFIX;
//the index of the first activity that's greater than the current timestamp
if (activityClosestToNowIndex < 0) {
activityClosestToNowIndex = index;
}
} else { } else {
activity.cssClass = '--is-past'; activity.cssClass = PAST_CSS_SUFFIX;
} }
if (!activity.key) { if (!activity.key) {
activity.key = uuid(); activity.key = uuid();
} }
activity.duration = activity.start - this.timestamp; if (activity.start < this.timestamp) {
//if the activity start time has passed, display the time to the end of the activity
activity.duration = activity.end - this.timestamp;
} else {
activity.duration = activity.start - this.timestamp;
}
return activity; return activity;
}); });
this.activityClosestToNowIndex = activityClosestToNowIndex;
this.firstCurrentActivityIndex = firstCurrentActivityIndex; this.firstCurrentActivityIndex = firstCurrentActivityIndex;
this.currentActivitiesCount = currentActivitiesCount; this.currentActivitiesCount = currentActivitiesCount;
@ -451,13 +465,22 @@ export default {
} }
this.firstCurrentActivityIndex = -1; this.firstCurrentActivityIndex = -1;
this.activityClosestToNowIndex = -1;
this.currentActivitiesCount = 0; this.currentActivitiesCount = 0;
this.$el.parentElement?.scrollTo({ top: 0 }); this.$el.parentElement?.scrollTo({ top: 0 });
this.autoScrolled = false; this.autoScrolled = false;
}, },
setScrollTop() { setScrollTop() {
//scroll to somewhere mid-way of the current activities //The view isn't ready yet
if (this.firstCurrentActivityIndex > -1) { if (!this.$el.parentElement) {
return;
}
const row = this.$el.querySelector('.js-list-item');
if (row && this.firstCurrentActivityIndex > -1) {
// scroll to somewhere mid-way of the current activities
const ROW_HEIGHT = row.getBoundingClientRect().height;
if (this.canAutoScroll() === false) { if (this.canAutoScroll() === false) {
return; return;
} }
@ -469,7 +492,22 @@ export default {
behavior: 'smooth' behavior: 'smooth'
}); });
this.autoScrolled = false; this.autoScrolled = false;
} else if (row && this.activityClosestToNowIndex > -1) {
// scroll to somewhere close to 'now'
const ROW_HEIGHT = row.getBoundingClientRect().height;
if (this.canAutoScroll() === false) {
return;
}
this.$el.parentElement.scrollTo({
top: ROW_HEIGHT * (this.activityClosestToNowIndex - 1),
behavior: 'smooth'
});
this.autoScrolled = false;
} else { } else {
// scroll to the top
this.resetScroll(); this.resetScroll();
} }
}, },

View File

@ -219,10 +219,10 @@ describe('the plugin', function () {
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua' 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua'
); );
expect(itemValues[0].innerHTML.trim()).toEqual( expect(itemValues[0].innerHTML.trim()).toEqual(
`${moment(twoHoursPast).format('YYYY-MM-DD HH:mm:ss:SSS')}Z` `${moment(twoHoursPast).format('YYYY-MM-DD HH:mm:ss')}Z`
); );
expect(itemValues[1].innerHTML.trim()).toEqual( expect(itemValues[1].innerHTML.trim()).toEqual(
`${moment(oneHourPast).format('YYYY-MM-DD HH:mm:ss:SSS')}Z` `${moment(oneHourPast).format('YYYY-MM-DD HH:mm:ss')}Z`
); );
done(); done();

View File

@ -63,14 +63,16 @@ export function millisecondsToDHMS(numericDuration) {
return `${dhms ? '+' : ''} ${dhms}`; return `${dhms ? '+' : ''} ${dhms}`;
} }
export function getPreciseDuration(value) { export function getPreciseDuration(value, excludeMilliSeconds) {
const ms = value || 0; const ms = value || 0;
const duration = [
return [
toDoubleDigits(Math.floor(normalizeAge(ms / ONE_DAY))), toDoubleDigits(Math.floor(normalizeAge(ms / ONE_DAY))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))), toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))), toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))),
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND))), toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)))
toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND))) ];
].join(':'); if (!excludeMilliSeconds) {
duration.push(toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND))));
}
return duration.join(':');
} }