mirror of
https://github.com/nasa/openmct.git
synced 2025-01-29 15:43:52 +00:00
Timelist centering algorithm change and duration formatting change (#7194)
* Change the centering algorithm for timelist Make duration formatting better * test: add time list countdown and countup test * test: respond to comments * chore: lint fix * fix: lint errors and visual suite failures * Change parameters to options object * Fix regression with auto scroll. Improve code readability * Add defaults for getPreciseDuration options object * Defaults for options * Add missing await --------- Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
This commit is contained in:
parent
64d4ddd80e
commit
67a1094a1f
@ -11,8 +11,9 @@ export const MISSION_TIME = 1732413600000; // Saturday, November 23, 2024 6:00:0
|
||||
|
||||
/**
|
||||
* URL Constants
|
||||
* - This is the URL that the browser will be directed to when running visual tests. This URL
|
||||
* - This is the URL that the browser will be directed to when running visual tests. This URL
|
||||
* - hides the tree and inspector to prevent visual noise
|
||||
* - sets the time bounds to a fixed range
|
||||
*/
|
||||
export const VISUAL_URL = './#/browse/mine?tc.mode=fixed&tc.startBound=1693592063607&tc.endBound=1693593893607&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true';
|
||||
export const VISUAL_URL =
|
||||
'./#/browse/mine?tc.mode=fixed&tc.startBound=1693592063607&tc.endBound=1693593893607&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true';
|
||||
|
@ -113,17 +113,35 @@ export async function assertPlanOrderedSwimLanes(page, plan, objectUrl) {
|
||||
* @param {string} planObjectUrl
|
||||
*/
|
||||
export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl) {
|
||||
const activities = Object.values(planJson).flat();
|
||||
// Get the earliest start value
|
||||
const start = Math.min(...activities.map((activity) => activity.start));
|
||||
const start = getEarliestStartTime(planJson);
|
||||
// Get the latest end value
|
||||
const end = Math.max(...activities.map((activity) => activity.end));
|
||||
const end = getLatestEndTime(planJson);
|
||||
// Set the start and end bounds to the earliest start and latest end
|
||||
await page.goto(
|
||||
`${planObjectUrl}?tc.mode=fixed&tc.startBound=${start}&tc.endBound=${end}&tc.timeSystem=utc&view=plan.view`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} planJson
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getEarliestStartTime(planJson) {
|
||||
const activities = Object.values(planJson).flat();
|
||||
return Math.min(...activities.map((activity) => activity.start));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} planJson
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getLatestEndTime(planJson) {
|
||||
const activities = Object.values(planJson).flat();
|
||||
return Math.max(...activities.map((activity) => activity.end));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the Open MCT API to set the status of a plan to 'draft'.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
|
38
e2e/test-data/examplePlans/ExamplePlan_Small3.json
Normal file
38
e2e/test-data/examplePlans/ExamplePlan_Small3.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"Group 1": [
|
||||
{
|
||||
"name": "Time until birthday",
|
||||
"start": 1650320402000,
|
||||
"end": 1660343797000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
},
|
||||
{
|
||||
"name": "Time until supper",
|
||||
"start": 1650320402000,
|
||||
"end": 1650420410000,
|
||||
"type": "Group 2",
|
||||
"color": "blue",
|
||||
"textColor": "white"
|
||||
}
|
||||
],
|
||||
"Group 2": [
|
||||
{
|
||||
"name": "Time since the last time I ate",
|
||||
"start": 1650320102001,
|
||||
"end": 1650320102001,
|
||||
"type": "Group 2",
|
||||
"color": "green",
|
||||
"textColor": "white"
|
||||
},
|
||||
{
|
||||
"name": "Time since last accident",
|
||||
"start": 1650320102002,
|
||||
"end": 1650320102002,
|
||||
"type": "Group 1",
|
||||
"color": "yellow",
|
||||
"textColor": "white"
|
||||
}
|
||||
]
|
||||
}
|
@ -19,10 +19,27 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import fs from 'fs';
|
||||
|
||||
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js';
|
||||
import { getEarliestStartTime } from '../../../helper/planningUtils';
|
||||
import { expect, test } from '../../../pluginFixtures.js';
|
||||
|
||||
const examplePlanSmall3 = JSON.parse(
|
||||
fs.readFileSync(
|
||||
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
|
||||
)
|
||||
);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const START_TIME_COLUMN = 0;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const END_TIME_COLUMN = 1;
|
||||
const TIME_TO_FROM_COLUMN = 2;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const ACTIVITY_COLUMN = 3;
|
||||
const HEADER_ROW = 0;
|
||||
const NUM_COLUMNS = 4;
|
||||
|
||||
const testPlan = {
|
||||
TEST_GROUP: [
|
||||
{
|
||||
@ -84,35 +101,29 @@ test.describe('Time List', () => {
|
||||
});
|
||||
|
||||
await test.step('Create a Plan and add it to the timelist', async () => {
|
||||
const createdPlan = await createPlanFromJSON(page, {
|
||||
await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: testPlan
|
||||
json: testPlan,
|
||||
parent: timelist.uuid
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
await page.goto(timelist.url);
|
||||
|
||||
// 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);
|
||||
const eventCount = await page.getByRole('row').count();
|
||||
// subtracting one for the header
|
||||
await expect(eventCount - 1).toEqual(testPlan.TEST_GROUP.length);
|
||||
});
|
||||
|
||||
await test.step('Does not show milliseconds in times', async () => {
|
||||
// Get the first activity
|
||||
const row = page.locator('.js-list-item').first();
|
||||
// Get an activity
|
||||
const row = page.getByRole('row').nth(2);
|
||||
// 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
|
||||
|
||||
@ -122,3 +133,162 @@ test.describe('Time List', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The regular expression used to parse the countdown string.
|
||||
* Some examples of valid Countdown strings:
|
||||
* ```
|
||||
* '35D 02:03:04'
|
||||
* '-1D 01:02:03'
|
||||
* '01:02:03'
|
||||
* '-05:06:07'
|
||||
* ```
|
||||
*/
|
||||
const COUNTDOWN_REGEXP = /(-)?(\d+D\s)?(\d{2}):(\d{2}):(\d{2})/;
|
||||
|
||||
/**
|
||||
* @typedef {Object} CountdownObject
|
||||
* @property {string} sign - The sign of the countdown ('-' if the countdown is negative, otherwise undefined).
|
||||
* @property {string} days - The number of days in the countdown (undefined if there are no days).
|
||||
* @property {string} hours - The number of hours in the countdown.
|
||||
* @property {string} minutes - The number of minutes in the countdown.
|
||||
* @property {string} seconds - The number of seconds in the countdown.
|
||||
* @property {string} toString - The countdown string.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Object representing the indices of the capture groups in a countdown regex match.
|
||||
*
|
||||
* @typedef {{ SIGN: number, DAYS: number, HOURS: number, MINUTES: number, SECONDS: number, REGEXP: RegExp }}
|
||||
* @property {number} SIGN - The index for the sign capture group (1 if a '-' sign is present, otherwise undefined).
|
||||
* @property {number} DAYS - The index for the days capture group (2 for the number of days, otherwise undefined).
|
||||
* @property {number} HOURS - The index for the hours capture group (3 for the hour part of the time).
|
||||
* @property {number} MINUTES - The index for the minutes capture group (4 for the minute part of the time).
|
||||
* @property {number} SECONDS - The index for the seconds capture group (5 for the second part of the time).
|
||||
*/
|
||||
const COUNTDOWN = Object.freeze({
|
||||
SIGN: 1,
|
||||
DAYS: 2,
|
||||
HOURS: 3,
|
||||
MINUTES: 4,
|
||||
SECONDS: 5
|
||||
});
|
||||
|
||||
test.describe('Time List with controlled clock', () => {
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: getEarliestStartTime(examplePlanSmall3),
|
||||
shouldAdvanceTime: true
|
||||
}
|
||||
});
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
test('Time List shows current events and counts down correctly in real-time mode', async ({
|
||||
page
|
||||
}) => {
|
||||
await test.step('Create a Time List, add a Plan to it, and switch to real-time mode', async () => {
|
||||
// Create Time List
|
||||
const timelist = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Time List'
|
||||
});
|
||||
|
||||
// Create a Plan with events that count down and up.
|
||||
// Add it as a child to the Time List.
|
||||
await createPlanFromJSON(page, {
|
||||
json: examplePlanSmall3,
|
||||
parent: timelist.uuid
|
||||
});
|
||||
|
||||
// Navigate to the Time List in real-time mode
|
||||
await page.goto(
|
||||
`${timelist.url}?tc.mode=local&tc.startDelta=900000&tc.endDelta=1800000&tc.timeSystem=utc&view=grid`
|
||||
);
|
||||
});
|
||||
|
||||
const countUpCells = [
|
||||
getCellByIndex(page, 1, TIME_TO_FROM_COLUMN),
|
||||
getCellByIndex(page, 2, TIME_TO_FROM_COLUMN)
|
||||
];
|
||||
const countdownCells = [
|
||||
getCellByIndex(page, 3, TIME_TO_FROM_COLUMN),
|
||||
getCellByIndex(page, 4, TIME_TO_FROM_COLUMN)
|
||||
];
|
||||
|
||||
// Verify that the countdown cells are counting down
|
||||
for (let i = 0; i < countdownCells.length; i++) {
|
||||
await test.step(`Countdown cell ${i + 1} counts down`, async () => {
|
||||
const countdownCell = countdownCells[i];
|
||||
// Get the initial countdown timestamp object
|
||||
const beforeCountdown = await getAndAssertCountdownObject(page, i + 3);
|
||||
// Wait until it changes
|
||||
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
|
||||
// Get the new countdown timestamp object
|
||||
const afterCountdown = await getAndAssertCountdownObject(page, i + 3);
|
||||
// Verify that the new countdown timestamp object is less than the old one
|
||||
expect(Number(afterCountdown.seconds)).toBeLessThan(Number(beforeCountdown.seconds));
|
||||
});
|
||||
}
|
||||
|
||||
// Verify that the count-up cells are counting up
|
||||
for (let i = 0; i < countUpCells.length; i++) {
|
||||
await test.step(`Count-up cell ${i + 1} counts up`, async () => {
|
||||
const countdownCell = countUpCells[i];
|
||||
// Get the initial count-up timestamp object
|
||||
const beforeCountdown = await getAndAssertCountdownObject(page, i + 1);
|
||||
// Wait until it changes
|
||||
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
|
||||
// Get the new count-up timestamp object
|
||||
const afterCountdown = await getAndAssertCountdownObject(page, i + 1);
|
||||
// Verify that the new count-up timestamp object is greater than the old one
|
||||
expect(Number(afterCountdown.seconds)).toBeGreaterThan(Number(beforeCountdown.seconds));
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the cell at the given row and column indices.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowIndex
|
||||
* @param {number} columnIndex
|
||||
* @returns {import('@playwright/test').Locator} cell
|
||||
*/
|
||||
function getCellByIndex(page, rowIndex, columnIndex) {
|
||||
return page.getByRole('cell').nth(rowIndex * NUM_COLUMNS + columnIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the innerText of the cell at the given row and column indices.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowIndex
|
||||
* @param {number} columnIndex
|
||||
* @returns {Promise<string>} text
|
||||
*/
|
||||
async function getCellTextByIndex(page, rowIndex, columnIndex) {
|
||||
const text = await getCellByIndex(page, rowIndex, columnIndex).innerText();
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text from the countdown cell in the given row, assert that it matches the countdown
|
||||
* regex, and return an object representing the countdown.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowIndex the row index
|
||||
* @returns {Promise<CountdownObject>} countdownObject
|
||||
*/
|
||||
async function getAndAssertCountdownObject(page, rowIndex) {
|
||||
const timeToFrom = await getCellTextByIndex(page, HEADER_ROW + rowIndex, TIME_TO_FROM_COLUMN);
|
||||
|
||||
expect(timeToFrom).toMatch(COUNTDOWN_REGEXP);
|
||||
const match = timeToFrom.match(COUNTDOWN_REGEXP);
|
||||
|
||||
return {
|
||||
sign: match[COUNTDOWN.SIGN],
|
||||
days: match[COUNTDOWN.DAYS],
|
||||
hours: match[COUNTDOWN.HOURS],
|
||||
minutes: match[COUNTDOWN.MINUTES],
|
||||
seconds: match[COUNTDOWN.SECONDS],
|
||||
toString: () => timeToFrom
|
||||
};
|
||||
}
|
||||
|
@ -77,9 +77,12 @@ const headerItems = [
|
||||
format: function (value) {
|
||||
let result;
|
||||
if (value < 0) {
|
||||
result = `+${getPreciseDuration(Math.abs(value), true)}`;
|
||||
result = `+${getPreciseDuration(Math.abs(value), {
|
||||
excludeMilliSeconds: true,
|
||||
useDayFormat: true
|
||||
})}`;
|
||||
} else if (value > 0) {
|
||||
result = `-${getPreciseDuration(value, true)}`;
|
||||
result = `-${getPreciseDuration(value, { excludeMilliSeconds: true, useDayFormat: true })}`;
|
||||
} else {
|
||||
result = 'Now';
|
||||
}
|
||||
@ -351,45 +354,55 @@ export default {
|
||||
return regex.test(name.toLowerCase());
|
||||
});
|
||||
},
|
||||
// Add activity classes, increase activity counts by type,
|
||||
// set indices of the first occurrences of current and future activities - used for scrolling
|
||||
styleActivity(activity, index) {
|
||||
if (this.timestamp >= activity.start && this.timestamp <= activity.end) {
|
||||
activity.cssClass = CURRENT_CSS_SUFFIX;
|
||||
if (this.firstCurrentActivityIndex < 0) {
|
||||
this.firstCurrentActivityIndex = index;
|
||||
}
|
||||
this.currentActivitiesCount = this.currentActivitiesCount + 1;
|
||||
} else if (this.timestamp < activity.start) {
|
||||
activity.cssClass = FUTURE_CSS_SUFFIX;
|
||||
//the index of the first activity that's greater than the current timestamp
|
||||
if (this.firstFutureActivityIndex < 0) {
|
||||
this.firstFutureActivityIndex = index;
|
||||
}
|
||||
this.futureActivitiesCount = this.futureActivitiesCount + 1;
|
||||
} else {
|
||||
activity.cssClass = PAST_CSS_SUFFIX;
|
||||
this.pastActivitiesCount = this.pastActivitiesCount + 1;
|
||||
}
|
||||
|
||||
if (!activity.key) {
|
||||
activity.key = uuid();
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
applyStyles(activities) {
|
||||
let firstCurrentActivityIndex = -1;
|
||||
let activityClosestToNowIndex = -1;
|
||||
let currentActivitiesCount = 0;
|
||||
const styledActivities = activities.map((activity, index) => {
|
||||
if (this.timestamp >= activity.start && this.timestamp <= activity.end) {
|
||||
activity.cssClass = CURRENT_CSS_SUFFIX;
|
||||
if (firstCurrentActivityIndex < 0) {
|
||||
firstCurrentActivityIndex = index;
|
||||
}
|
||||
this.firstCurrentOrFutureActivityIndex = -1;
|
||||
this.firstCurrentActivityIndex = -1;
|
||||
this.firstFutureActivityIndex = -1;
|
||||
this.currentActivitiesCount = 0;
|
||||
this.pastActivitiesCount = 0;
|
||||
this.futureActivitiesCount = 0;
|
||||
|
||||
currentActivitiesCount = currentActivitiesCount + 1;
|
||||
} else if (this.timestamp < activity.start) {
|
||||
activity.cssClass = FUTURE_CSS_SUFFIX;
|
||||
//the index of the first activity that's greater than the current timestamp
|
||||
if (activityClosestToNowIndex < 0) {
|
||||
activityClosestToNowIndex = index;
|
||||
}
|
||||
} else {
|
||||
activity.cssClass = PAST_CSS_SUFFIX;
|
||||
}
|
||||
const styledActivities = activities.map(this.styleActivity);
|
||||
|
||||
if (!activity.key) {
|
||||
activity.key = uuid();
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
this.activityClosestToNowIndex = activityClosestToNowIndex;
|
||||
this.firstCurrentActivityIndex = firstCurrentActivityIndex;
|
||||
this.currentActivitiesCount = currentActivitiesCount;
|
||||
if (this.firstCurrentActivityIndex > -1) {
|
||||
this.firstCurrentOrFutureActivityIndex = this.firstCurrentActivityIndex;
|
||||
} else if (this.firstFutureActivityIndex > -1) {
|
||||
this.firstCurrentOrFutureActivityIndex = this.firstFutureActivityIndex;
|
||||
}
|
||||
|
||||
return styledActivities;
|
||||
},
|
||||
@ -404,9 +417,10 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
this.firstCurrentActivityIndex = -1;
|
||||
this.activityClosestToNowIndex = -1;
|
||||
this.firstCurrentOrFutureActivityIndex = -1;
|
||||
this.pastActivitiesCount = 0;
|
||||
this.currentActivitiesCount = 0;
|
||||
this.futureActivitiesCount = 0;
|
||||
this.$el.parentElement?.scrollTo({ top: 0 });
|
||||
this.autoScrolled = false;
|
||||
},
|
||||
@ -416,40 +430,51 @@ export default {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollOffset =
|
||||
this.currentActivitiesCount > 0 ? Math.floor(this.currentActivitiesCount / 2) : 0;
|
||||
this.$el.parentElement?.scrollTo({
|
||||
top: ROW_HEIGHT * (this.firstCurrentActivityIndex + scrollOffset),
|
||||
behavior: 'smooth'
|
||||
});
|
||||
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 {
|
||||
// scroll to the top
|
||||
this.resetScroll();
|
||||
if (this.canAutoScroll() === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// See #7167 for scrolling algorithm
|
||||
const scrollTop = this.calculateScrollOffset();
|
||||
|
||||
if (scrollTop === undefined) {
|
||||
this.resetScroll();
|
||||
} else {
|
||||
this.$el.parentElement?.scrollTo({
|
||||
top: scrollTop,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
this.autoScrolled = false;
|
||||
}
|
||||
},
|
||||
calculateScrollOffset() {
|
||||
let scrollTop;
|
||||
|
||||
//No scrolling necessary if no past events are present
|
||||
if (this.pastActivitiesCount > 0) {
|
||||
const row = this.$el.querySelector('.js-list-item');
|
||||
const ROW_HEIGHT = row.getBoundingClientRect().height;
|
||||
|
||||
const maxViewableActivities =
|
||||
Math.floor(this.$el.parentElement.getBoundingClientRect().height / ROW_HEIGHT) - 1;
|
||||
|
||||
const currentAndFutureActivities = this.currentActivitiesCount + this.futureActivitiesCount;
|
||||
|
||||
//If there is more viewable area than all current and future activities combined, then show some past events
|
||||
const numberOfPastEventsToShow = maxViewableActivities - currentAndFutureActivities;
|
||||
if (numberOfPastEventsToShow > 0) {
|
||||
//some past events can be shown - get that scroll index
|
||||
if (this.pastActivitiesCount > numberOfPastEventsToShow) {
|
||||
scrollTop =
|
||||
ROW_HEIGHT * (this.firstCurrentOrFutureActivityIndex + numberOfPastEventsToShow);
|
||||
}
|
||||
} else {
|
||||
// only show current and future events
|
||||
scrollTop = ROW_HEIGHT * this.firstCurrentOrFutureActivityIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return scrollTop;
|
||||
},
|
||||
deferAutoScroll() {
|
||||
//if this is not a user-triggered event, don't defer auto scrolling
|
||||
|
@ -63,10 +63,12 @@ export function millisecondsToDHMS(numericDuration) {
|
||||
return `${dhms ? '+' : ''} ${dhms}`;
|
||||
}
|
||||
|
||||
export function getPreciseDuration(value, excludeMilliSeconds) {
|
||||
export function getPreciseDuration(value, { excludeMilliSeconds, useDayFormat } = {}) {
|
||||
let preciseDuration;
|
||||
const ms = value || 0;
|
||||
|
||||
const duration = [
|
||||
toDoubleDigits(Math.floor(normalizeAge(ms / ONE_DAY))),
|
||||
Math.floor(normalizeAge(ms / ONE_DAY)),
|
||||
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_DAY) / ONE_HOUR))),
|
||||
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_HOUR) / ONE_MINUTE))),
|
||||
toDoubleDigits(Math.floor(normalizeAge((ms % ONE_MINUTE) / ONE_SECOND)))
|
||||
@ -74,5 +76,20 @@ export function getPreciseDuration(value, excludeMilliSeconds) {
|
||||
if (!excludeMilliSeconds) {
|
||||
duration.push(toTripleDigits(Math.floor(normalizeAge(ms % ONE_SECOND))));
|
||||
}
|
||||
return duration.join(':');
|
||||
|
||||
if (useDayFormat) {
|
||||
// Format days as XD
|
||||
const days = duration.shift();
|
||||
if (days > 0) {
|
||||
preciseDuration = `${days}D ${duration.join(':')}`;
|
||||
} else {
|
||||
preciseDuration = duration.join(':');
|
||||
}
|
||||
} else {
|
||||
const days = toDoubleDigits(duration.shift());
|
||||
duration.unshift(days);
|
||||
preciseDuration = duration.join(':');
|
||||
}
|
||||
|
||||
return preciseDuration;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user