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

Advanced Timeline Filtering #1535

Open
wants to merge 86 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
5751fad
WIP
AaronPlave Oct 21, 2024
904bbc5
Fixes for spans
AaronPlave Oct 30, 2024
ae3f9c7
Componentize editor section headers. Remove unused styles.
AaronPlave Oct 31, 2024
99c982e
Refactoring, fixing, improving timeline editor
AaronPlave Oct 31, 2024
edcc6c8
Add icon
AaronPlave Nov 5, 2024
792e79f
Add svelte dragging library
AaronPlave Nov 5, 2024
45eee09
Style refactor
AaronPlave Nov 5, 2024
707d3da
Make menu items tab focusable
AaronPlave Nov 5, 2024
259c0d7
Style tweak
AaronPlave Nov 5, 2024
79c587e
WIP activity layer filtering
AaronPlave Nov 5, 2024
8daf9c8
style refactor
AaronPlave Nov 5, 2024
1bd6bfe
Get default activity arguments during plan load
AaronPlave Nov 12, 2024
11db3a1
Potentially remove unused input workaround
AaronPlave Nov 12, 2024
50ceb0b
WIP filtering
AaronPlave Nov 12, 2024
0f8cce2
Basic manual type selection list filtering
AaronPlave Nov 12, 2024
365aff8
add cssgrid to activity filter builder
AaronPlave Nov 12, 2024
e970af6
Bring back resource editing in old form for now
AaronPlave Nov 12, 2024
55b3492
Remove all layer fixes
AaronPlave Nov 12, 2024
f37dc00
Style fix
AaronPlave Nov 12, 2024
6a8e19e
Refactor
AaronPlave Nov 12, 2024
c3ee062
Show an indication dot when an activity layer has active filters
AaronPlave Nov 12, 2024
cb30d5a
Show resulting type and instance counts
AaronPlave Nov 12, 2024
3b7c315
Greater/less than support
AaronPlave Nov 12, 2024
6f60de9
Remove log
AaronPlave Nov 12, 2024
6174ce4
use > and <
AaronPlave Nov 12, 2024
4333a1c
Reactivity fix
AaronPlave Nov 12, 2024
2de0b28
Support boolean comparison
AaronPlave Nov 12, 2024
9ab7813
Input filtering
AaronPlave Nov 21, 2024
e8fef7b
Bug fix
AaronPlave Nov 21, 2024
4d17e0f
Support for min col and row css grid sizing
AaronPlave Nov 21, 2024
bc07512
Add derived subsystem tags store. Fix subsystem tags filtering.
AaronPlave Nov 21, 2024
05f7a52
Tooltip tweaks
AaronPlave Nov 21, 2024
3a34d2c
Fixes
AaronPlave Nov 21, 2024
b20d574
Scheduling goal id filtering WIP support
AaronPlave Nov 21, 2024
13f75ca
Manual type selection menu positioning fix
AaronPlave Nov 21, 2024
953b0db
Filter parameter possibilities by resulting types if any. List all re…
AaronPlave Nov 22, 2024
3146322
Tweak SearchableDropdown to be more like a dropdown
AaronPlave Nov 22, 2024
2e8da82
Popper positioning workaround when inside css transformed parent. Use…
AaronPlave Nov 22, 2024
350da54
Fixes and refactoring
AaronPlave Nov 25, 2024
a12b203
Fixes
AaronPlave Nov 26, 2024
7a7b800
Support within and not within
AaronPlave Nov 26, 2024
bbd37d0
Apply subfilters to resulting types
AaronPlave Nov 30, 2024
f9e5799
Type fixes
AaronPlave Nov 30, 2024
f18eb05
Type fixes
AaronPlave Nov 30, 2024
b4eab88
Fixes and refactoring
AaronPlave Dec 2, 2024
13a32b0
Bug fix for confirm modal when adding activities to timeline
AaronPlave Dec 3, 2024
ac12603
Type fix
AaronPlave Dec 5, 2024
de1e557
Swap x-range icon
AaronPlave Dec 17, 2024
bbb6d56
Add left slot to MenuHeader
AaronPlave Dec 17, 2024
2ed9c8d
Refactor SearchableDropdown to allow for multiple options if requested
AaronPlave Dec 17, 2024
7cb55d2
Style and functionality updates for all layers
AaronPlave Dec 17, 2024
5578986
Bug fixes
AaronPlave Dec 17, 2024
839c7b3
Bug fixes
AaronPlave Dec 17, 2024
f515dda
Use resource layer name as override on SearchableDropdown selected op…
AaronPlave Dec 17, 2024
95331f6
Show units for duration, int, and real parameters. Filter window drag…
AaronPlave Dec 17, 2024
3c6d4c0
Show all activities when no filters exist on layer. Bug fix.
AaronPlave Dec 17, 2024
5dac40f
Virtualization of SearchableDropdown. Fixes.
AaronPlave Dec 18, 2024
49c817b
View migration to v2
AaronPlave Dec 19, 2024
50a0725
Prevent highlight on drag
AaronPlave Dec 19, 2024
340b531
Improve auto width sizing of SearchableDropdown
AaronPlave Dec 19, 2024
ea5a46a
Fixes
AaronPlave Dec 20, 2024
ec6964d
Type fix
AaronPlave Dec 20, 2024
89709b8
Type fixes
AaronPlave Dec 20, 2024
94da4eb
Type fix
AaronPlave Dec 20, 2024
52eb643
Tests
AaronPlave Dec 20, 2024
50cadc9
Remove unused code
AaronPlave Dec 20, 2024
4dac985
Type fixes
AaronPlave Dec 20, 2024
66e0527
Tests. Rename field Tag -> Tags. Remove unused is_one_of and is_not_o…
AaronPlave Dec 20, 2024
1b38955
Fix
AaronPlave Dec 20, 2024
c1ab4aa
Test fix
AaronPlave Dec 20, 2024
dbdb693
Unit test fixes
AaronPlave Dec 23, 2024
b7b8c99
Tweak
AaronPlave Dec 23, 2024
6aa7820
Aria labels
AaronPlave Dec 23, 2024
2c7ec2c
Dispatch show event
AaronPlave Dec 23, 2024
2f233d9
Aria fixes
AaronPlave Dec 23, 2024
d8126ef
Basic test for DynamicFilter
AaronPlave Dec 23, 2024
310537e
Remove log
AaronPlave Dec 23, 2024
8c81be6
Remove .only
AaronPlave Dec 23, 2024
0b2c96b
Fixes
AaronPlave Dec 30, 2024
7052976
e2e tests and fixes
AaronPlave Dec 30, 2024
2dbe547
Remove pause in e2e test
AaronPlave Dec 30, 2024
7de3a84
Test fix
AaronPlave Dec 31, 2024
82ea940
Test fixes
AaronPlave Dec 31, 2024
791076e
Fixes
AaronPlave Dec 31, 2024
b53591d
Remove logs
AaronPlave Dec 31, 2024
f544330
Add tooltip
AaronPlave Jan 2, 2025
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
25 changes: 13 additions & 12 deletions e2e-tests/fixtures/Plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ export class Plan {
}

async fillActivityPresetName(presetName: string) {
await this.panelActivityForm.getByRole('button', { name: 'Set Preset' }).click();
await this.panelActivityForm.getByRole('combobox', { name: 'None' }).click();
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);
Expand All @@ -269,7 +269,7 @@ export class Plan {
}

async fillSimulationTemplateName(templateName: string) {
await this.panelSimulation.getByRole('button', { name: 'Set Template' }).click();
await this.panelSimulation.locator('div[name="Set Template"]').click();
await this.panelSimulation.locator('.dropdown-header').waitFor({ state: 'attached' });
await this.panelSimulation.getByPlaceholder('Enter template name').click();
await this.panelSimulation.getByPlaceholder('Enter template name').fill(templateName);
Expand Down Expand Up @@ -372,22 +372,23 @@ export class Plan {
}

async selectActivityAnchorByIndex(index: number) {
await this.panelActivityForm.getByRole('button', { name: 'Set Anchor' }).click();
const anchorCollapse = this.panelActivityForm.getByRole('group', { name: 'Anchor-collapse' });
await anchorCollapse.getByRole('combobox').click();

await this.panelActivityForm.getByRole('menuitem').nth(index).waitFor({ state: 'attached' });
const anchorMenuName = await this.panelActivityForm.getByRole('menuitem').nth(index)?.innerText();
await this.panelActivityForm.getByRole('menuitem').nth(index).click();
await this.panelActivityForm.getByRole('menuitem').nth(index).waitFor({ state: 'detached' });
await anchorCollapse.getByRole('menuitem').nth(index).waitFor({ state: 'attached' });
const anchorMenuName = await anchorCollapse.getByRole('menuitem').nth(index)?.innerText();
await anchorCollapse.getByRole('menuitem').nth(index).click();
await anchorCollapse.getByRole('menuitem').nth(index).waitFor({ state: 'detached' });

await this.page.waitForFunction(
anchorMenuName => document.querySelector('.anchor-form .selected-display-value')?.innerHTML === anchorMenuName,
anchorMenuName,
);
await expect(this.panelActivityForm.getByRole('textbox', { name: anchorMenuName })).toBeVisible();
await expect(anchorCollapse.getByRole('combobox', { name: anchorMenuName })).toBeVisible();
}

async selectActivityPresetByName(presetName: string) {
await this.panelActivityForm.getByRole('button', { name: 'Set Preset' }).click();
await this.panelActivityForm.locator('div[name="Set Preset"]').click();

await this.panelActivityForm.getByRole('menuitem', { name: presetName }).waitFor({ state: 'attached' });
await this.panelActivityForm.getByRole('menuitem', { name: presetName }).click();
Expand All @@ -413,11 +414,11 @@ export class Plan {
document.querySelector('.activity-preset-input-container .selected-display-value')?.innerHTML === presetName,
presetName,
);
await expect(this.panelActivityForm.getByRole('textbox', { name: presetName })).toBeVisible();
await expect(this.panelActivityForm.getByRole('combobox', { name: presetName })).toBeVisible();
}

async selectSimulationTemplateByName(templateName: string) {
await this.panelSimulation.getByRole('button', { name: 'Set Template' }).click();
await this.panelSimulation.locator('div[name="Set Template"]').click();

await this.panelSimulation.getByRole('menuitem', { name: templateName }).waitFor({ state: 'attached' });
await this.panelSimulation.getByRole('menuitem', { name: templateName }).click();
Expand All @@ -444,7 +445,7 @@ export class Plan {
templateName,
templateName,
);
await expect(this.panelSimulation.getByRole('textbox', { name: templateName })).toBeVisible();
await expect(this.panelSimulation.getByRole('combobox', { name: templateName })).toBeVisible();
}

async showConstraintsLayout() {
Expand Down
2 changes: 1 addition & 1 deletion e2e-tests/tests/plan-activities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ test.describe.serial('Plan Activities', () => {
() => document.querySelector('.anchor-form .selected-display-value')?.innerHTML === 'To Plan',
);

await expect(plan.panelActivityForm.getByRole('textbox', { name: 'To Plan' })).toBeVisible();
await expect(plan.panelActivityForm.getByRole('combobox', { name: 'To Plan' })).toBeVisible();
});

test('Deleting multiple activity directives but only 1 has a remaining anchored dependent should prompt for just the one with a remaining dependent', async () => {
Expand Down
10 changes: 5 additions & 5 deletions e2e-tests/tests/plan-activity-presets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ test.beforeAll(async ({ baseURL, browser }) => {

await plan.selectActivityPresetByName('None');

await expect(plan.panelActivityForm.getByRole('textbox', { name: 'None' })).toBeVisible();
await expect(plan.panelActivityForm.getByRole('combobox', { name: 'None' })).toBeVisible();
});

test.afterAll(async () => {
Expand All @@ -72,18 +72,18 @@ test.afterAll(async () => {
test.describe.serial('Plan Activity Presets', () => {
test(`Setting a preset to a directive should update the parameter values`, async () => {
await plan.selectActivityPresetByName('Preset 1');
await expect(page.getByRole('textbox', { name: 'Preset 1' })).toBeVisible();
await expect(page.getByRole('combobox', { name: 'Preset 1' })).toBeVisible();
});

test(`Removing an activity preset from a directive should reflect that it is no longer present`, async () => {
await plan.selectActivityPresetByName('None');
await expect(page.getByRole('textbox', { name: 'None' })).toBeVisible();
await expect(page.getByRole('combobox', { name: 'None' })).toBeVisible();
});

test('Deleting an activity preset should remove it from the list of presets', async () => {
await plan.selectActivityPresetByName('Preset 1');

await page.getByRole('button', { name: 'Set Preset' }).click();
await page.getByRole('combobox', { name: 'Preset 1' }).click();

await page.getByRole('button', { name: 'Delete preset' }).waitFor({ state: 'attached' });
await page.getByRole('button', { name: 'Delete preset' }).click();
Expand All @@ -96,6 +96,6 @@ test.describe.serial('Plan Activity Presets', () => {
() => document.querySelector('.activity-preset-input-container .selected-display-value')?.innerHTML === 'None',
);

await expect(page.getByRole('textbox', { name: 'None' })).toBeVisible();
await expect(page.getByRole('combobox', { name: 'None' })).toBeVisible();
});
});
10 changes: 5 additions & 5 deletions e2e-tests/tests/plan-simulation-templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ test.beforeAll(async ({ baseURL, browser }) => {

await plan.selectSimulationTemplateByName('None');

expect(page.getByRole('textbox', { name: 'None' })).toBeVisible();
expect(page.getByRole('combobox', { name: 'None' })).toBeVisible();
});

test.afterAll(async () => {
Expand All @@ -75,19 +75,19 @@ test.describe.serial('Plan Simulation Templates', async () => {
test(`Setting a simulation template to a simulation should update the parameter values`, async () => {
await plan.selectSimulationTemplateByName('Template 1');

expect(plan.panelSimulation.getByRole('textbox', { name: 'Template 1' })).toBeVisible();
expect(plan.panelSimulation.getByRole('combobox', { name: 'Template 1' })).toBeVisible();
});

test(`Removing an simulation template from a simulation should reflect that it is no longer present`, async () => {
await plan.selectSimulationTemplateByName('None');

expect(page.getByRole('textbox', { name: 'None' })).toBeVisible();
expect(page.getByRole('combobox', { name: 'None' })).toBeVisible();
});

test('Deleting an simulation template should remove it from the list of templates', async () => {
await plan.selectSimulationTemplateByName('Template 1');

await page.getByRole('button', { name: 'Set Template' }).click();
await plan.panelSimulation.locator('div[name="Set Template"]').click();

await page.getByRole('button', { name: 'Delete Template' }).waitFor({ state: 'attached' });
await page.getByRole('button', { name: 'Delete Template' }).click();
Expand All @@ -98,6 +98,6 @@ test.describe.serial('Plan Simulation Templates', async () => {

await page.waitForFunction(() => document.querySelector('.selected-display-value')?.innerHTML === 'None');

expect(page.getByRole('textbox', { name: 'None' })).toBeVisible();
expect(page.getByRole('combobox', { name: 'None' })).toBeVisible();
});
});
126 changes: 110 additions & 16 deletions e2e-tests/tests/timeline-view-editing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ test.describe.serial('Timeline View Editing', () => {
test('Add an activity to the parent plan', async () => {
await plan.showPanel(PanelNames.TIMELINE_ITEMS);
await plan.addActivity('PickBanana');
await plan.addActivity('PeelBanana');
});

test('Change the start time of the activity', async () => {
Expand Down Expand Up @@ -105,26 +106,121 @@ test.describe.serial('Timeline View Editing', () => {
// Look for back button indicating that the row editor is active
expect(page.locator('.section-back-button ').first()).toBeDefined();

const existingLayerCount = await page.locator('.timeline-layer').count();

// Give the row a name
await page.locator('input[name="name"]').first().fill(rowName);
await page.locator('input[name="name"]').first().blur();
});

test('Add an activity layer', async () => {
const activityLayerEditor = page.getByLabel('Activity Layer-editor');
const existingLayerCount = await activityLayerEditor.locator('.timeline-layer-editor').count();

// Add a layer
await page.getByRole('button', { name: 'New Layer' }).click();
const newLayerCount = await page.locator('.timeline-layer').count();
// Add an activity layer
await activityLayerEditor.getByRole('button', { name: 'New Activity Layer' }).click();
const newLayerCount = await activityLayerEditor.locator('.timeline-layer-editor').count();
expect(newLayerCount - existingLayerCount).toEqual(1);

// Expect an activity layer to be created by default
expect(await page.locator('select[name="chartType"]').last().inputValue()).toBe('activity');
// Expect the activity layer to include all activities
expect(await activityLayerEditor.locator('.timeline-layer-editor').first()).toHaveText('All Activities');
});

// Expect the filter list to open
await page.getByPlaceholder('Search').last().click();
await expect(page.locator('.menu-slot > .header')).toBeDefined();
test('Edit an activity layer', async () => {
const activityLayerEditor = page.getByLabel('Activity Layer-editor');

// Open the activity filter builder
await activityLayerEditor
.locator('.timeline-layer-editor')
.first()
.getByLabel('activity-filter-builder-trigger')
.click();

// Expect that the modal is present
const modal = activityLayerEditor.getByLabel('activity-filter-builder');
expect(modal).toBeDefined();

// Expect that layer name is showing in the name input
expect(modal.locator('input[name="layer-name"]')).toHaveValue('All Activities');

// Expect that the resulting types list is not empty
const resultingTypesList = modal.locator('.resulting-types-list');
const allActivityTypesCount = await resultingTypesList.locator('.activity-type-result').count();
expect(allActivityTypesCount).toBeGreaterThan(0);

// Expect that manually selecting types cause the types to appear in the resulting types list
await modal.locator("input[name='manual-types-filter-input']").click();
expect(await modal.locator('.manual-types-menu').first()).toBeDefined();
await modal.getByRole('menuitem', { name: 'ChangeProducer' }).click();
await modal.getByRole('menuitem', { name: 'ControllableDurationActivity' }).click();
await page.keyboard.press('Escape');

expect(await resultingTypesList.getByText('ChangeProducer')).toBeDefined();
expect(await resultingTypesList.getByText('ControllableDurationActivity')).toBeDefined();

// Expect that dynamic types can be added
await modal.getByLabel('dynamic-types').getByRole('button', { name: 'Add Filter' }).click();
expect(await modal.getByLabel('dynamic-types').getByRole('listitem').count()).toBe(1);
await modal.getByLabel('dynamic-types').getByRole('listitem').locator("input[name='filter-value']").fill('banana');
expect(await resultingTypesList.locator('.activity-type-result').count()).toEqual(11);

// Expect that global filters can be added
await modal.getByLabel('global-filters').getByRole('button', { name: 'Add Filter' }).click();
expect(await modal.getByLabel('global-filters').getByRole('listitem').count()).toBe(1);
// Select parameter field
await modal.getByLabel('global-filters').locator("select[aria-label='field']").selectOption('Parameter');
// Select specific parameter
await modal.getByLabel('global-filters').getByText('Select Parameter').click();
await modal.getByLabel('global-filters').getByText('quantity (int)').click();
// Select operator
await modal.getByLabel('global-filters').locator("select[aria-label='operator']").selectOption('equals');
// Fill filter value input
await modal.getByLabel('global-filters').getByRole('listitem').locator("input[name='filter-value']").fill('10');
// Ensure that only one instance (PickBanana) is listed
expect(await modal.getByText('1 instance')).toBeDefined();

// Expect that type subfilters can be added
const activityResult = resultingTypesList.getByRole('listitem', { name: 'activity-type-result-PickBanana' });
await activityResult.getByRole('button', { name: 'Add Filter' }).click();
expect(await activityResult.getByRole('listitem').count()).toBe(1);
// Select name field
await activityResult.locator("select[aria-label='field']").selectOption('Name');
// Select operator
await activityResult.locator("select[aria-label='operator']").selectOption('includes');
// Fill filter value input
await activityResult.getByRole('listitem').locator("input[name='filter-value']").fill('foo');
// Ensure that only one instance (PickBanana) is listed
expect(await modal.getByText('0 instances')).toBeDefined();

// Expect that type subfilters can be removed
await activityResult.getByRole('button', { name: 'Remove filter' }).click();
expect(await modal.getByText('1 instance')).toBeDefined();

// Expect that global filters can be removed
await modal.getByLabel('global-filters').getByRole('button', { name: 'Remove filter' }).click();
expect(await modal.getByText('2 instances')).toBeDefined();

// Expect that dynamic types can be removed
await modal.getByLabel('dynamic-types').getByRole('button', { name: 'Remove filter' }).click();
expect(await resultingTypesList.locator('.activity-type-result').count()).toEqual(2);

// Expect that manual types can be cleared
await modal.locator("input[name='manual-types-filter-input']").click();
await modal.getByRole('menuitem', { name: 'ChangeProducer' }).click();
await page.keyboard.press('Escape');
await modal.getByRole('button', { name: 'Remove Types' }).click();
expect(await resultingTypesList.locator('.activity-type-result').count()).toEqual(allActivityTypesCount);

// Give the layer a new name
await modal.locator('input[name="layer-name"]').fill('Foo');

// Close the modal
await modal.getByRole('button', { name: 'close' }).click();

// Expect name to match given name
expect(await activityLayerEditor.locator('.timeline-layer-editor').first()).toHaveText('Foo');
});

// Add all activities
await page.locator('button', { hasText: /Select [0-9]* activit/ }).click();
test('Change activity layer settings', async () => {
const activityLayerEditor = await page.getByLabel('Activity Layer-editor');

// Expect to not see an activity tree group in this row
expect(await page.locator('.timeline-row-wrapper', { hasText: rowName }).locator('.activity-tree').count()).toBe(0);
Expand All @@ -141,9 +237,7 @@ test.describe.serial('Timeline View Editing', () => {
).toBe(1);

// Delete an activity layer
await page.getByRole('button', { name: 'Layer Settings' }).last().click();
await page.getByText('Delete Layer').click();
const finalLayerCount = await page.locator('.timeline-layer').count();
expect(finalLayerCount - newLayerCount).toEqual(-1);
await activityLayerEditor.locator('.timeline-layer-editor').first().getByRole('button', { name: 'Delete' }).click();
expect(await activityLayerEditor.locator('.timeline-layer-editor').count()).toBe(0);
});
});
34 changes: 34 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,11 @@
"@lezer/generator": "^1.7.0",
"@lezer/highlight": "^1.2.0",
"@lezer/lr": "^1.4.0",
"@neodrag/svelte": "^2.0.6",
"@playwright/test": "^1.44.0",
"@poppanator/sveltekit-svg": "^4.2.1",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tanstack/svelte-virtual": "^3.11.2",
"@testing-library/svelte": "^4.0.2",
"@types/cookie": "^0.6.0",
"@types/d3-array": "^3.0.5",
Expand Down
Loading
Loading