Skip to content

Commit

Permalink
Copy & Paste of Activity Directives (#1544)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivydeliz committed Nov 26, 2024
1 parent 452f1f0 commit fe07a65
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 15 deletions.
66 changes: 65 additions & 1 deletion src/components/activity/ActivityDirectivesTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@
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';
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import {
canPasteActivityDirectivesFromClipboard,
copyActivityDirectivesToClipboard,
pasteActivityDirectivesFromClipboard,
} from '../../utilities/activities';
import { isMetaOrCtrlPressed } from '../../utilities/keyboardEvents';
export let activityDirectives: ActivityDirective[] = [];
export let activityDirectiveErrorRollupsMap: Record<ActivityDirectiveId, ActivityErrorRollup> | undefined = undefined;
Expand Down Expand Up @@ -44,15 +50,22 @@
let activityErrorColumnDef: DataGridColumnDef | null = null;
let activityDirectivesWithErrorCounts: ActivityDirectiveWithErrorCounts[] = [];
let completeColumnDefs: ColDef[] = columnDefs;
let hasCreatePermission: boolean = false;
let hasDeletePermission: boolean = false;
let isDeletingDirective: boolean = false;
let showCopyMenu: boolean = true;
$: hasDeletePermission =
plan !== null ? featurePermissions.activityDirective.canDelete(user, plan) && !planReadOnly : false;
$: hasCreatePermission =
plan !== null ? featurePermissions.activityDirective.canCreate(user, plan) && !planReadOnly : false;
$: activityDirectivesWithErrorCounts = activityDirectives.map(activityDirective => ({
...activityDirective,
errorCounts: activityDirectiveErrorRollupsMap?.[activityDirective.id]?.errorCounts,
}));
$: {
activityActionColumnDef = {
cellClass: 'action-cell-container',
Expand Down Expand Up @@ -117,6 +130,29 @@
completeColumnDefs = [activityErrorColumnDef, ...(columnDefs ?? []), activityActionColumnDef];
}
onMount(() => {
document.addEventListener(`keydown`, onKeyDown);
});
onDestroy(() => {
document.removeEventListener(`keydown`, onKeyDown);
});
function onKeyDown(event: KeyboardEvent) {
if (isMetaOrCtrlPressed(event)) {
if (event.key === 'c') {
const activities = getSelectedActivityDirectives();
if (activities.length > 0 && plan !== null) {
copyActivityDirectivesToClipboard(plan, activities);
}
} else if (event.key === 'v') {
if (plan !== null) {
pasteActivityDirectivesFromClipboard(plan, user);
}
}
}
}
async function deleteActivityDirective({ id }: ActivityDirective) {
if (!isDeletingDirective && plan !== null) {
isDeletingDirective = true;
Expand All @@ -138,13 +174,36 @@
return activityDirective.id;
}
function getSelectedActivityDirectives(): ActivityDirective[] {
const directives: ActivityDirective[] = [];
bulkSelectedActivityDirectiveIds.forEach(id => {
const found = activityDirectives.find(item => item.id === id);
if (found !== null && found !== undefined) {
directives.push(found);
}
});
return directives;
}
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);
}
}
function copyActivityDirectives({ detail: activities }: CustomEvent<ActivityDirective[]>) {
plan !== null && copyActivityDirectivesToClipboard(plan, activities);
}
function canPasteActivityDirectives(): boolean {
return plan !== null && hasCreatePermission && canPasteActivityDirectivesFromClipboard(plan);
}
function pasteActivityDirectives() {
plan !== null && pasteActivityDirectivesFromClipboard(plan, user);
}
</script>

<BulkActionDataGrid
Expand All @@ -161,10 +220,12 @@
pluralItemDisplayText="Activity Directives"
scrollToSelection={true}
singleItemDisplayText="Activity Directive"
{showCopyMenu}
suppressDragLeaveHidesColumns={false}
{user}
{filterExpression}
on:bulkDeleteItems={deleteActivityDirectives}
on:bulkCopyItems={copyActivityDirectives}
on:columnMoved
on:columnPinned
on:columnResized
Expand All @@ -178,5 +239,8 @@
<ContextMenuItem on:click={scrollTimelineToActivityDirective}>Scroll to Activity</ContextMenuItem>
<ContextMenuSeparator></ContextMenuSeparator>
{/if}
{#if canPasteActivityDirectives()}
<ContextMenuItem on:click={pasteActivityDirectives}>Paste</ContextMenuItem>
{/if}
</svelte:fragment>
</BulkActionDataGrid>
42 changes: 40 additions & 2 deletions src/components/timeline/TimelineContextMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@
TimeRange,
VerticalGuide,
} from '../../types/timeline';
import { getAllSpansForActivityDirective, getSpanRootParent } from '../../utilities/activities';
import {
canPasteActivityDirectivesFromClipboard,
copyActivityDirectivesToClipboard,
getAllSpansForActivityDirective,
getSpanRootParent,
pasteActivityDirectivesFromClipboard,
} from '../../utilities/activities';
import effects from '../../utilities/effects';
import { getTarget } from '../../utilities/generic';
import { permissionHandler } from '../../utilities/permissionHandler';
Expand All @@ -31,6 +37,7 @@
import ContextMenuItem from '../context-menu/ContextMenuItem.svelte';
import ContextMenuSeparator from '../context-menu/ContextMenuSeparator.svelte';
import ContextSubMenuItem from '../context-menu/ContextSubMenuItem.svelte';
import { featurePermissions } from '../../utilities/permissions';
export let activityDirectivesMap: ActivityDirectivesMap;
export let contextMenu: MouseOver | null;
Expand Down Expand Up @@ -112,6 +119,7 @@
$: activityDirectiveStartDate = activityDirective
? new Date(getUnixEpochTimeFromInterval(planStartTimeYmd, activityDirective.start_offset))
: null;
// Explicitly keep track of offsetX because Firefox ends up zeroing it out on the original `contextmenu` MouseEvent
$: offsetX = contextMenu?.e.offsetX;
Expand Down Expand Up @@ -241,6 +249,24 @@
export function isShown() {
return contextMenuComponent.isShown();
}
function copyActivityDirective(activity: ActivityDirective) {
plan !== null && copyActivityDirectivesToClipboard(plan, [activity]);
}
function canPasteActivityDirectives(): boolean {
return (
plan !== null &&
featurePermissions.activityDirective.canCreate(user, plan) &&
canPasteActivityDirectivesFromClipboard(plan)
);
}
function pasteActivityDirectivesAtTime(time: Date | false | null) {
if (plan !== null && time instanceof Date) {
pasteActivityDirectivesFromClipboard(plan, user, time.getTime());
}
}
</script>

<ContextMenu hideAfterClick on:hide bind:this={contextMenuComponent}>
Expand Down Expand Up @@ -311,6 +337,9 @@
Set Simulation End at Directive Start
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem on:click={() => activityDirective !== null && copyActivityDirective(activityDirective)}>
Copy Activity Directive
</ContextMenuItem>
<ContextMenuItem
on:click={() => {
if (activityDirective !== null) {
Expand All @@ -329,7 +358,7 @@
],
]}
>
Delete Directive
Delete Activity Directive
</ContextMenuItem>
{:else if span}
<ContextMenuItem on:click={jumpToActivityDirective}>Jump to Activity Directive</ContextMenuItem>
Expand Down Expand Up @@ -398,6 +427,15 @@
>
Set Simulation End
</ContextMenuItem>
{#if canPasteActivityDirectives()}
<ContextMenuSeparator />
<ContextMenuItem
on:click={() =>
pasteActivityDirectivesAtTime(xScaleView && offsetX !== undefined && xScaleView.invert(offsetX))}
>
Paste at Time
</ContextMenuItem>
{/if}
{/if}
<ContextMenuSeparator />
{#if span}
Expand Down
42 changes: 31 additions & 11 deletions src/components/ui/DataGrid/BulkActionDataGrid.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
// eslint-disable-next-line
interface $$Events extends ComponentEvents<DataGrid<RowData>> {
bulkCopyItems: CustomEvent<RowData[]>;
bulkDeleteItems: CustomEvent<RowData[]>;
}
import { browser } from '$app/environment';
import type { ColDef, ColumnState, IRowNode, RedrawRowsParams } from 'ag-grid-community';
import { keyBy } from 'lodash-es';
import { createEventDispatcher, onDestroy, type ComponentEvents } from 'svelte';
import { type ComponentEvents, createEventDispatcher, onDestroy } from 'svelte';
import type { User } from '../../../types/app';
import type { Dispatcher } from '../../../types/component';
import type { RowId, TRowData } from '../../../types/data-grid';
Expand All @@ -35,6 +37,7 @@
export let selectedItemId: RowId | null = null;
export let selectedItemIds: RowId[] = [];
export let showContextMenu: boolean = true;
export let showCopyMenu: boolean = false;
export let singleItemDisplayText: string = '';
export let suppressDragLeaveHidesColumns: boolean = true;
export let suppressRowClickSelection: boolean = false;
Expand Down Expand Up @@ -89,23 +92,33 @@
onDestroy(() => onBlur());
function bulkCopyItems() {
const selectedRows = getRowDataFromSelectedItems();
if (selectedRows.length) {
dispatch('bulkCopyItems', selectedRows);
}
}
function bulkDeleteItems() {
if (deletePermission) {
const selectedItemIdsMap = keyBy(selectedItemIds);
const selectedRows: RowData[] = items.reduce((selectedRows: RowData[], row: RowData) => {
const id = getRowId(row);
if (selectedItemIdsMap[id] !== undefined) {
selectedRows.push(row);
}
return selectedRows;
}, []);
const selectedRows = getRowDataFromSelectedItems();
if (selectedRows.length) {
dispatch('bulkDeleteItems', selectedRows);
}
}
}
function getRowDataFromSelectedItems(): RowData[] {
const selectedItemIdsMap = keyBy(selectedItemIds);
return items.reduce((selectedRows: RowData[], row: RowData) => {
const id = getRowId(row);
if (selectedItemIdsMap[id] !== undefined) {
selectedRows.push(row);
}
return selectedRows;
}, []);
}
function onBlur() {
if (browser) {
document.removeEventListener('keydown', onKeyDown);
Expand Down Expand Up @@ -172,13 +185,20 @@
>
<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}
</ContextMenuItem>

{#if selectedItemIds.length}
{#if showCopyMenu}
<ContextMenuItem on:click={bulkCopyItems}>
Copy {selectedItemIds.length}
{selectedItemIds.length > 1 ? pluralItemDisplayText : singleItemDisplayText}
</ContextMenuItem>
{/if}

<ContextMenuItem
use={[
[
Expand Down
Loading

0 comments on commit fe07a65

Please sign in to comment.