Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "scroll to activity" to activity directive table context menu (#1433) #1538

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}
duranb marked this conversation as resolved.
Show resolved Hide resolved
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', {
ivydeliz marked this conversation as resolved.
Show resolved Hide resolved
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
Loading