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

Copy & Paste Activity Directives #1565

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from

Conversation

ivydeliz
Copy link
Contributor

@ivydeliz ivydeliz commented Nov 26, 2024

Feature #1544

@ivydeliz ivydeliz changed the title Copy & Paste of Activity Directives Copy & Paste Activity Directives Nov 27, 2024
}

function pasteActivityDirectives() {
plan !== null && pasteActivityDirectivesFromClipboard(plan, user);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to check the permission as well in case the user uses the keyboard shortcut.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is already handled by the resulting cloneActivityDirectives effect?

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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use an if conditional here instead of && to make it a little easier to glance at? It currently looks as if it should return boolean.

});
}

effects.cloneActivityDirectives(activities, destinationPlan, user).then(() => {}); //TODO select clones?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the empty then can be removed.

@@ -0,0 +1,7 @@
export function setClipboardContent(content: string) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename these to be more explicit that it's using sessionStorage to disambiguate from normal clipboard?

@@ -334,6 +334,17 @@ const gql = {
}
`,

CREATE_ACTIVITY_DIRECTIVES: `#graphql
mutation CreateActivityDirectives($activityDirectivesInsertInput: [activity_directive_insert_input!]!) {
insert_activity_directive(objects: $activityDirectivesInsertInput) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add an INSERT_ACTIVITY_DIRECTIVES to the Queries enum and use the enum here instead? The Queries enum is a way to tie the query to a permission. Maybe these call for an explicit canBulkCreate and canBulkUpdate feature permission for activityDirective in the permissions.ts utility file.

@@ -3449,6 +3460,16 @@ const gql = {
}
`,

UPDATE_ACTIVITY_DIRECTIVES: `#graphql
mutation UpdateActivityDirective($updates: [activity_directive_updates!]!) {
update_activity_directive_many(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add an UPDATE_ACTIVITY_DIRECTIVES to the Queries enum for this as well?

@ivydeliz ivydeliz marked this pull request as ready for review December 5, 2024 20:42
@ivydeliz ivydeliz requested a review from a team as a code owner December 5, 2024 20:42
@@ -117,6 +130,29 @@
completeColumnDefs = [activityErrorColumnDef, ...(columnDefs ?? []), activityActionColumnDef];
}

onMount(() => {
document.addEventListener(`keydown`, onKeyDown);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm getting an error about document not being defined when i switch to this branch and svelte-kit tries to do the first server side render.

ReferenceError: document is not defined
    at /Users/aplave/code/aerie-ui/src/components/activity/ActivityDirectivesTable.svelte:138:5

An alternative here would be to use:

<svelte:window on:keydown={onKeydown} />

instead which effectively does what you're doing with mount/destroy but shouldn't have the same issue with SSR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed they keyboard events altogether for each panel to handle key events when mouse is hovering over that panel, to avoid the double copy & paste. Curious to see what you think of the new code.

on:click={() =>
pasteActivityDirectivesAtTime(xScaleView && offsetX !== undefined && xScaleView.invert(offsetX))}
>
Paste at Time
Copy link
Contributor

@AaronPlave AaronPlave Dec 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider renaming to "Paste Activity Directive(s)". Would be nice to include the count of activity directives if possible or pluralize.

@@ -117,6 +130,29 @@
completeColumnDefs = [activityErrorColumnDef, ...(columnDefs ?? []), activityActionColumnDef];
}

onMount(() => {
document.addEventListener(`keydown`, onKeyDown);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be intentional and I'm still trying to think through it but having this keyboard listener be part of the activity directives table means that this global copy+paste for directives can only happen if the user has the table open. This is probably fine since when you click on an activity directive in the timeline the activity directive table auto opens if necessary which would initialize the listener. Only case is if you click on a directive, swap the directive table to something else, and try to copy paste. That's probably not enough of an edge case to care about though?

@@ -178,5 +239,8 @@
<ContextMenuItem on:click={scrollTimelineToActivityDirective}>Scroll to Activity</ContextMenuItem>
<ContextMenuSeparator></ContextMenuSeparator>
{/if}
{#if canPasteActivityDirectives()}
<ContextMenuItem on:click={pasteActivityDirectives}>Paste</ContextMenuItem>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using the same language for pasting as the timeline context menu

if (activity.start_time_ms !== null) {
activity.start_time_ms += diff;
const startTimeDoy = getDoyTime(new Date(activity.start_time_ms));
activity.start_offset = getIntervalFromDoyRange(destinationPlan.start_time_doy, startTimeDoy);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How should we handle activities that end up with a time outside of the plan boundaries? Say you copy A and B where A is at the start of the plan and B is at the end of the plan, then paste at the end of the plan, B will be outside of the plan bounds.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right! I didn't even think about this. Good to chat about this in the walkthrough.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We said we're ok with this!

const activities: ActivityDirective[] = clipboard.activities;

//transpose in time if we're given a time, otherwise it paste at the current time
if (pasteStartingAtTime !== undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be stronger to check typeof pasteStartingAtTime === 'number'?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!

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
Copy link
Contributor

@AaronPlave AaronPlave Dec 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something odd is happening with copied anchored activities where the anchor offset is not being properly preserved and the issue is greater the further in the plan the paste is from the plan start.

Screen.Recording.2024-12-05.at.6.45.50.PM.mov

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! I only need to update the offset of non-anchored activities. Anchored activities need to stay with the same offset. Good catch!

throw Error(`Plan is not defined`);
}
if (!queryPermissions.CREATE_ACTIVITY_DIRECTIVE(user, plan)) {
throwPermissionError('cloning activity directives into the plan');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer "clone" instead of "cloning" here to match other permission error grammar

return false;
}

const clipboard = JSON.parse(serializedClipboard);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider try/catching instances of parsing from session storage in case the JSON blob gets corrupted somehow (know it is unlikely but might be possible if a user refreshes the page while writing a huge set of activities to session storage).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good checks

Copy link
Contributor

@AaronPlave AaronPlave left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great, so nice to be able to copy paste! Left a bunch of comments but nothing huge.

});

function onKeyDown(event: KeyboardEvent) {
if (isMetaOrCtrlPressed(event)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also detect if the event is coming from an input so that we don't accidentally copy/paste when a user is trying to copy/paste text?

@dandelany
Copy link
Collaborator

dandelany commented Dec 6, 2024

@ivydeliz gave a walkthrough this morning and it's looking good! A few issues/changes we discussed (some of these are duplicates of comments above):

  • bug with offsets when you paste two directives where one is anchored to the other - the offset time changes for the anchored directive, but it should remain the same offset, just anchored to the new directive
  • bug with keyboard shortcuts - the listener runs for all copy/paste keyboard actions even when not intended. Ideally should be possible to Ctrl+C most text on the page and Ctrl+V into any input box without triggering directive copy/paste
  • We decided to change the paste text in the menu when you right click on the timeline from "Paste at Time" to "Paste Directive at Time" when a single directive is copied, or eg. "Paste 5 Directives at Time" when multiple are copied

We also had a question about how to handle the case where some of the pasted activities fall outside of the plan bounds entirely. We currently allow this to happen - which is probably correct - but would be good to get input from @mattdailis or @Mythicaeda . We also noted a UI inconsistency - it seems that activity directives which fall before the plan bounds are shown with a "outside plan bounds" icon/warning in the directives table, but directives which are after the plan bounds don't show this - not clear if this is intentional or something we should fix in a follow-up

@Mythicaeda
Copy link
Contributor

Mythicaeda commented Dec 9, 2024

We also noted a UI inconsistency - it seems that activity directives which fall before the plan bounds are shown with a "outside plan bounds" icon/warning in the directives table, but directives which are after the plan bounds don't show this - not clear if this is intentional or something we should fix in a follow-up

I can shed some light on this, @dandelany. This inconsistency is because while we know for a fact whether or not an activity falls before the plan start time, we may not know if an activity falls after the plan end time due to anchors.

Quick Case breakdown:

  1. Activity A's start time is one hour after the plan's end time.
  2. Activity A's start time is one hour before the plan's end time. Activity B is anchored to start as soon as Activity A finishes. Activity A may run either two minutes or two hours.

In Case 1, we know statically that Activity A is outside the plan bounds. In Case 2, we don't know until the user simulates if Activity B will be within the plan bounds. We felt it would be more confusing (and possibly misleading) to the user to mark Case 1 and not Case 2 than it would be to not mark either case.

Changing this would require a DB change, as that's what runs the validation logic for activity start times.

@Mythicaeda
Copy link
Contributor

We also had a question about how to handle the case where some of the pasted activities fall outside of the plan bounds entirely. We currently allow this to happen - which is probably correct

My take is:

  • when copying an anchor chain, copy-paste should not change anchor relationships -- if the user copied an anchor chain and an activity that is not the root lands outside the plan bounds, we should let the user handle that
  • when you copy an activity that is anchored to something and paste it such that it is no longer anchored, then it's start offset should be at minimum plan-start and at maximum plan end (I'm assuming that if you're hovering over a time in the timeline that it's start offset is set to that time)
    • I think this logic still makes sense if it's also applied to activities that weren't anchored to anything when you copied them, but am willing to hear @mattdailis's thoughts if he disagrees

I feel the strongest about the first bullet.

@ivydeliz ivydeliz force-pushed the feature/copy-and-paste-of-activity-directives branch from b7cc98a to c045689 Compare December 19, 2024 03:34
@ivydeliz ivydeliz force-pushed the feature/copy-and-paste-of-activity-directives branch from c045689 to d46bd1c Compare December 19, 2024 03:40
@ivydeliz ivydeliz force-pushed the feature/copy-and-paste-of-activity-directives branch from d46bd1c to fece7d7 Compare December 19, 2024 03:48
@ivydeliz ivydeliz force-pushed the feature/copy-and-paste-of-activity-directives branch from fece7d7 to 2b27699 Compare December 19, 2024 03:57
@ivydeliz ivydeliz requested a review from AaronPlave December 19, 2024 04:08
}
}

function onKeyDownOverPanel({ detail }: CustomEvent<KeyboardEvent>) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my concern with this approach would be that it is a little less predictable as to when exactly I can paste. For example, if i click on a directive in the table and then hover over the timeline without clicking on it i can't paste the directive. Curious to hear pros and cons of this vs always allowing directive paste except when the event comes from an input?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also not sure if this is the root cause but something in this PR has broken date picker day selection. But this feels like something that could be involved somehow?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Todo
Development

Successfully merging this pull request may close these issues.

5 participants