Skip to content

Commit

Permalink
Add "scroll to activity" to activity directive table context menu (#1433
Browse files Browse the repository at this point in the history
) (#1538)

Add "Scroll to Activity" to activity directive table context menu (#1433)
  • Loading branch information
ivydeliz authored Nov 12, 2024
1 parent f3af153 commit b99e922
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 18 deletions.
26 changes: 25 additions & 1 deletion src/components/activity/ActivityDirectivesTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
import BulkActionDataGrid from '../ui/DataGrid/BulkActionDataGrid.svelte';
import type DataGrid from '../ui/DataGrid/DataGrid.svelte';
import DataGridActions from '../ui/DataGrid/DataGridActions.svelte';
import ContextMenuItem from '../context-menu/ContextMenuItem.svelte';
import ContextMenuSeparator from '../context-menu/ContextMenuSeparator.svelte';
import { createEventDispatcher } from 'svelte';
export let activityDirectives: ActivityDirective[] = [];
export let activityDirectiveErrorRollupsMap: Record<ActivityDirectiveId, ActivityErrorRollup> | undefined = undefined;
Expand All @@ -22,10 +25,15 @@
export let dataGrid: DataGrid<ActivityDirective> | undefined = undefined;
export let plan: Plan | null;
export let selectedActivityDirectiveId: ActivityDirectiveId | null = null;
export let bulkSelectedActivityDirectiveIds: ActivityDirectiveId[] = [];
export let planReadOnly: boolean = false;
export let user: User | null;
export let filterExpression: string = '';
const dispatch = createEventDispatcher<{
scrollTimelineToTime: number;
}>();
type ActivityDirectiveWithErrorCounts = ActivityDirective & { errorCounts?: ActivityErrorCounts };
type CellRendererParams = {
deleteActivityDirective: (activity: ActivityDirective) => void;
Expand Down Expand Up @@ -129,11 +137,20 @@
function getRowId(activityDirective: ActivityDirective): ActivityDirectiveId {
return activityDirective.id;
}
function scrollTimelineToActivityDirective() {
const directiveId = bulkSelectedActivityDirectiveIds.length > 0 && bulkSelectedActivityDirectiveIds[0];
const directive = activityDirectives.find(item => item.id === directiveId) ?? null;
if (directive?.start_time_ms !== undefined && directive?.start_time_ms !== null) {
dispatch('scrollTimelineToTime', directive.start_time_ms);
}
}
</script>

<BulkActionDataGrid
bind:dataGrid
bind:selectedItemId={selectedActivityDirectiveId}
bind:selectedItemIds={bulkSelectedActivityDirectiveIds}
autoSizeColumnsToFit={false}
columnDefs={completeColumnDefs}
{columnStates}
Expand All @@ -155,4 +172,11 @@
on:gridSizeChanged
on:selectionChanged
on:rowDoubleClicked
/>
>
<svelte:fragment slot="context-menu">
{#if bulkSelectedActivityDirectiveIds.length === 1}
<ContextMenuItem on:click={scrollTimelineToActivityDirective}>Scroll to Activity</ContextMenuItem>
<ContextMenuSeparator></ContextMenuSeparator>
{/if}
</svelte:fragment>
</BulkActionDataGrid>
15 changes: 14 additions & 1 deletion src/components/activity/ActivityDirectivesTablePanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import { InvalidDate } from '../../constants/time';
import { activityDirectivesMap, selectActivity, selectedActivityDirectiveId } from '../../stores/activities';
import { activityErrorRollupsMap } from '../../stores/errors';
import { plan, planReadOnly } from '../../stores/plan';
import { maxTimeRange, plan, planReadOnly, viewTimeRange } from '../../stores/plan';
import { plugins } from '../../stores/plugins';
import { view, viewTogglePanel, viewUpdateActivityDirectivesTable } from '../../stores/views';
import type { ActivityDirective } from '../../types/activity';
Expand All @@ -30,6 +30,8 @@
import Panel from '../ui/Panel.svelte';
import ActivityDirectivesTable from './ActivityDirectivesTable.svelte';
import ActivityTableMenu from './ActivityTableMenu.svelte';
import { get } from 'svelte/store';
import { getTimeRangeAroundTime } from '../../utilities/timeline';
export let gridSection: ViewGridSection;
export let user: User | null;
Expand Down Expand Up @@ -364,6 +366,16 @@
viewUpdateActivityDirectivesTable({ autoSizeColumns: 'off' });
}
}
function scrollTimelineToTime({ detail }: CustomEvent<number>) {
const currentTimeRange = get(viewTimeRange);
const centeredTimeRange = getTimeRangeAroundTime(
detail,
currentTimeRange.end - currentTimeRange.start,
get(maxTimeRange),
);
viewTimeRange.set(centeredTimeRange);
}
</script>

<Panel padBody={false}>
Expand Down Expand Up @@ -417,6 +429,7 @@
on:gridSizeChanged={onGridSizeChangedDebounced}
on:rowDoubleClicked={onRowDoubleClicked}
on:selectionChanged={onSelectionChanged}
on:scrollTimelineToTime={scrollTimelineToTime}
/>
</svelte:fragment>
</Panel>
Expand Down
19 changes: 5 additions & 14 deletions src/components/timeline/TimelineViewControls.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,8 @@
} from '../../stores/simulation';
import { timelineInteractionMode, timelineLockStatus, viewIsModified } from '../../stores/views';
import type { TimeRange } from '../../types/timeline';
import {
getActivityDirectiveStartTimeMs,
getDoyTimeFromInterval,
getIntervalInMs,
getUnixEpochTime,
} from '../../utilities/time';
import { TimelineLockStatus } from '../../utilities/timeline';
import { getActivityDirectiveStartTimeMs, getDoyTimeFromInterval, getUnixEpochTime } from '../../utilities/time';
import { getTimeRangeAroundTime, TimelineLockStatus } from '../../utilities/timeline';
import { showFailureToast, showSuccessToast } from '../../utilities/toast';
import { tooltip } from '../../utilities/tooltip';
import Input from '../form/Input.svelte';
Expand Down Expand Up @@ -217,13 +212,9 @@
function scrollToSelection() {
const time = getSelectionTime();
if (!isNaN(time) && (time < viewTimeRange.start || time > viewTimeRange.end)) {
const midSpan = time + getIntervalInMs($selectedSpan?.duration) / 2;
const start = Math.max(maxTimeRange.start, midSpan - viewDuration / 2);
const end = Math.min(maxTimeRange.end, midSpan + viewDuration / 2);
dispatch('viewTimeRangeChanged', {
end,
start,
});
const currentTimeRangeSpan = viewTimeRange.end - viewTimeRange.start;
const centeredTimeRange = getTimeRangeAroundTime(time, currentTimeRangeSpan, maxTimeRange);
dispatch('viewTimeRangeChanged', centeredTimeRange);
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/components/ui/DataGrid/BulkActionDataGrid.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
export let pluralItemDisplayText: string = '';
export let scrollToSelection: boolean = false;
export let selectedItemId: RowId | null = null;
export let selectedItemIds: RowId[] = [];
export let showContextMenu: boolean = true;
export let singleItemDisplayText: string = '';
export let suppressDragLeaveHidesColumns: boolean = true;
Expand All @@ -48,7 +49,6 @@
let isFiltered: boolean = false;
let deletePermission: boolean = true;
let selectedItemIds: RowId[] = [];
$: if (typeof hasDeletePermission === 'function' && user) {
if (selectedItemIds.length > 0) {
Expand Down Expand Up @@ -172,6 +172,8 @@
>
<svelte:fragment slot="context-menu">
{#if showContextMenu}
<!-- to further extend context menu -->
<slot name="context-menu" />
<ContextMenuHeader>Bulk Actions</ContextMenuHeader>
<ContextMenuItem on:click={selectAllItems}>
Select All {isFiltered ? 'Visible ' : ''}{pluralItemDisplayText}
Expand Down
52 changes: 51 additions & 1 deletion src/utilities/timeline.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { keyBy } from 'lodash-es';
import { expect, test } from 'vitest';
import { describe, expect, test } from 'vitest';
import {
ViewDiscreteLayerColorPresets,
ViewLineLayerColorPresets,
Expand All @@ -25,6 +25,7 @@ import {
externalEventInView,
filterResourcesByLayer,
generateDiscreteTreeUtil,
getTimeRangeAroundTime,
getUniqueColorForActivityLayer,
getUniqueColorForLineLayer,
getUniqueColorSchemeForXRangeLayer,
Expand All @@ -36,6 +37,7 @@ import {
paginateNodes,
spanInView,
} from './timeline';
import { convertUTCToMs } from './time';

const testSpans: Span[] = [
generateSpan({
Expand Down Expand Up @@ -1185,3 +1187,51 @@ test('getUniqueColorSchemeForXRangeLayer', () => {
const existingScheme = (row2.layers[0] as XRangeLayer).colorScheme;
expect(getUniqueColorSchemeForXRangeLayer(row2)).not.toBe(existingScheme);
});

describe('getTimeRangeAroundTime', () => {
const hourInMs = 3600000;
const TEST_TIME = convertUTCToMs(`2024-10-14T16:06:00Z`);

test('Should return TimeRange centered on time with +/- 1 day, unbounded', () => {
const timeRange = getTimeRangeAroundTime(TEST_TIME, 48 * hourInMs);
expect(timeRange).toStrictEqual({
end: convertUTCToMs(`2024-10-15T16:06:00Z`), //1 day after TEST_TIME
start: convertUTCToMs(`2024-10-13T16:06:00Z`), //1 day before TEST_TIME
});
expect(timeRange.end - timeRange.start).toBe(48 * hourInMs);
});

test('Should return TimeRange centered on time with +/- 1 hour, unbounded', () => {
const timeRange = getTimeRangeAroundTime(TEST_TIME, 2 * hourInMs);
expect(timeRange).toStrictEqual({
end: convertUTCToMs(`2024-10-14T17:06:00Z`), //1 hour after TEST_TIME
start: convertUTCToMs(`2024-10-14T15:06:00Z`), //1 hour before TEST_TIME
});
expect(timeRange.end - timeRange.start).toBe(2 * hourInMs);
});

test('Should return TimeRange with 48 hour span with time in it, bounded by the start', () => {
const timeRange = getTimeRangeAroundTime(TEST_TIME, 48 * hourInMs, {
end: convertUTCToMs(`2024-10-20T00:00:00Z`),
start: convertUTCToMs(`2024-10-14T00:00:00Z`),
});

expect(timeRange).toStrictEqual({
end: convertUTCToMs(`2024-10-16T00:00:00Z`), //bounded start + 48 hours
start: convertUTCToMs(`2024-10-14T00:00:00Z`), //bounded start
});
expect(timeRange.end - timeRange.start).toBe(48 * hourInMs);
});

test('Should return TimeRange with 48 hour span with time in it, bounded by the end', () => {
const timeRange = getTimeRangeAroundTime(TEST_TIME, 48 * hourInMs, {
end: convertUTCToMs(`2024-10-14T11:59:59Z`),
start: convertUTCToMs(`2024-10-10T00:00:00Z`),
});
expect(timeRange).toStrictEqual({
end: convertUTCToMs(`2024-10-14T11:59:59Z`), //bounded end
start: convertUTCToMs(`2024-10-12T11:59:59Z`), //bounded end - 48 hours
});
expect(timeRange.end - timeRange.start).toBe(48 * hourInMs);
});
});
24 changes: 24 additions & 0 deletions src/utilities/timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,30 @@ export function getUniqueColorForLineLayer(row?: Row): string {
return color;
}

export function getTimeRangeAroundTime(time: number, timeRangeSpan: number, maxTimeRange?: TimeRange): TimeRange {
const padding = timeRangeSpan / 2;
let start = time - padding;
let end = time + padding;

// optional maxTimeRange for bounding the results bounds
if (maxTimeRange !== undefined && maxTimeRange !== null) {
//span is larger than the max time range, well it can't get larger than that
if (timeRangeSpan >= maxTimeRange.end - maxTimeRange.start) {
return maxTimeRange;
}

//bound the start or end of the TimeRange, but keep the timeRangeSpan the same
if (time - padding < maxTimeRange.start) {
start = maxTimeRange.start;
end = maxTimeRange.start + timeRangeSpan;
} else if (time + padding > maxTimeRange.end) {
start = maxTimeRange.end - timeRangeSpan;
end = maxTimeRange.end;
}
}
return { end, start };
}

/**
* Returns a new vertical guide
*/
Expand Down

0 comments on commit b99e922

Please sign in to comment.