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 Dec 19, 2024
1 parent a9d1030 commit 2b27699
Show file tree
Hide file tree
Showing 11 changed files with 435 additions and 18 deletions.
66 changes: 66 additions & 0 deletions src/components/activity/ActivityDirectivesTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
import ContextMenuItem from '../context-menu/ContextMenuItem.svelte';
import ContextMenuSeparator from '../context-menu/ContextMenuSeparator.svelte';
import { createEventDispatcher } from 'svelte';
import {
canPasteActivityDirectivesFromClipboard,
copyActivityDirectivesToClipboard,
getPasteActivityDirectivesText,
getActivityDirectivesToPaste,
} from '../../utilities/activities';
import { isMetaOrCtrlPressed } from '../../utilities/keyboardEvents';
export let activityDirectives: ActivityDirective[] = [];
export let activityDirectiveErrorRollupsMap: Record<ActivityDirectiveId, ActivityErrorRollup> | undefined = undefined;
Expand All @@ -31,6 +38,7 @@
export let filterExpression: string = '';
const dispatch = createEventDispatcher<{
createActivityDirectives: ActivityDirective[];
scrollTimelineToTime: number;
}>();
Expand All @@ -44,15 +52,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 +132,22 @@
completeColumnDefs = [activityErrorColumnDef, ...(columnDefs ?? []), activityActionColumnDef];
}
export function onKeyDown(event: KeyboardEvent) {
if (plan !== null && isMetaOrCtrlPressed(event)) {
if (event.key === 'c') {
const activities = getSelectedActivityDirectives();
if (activities.length > 0) {
copyActivityDirectivesToClipboard(plan, activities);
}
} else if (event.key === 'v') {
const directives = getActivityDirectivesToPaste(plan);
if (directives !== undefined) {
dispatch(`createActivityDirectives`, directives);
}
}
}
}
async function deleteActivityDirective({ id }: ActivityDirective) {
if (!isDeletingDirective && plan !== null) {
isDeletingDirective = true;
Expand All @@ -138,13 +169,43 @@
return activityDirective.id;
}
export 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[]>) {
if (plan !== null) {
copyActivityDirectivesToClipboard(plan, activities);
}
}
function canPasteActivityDirectives(): boolean {
return plan !== null && hasCreatePermission && canPasteActivityDirectivesFromClipboard(plan);
}
function pasteActivityDirectives() {
if (plan !== null && canPasteActivityDirectives()) {
const directives = getActivityDirectivesToPaste(plan);
if (directives !== undefined) {
dispatch(`createActivityDirectives`, directives);
}
}
}
</script>

<BulkActionDataGrid
Expand All @@ -161,10 +222,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 +241,8 @@
<ContextMenuItem on:click={scrollTimelineToActivityDirective}>Scroll to Activity</ContextMenuItem>
<ContextMenuSeparator></ContextMenuSeparator>
{/if}
{#if canPasteActivityDirectives()}
<ContextMenuItem on:click={pasteActivityDirectives}>{getPasteActivityDirectivesText()}</ContextMenuItem>
{/if}
</svelte:fragment>
</BulkActionDataGrid>
17 changes: 16 additions & 1 deletion src/components/activity/ActivityDirectivesTablePanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import ActivityTableMenu from './ActivityTableMenu.svelte';
import { get } from 'svelte/store';
import { getTimeRangeAroundTime } from '../../utilities/timeline';
import effects from '../../utilities/effects';
export let gridSection: ViewGridSection;
export let user: User | null;
Expand All @@ -46,6 +47,7 @@
let derivedColumnDefs: ColDef[] = [];
let filterExpression: string = '';
let onGridSizeChangedDebounced = debounce(onGridSizeChanged, 100);
let tableInstance: ActivityDirectivesTable;
let viewUpdateActivityDirectivesTableDebounced = debounce(viewUpdateActivityDirectivesTable, 100);
$: activityDirectivesTable = $view?.definition.plan.activityDirectivesTable;
Expand Down Expand Up @@ -246,6 +248,13 @@
dataGrid?.sizeColumnsToFit();
}
function createActivityDirectives({ detail }: CustomEvent<ActivityDirective[]>) {
const p = get(plan);
if (p !== null) {
effects.cloneActivityDirectives(detail, p, user);
}
}
function onGridSizeChanged() {
if (activityDirectivesTable?.autoSizeColumns === 'fill') {
autoSizeSpace();
Expand Down Expand Up @@ -291,6 +300,10 @@
}, 0);
}
function onKeyDownOverPanel({ detail }: CustomEvent<KeyboardEvent>) {
tableInstance.onKeyDown(detail);
}
function onColumnMoved() {
const columnStates = dataGrid?.getColumnState();
const updatedColumnStates = (columnStates ?? []).filter(columnState => columnState.colId !== 'actions');
Expand Down Expand Up @@ -378,7 +391,7 @@
}
</script>

<Panel padBody={false}>
<Panel padBody={false} on:onKeyDownOverPanel={onKeyDownOverPanel}>
<svelte:fragment slot="header">
<GridMenu {gridSection} title="Activity Directives Table" />
<div class="table-menu">
Expand Down Expand Up @@ -412,6 +425,7 @@

<svelte:fragment slot="body">
<ActivityDirectivesTable
bind:this={tableInstance}
bind:dataGrid
bind:selectedActivityDirectiveId={$selectedActivityDirectiveId}
activityDirectives={Object.values($activityDirectivesMap)}
Expand All @@ -426,6 +440,7 @@
on:columnPinned={onColumnPinned}
on:columnResized={onColumnResized}
on:columnVisible={onColumnVisible}
on:createActivityDirectives={createActivityDirectives}
on:gridSizeChanged={onGridSizeChangedDebounced}
on:rowDoubleClicked={onRowDoubleClicked}
on:selectionChanged={onSelectionChanged}
Expand Down
51 changes: 49 additions & 2 deletions src/components/timeline/TimelineContextMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@
TimeRange,
VerticalGuide,
} from '../../types/timeline';
import { getAllSpansForActivityDirective, getSpanRootParent } from '../../utilities/activities';
import {
canPasteActivityDirectivesFromClipboard,
copyActivityDirectivesToClipboard,
getAllSpansForActivityDirective,
getSpanRootParent,
getPasteActivityDirectivesText,
getActivityDirectivesToPaste,
} from '../../utilities/activities';
import effects from '../../utilities/effects';
import { getTarget } from '../../utilities/generic';
import { permissionHandler } from '../../utilities/permissionHandler';
Expand All @@ -31,6 +38,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 All @@ -49,6 +57,7 @@
const dispatch = createEventDispatcher<{
collapseDiscreteTree: Row;
createActivityDirectives: ActivityDirective[];
deleteActivityDirective: number;
deleteRow: Row;
duplicateRow: Row;
Expand All @@ -57,6 +66,7 @@
jumpToActivityDirective: number;
jumpToSpan: number;
moveRow: { direction: 'up' | 'down'; row: Row };
pasteActivityDirectivesAtTime: Date | null;
toggleActivityComposition: { composition: ActivityOptions['composition']; row: Row };
updateVerticalGuides: VerticalGuide[];
viewTimeRangeChanged: TimeRange;
Expand Down Expand Up @@ -112,6 +122,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 +252,27 @@
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 && featurePermissions.activityDirective.canCreate(user, plan) && time instanceof Date) {
const directives = getActivityDirectivesToPaste(plan, time.getTime());
if (directives !== undefined) {
effects.cloneActivityDirectives(directives, plan, user);
}
}
}
</script>

<ContextMenu hideAfterClick on:hide bind:this={contextMenuComponent}>
Expand Down Expand Up @@ -311,6 +343,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 +364,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 +433,18 @@
>
Set Simulation End
</ContextMenuItem>
{#if canPasteActivityDirectives()}
<ContextMenuSeparator />
<ContextMenuItem
on:click={() => {
if (xScaleView && offsetX !== undefined) {
pasteActivityDirectivesAtTime(xScaleView.invert(offsetX));
}
}}
>
{getPasteActivityDirectivesText()} at Time
</ContextMenuItem>
{/if}
{/if}
<ContextMenuSeparator />
{#if span}
Expand Down
33 changes: 31 additions & 2 deletions src/components/timeline/TimelinePanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
viewUpdateRow,
viewUpdateTimeline,
} from '../../stores/views';
import type { ActivityDirectiveId } from '../../types/activity';
import type { ActivityDirective, ActivityDirectiveId } from '../../types/activity';
import type { User } from '../../types/app';
import type {
ActivityOptions,
Expand All @@ -41,6 +41,9 @@
import PanelHeaderActions from '../ui/PanelHeaderActions.svelte';
import Timeline from './Timeline.svelte';
import TimelineViewControls from './TimelineViewControls.svelte';
import { isMetaOrCtrlPressed } from '../../utilities/keyboardEvents';
import { copyActivityDirectivesToClipboard, getActivityDirectivesToPaste } from '../../utilities/activities';
import { get } from 'svelte/store';
export let user: User | null;
Expand Down Expand Up @@ -161,6 +164,32 @@
}
}
function getSelectedActivityDirective(): ActivityDirective | undefined {
if ($selectedActivityDirectiveId !== null) {
return $activityDirectivesMap[$selectedActivityDirectiveId];
}
}
function onKeyDownOverPanel({ detail }: CustomEvent<KeyboardEvent>) {
const p = get(plan);
if (p !== null && isMetaOrCtrlPressed(detail)) {
if (detail.key === 'c') {
const selected = getSelectedActivityDirective();
if (selected !== undefined) {
const activities: ActivityDirective[] = [selected];
if (activities.length > 0) {
copyActivityDirectivesToClipboard(p, activities);
}
}
} else if (detail.key === 'v') {
const directives = getActivityDirectivesToPaste(p);
if (directives !== undefined) {
effects.cloneActivityDirectives(directives, p, user);
}
}
}
}
function onUpdateYAxes(event: CustomEvent<{ axes: Axis[]; id: number }>) {
const {
detail: { axes, id },
Expand All @@ -169,7 +198,7 @@
}
</script>

<Panel padBody={false}>
<Panel padBody={false} on:onKeyDownOverPanel={onKeyDownOverPanel}>
<svelte:fragment slot="header">
<div class="st-typography-medium timeline-title">Timeline</div>
<PanelHeaderActions>
Expand Down
Loading

0 comments on commit 2b27699

Please sign in to comment.