diff --git a/e2e-tests/fixtures/Constraints.ts b/e2e-tests/fixtures/Constraints.ts index 819c0b9b0f..4daf0335de 100644 --- a/e2e-tests/fixtures/Constraints.ts +++ b/e2e-tests/fixtures/Constraints.ts @@ -44,9 +44,14 @@ export class Constraints { await this.page.waitForURL(`${baseURL}/constraints`); } - async deleteConstraint() { + async deleteConstraint(name: string) { await this.goto(); - await this.filterTable(this.constraintName); + this.tableRow = await this.table.getByRole('row', { name }); + this.tableRowDeleteButton = await this.tableRow + .getByRole('gridcell') + .getByRole('button', { name: 'Delete Constraint' }); + + await this.filterTable(name); await expect(this.tableRow).toBeVisible(); await this.tableRow.hover(); diff --git a/e2e-tests/fixtures/Plan.ts b/e2e-tests/fixtures/Plan.ts index 95ed507025..12608ffea5 100644 --- a/e2e-tests/fixtures/Plan.ts +++ b/e2e-tests/fixtures/Plan.ts @@ -24,6 +24,7 @@ export class Plan { constraintManageButton: Locator; constraintModalFilter: Locator; constraintNewButton: Locator; + constraintStatusSelector: (status: string) => string; externalSourceManageButton: Locator; gridMenu: Locator; gridMenuButton: Locator; @@ -96,6 +97,8 @@ export class Plan { public planName = plans.planName, ) { this.constraintListItemSelector = `.constraint-list-item:has-text("${constraints.constraintName}")`; + this.constraintStatusSelector = (status: string) => + `.nav-button:has-text("Constraints") .status-badge[aria-label=${status}]`; this.schedulingConditionListItemSelector = (conditionName: string) => `.scheduling-condition:has-text("${conditionName}")`; this.schedulingGoalListItemSelector = (goalName: string) => `.scheduling-goal:has-text("${goalName}")`; @@ -485,10 +488,10 @@ export class Plan { await this.waitForSimulationStatus(expectedFinalState); } - async removeConstraint() { + async removeConstraint(name: string) { await this.constraintManageButton.click(); - await this.constraintModalFilter.fill(this.constraints.constraintName); - const row = this.page.getByRole('row', { name: this.constraints.constraintName }); + await this.constraintModalFilter.fill(name); + const row = this.page.getByRole('row', { name }); await expect(row).toBeVisible(); // Use click with force for AG Grid checkboxes - check/uncheck fails with Chrome for Testing const checkbox = row.getByRole('checkbox'); @@ -496,7 +499,7 @@ export class Plan { await checkbox.click({ force: true }); await expect(checkbox).not.toBeChecked(); await this.page.getByRole('button', { name: 'Update' }).click(); - await this.page.locator(this.constraintListItemSelector).waitFor({ state: 'detached' }); + await this.page.locator(this.constraintListItemSelector).first().waitFor({ state: 'detached' }); } async removePlanCollaborator(name: string) { @@ -738,7 +741,7 @@ export class Plan { this.changeMissionModelMigrateButton = this.changeMissionModelModal.getByRole('button', { name: 'Change Mission Model', }); - this.constraintManageButton = page.locator(`button[name="manage-constraints"]`); + this.constraintManageButton = page.getByRole('button', { name: 'Manage Constraints' }).first(); this.constraintModalFilter = page.locator('.modal').getByPlaceholder('Filter constraints'); this.constraintNewButton = page.locator(`button[name="new-constraint"]`); this.consoleContainer = page.getByTestId('console'); @@ -832,6 +835,11 @@ export class Plan { await expect(this.page.locator(this.activityCheckingStatusSelector(status))).toBeVisible(); } + async waitForConstraintStatus(status: string) { + await expect(this.page.locator(this.constraintStatusSelector(status))).toBeAttached({ timeout: 10000 }); + await expect(this.page.locator(this.constraintStatusSelector(status))).toBeVisible(); + } + async waitForPlanCollaboratorLoad() { await expect(this.planCollaboratorInputContainer).toBeVisible({ timeout: 10000 }); await expect(this.planCollaboratorLoadingInput).not.toBeVisible({ timeout: 10000 }); diff --git a/e2e-tests/tests/constraints.test.ts b/e2e-tests/tests/constraints.test.ts index 7418a755e3..13bf6a182d 100644 --- a/e2e-tests/tests/constraints.test.ts +++ b/e2e-tests/tests/constraints.test.ts @@ -1,7 +1,15 @@ -import test from '@playwright/test'; +import test, { expect } from '@playwright/test'; +import { adjectives, animals, colors, uniqueNamesGenerator } from 'unique-names-generator'; +import { Status } from '../../src/enums/status.js'; +import { PanelNames } from '../fixtures/Plan.js'; import { setupTest, teardownTest, type FullSetupResult } from '../utilities/api.js'; let setup: FullSetupResult; +let originalConstraintName: string; +const newConstraintName: string = + 'FAILING_TEST_CONSTRAINT_' + uniqueNamesGenerator({ dictionaries: [adjectives, colors, animals] }); +const newConstraintDefinition: string = + "export default function peelFailing(): Constraint { return Real.Resource('/peel').lessThan(-1000); }"; test.beforeAll(async ({ browser }) => { setup = await setupTest(browser); @@ -21,8 +29,34 @@ test.describe.serial('Constraints', () => { await setup.plan.createConstraint(baseURL); }); + // largely a test of backend functionality, but also tests that constraint results show correctly + test('Evaluate a failing and passing constraint', async ({ baseURL }) => { + originalConstraintName = setup.plan.constraints.constraintName; + + // create a new constraint + setup.plan.constraints.constraintName = newConstraintName; + setup.plan.constraints.constraintDefinition = newConstraintDefinition; + await setup.plan.createConstraint(baseURL); + + // simulate + await setup.plan.showPanel(PanelNames.SIMULATION, true); + await setup.plan.runSimulation(Status.Complete); + + // evaluate constraints + await setup.page.getByRole('navigation').getByText('Constraints').click(); + await setup.page.getByText('Check Constraints', { exact: true }).click(); + + // check results, should say "2 of 2 constraints, 1 of 1 violations" + await setup.plan.showPanel(PanelNames.CONSTRAINTS, true); + await expect(setup.page.getByText('2 of 2 constraints, 1 of 1').first()).toBeVisible({ timeout: 10000 }); + }); + test('Delete constraint', async () => { - await setup.plan.removeConstraint(); - await setup.constraints.deleteConstraint(); + await setup.plan.removeConstraint(originalConstraintName); + await setup.plan.removeConstraint(newConstraintName); + + await setup.page.pause(); + await setup.constraints.deleteConstraint(originalConstraintName); + await setup.constraints.deleteConstraint(newConstraintName); }); }); diff --git a/e2e-tests/tests/plan-expansion.test.ts b/e2e-tests/tests/plan-expansion.test.ts index 1771bc99bf..a2b259a2ba 100644 --- a/e2e-tests/tests/plan-expansion.test.ts +++ b/e2e-tests/tests/plan-expansion.test.ts @@ -1,6 +1,8 @@ import test, { expect } from '@playwright/test'; import { adjectives, animals, colors, uniqueNamesGenerator } from 'unique-names-generator'; import { Dictionaries } from '../fixtures/Dictionaries.js'; +import { ExpansionRules } from '../fixtures/ExpansionRules.js'; +import { ExpansionSets } from '../fixtures/ExpansionSets.js'; import { Parcels } from '../fixtures/Parcels.js'; import { PanelNames } from '../fixtures/Plan.js'; import { setupTest, teardownTest, type FullSetupResult } from '../utilities/api.js'; @@ -12,11 +14,15 @@ let setup: FullSetupResult; let dictionaryName: string; let dictionaries: Dictionaries; let parcels: Parcels; +let expansionRules: ExpansionRules; +let expansionSets: ExpansionSets; test.beforeAll(async ({ baseURL, browser }) => { setup = await setupTest(browser); dictionaries = new Dictionaries(setup.page); parcels = new Parcels(setup.page); + expansionRules = new ExpansionRules(setup.page, parcels, setup.models); + expansionSets = new ExpansionSets(setup.page, parcels, setup.models, expansionRules); await dictionaries.goto(); await dictionaries.createCommandDictionary(); @@ -26,7 +32,6 @@ test.beforeAll(async ({ baseURL, browser }) => { }); test.afterAll(async () => { - await parcels.goto(); await teardownTest(setup); }); @@ -58,4 +63,58 @@ test.describe.serial('Plan Expansion', () => { await setup.plan.showPanel(PanelNames.EXPANSION); await setup.plan.applySequenceFilter(sequenceFilterName, setup.plans.planId); }); + + test('Planners warned if constraints have issues', async ({ baseURL }) => { + await expansionRules.goto(); + await expansionRules.createExpansionRule(baseURL); + await expansionSets.goto(); + await expansionSets.createExpansionSet(baseURL); + const expansionSetId = await expansionSets.page + .getByRole('tabpanel') + .filter({ hasText: 'Expansion Sets' }) + .getByRole('treegrid') + .getByRole('row', { name: expansionSets.expansionSetName }) + .getByRole('gridcell') + .first() + .textContent(); + + await setup.plan.goto(); + + setup.plan.constraints.constraintDefinition = + "export default function peelFailing(): Constraint { return Real.Resource('/peel').lessThan(-1000); }"; + await setup.plan.showConstraintsLayout(); + await setup.plan.createConstraint(baseURL); + + // running a simulation on the plan is required before being able to check constraints + await setup.plan.showPanel(PanelNames.SIMULATION, true); + // run the simulation if it already hasn't been + if (await setup.plan.simulateButton.isEnabled()) { + await setup.plan.runSimulation(); + } + + // evaluate constraints + await setup.plan.navButtonConstraints.click(); + await setup.plan.navButtonConstraintsMenu.getByText('Check Constraints', { exact: true }).click(); + + // expand + await setup.plan.showPanel(PanelNames.EXPANSION); + + await setup.page + .locator('select[name="expansionSetId"]') + .selectOption({ label: `${expansionSets.expansionSetName} (${expansionSetId})` }); + + const sequenceName = `${sequenceFilterName} Sequence (Plan ${setup.planId})`; + await setup.page.getByText(sequenceName, { exact: true }).hover(); + + const expansionButton = setup.page.getByRole('button', { name: `Expand '${sequenceName}'` }); + await expansionButton.waitFor({ state: 'visible' }); + await expansionButton.click(); + + // check warning + await expect(setup.page.getByText('Violating Constraints')).toBeVisible(); + await expect(setup.page.getByText('This constraint is currently')).toBeVisible(); + await expect(setup.page.getByText('(1 violation)')).toBeVisible(); + + await setup.page.getByRole('button', { name: 'Expand Anyways' }).click(); + }); }); diff --git a/src/components/expansion/ExpansionPanel.svelte b/src/components/expansion/ExpansionPanel.svelte index 15493a9053..7efb5fb4bd 100644 --- a/src/components/expansion/ExpansionPanel.svelte +++ b/src/components/expansion/ExpansionPanel.svelte @@ -8,20 +8,30 @@ import { Download, FileCode2, SquareCode } from 'lucide-svelte'; import { SEQUENCE_EXPANSION_MODE } from '../../constants/command-expansion'; import { SequencingMode } from '../../enums/sequencing'; + import { Status } from '../../enums/status'; + import { allowedConstraintPlanSpecs, checkConstraintsStatus, constraintResponseMap } from '../../stores/constraints'; import { expansionSequences, expansionSets, filteredExpansionSequences } from '../../stores/expansion'; import { plan } from '../../stores/plan'; + import { plugins } from '../../stores/plugins'; import { expandedTemplates } from '../../stores/sequence-template'; import { sequenceFilters } from '../../stores/sequencing'; - import { simulationDatasetLatest, simulationDatasetsPlan } from '../../stores/simulation'; + import { + simulationDatasetId, + simulationDatasetLatest, + simulationDatasetsPlan, + simulationStatus, + } from '../../stores/simulation'; import type { User } from '../../types/app'; + import type { ConstraintInvocationMap, ConstraintResponse } from '../../types/constraint'; import type { ExpansionSequence, SequenceFilter } from '../../types/expansion'; import type { ActivityLayerFilter } from '../../types/timeline'; import type { ViewGridSection } from '../../types/view'; import effects from '../../utilities/effects'; import { downloadBlob, downloadJSON } from '../../utilities/generic'; - import { showExpansionSequenceModal, showNewSequenceModal } from '../../utilities/modal'; + import { showConfirmExpansionModal, showExpansionSequenceModal, showNewSequenceModal } from '../../utilities/modal'; import { permissionHandler } from '../../utilities/permissionHandler'; import { featurePermissions } from '../../utilities/permissions'; + import { convertDoyToYmd, formatDate } from '../../utilities/time'; import { tooltip } from '../../utilities/tooltip'; import GridMenu from '../menus/GridMenu.svelte'; import ModalFooter from '../modals/ModalFooter.svelte'; @@ -57,6 +67,14 @@ let hasCreatePermissionSequence: boolean = false; let hasCreatePermissionSequenceFilter: boolean = false; + let allConstraintsHaveBeenChecked = true; + let allConstraintsThatAreCheckedPass = true; + let simulationOutOfDate = false; + + // copied from ConstraintsPanel. Unable to move this into a store, as ConstraintsPanel directly modifies startTime, though we do not need to here. + let startTime: string; + let endTime: string; + $: if (user !== null && $plan !== null && $plan.model) { hasDeletePermissionSequence = featurePermissions.expansionSequences.canDelete(user, $plan); hasDeletePermissionSequenceFilter = featurePermissions.sequenceFilter.canDelete(user, $plan.model); @@ -73,22 +91,86 @@ $: sequencesAndFilters = [...relevantExpansionSequences, ...$sequenceFilters]; $: { - if ($simulationDatasetLatest) { - if (relevantExpansionSequences.length > 0) { - if (SEQUENCE_EXPANSION_MODE === SequencingMode.TEMPLATING) { - isExpansionDisabled = false; - expansionDisabledMessage = 'Expansion not available for sequence templates'; + if (!simulationOutOfDate) { + if ($simulationDatasetLatest) { + if (relevantExpansionSequences.length > 0) { + if (SEQUENCE_EXPANSION_MODE === SequencingMode.TEMPLATING) { + isExpansionDisabled = false; + expansionDisabledMessage = 'Expansion not available for sequence templates'; + } else { + isExpansionDisabled = selectedExpansionSetId === null; + expansionDisabledMessage = selectedExpansionSetId === null ? 'No expansion set selected' : ''; + } } else { - isExpansionDisabled = selectedExpansionSetId === null; - expansionDisabledMessage = selectedExpansionSetId === null ? 'No expansion set selected' : ''; + isExpansionDisabled = true; + expansionDisabledMessage = 'No relevant expansion sequences found'; } } else { isExpansionDisabled = true; - expansionDisabledMessage = 'No relevant expansion sequences found'; + expansionDisabledMessage = 'Completed simulation required'; } } else { - isExpansionDisabled = true; - expansionDisabledMessage = 'Completed simulation required'; + // Question for PlanDev team: should we include this? Or only in the modal. + isExpansionDisabled = false; + expansionDisabledMessage = 'Simulation is out of date.'; + } + } + + // copied from ConstraintsPanel + $: if ($plan) { + startTime = formatDate(new Date($plan.start_time), $plugins.time.primary.format); + const endTimeYmd = convertDoyToYmd($plan.end_time_doy); + if (endTimeYmd) { + endTime = formatDate(new Date(endTimeYmd), $plugins.time.primary.format); + } else { + endTime = ''; + } + } + $: startTimeMs = typeof startTime === 'string' ? $plugins.time.primary.parse(startTime)?.getTime() : null; + $: endTimeMs = typeof endTime === 'string' ? $plugins.time.primary.parse(endTime)?.getTime() : null; + let constraintToConstraintResponseMap: ConstraintInvocationMap = {}; + $: if ($allowedConstraintPlanSpecs && $constraintResponseMap && startTimeMs && endTimeMs) { + constraintToConstraintResponseMap = {}; + $allowedConstraintPlanSpecs.forEach(constraintPlanSpec => { + const { constraint_id: constraintId, invocation_id: invocationId } = constraintPlanSpec; + const constraintResponse = $constraintResponseMap[constraintId]?.[invocationId]; + if (constraintResponse) { + if (!constraintToConstraintResponseMap[constraintId]) { + constraintToConstraintResponseMap[constraintId] = {}; + } + + constraintToConstraintResponseMap[constraintId][invocationId] = { + constraintId, + constraintInvocationId: invocationId, + constraintName: constraintResponse.constraintName, + errors: constraintResponse.errors, + results: constraintResponse.results && { + ...constraintResponse.results, + violations: + constraintResponse.results.violations?.map(violation => ({ + ...violation, + // Filter violations/windows by time bounds + windows: violation.windows.filter( + window => window.end >= (startTimeMs ?? 0) && window.start <= (endTimeMs ?? 0), + ), + })) ?? null, + }, + type: constraintResponse.type, + }; + } + }); + } + + $: constraintPlanSpecsInPlan = $allowedConstraintPlanSpecs.filter(spec => $plan && spec.plan_id === $plan.id); + $: { + simulationOutOfDate = $simulationStatus === Status.Modified; + + for (let constraintSpec of constraintPlanSpecsInPlan) { + let checked = constraintToConstraintResponseMap[constraintSpec.constraint_id]?.[constraintSpec.invocation_id]; + allConstraintsHaveBeenChecked &&= !!checked; + if (checked) { + allConstraintsThatAreCheckedPass &&= (checked.results.violations?.length ?? 0) <= 0; + } } } @@ -166,12 +248,33 @@ } } - function onExpandSequence(sequence: ExpansionSequence) { - if ($simulationDatasetLatest !== null && $plan !== null) { - if (SEQUENCE_EXPANSION_MODE === SequencingMode.TEMPLATING) { - effects.expandTemplates([sequence.seq_id], $simulationDatasetLatest.id, $plan, user); - } else if (selectedExpansionSetId !== null) { - effects.expand(selectedExpansionSetId, $simulationDatasetLatest.id, $plan, user); + async function onExpandSequence(sequence: ExpansionSequence) { + var result = { confirm: true }; + if ( + !( + $checkConstraintsStatus !== Status.Failed && + $checkConstraintsStatus !== Status.Incomplete && + !simulationOutOfDate && + allConstraintsHaveBeenChecked && + allConstraintsThatAreCheckedPass + ) + ) { + result = await showConfirmExpansionModal( + simulationOutOfDate, + allConstraintsHaveBeenChecked, + allConstraintsThatAreCheckedPass, + constraintPlanSpecsInPlan, + constraintToConstraintResponseMap, + $simulationDatasetId, + ); + } + if (result.confirm) { + if ($simulationDatasetLatest !== null && $plan !== null) { + if (SEQUENCE_EXPANSION_MODE === SequencingMode.TEMPLATING) { + effects.expandTemplates([sequence.seq_id], $simulationDatasetLatest.id, $plan, user); + } else if (selectedExpansionSetId !== null) { + effects.expand(selectedExpansionSetId, $simulationDatasetLatest.id, $plan, user); + } } } } @@ -411,9 +514,9 @@ aria-label={`Expand '${sequenceOrFilter.seq_id}'`} class="st-button icon" disabled={isExpansionDisabled} - on:click|stopPropagation={() => { + on:click|stopPropagation={async () => { if (isExpansionSequence(sequenceOrFilter)) { - onExpandSequence(sequenceOrFilter); + await onExpandSequence(sequenceOrFilter); } }} > diff --git a/src/components/modals/ConfirmExpansionModal.svelte b/src/components/modals/ConfirmExpansionModal.svelte new file mode 100644 index 0000000000..3d30b63be9 --- /dev/null +++ b/src/components/modals/ConfirmExpansionModal.svelte @@ -0,0 +1,189 @@ + + + + + + + + {warningMessage} + + {#if $checkConstraintsStatus === Status.Failed} +

The most recent constraint evaluation failed, so the current constraint status is unknown.

+ {#if uncheckedConstraints.length > 0 || failingConstraints.length > 0} +
+

For the most recent constraint run, the results were that:

+
+ {/if} + {:else if $checkConstraintsStatus === Status.Incomplete} +

The most recent constraint evaluation didn't finish, so the current constraint status is incomplete.

+ {#if uncheckedConstraints.length > 0 || failingConstraints.length > 0} +
+

For the most recent constraint run, the results were that:

+
+ {/if} + {:else if simulationOutOfDate} + +

+ The plan has changed since the last simulation, so the constraint results in the Constraints panel no longer + reflect the current plan. +

+ {#if uncheckedConstraints.length > 0 || failingConstraints.length > 0} +
+

For the most recent constraint run, the results were that:

+
+ {/if} + {/if} + {#if failingConstraints.length > 0} + {#if simulationOutOfDate} +

+ The last evaluation found violations in {failingConstraints.length === 1 + ? 'this constraint' + : 'these constraints'}: +

+ {:else} +

{failingConstraints.length === 1 ? 'This constraint is' : 'These constraints are'} currently violated:

+ {/if} +
    + {#each failingConstraints as failing} +
  • {failing.name} ({failing.viols} violation{pluralize(failing.viols)})
  • + {/each} +
+
+ {/if} + {#if uncheckedConstraints.length > 0} + {#if simulationOutOfDate} +

+ From the last evaluation, {uncheckedConstraints.length === 1 + ? "this constraint hasn't" + : "these constraints haven't"} been checked and may be violated: +

+ {:else} +

+ {uncheckedConstraints.length === 1 ? "This constraint hasn't" : "These constraints haven't"} been checked and may + be violated: +

+ {/if} +
    + {#each uncheckedConstraints as unchecked} +
  • {unchecked}
  • + {/each} +
+ {/if} +
+ {#if simulationOutOfDate} +

Expansion will run against the previous simulation and may not match the current plan.

+ {:else} +

Expanding now may produce sequences that violate mission constraints.

+ {/if} +
+
+

Do you want to expand anyway?

+
+ + + + +
+ + diff --git a/src/utilities/effects.ts b/src/utilities/effects.ts index e751630f5e..2e6c6c6c40 100644 --- a/src/utilities/effects.ts +++ b/src/utilities/effects.ts @@ -4227,7 +4227,15 @@ const effects = { } const startTime = performance.now(); - const data = await reqHasura<{ id: number }>(gql.EXPAND, { expansionSetId, simulationDatasetId }, user); + const data = await reqHasura<{ id: number }>( + gql.EXPAND, + { + bypassConstraints: false, + expansionSetId, + simulationDatasetId, + }, + user, + ); if (data.expand != null) { planExpansionStatusStore.set(Status.Complete); showSuccessToast('Plan Expanded Successfully'); @@ -4246,7 +4254,13 @@ const effects = { } }, - async expandTemplates(seqIds: string[], simulationDatasetId: number, plan: Plan, user: User | null): Promise { + async expandTemplates( + seqIds: string[], + simulationDatasetId: number, + plan: Plan, + user: User | null, + bypassConstraints: boolean = true, + ): Promise { try { if (!plan.model) { throw Error(`No model found for plan ${plan.id}, cannot expand templates`); @@ -4261,6 +4275,7 @@ const effects = { const data = await reqHasura<{ success: boolean }>( gql.EXPAND_TEMPLATES, { + bypassConstraints, modelId: plan.model.id, seqIds, simulationDatasetId, @@ -4283,6 +4298,8 @@ const effects = { } } catch (e) { catchError('Sequence Templating Failed', e as Error); + + // TODO: show error more cleanly; e.extensions.internal.response.body sequenceTemplateExpansionStatus.set(Status.Failed); sequenceTemplateExpansionError.set(e as string); showFailureToast('Sequence Templating Failed'); diff --git a/src/utilities/gql.ts b/src/utilities/gql.ts index 8da0bf94e3..45ac22b7c4 100644 --- a/src/utilities/gql.ts +++ b/src/utilities/gql.ts @@ -1155,8 +1155,8 @@ const gql = { `, EXPAND: `#graphql - mutation Expand($expansionSetId: Int!, $simulationDatasetId: Int!) { - expand: ${Queries.EXPAND_ALL_ACTIVITIES}(expansionSetId: $expansionSetId, simulationDatasetId: $simulationDatasetId) { + mutation Expand($expansionSetId: Int!, $simulationDatasetId: Int!, $bypassConstraints: Boolean) { + expand: ${Queries.EXPAND_ALL_ACTIVITIES}(expansionSetId: $expansionSetId, simulationDatasetId: $simulationDatasetId, bypassConstraints: $bypassConstraints) { id } } @@ -1167,11 +1167,13 @@ const gql = { $seqIds: [String!]!, $modelId: Int!, $simulationDatasetId: Int! + $bypassConstraints: Boolean ) { expandTemplates: ${Queries.EXPAND_ALL_TEMPLATES}( seqIds: $seqIds, simulationDatasetId: $simulationDatasetId, modelId: $modelId, + bypassConstraints: $bypassConstraints ) { success } diff --git a/src/utilities/modal.ts b/src/utilities/modal.ts index bd491da193..583dd0177e 100644 --- a/src/utilities/modal.ts +++ b/src/utilities/modal.ts @@ -5,6 +5,7 @@ import ApplySequenceFilterModal from '../components/modals/ApplySequenceFilterMo import CancelActionRunModal from '../components/modals/CancelActionRunModal.svelte'; import ChangePlanBoundsModal from '../components/modals/ChangePlanBoundsModal.svelte'; import ConfirmActivityCreationModal from '../components/modals/ConfirmActivityCreationModal.svelte'; +import ConfirmExpansionModal from '../components/modals/ConfirmExpansionModal.svelte'; import ConfirmModal from '../components/modals/ConfirmModal.svelte'; import CreatePlanBranchModal from '../components/modals/CreatePlanBranchModal.svelte'; import CreatePlanSnapshotModal from '../components/modals/CreatePlanSnapshotModal.svelte'; @@ -45,6 +46,7 @@ import NewSequenceTemplateModal from '../components/sequence-templates/NewSequen import { type ActionDefinition } from '../types/actions'; import type { ActivityDirectiveDeletionMap, ActivityDirectiveId } from '../types/activity'; import type { User } from '../types/app'; +import type { ConstraintInvocationMap, ConstraintPlanSpecification, ConstraintResponse } from '../types/constraint'; import type { ExpansionSequence } from '../types/expansion'; import type { DerivationGroup, ExternalSourcePkey, ExternalSourceSlim } from '../types/external-source'; import type { ModalElement, ModalElementValue } from '../types/modal'; @@ -95,6 +97,52 @@ export async function showAboutModal(): Promise { }); } +export async function showConfirmExpansionModal( + simulationOutOfDate: boolean, + allConstraintsHaveBeenChecked: boolean, + allConstraintsThatAreCheckedPass: boolean, + constraintPlanSpecsInPlan: ConstraintPlanSpecification[], + constraintToConstraintResponseMap: ConstraintInvocationMap, + simulationDatasetId: number, +): Promise { + return new Promise(resolve => { + if (browser) { + const target: ModalElement | null = document.querySelector('#svelte-modal'); + + if (target) { + const confirmModal = new ConfirmExpansionModal({ + props: { + allConstraintsHaveBeenChecked, + allConstraintsThatAreCheckedPass, + constraintPlanSpecsInPlan, + constraintToConstraintResponseMap, + simulationDatasetId, + simulationOutOfDate, + }, + target, + }); + target.resolve = resolve; + + confirmModal.$on('close', () => { + target.replaceChildren(); + target.resolve = null; + resolve({ confirm: false }); + confirmModal.$destroy(); + }); + + confirmModal.$on('confirm', () => { + target.replaceChildren(); + target.resolve = null; + resolve({ confirm: true }); + confirmModal.$destroy(); + }); + } + } else { + resolve({ confirm: false }); + } + }); +} + /** * Shows an ActionCreationModal component. */