Skip to content

Commit

Permalink
Copy & Paste of Activity Directives between Plans (#1625)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivydeliz committed Feb 10, 2025
1 parent 0803e6d commit e5c2ec1
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 107 deletions.
37 changes: 25 additions & 12 deletions src/components/activity/ActivityDirectivesTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import type { ActivityErrorCounts, ActivityErrorRollup } from '../../types/errors';
import type { Plan } from '../../types/plan';
import {
canPasteActivityDirectivesFromClipboard,
getActivityDirectivesClipboardCount,
copyActivityDirectivesToClipboard,
getActivityDirectivesToPaste,
getPasteActivityDirectivesText,
Expand Down Expand Up @@ -166,17 +166,28 @@
}
}
function canPasteActivityDirectives(): boolean {
return plan !== null && hasCreatePermission && canPasteActivityDirectivesFromClipboard(plan);
function canPasteActivityDirectives(): Promise<number> {
return new Promise(resolve => {
getActivityDirectivesClipboardCount().then(result => {
if (plan !== null && hasCreatePermission && result > 0) {
resolve(result); //returns how many activity directives there are
} else {
resolve(-1);
}
});
});
}
function pasteActivityDirectives() {
if (plan !== null && canPasteActivityDirectives()) {
const directives = getActivityDirectivesToPaste(plan);
if (directives !== undefined) {
dispatch(`createActivityDirectives`, directives);
canPasteActivityDirectives().then(pasteCount => {
if (pasteCount > 0 && plan !== null) {
getActivityDirectivesToPaste(plan).then(directives => {
if (directives !== undefined) {
dispatch(`createActivityDirectives`, directives);
}
});
}
}
});
}
</script>

Expand Down Expand Up @@ -214,9 +225,11 @@
<ContextMenuItem on:click={scrollTimelineToActivityDirective}>Scroll to Activity</ContextMenuItem>
<ContextMenuSeparator></ContextMenuSeparator>
{/if}
{#if canPasteActivityDirectives()}
<ContextMenuItem on:click={pasteActivityDirectives}>{getPasteActivityDirectivesText()}</ContextMenuItem>
<ContextMenuSeparator></ContextMenuSeparator>
{/if}
{#await canPasteActivityDirectives() then howMany}
{#if howMany > 0}
<ContextMenuItem on:click={pasteActivityDirectives}>{getPasteActivityDirectivesText(howMany)}</ContextMenuItem>
<ContextMenuSeparator></ContextMenuSeparator>
{/if}
{/await}
</svelte:fragment>
</BulkActionDataGrid>
55 changes: 32 additions & 23 deletions src/components/timeline/TimelineContextMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
VerticalGuide,
} from '../../types/timeline';
import {
canPasteActivityDirectivesFromClipboard,
getActivityDirectivesClipboardCount,
copyActivityDirectivesToClipboard,
getAllSpansForActivityDirective,
getSpanRootParent,
Expand Down Expand Up @@ -256,20 +256,28 @@
plan !== null && copyActivityDirectivesToClipboard(plan, [activity]);
}
function canPasteActivityDirectives(): boolean {
return (
plan !== null &&
featurePermissions.activityDirective.canCreate(user, plan) &&
canPasteActivityDirectivesFromClipboard(plan)
);
function canPasteActivityDirectives(): Promise<number> {
return new Promise(resolve => {
if (plan === null || !featurePermissions.activityDirective.canCreate(user, plan)) {
resolve(-1);
}
getActivityDirectivesClipboardCount().then(result => {
if (result > 0) {
resolve(result); //returns how many activity directives there are
} else {
resolve(-1);
}
});
});
}
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);
}
getActivityDirectivesToPaste(plan, time.getTime()).then(directives => {
if (plan != null && directives !== undefined) {
effects.cloneActivityDirectives(directives, plan, user);
}
});
}
}
</script>
Expand Down Expand Up @@ -432,18 +440,19 @@
>
Set Simulation End
</ContextMenuItem>
{#if canPasteActivityDirectives()}
<ContextMenuSeparator />
<ContextMenuItem
on:click={() => {
if (xScaleView && offsetX !== undefined) {
pasteActivityDirectivesAtTime(xScaleView.invert(offsetX));
}
}}
>
{getPasteActivityDirectivesText()} at Time
</ContextMenuItem>
{/if}
{#await canPasteActivityDirectives() then howMany}
{#if howMany > 0}
<ContextMenuSeparator />
<ContextMenuItem
on:click={() => {
if (xScaleView && offsetX !== undefined) {
pasteActivityDirectivesAtTime(xScaleView.invert(offsetX));
}
}}
>{getPasteActivityDirectivesText(howMany)} at Time
</ContextMenuItem>
{/if}
{/await}
{/if}
<ContextMenuSeparator />
{#if span}
Expand Down
124 changes: 59 additions & 65 deletions src/utilities/activities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
getIntervalInMs,
getUnixEpochTime,
} from './time';
import { getSessionStorageClipboard, setSessionStorageClipboard } from './sessionStorageClipboard';
import { showSuccessToast } from './toast';
import { getClipboardContent, setClipboardContent } from './clipboard';
import { showFailureToast, showSuccessToast } from './toast';

/**
* Updates activity metadata with a new key/value and removes any empty values.
Expand Down Expand Up @@ -191,87 +191,81 @@ export function copyActivityDirectivesToClipboard(sourcePlan: Plan, activities:

const clipboard = {
activities: clippedActivities,
sourcePlanId: sourcePlan.id,
sourcePlan: sourcePlan.id,
type: `aerie_activity_directives`,
};

setSessionStorageClipboard(JSON.stringify(clipboard));
showSuccessToast(`Copied ${activities.length} Activity Directive${activities.length === 1 ? '' : 's'}`);
const noun = `Activity Directive${activities.length === 1 ? '' : 's'}`;
setClipboardContent(
clipboard,
() => showSuccessToast(`Copied ${activities.length} ${noun}`),
() => showFailureToast(`Failed to copy ${activities.length} ${noun}`),
);
}

export function getPasteActivityDirectivesText(): string | undefined {
try {
const serializedClipboard = getSessionStorageClipboard();
if (serializedClipboard !== null) {
const clipboard = JSON.parse(serializedClipboard);
if (Array.isArray(clipboard.activities)) {
const n = clipboard.activities.length;
return `Paste ${n} Activity Directive${n === 1 ? '' : 's'}`;
}
}
} catch (e) {
console.error('e');
}
export function getPasteActivityDirectivesText(count: number): string {
return `Paste ${count} Activity Directive${count === 1 ? '' : 's'}`;
}

export function canPasteActivityDirectivesFromClipboard(destinationPlan: Plan | null): boolean {
if (destinationPlan === null) {
return false;
}

try {
const serializedClipboard = getSessionStorageClipboard();
if (serializedClipboard === null) {
return false;
}

const clipboard = JSON.parse(serializedClipboard);
//current scope, allows copy/paste in the same plan
if (clipboard.sourcePlanId !== destinationPlan.id) {
return false;
}
} catch (e) {
console.error(e);
return false;
}

return true;
export async function getActivityDirectivesClipboardCount(): Promise<number> {
return new Promise(resolve => {
getClipboardContent().then((serializedClipboard: string) => {
try {
const clipboard = JSON.parse(serializedClipboard);
if (clipboard.type === 'aerie_activity_directives' && clipboard.activities !== undefined) {
resolve(clipboard.activities.length);
}
} catch (e) {
//eh silent
}
resolve(-1);
});
});
}

export function getActivityDirectivesToPaste(
export async function getActivityDirectivesToPaste(
destinationPlan: Plan,
pasteStartingAtTime?: number,
): ActivityDirective[] | undefined {
): Promise<ActivityDirective[] | undefined> {
try {
const serializedClipboard = getSessionStorageClipboard();
const serializedClipboard = await getClipboardContent();
if (serializedClipboard !== null) {
const clipboard = JSON.parse(serializedClipboard);
const activities: ActivityDirective[] = clipboard.activities;

//transpose in time if we're given a time, otherwise it paste at the current time
if (typeof pasteStartingAtTime === 'number') {
const starts: number[] = [];
activities.forEach(a => {
//unachored activities are the ones we're trying to place relative to each other in time, anchored will be calculated from offset
if (a.anchor_id === null && a.start_time_ms !== null) {
starts.push(a.start_time_ms);
}
});
const starts: number[] = [];
activities.forEach(a => {
//unachored activities are the ones we're trying to place relative to each other in time, anchored will be calculated from offset
if (a.anchor_id === null && a.start_time_ms !== null) {
starts.push(a.start_time_ms);
}
});

const planStart = getUnixEpochTime(destinationPlan.start_time_doy);
const earliestStart = Math.max(planStart, Math.min(...starts)); //bounded by plan start
const diff = pasteStartingAtTime - earliestStart;
//bounded by plan start and plan end
const planStart = getUnixEpochTime(destinationPlan.start_time_doy);
const planEnd = getUnixEpochTime(destinationPlan.end_time_doy);
const earliestStart = Math.min(...starts);
if (earliestStart < planStart || earliestStart > planEnd) {
pasteStartingAtTime = planStart; //if out of bounds, paste starting at the start of the plan.
}

activities.forEach(activity => {
if (activity.start_time_ms !== null) {
//anchored activities don't need offset to be updated
if (activity.anchor_id === null) {
activity.start_time_ms += diff;
const startTimeDoy = getDoyTime(new Date(activity.start_time_ms));
activity.start_offset = getIntervalFromDoyRange(destinationPlan.start_time_doy, startTimeDoy);
}
}
});
//transpose in time if we're given a time or if it was out of bounds
let diff = 0;
if (typeof pasteStartingAtTime === 'number') {
diff = pasteStartingAtTime - earliestStart;
}

activities.forEach(activity => {
if (activity.start_time_ms !== null) {
//anchored activities don't need offset to be updated
if (activity.anchor_id === null) {
activity.start_time_ms += diff;
const startTimeDoy = getDoyTime(new Date(activity.start_time_ms));
activity.start_offset = getIntervalFromDoyRange(destinationPlan.start_time_doy, startTimeDoy);
}
}
});

return activities;
}
} catch (e) {
Expand Down
28 changes: 28 additions & 0 deletions src/utilities/clipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export function setClipboardContent(content: object, success?: () => void, fail?: () => void) {
navigator.permissions.query({ name: 'clipboard-write' as PermissionName }).then(result => {
if (result.state === 'granted' || result.state === 'prompt') {
navigator.clipboard.writeText(JSON.stringify(content)).then(
() => {
if (success !== undefined) {
success(); //optional success callback
}
},
() => {
if (fail !== undefined) {
fail(); //optional fail callback
}
},
);
}
});
}

export async function getClipboardContent(): Promise<string> {
return new Promise(resolve => {
try {
window.navigator.clipboard.readText().then(clipText => resolve(clipText));
} catch (e) {
resolve(`{}`); //empty object string?
}
});
}
7 changes: 0 additions & 7 deletions src/utilities/sessionStorageClipboard.ts

This file was deleted.

0 comments on commit e5c2ec1

Please sign in to comment.