Skip to content

Commit

Permalink
Utilize new SearchableDropdown for anchor selection (#573)
Browse files Browse the repository at this point in the history
* create generic dropdown components from activity preset component
  • Loading branch information
duranb authored Apr 25, 2023
1 parent fd542f7 commit db581ab
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 122 deletions.
24 changes: 21 additions & 3 deletions e2e-tests/fixtures/Plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,14 @@ export class Plan {
}

async deleteAllActivities() {
await this.page.getByRole('gridcell').first().click({ button: 'right' });
await this.panelActivityDirectivesTable.getByRole('gridcell').first().click({ button: 'right' });
await this.page.locator('.context-menu > .context-menu-item:has-text("Select All Activity Directives")').click();
await this.page.getByRole('gridcell').first().click({ button: 'right' });
await this.panelActivityDirectivesTable.getByRole('gridcell').first().click({ button: 'right' });
await this.page.getByText(/Delete \d+ Activit(y|ies) Directives?/).click();
await this.page.getByRole('button', { name: 'Confirm' }).click();

const applyPresetButton = this.page.getByRole('button', { name: 'Confirm' });
await applyPresetButton.waitFor({ state: 'attached', timeout: 1000 });
await applyPresetButton.click();
}

async fillActivityPresetName(presetName: string) {
Expand Down Expand Up @@ -172,6 +175,21 @@ export class Plan {
await this.page.waitForSelector(this.schedulingStatusSelector('Complete'), { state: 'visible', strict: true });
}

async selectActivityAnchorByIndex(index: number) {
await this.panelActivityForm.getByRole('button', { name: 'Set Anchor' }).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 this.page.waitForFunction(
anchorMenuName => document.querySelector('.anchor-form .selected-display-value')?.innerHTML === anchorMenuName,
anchorMenuName,
);
expect(await this.panelActivityForm.getByRole('textbox', { name: anchorMenuName })).toBeVisible();
}

async selectActivityPresetByName(presetName: string) {
await this.panelActivityForm.getByRole('button', { name: 'Set Preset' }).click();

Expand Down
60 changes: 26 additions & 34 deletions e2e-tests/tests/plan-activities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ test.beforeAll(async ({ browser }) => {
await plan.goto();
});

test.beforeEach(async () => {
await plan.panelActivityTypes.getByRole('button', { name: 'CreateActivity-GrowBanana' }).click();
await plan.panelActivityTypes.getByRole('button', { name: 'CreateActivity-PickBanana' }).click();
await plan.panelActivityTypes.getByRole('button', { name: 'CreateActivity-ThrowBanana' }).click();
await plan.panelActivityDirectivesTable.getByRole('gridcell', { name: 'PickBanana' }).first().click();
await plan.panelActivityForm.locator('summary').filter({ hasText: 'Anchor' }).click();

await plan.selectActivityAnchorByIndex(1);
});

test.afterEach(async () => {
await plan.deleteAllActivities();
});
Expand All @@ -48,52 +58,34 @@ test.afterAll(async () => {

test.describe.serial('Plan Activities', () => {
test('Deleting an activity directive with another directive anchored to it should and selecting re-anchor to plan should re-anchor to plan', async () => {
await page.getByRole('button', { name: 'CreateActivity-GrowBanana' }).click();
await page.getByRole('button', { name: 'CreateActivity-PickBanana' }).click();
await page.getByRole('button', { name: 'CreateActivity-ThrowBanana' }).click();
await page.getByRole('gridcell', { name: 'PickBanana' }).first().click();
await page.locator('summary').filter({ hasText: 'Anchor' }).click();
const firstOptionValue = await page.evaluate(
() => (document.getElementById('anchors') as HTMLDataListElement).options[1].value,
);
await page.getByRole('combobox', { name: 'null' }).fill(firstOptionValue);
await page.getByRole('gridcell', { name: 'GrowBanana' }).nth(1).click();
await page.getByRole('button', { name: 'Delete Activity Directive' }).click();
await plan.panelActivityDirectivesTable.getByRole('gridcell', { name: 'GrowBanana' }).nth(1).click();
await plan.panelActivityDirectivesTable.getByRole('button', { name: 'Delete Activity Directive' }).click();
await page.locator('.modal-content select').nth(1).selectOption('anchor-plan');
await page.getByRole('button', { name: 'Confirm' }).click();
await page.getByRole('gridcell', { name: 'PickBanana' }).nth(1).click();
await page.locator('summary').filter({ hasText: 'Anchor' }).click();
const anchorValue = await page.evaluate(
() => (document.querySelector('input[list][name="anchor_id"]') as HTMLInputElement).value,
await plan.panelActivityDirectivesTable.getByRole('gridcell', { name: 'PickBanana' }).nth(1).click();
await plan.panelActivityForm.locator('summary').filter({ hasText: 'Anchor' }).click();
await page.waitForFunction(
() => document.querySelector('.anchor-form .selected-display-value')?.innerHTML === 'To Plan',
);
expect(anchorValue).toEqual('To Plan');

await plan.panelActivityForm.locator('summary').filter({ hasText: 'Anchor' }).click();
expect(await plan.panelActivityForm.getByRole('textbox', { 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 () => {
await page.getByRole('button', { name: 'CreateActivity-GrowBanana' }).click();
await page.getByRole('button', { name: 'CreateActivity-PickBanana' }).click();
await page.getByRole('button', { name: 'CreateActivity-ThrowBanana' }).click();
await page.getByRole('gridcell', { name: 'PickBanana' }).first().click();
await page.locator('summary').filter({ hasText: 'Anchor' }).click();
const firstOptionValue = await page.evaluate(
() => (document.getElementById('anchors') as HTMLDataListElement).options[1].value,
);
await page.getByRole('combobox', { name: 'null' }).fill(firstOptionValue);
await page.getByRole('gridcell', { name: 'ThrowBanana' }).nth(1).click();
await plan.panelActivityDirectivesTable.getByRole('gridcell', { name: 'ThrowBanana' }).nth(1).click();
await page.locator('summary').filter({ hasText: 'Anchor' }).click();
const secondOptionValue = await page.evaluate(
() => (document.getElementById('anchors') as HTMLDataListElement).options[2].value,
);
await page.getByRole('combobox', { name: 'null' }).fill(secondOptionValue);
await page.getByRole('combobox', { name: 'null' }).click();
await page.getByRole('gridcell', { name: 'GrowBanana' }).nth(1).click();
await page

await plan.selectActivityAnchorByIndex(2);

await plan.panelActivityDirectivesTable.getByRole('gridcell', { name: 'GrowBanana' }).nth(1).click();
await plan.panelActivityDirectivesTable
.getByRole('gridcell', { name: 'PickBanana' })
.nth(1)
.click({
modifiers: ['Meta'],
});
await page.getByRole('gridcell', { name: 'GrowBanana' }).nth(1).click({
await plan.panelActivityDirectivesTable.getByRole('gridcell', { name: 'GrowBanana' }).nth(1).click({
button: 'right',
});
await page.getByText('Delete 2 Activity Directives').click();
Expand Down
8 changes: 6 additions & 2 deletions e2e-tests/tests/plan-activity-presets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ test.beforeAll(async ({ browser }) => {
await plan.panelActivityForm.getByRole('button', { name: 'Enter a unique name for the new preset' }).click();
await plan.panelActivityForm.locator('.dropdown-header').waitFor({ state: 'detached' });

await page.waitForFunction(() => document.querySelector('.selected-display-value')?.innerHTML === 'Preset 2');
await page.waitForFunction(
() => document.querySelector('.activity-preset-input-container .selected-display-value')?.innerHTML === 'Preset 2',
);

await plan.selectActivityPresetByName('None');

Expand Down Expand Up @@ -92,7 +94,9 @@ test.describe.serial('Plan Activity Presets', () => {
await page.locator('.modal').waitFor({ state: 'attached' });
await page.locator('.modal').getByRole('button', { name: 'Delete' }).click();

await page.waitForFunction(() => document.querySelector('.selected-display-value')?.innerHTML === 'None');
await page.waitForFunction(
() => document.querySelector('.activity-preset-input-container .selected-display-value')?.innerHTML === 'None',
);

expect(await page.getByRole('textbox', { name: 'None' })).toBeVisible();
});
Expand Down
112 changes: 29 additions & 83 deletions src/components/activity/ActivityAnchorForm.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<svelte:options immutable={true} />

<script lang="ts">
import SearchIcon from '@nasa-jpl/stellar/icons/search.svg?component';
import { createEventDispatcher } from 'svelte';
import type { ActivityDirective, ActivityDirectiveId, ActivityDirectivesMap } from '../../types/activity';
import type { DropdownOptions, SelectedDropdownOptionValue } from '../../types/dropdown';
import { getTarget } from '../../utilities/generic';
import { convertDurationStringToInterval, convertUsToDurationString, getIntervalInMs } from '../../utilities/time';
import { tooltip } from '../../utilities/tooltip';
import Input from '../form/Input.svelte';
import Highlight from '../ui/Highlight.svelte';
import SearchableDropdown from '../ui/SearchableDropdown.svelte';
export let activityDirective: ActivityDirective;
export let activityDirectivesMap: ActivityDirectivesMap = {};
Expand All @@ -20,21 +21,23 @@
const dispatch = createEventDispatcher();
const anchorTextDelimiter = ' - ';
const defaultAnchorString = 'To Plan';
let anchorableActivityDirectives: string[] = [];
let anchorableActivityDirectives: ActivityDirective[] = [];
let anchoredActivity: ActivityDirective | null = null;
let anchorInputString: string = '';
let anchoredActivityError: string | null = null;
let previousActivityDirectiveId: ActivityDirectiveId;
let isRelativeOffset: boolean = false;
let startOffsetString: string = '';
let startOffsetError: string | null = null;
let searchableOptions: DropdownOptions = [];
$: anchoredActivity = anchorId !== null ? activityDirectivesMap[anchorId] : null;
$: anchorableActivityDirectives = Object.values(activityDirectivesMap)
.filter(directive => directive.id !== activityDirective.id)
.map(formatActivityDirectiveAnchorText);
$: anchorableActivityDirectives = Object.values(activityDirectivesMap).filter(
directive => directive.id !== activityDirective.id,
);
$: searchableOptions = anchorableActivityDirectives.map((anchorableActivity: ActivityDirective) => ({
display: formatActivityDirectiveAnchorText(anchorableActivity),
value: anchorableActivity.id,
}));
$: startOffsetError = validateStartOffset(startOffsetString, activityDirective);
$: if (startOffset) {
Expand All @@ -50,19 +53,12 @@
activityDirective.anchor_id !== null ||
(activityDirective.anchor_id === null && !activityDirective.anchored_to_start) ||
!!startOffsetError;
anchorInputString = anchoredActivity ? formatActivityDirectiveAnchorText(anchoredActivity) : defaultAnchorString;
validateAnchorInput(anchorInputString);
}
function formatActivityDirectiveAnchorText(activityDirective: ActivityDirective) {
return `${activityDirective.id}${anchorTextDelimiter}${activityDirective.name}`;
}
function getActivityDirectiveIdFromAnchorText(anchorText: string): number {
return parseInt(anchorText.split(anchorTextDelimiter)[0], 10);
}
function validateStartOffset(offsetString: string, activityDirective: ActivityDirective) {
let validationError = activityDirective.anchor_validations?.reason_invalid
? activityDirective.anchor_validations.reason_invalid
Expand All @@ -77,11 +73,9 @@
return validationError;
}
function getAnchorActivityDirective(inputString: string): ActivityDirective | null {
const activityDirectiveId = getActivityDirectiveIdFromAnchorText(inputString);
if (!Number.isNaN(activityDirectiveId) && !/to plan/i.test(anchorInputString)) {
return activityDirectivesMap[activityDirectiveId];
function getAnchorActivityDirective(inputString: SelectedDropdownOptionValue): ActivityDirective | null {
if (!Number.isNaN(inputString) && inputString) {
return activityDirectivesMap[inputString];
}
return null;
Expand All @@ -91,10 +85,8 @@
anchoredActivity = activityDirective;
if (activityDirective === null) {
dispatch('updateAnchor', null);
anchorInputString = defaultAnchorString;
} else {
dispatch('updateAnchor', anchoredActivity.id);
anchorInputString = formatActivityDirectiveAnchorText(anchoredActivity);
}
}
Expand All @@ -107,11 +99,10 @@
dispatch('updateStartOffset', offset);
}
function onUpdateAnchor() {
if (validateAnchorInput(anchorInputString)) {
const activityToAnchorTo = getAnchorActivityDirective(anchorInputString);
updateAnchor(activityToAnchorTo);
}
function onSelectAnchor(event: CustomEvent<SelectedDropdownOptionValue>) {
const { detail: anchorInputString } = event;
const activityToAnchorTo = getAnchorActivityDirective(anchorInputString);
updateAnchor(activityToAnchorTo);
}
function onAnchorToStart() {
Expand Down Expand Up @@ -139,31 +130,6 @@
startOffsetError = error.message;
}
}
function validateAnchorInput(inputString: string): boolean {
anchoredActivityError = null;
try {
const activityDirectiveId = getActivityDirectiveIdFromAnchorText(inputString);
if (!Number.isNaN(activityDirectiveId) && !/to plan/i.test(anchorInputString)) {
const activityToAnchorTo = activityDirectivesMap[activityDirectiveId];
if (!activityToAnchorTo) {
throw Error('Activity corresponding to chosen anchor was not found');
}
if (activityToAnchorTo.id === activityDirective.id) {
throw Error('Current selected activity anchor cannot be itself');
}
}
return true;
} catch (e) {
anchoredActivityError = e.message;
return false;
}
}
</script>

<fieldset class="anchor-container">
Expand All @@ -172,31 +138,17 @@
<div class="anchor-form">
<Highlight highlight={highlightKeysMap.anchor_id}>
<Input layout="inline">
<label use:tooltip={{ content: 'Activity to anchor to', placement: 'top' }} for="anchor_id">Relative to</label
>
<Input>
<div class="search-icon" slot="left">
<!-- this conditional is required to trigger the `Input` component to recalculate the internal layout -->
{#if isRelativeOffset}<SearchIcon />{/if}
</div>
<input
autocomplete="off"
class="st-input w-100"
class:error={!!anchoredActivityError}
{disabled}
list="anchors"
name="anchor_id"
bind:value={anchorInputString}
on:change={onUpdateAnchor}
use:tooltip={{ content: anchoredActivityError, placement: 'top' }}
/>
<datalist id="anchors">
<option value="To Plan" />
{#each anchorableActivityDirectives as anchorableActivity}
<option value={anchorableActivity} />
{/each}
</datalist>
</Input>
<label use:tooltip={{ content: 'Activity to anchor to', placement: 'top' }} for="anchor_id">
Relative to
</label>
<SearchableDropdown
options={searchableOptions}
placeholder="To Plan"
searchPlaceholder="Search Directives"
settingsIconTooltip="Set Anchor"
selectedOptionValue={anchorId}
on:selectOption={onSelectAnchor}
/>
</Input>
</Highlight>
<Highlight highlight={highlightKeysMap.anchored_to_start}>
Expand Down Expand Up @@ -287,10 +239,4 @@
.anchor-boundaries .anchor-boundary.disabled {
cursor: not-allowed;
}
.search-icon {
align-items: center;
color: var(--st-gray-50);
display: flex;
}
</style>

0 comments on commit db581ab

Please sign in to comment.