diff --git a/e2e-tests/fixtures/Plan.ts b/e2e-tests/fixtures/Plan.ts index 084d3b011c..da69b548ac 100644 --- a/e2e-tests/fixtures/Plan.ts +++ b/e2e-tests/fixtures/Plan.ts @@ -249,7 +249,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); @@ -271,7 +271,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); @@ -374,22 +374,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(); @@ -415,11 +416,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(); @@ -446,7 +447,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() { diff --git a/e2e-tests/tests/plan-activities.test.ts b/e2e-tests/tests/plan-activities.test.ts index 4ccc0d97dc..0822ee5487 100644 --- a/e2e-tests/tests/plan-activities.test.ts +++ b/e2e-tests/tests/plan-activities.test.ts @@ -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 () => { diff --git a/e2e-tests/tests/plan-activity-presets.test.ts b/e2e-tests/tests/plan-activity-presets.test.ts index 086966b05c..37f657938c 100644 --- a/e2e-tests/tests/plan-activity-presets.test.ts +++ b/e2e-tests/tests/plan-activity-presets.test.ts @@ -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 () => { @@ -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(); @@ -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(); }); }); diff --git a/e2e-tests/tests/plan-simulation-templates.test.ts b/e2e-tests/tests/plan-simulation-templates.test.ts index 10bd72bed0..9079dcd036 100644 --- a/e2e-tests/tests/plan-simulation-templates.test.ts +++ b/e2e-tests/tests/plan-simulation-templates.test.ts @@ -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 () => { @@ -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 page.getByRole('combobox', { name: 'Template 1' }).click(); await page.getByRole('button', { name: 'Delete Template' }).waitFor({ state: 'attached' }); await page.getByRole('button', { name: 'Delete Template' }).click(); @@ -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(); }); }); diff --git a/e2e-tests/tests/timeline-view-editing.test.ts b/e2e-tests/tests/timeline-view-editing.test.ts index 5b9e39ea52..62d64a3655 100644 --- a/e2e-tests/tests/timeline-view-editing.test.ts +++ b/e2e-tests/tests/timeline-view-editing.test.ts @@ -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 () => { @@ -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('Activity Layer'); + }); + + 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('Toggle activity filter builder modal') + .click(); + + // Expect that the modal is present + const modal = activityLayerEditor.getByRole('dialog'); + expect(modal).toBeDefined(); + + // Expect that layer name is showing in the name input + expect(modal.locator('input[name="layer-name"]')).toHaveValue('Activity Layer'); + + // 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 other filters can be added + await modal.getByLabel('other-filters').getByRole('button', { name: 'Add Filter' }).click(); + expect(await modal.getByLabel('other-filters').getByRole('listitem').count()).toBe(1); + // Select parameter field + await modal.getByLabel('other-filters').locator("select[aria-label='field']").selectOption('Parameter'); + // Select specific parameter + await modal.getByLabel('other-filters').getByText('Select Parameter').click(); + await modal.getByLabel('other-filters').getByText('quantity (int)').click(); + // Select operator + await modal.getByLabel('other-filters').locator("select[aria-label='operator']").selectOption('equals'); + // Fill filter value input + await modal.getByLabel('other-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 other filters can be removed + await modal.getByLabel('other-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); - // Expect the filter list to open - await page.getByPlaceholder('Search').last().click(); - await expect(page.locator('.menu-slot > .header')).toBeDefined(); + // Give the layer a new name + await modal.locator('input[name="layer-name"]').fill('Foo'); - // Add all activities - await page.locator('button', { hasText: /Select [0-9]* activit/ }).click(); + // 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'); + }); + + 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); @@ -141,9 +237,55 @@ 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); + }); + + test('Add a resource layer', async () => { + const resourceLayerEditor = await page.getByLabel('Resource Layer-editor'); + const yAxisEditor = await page.getByLabel('Y Axis-editor'); + const existingLayerCount = await resourceLayerEditor.locator('.timeline-layer-editor').count(); + const existingYAxesCount = await yAxisEditor.locator('.timeline-y-axis').count(); + + // Expect no y-axis label to exist for the row in the timeline + expect( + await page.locator('.timeline-row-wrapper', { hasText: rowName }).locator('.row-header-y-axis-label').count(), + ).toBe(0); + + // Add a resource layer + await resourceLayerEditor.getByRole('button', { name: 'New Resource Layer' }).click(); + const newLayerCount = await resourceLayerEditor.locator('.timeline-layer-editor').count(); + expect(newLayerCount - existingLayerCount).toEqual(1); + + // Expect a y-axis to have been automatically created + const newYAxisCount = await yAxisEditor.locator('.timeline-y-axis').count(); + expect(newYAxisCount - existingYAxesCount).toEqual(1); + + // Select a resource + await resourceLayerEditor.getByRole('combobox').click(); + await resourceLayerEditor.getByRole('menuitem', { name: '/peel' }).waitFor({ state: 'attached' }); + await resourceLayerEditor.getByRole('menuitem', { name: '/peel' }).click(); + await resourceLayerEditor.getByRole('menuitem', { name: '/peel' }).waitFor({ state: 'detached' }); + + // Run simulation + await plan.showPanel(PanelNames.SIMULATION, true); + await plan.runSimulation(); + + // Expect the resource to have a y-axis label in the timline + expect( + await page.locator('.timeline-row-wrapper', { hasText: rowName }).locator('.row-header-y-axis-label').count(), + ).toBe(1); + + // Duplicate a resource layer + await resourceLayerEditor + .locator('.timeline-layer-editor') + .first() + .getByRole('button', { name: 'Duplicate' }) + .click(); + expect(await resourceLayerEditor.locator('.timeline-layer-editor').count()).toBe(2); + + // Delete a resource layer + await resourceLayerEditor.locator('.timeline-layer-editor').first().getByRole('button', { name: 'Delete' }).click(); + expect(await resourceLayerEditor.locator('.timeline-layer-editor').count()).toBe(1); }); }); diff --git a/package-lock.json b/package-lock.json index 06f8c5ece2..62be98fb51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,9 +56,11 @@ "@lezer/generator": "^1.7.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.4.0", + "@neodrag/svelte": "^2.0.6", "@playwright/test": "^1.49.1", "@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", @@ -1133,6 +1135,12 @@ "resolved": "https://registry.npmjs.org/@nasa-jpl/stellar/-/stellar-1.1.18.tgz", "integrity": "sha512-e+26M01HFrGBZBQwsxxoJ8OSbRKn/zdUatwRryuPaEIm9RNJwDiejYqIoWCLn9s04hVeYr4PRe6IXa0nPR95vg==" }, + "node_modules/@neodrag/svelte": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@neodrag/svelte/-/svelte-2.0.6.tgz", + "integrity": "sha512-jjmTjRTMJaer2IyEIoS5xbccmFmOpkeoTKpBORkMItCPjHWE19eW3kvH9SuTvZJAKnKERVNGdW3VBuDxZif9Dg==", + "dev": true + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1595,6 +1603,32 @@ "vite": "^5.0.0" } }, + "node_modules/@tanstack/svelte-virtual": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/@tanstack/svelte-virtual/-/svelte-virtual-3.11.2.tgz", + "integrity": "sha512-o0VWDf8GlkZ8S5E2GjQq39qhZIB6U1Kej05/aTdxIQy18c022CxYgGWUydmrWJE3DV2ZA/q+Zm30oqnSRhQ4Lw==", + "dev": true, + "dependencies": { + "@tanstack/virtual-core": "3.11.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "svelte": "^3.48.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz", + "integrity": "sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "9.3.4", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", diff --git a/package.json b/package.json index 32e5b016f9..e1c1666e39 100644 --- a/package.json +++ b/package.json @@ -86,8 +86,10 @@ "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.4.0", "@playwright/test": "^1.49.1", + "@neodrag/svelte": "^2.0.6", "@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", diff --git a/src/assets/filter-with-plus.svg b/src/assets/filter-with-plus.svg new file mode 100644 index 0000000000..5be45c60db --- /dev/null +++ b/src/assets/filter-with-plus.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/src/assets/timeline-x-range-layer.svg b/src/assets/timeline-x-range-layer.svg index c4bb6d1bc1..9213901872 100644 --- a/src/assets/timeline-x-range-layer.svg +++ b/src/assets/timeline-x-range-layer.svg @@ -1,5 +1,4 @@ - - + diff --git a/src/components/ActivityList.svelte b/src/components/ActivityList.svelte index 61cc1b0ed9..946f002ade 100644 --- a/src/components/ActivityList.svelte +++ b/src/components/ActivityList.svelte @@ -1,28 +1,13 @@ @@ -32,6 +17,6 @@ typeName="activity" typeNamePlural="Activities" {getFilterValueFromItem} - filterOptions={activitySubsystems.map(s => ({ color: s.color || '', label: s.name, value: s.id.toString() }))} + filterOptions={$subsystemTags.map(s => ({ color: s.color || '', label: s.name, value: s.id }))} filterName="Subsystem" /> diff --git a/src/components/Collapse.svelte b/src/components/Collapse.svelte index 48ac6c4a34..e6fc5db6c1 100644 --- a/src/components/Collapse.svelte +++ b/src/components/Collapse.svelte @@ -32,7 +32,7 @@ $: expanded = defaultExpanded; -
+
{/if} {#each rows as row} - - {#each row.layers.filter(l => l.chartType === chartType) as layer} - onSelect(layerItem, row, layer)}> -
- {layer.name || `${layer.chartType} Layer`} -
-
- {/each} + + {#if isResourceChart} onSelect(layerItem, row)}> -
New Layer +
+ {row.name}
-
+ {:else} + + {#each row.layers.filter(l => l.chartType === chartType) as layer} + onSelect(layerItem, row, layer)}> +
+ {layer.name || `${layer.chartType} Layer`} +
+
+ {/each} + onSelect(layerItem, row)}> +
New Layer +
+
+
+ {/if} {/each} onSelect(layerItem)}>
New Row +
diff --git a/src/components/ResourceList.svelte b/src/components/ResourceList.svelte index c0f984a78a..fbdd60917a 100644 --- a/src/components/ResourceList.svelte +++ b/src/components/ResourceList.svelte @@ -114,7 +114,7 @@
-
+ -
+ diff --git a/src/components/RowVirtualizerFixed.svelte b/src/components/RowVirtualizerFixed.svelte new file mode 100644 index 0000000000..2baa688f5c --- /dev/null +++ b/src/components/RowVirtualizerFixed.svelte @@ -0,0 +1,50 @@ + + + + +
+
+ {#each $virtualizer.getVirtualItems() as item, idx (idx)} +
+ +
+ {/each} +
+
+ + diff --git a/src/components/TimelineItemList.svelte b/src/components/TimelineItemList.svelte index 0e3314d6a3..9604cccf5e 100644 --- a/src/components/TimelineItemList.svelte +++ b/src/components/TimelineItemList.svelte @@ -19,6 +19,7 @@ import Input from './form/Input.svelte'; import LayerPicker from './LayerPicker.svelte'; import Menu from './menus/Menu.svelte'; + import RowVirtualizerFixed from './RowVirtualizerFixed.svelte'; import ListItem from './ui/ListItem.svelte'; import Tag from './ui/Tags/Tag.svelte'; @@ -28,7 +29,7 @@ export let items: TimelineItemType[] = []; export let filterOptions: TimelineItemListFilterOption[] = []; export let filterName: string = 'Filter'; - export let getFilterValueFromItem: (item: TimelineItemType) => string; + export let getFilterValueFromItem: (item: TimelineItemType) => string | number; let menu: Menu; let filteredItems: TimelineItemType[] = []; @@ -92,8 +93,9 @@ dragImage.className = 'st-typography-medium'; document.body.appendChild(dragImage); if (event.dataTransfer) { + const metadata = { selectedFilters, textFilters }; event.dataTransfer.setDragImage(dragImage, 0, 0); - event.dataTransfer.setData('text/plain', JSON.stringify({ items, type: typeName })); + event.dataTransfer.setData('text/plain', JSON.stringify({ items, metadata, type: typeName })); event.dataTransfer.dropEffect = typeName === 'activity' ? 'copy' : 'link'; event.dataTransfer.effectAllowed = typeName === 'activity' ? 'copyLink' : 'link'; } @@ -115,7 +117,8 @@ function onBulkLayerPicked(event: CustomEvent<{ layer?: Layer; row?: Row }>) { addTextFilter(); - viewAddFilterToRow(filteredItems, typeName, event.detail.row?.id, event.detail.layer); + const metadata = { selectedFilters, textFilters }; + viewAddFilterToRow(filteredItems, typeName, metadata, event.detail.row?.id, event.detail.layer); } function onBulkAddToRow(e: MouseEvent) { @@ -126,7 +129,7 @@ function onIndividualLayerPicked(event: CustomEvent<{ item?: TimelineItemType; layer?: Layer; row?: Row }>) { if (event.detail.item) { - viewAddFilterToRow([event.detail.item], typeName, event.detail.row?.id, event.detail.layer); + viewAddFilterToRow([event.detail.item], typeName, {}, event.detail.row?.id, event.detail.layer); } } @@ -136,6 +139,7 @@ } +
-