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

Merged
merged 1 commit into from
Jan 21, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/components/activity/ActivityDirectivesTable.svelte
Original file line number Diff line number Diff line change
@@ -17,6 +17,12 @@
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';

export let activityDirectives: ActivityDirective[] = [];
export let activityDirectiveErrorRollupsMap: Record<ActivityDirectiveId, ActivityErrorRollup> | undefined = undefined;
@@ -31,6 +37,7 @@
export let filterExpression: string = '';

const dispatch = createEventDispatcher<{
createActivityDirectives: ActivityDirective[];
scrollTimelineToTime: number;
}>();

@@ -44,15 +51,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',
@@ -145,6 +159,25 @@
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
@@ -161,10 +194,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
@@ -178,5 +213,9 @@
<ContextMenuItem on:click={scrollTimelineToActivityDirective}>Scroll to Activity</ContextMenuItem>
<ContextMenuSeparator></ContextMenuSeparator>
{/if}
{#if canPasteActivityDirectives()}
<ContextMenuItem on:click={pasteActivityDirectives}>{getPasteActivityDirectivesText()}</ContextMenuItem>
<ContextMenuSeparator></ContextMenuSeparator>
{/if}
</svelte:fragment>
</BulkActionDataGrid>
9 changes: 9 additions & 0 deletions src/components/activity/ActivityDirectivesTablePanel.svelte
Original file line number Diff line number Diff line change
@@ -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;
@@ -246,6 +247,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();
@@ -426,6 +434,7 @@
on:columnPinned={onColumnPinned}
on:columnResized={onColumnResized}
on:columnVisible={onColumnVisible}
on:createActivityDirectives={createActivityDirectives}
on:gridSizeChanged={onGridSizeChangedDebounced}
on:rowDoubleClicked={onRowDoubleClicked}
on:selectionChanged={onSelectionChanged}
50 changes: 48 additions & 2 deletions src/components/timeline/TimelineContextMenu.svelte
Original file line number Diff line number Diff line change
@@ -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';
@@ -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;
@@ -49,6 +57,7 @@

const dispatch = createEventDispatcher<{
collapseDiscreteTree: Row;
createActivityDirectives: ActivityDirective[];
deleteActivityDirective: number;
deleteRow: Row;
duplicateRow: Row;
@@ -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;
@@ -241,6 +251,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}>
@@ -311,6 +342,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) {
@@ -329,7 +363,7 @@
],
]}
>
Delete Directive
Delete Activity Directive
</ContextMenuItem>
{:else if span}
<ContextMenuItem on:click={jumpToActivityDirective}>Jump to Activity Directive</ContextMenuItem>
@@ -398,6 +432,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}
42 changes: 31 additions & 11 deletions src/components/ui/DataGrid/BulkActionDataGrid.svelte
Original file line number Diff line number Diff line change
@@ -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';
@@ -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;
@@ -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);
@@ -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={[
[
Loading

Unchanged files with check annotations Beta

await expect(filterIcon).toBeVisible();
await filterIcon.click();
await this.page.locator('.ag-popup').getByRole('textbox', { name: 'Filter Value' }).first().fill(modelName);
await expect(this.table.getByRole('row', { name: modelName })).toBeVisible();

Check failure on line 110 in e2e-tests/fixtures/Models.ts

GitHub Actions / e2e-test

[e2e tests] › tests/model.test.ts:143:3 › Model › Should successfully save the model changes

1) [e2e tests] › tests/model.test.ts:143:3 › Model › Should successfully save the model changes ── Error: Timed out 5000ms waiting for expect(locator).toBeVisible() Locator: getByRole('treegrid').getByRole('row', { name: 'gigantic_fuchsia_muskox' }) Expected: visible Received: <element(s) not found> Call log: - expect.toBeVisible with timeout 5000ms - waiting for getByRole('treegrid').getByRole('row', { name: 'gigantic_fuchsia_muskox' }) at fixtures/Models.ts:110 108 | await filterIcon.click(); 109 | await this.page.locator('.ag-popup').getByRole('textbox', { name: 'Filter Value' }).first().fill(modelName); > 110 | await expect(this.table.getByRole('row', { name: modelName })).toBeVisible(); | ^ 111 | await this.page.keyboard.press('Escape'); 112 | } 113 | at Models.filterTable (/home/runner/work/***-ui/***-ui/e2e-tests/fixtures/Models.ts:110:68) at Models.deleteModel (/home/runner/work/***-ui/***-ui/e2e-tests/fixtures/Models.ts:57:5) at /home/runner/work/***-ui/***-ui/e2e-tests/tests/model.test.ts:62:3
await this.page.keyboard.press('Escape');
}
let schedulingConditions: SchedulingConditions;
let schedulingGoals: SchedulingGoals;
test.beforeAll(async ({ baseURL, browser }) => {

Check failure on line 18 in e2e-tests/tests/plan-activity-presets.test.ts

GitHub Actions / e2e-test

[e2e tests] › tests/plan-activity-presets.test.ts:73:3 › Plan Activity Presets › Setting a preset to a directive should update the parameter values

2) [e2e tests] › tests/plan-activity-presets.test.ts:73:3 › Plan Activity Presets › Setting a preset to a directive should update the parameter values "beforeAll" hook timeout of 30000ms exceeded. 16 | let schedulingGoals: SchedulingGoals; 17 | > 18 | test.beforeAll(async ({ baseURL, browser }) => { | ^ 19 | context = await browser.newContext(); 20 | page = await context.newPage(); 21 | at /home/runner/work/***-ui/***-ui/e2e-tests/tests/plan-activity-presets.test.ts:18:6

Check failure on line 18 in e2e-tests/tests/plan-activity-presets.test.ts

GitHub Actions / e2e-test

[e2e tests] › tests/plan-activity-presets.test.ts:73:3 › Plan Activity Presets › Setting a preset to a directive should update the parameter values

2) [e2e tests] › tests/plan-activity-presets.test.ts:73:3 › Plan Activity Presets › Setting a preset to a directive should update the parameter values Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── "beforeAll" hook timeout of 30000ms exceeded. 16 | let schedulingGoals: SchedulingGoals; 17 | > 18 | test.beforeAll(async ({ baseURL, browser }) => { | ^ 19 | context = await browser.newContext(); 20 | page = await context.newPage(); 21 | at /home/runner/work/***-ui/***-ui/e2e-tests/tests/plan-activity-presets.test.ts:18:6
context = await browser.newContext();
page = await context.newPage();
}
async fillActivityPresetName(presetName: string) {
await this.panelActivityForm.getByRole('combobox', { name: 'None' }).click();

Check failure on line 252 in e2e-tests/fixtures/Plan.ts

GitHub Actions / e2e-test

[e2e tests] › tests/plan-activity-presets.test.ts:73:3 › Plan Activity Presets › Setting a preset to a directive should update the parameter values

2) [e2e tests] › tests/plan-activity-presets.test.ts:73:3 › Plan Activity Presets › Setting a preset to a directive should update the parameter values Error: locator.click: Target page, context or browser has been closed Call log: - waiting for locator('[data-component-name="ActivityFormPanel"]').getByRole('combobox', { name: 'None' }) at fixtures/Plan.ts:252 250 | 251 | async fillActivityPresetName(presetName: string) { > 252 | await this.panelActivityForm.getByRole('combobox', { name: 'None' }).click(); | ^ 253 | await this.panelActivityForm.locator('.dropdown-header').waitFor({ state: 'attached' }); 254 | await this.panelActivityForm.getByPlaceholder('Enter preset name').click(); 255 | await this.panelActivityForm.getByPlaceholder('Enter preset name').fill(presetName); at Plan.fillActivityPresetName (/home/runner/work/***-ui/***-ui/e2e-tests/fixtures/Plan.ts:252:74) at /home/runner/work/***-ui/***-ui/e2e-tests/tests/plan-activity-presets.test.ts:49:14

Check failure on line 252 in e2e-tests/fixtures/Plan.ts

GitHub Actions / e2e-test

[e2e tests] › tests/plan-activity-presets.test.ts:73:3 › Plan Activity Presets › Setting a preset to a directive should update the parameter values

2) [e2e tests] › tests/plan-activity-presets.test.ts:73:3 › Plan Activity Presets › Setting a preset to a directive should update the parameter values Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: locator.click: Target page, context or browser has been closed Call log: - waiting for locator('[data-component-name="ActivityFormPanel"]').getByRole('combobox', { name: 'None' }) at fixtures/Plan.ts:252 250 | 251 | async fillActivityPresetName(presetName: string) { > 252 | await this.panelActivityForm.getByRole('combobox', { name: 'None' }).click(); | ^ 253 | await this.panelActivityForm.locator('.dropdown-header').waitFor({ state: 'attached' }); 254 | await this.panelActivityForm.getByPlaceholder('Enter preset name').click(); 255 | await this.panelActivityForm.getByPlaceholder('Enter preset name').fill(presetName); at Plan.fillActivityPresetName (/home/runner/work/***-ui/***-ui/e2e-tests/fixtures/Plan.ts:252:74) at /home/runner/work/***-ui/***-ui/e2e-tests/tests/plan-activity-presets.test.ts:49:14
await this.panelActivityForm.locator('.dropdown-header').waitFor({ state: 'attached' });
await this.panelActivityForm.getByPlaceholder('Enter preset name').click();
await this.panelActivityForm.getByPlaceholder('Enter preset name').fill(presetName);
test('Running the same scheduling goal twice in a row should show +0 in that goals badge', async () => {
await expect(plan.schedulingGoalEnabledCheckboxSelector(goalName1)).toBeChecked();
await plan.runScheduling();
await expect(plan.schedulingGoalDifferenceBadge(goalName1)).toHaveText('+10');

Check failure on line 98 in e2e-tests/tests/scheduling.test.ts

GitHub Actions / e2e-test

[e2e tests] › tests/scheduling.test.ts:95:3 › Scheduling › Running the same scheduling goal twice in a row should show +0 in that goals badge

3) [e2e tests] › tests/scheduling.test.ts:95:3 › Scheduling › Running the same scheduling goal twice in a row should show +0 in that goals badge Error: Timed out 5000ms waiting for expect(locator).toHaveText(expected) Locator: locator('.scheduling-goal:has-text("excessive_lavender_elephant")').locator('.difference-badge') Expected string: "+10" Received string: "+0" Call log: - expect.toHaveText with timeout 5000ms - waiting for locator('.scheduling-goal:has-text("excessive_lavender_elephant")').locator('.difference-badge') 9 × locator resolved to <div aria-label="New Satisfied Activities" class="difference-badge svelte-w6ai2h">+0</div> - unexpected value "+0" 96 | await expect(plan.schedulingGoalEnabledCheckboxSelector(goalName1)).toBeChecked(); 97 | await plan.runScheduling(); > 98 | await expect(plan.schedulingGoalDifferenceBadge(goalName1)).toHaveText('+10'); | ^ 99 | await plan.runScheduling(); 100 | await expect(plan.schedulingGoalDifferenceBadge(goalName1)).toHaveText('+0'); 101 | }); at /home/runner/work/***-ui/***-ui/e2e-tests/tests/scheduling.test.ts:98:65
await plan.runScheduling();
await expect(plan.schedulingGoalDifferenceBadge(goalName1)).toHaveText('+0');
});