-
Notifications
You must be signed in to change notification settings - Fork 6
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
base: develop
Are you sure you want to change the base?
Conversation
} | ||
|
||
function pasteActivityDirectives() { | ||
plan !== null && pasteActivityDirectivesFromClipboard(plan, user); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
src/utilities/activities.ts
Outdated
}); | ||
} | ||
|
||
effects.cloneActivityDirectives(activities, destinationPlan, user).then(() => {}); //TODO select clones? |
There was a problem hiding this comment.
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.
src/utilities/clipboard.ts
Outdated
@@ -0,0 +1,7 @@ | |||
export function setClipboardContent(content: string) { |
There was a problem hiding this comment.
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?
src/utilities/gql.ts
Outdated
@@ -334,6 +334,17 @@ const gql = { | |||
} | |||
`, | |||
|
|||
CREATE_ACTIVITY_DIRECTIVES: `#graphql | |||
mutation CreateActivityDirectives($activityDirectivesInsertInput: [activity_directive_insert_input!]!) { | |||
insert_activity_directive(objects: $activityDirectivesInsertInput) { |
There was a problem hiding this comment.
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.
src/utilities/gql.ts
Outdated
@@ -3449,6 +3460,16 @@ const gql = { | |||
} | |||
`, | |||
|
|||
UPDATE_ACTIVITY_DIRECTIVES: `#graphql | |||
mutation UpdateActivityDirective($updates: [activity_directive_updates!]!) { | |||
update_activity_directive_many( |
There was a problem hiding this comment.
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?
@@ -117,6 +130,29 @@ | |||
completeColumnDefs = [activityErrorColumnDef, ...(columnDefs ?? []), activityActionColumnDef]; | |||
} | |||
|
|||
onMount(() => { | |||
document.addEventListener(`keydown`, onKeyDown); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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
src/utilities/activities.ts
Outdated
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
src/utilities/activities.ts
Outdated
const activities: ActivityDirective[] = clipboard.activities; | ||
|
||
//transpose in time if we're given a time, otherwise it paste at the current time | ||
if (pasteStartingAtTime !== undefined) { |
There was a problem hiding this comment.
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'
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good!
src/utilities/activities.ts
Outdated
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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!
src/utilities/effects.ts
Outdated
throw Error(`Plan is not defined`); | ||
} | ||
if (!queryPermissions.CREATE_ACTIVITY_DIRECTIVE(user, plan)) { | ||
throwPermissionError('cloning activity directives into the plan'); |
There was a problem hiding this comment.
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
src/utilities/activities.ts
Outdated
return false; | ||
} | ||
|
||
const clipboard = JSON.parse(serializedClipboard); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good checks
There was a problem hiding this 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)) { |
There was a problem hiding this comment.
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?
@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):
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 |
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:
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. |
My take is:
I feel the strongest about the first bullet. |
b7cc98a
to
c045689
Compare
c045689
to
d46bd1c
Compare
d46bd1c
to
fece7d7
Compare
fece7d7
to
2b27699
Compare
} | ||
} | ||
|
||
function onKeyDownOverPanel({ detail }: CustomEvent<KeyboardEvent>) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
Feature #1544