Initial implementation of custom profile editor + formatting
This commit is contained in:
parent
82b36f96d0
commit
ffb27c2e0a
21 changed files with 3045 additions and 2034 deletions
|
|
@ -1,302 +1,301 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Button,
|
||||
FormGroup,
|
||||
Label,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupText,
|
||||
Icon,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Button,
|
||||
FormGroup,
|
||||
Label,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupText,
|
||||
Icon,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { PREDICTION_MODE_MAP, PPREDICTION_MODE_NAMES } from "$lib/types";
|
||||
import type { SavedScenario } from "$lib/types";
|
||||
import { getSavedScenarios, updateScenario, saveScenario } from "$lib/api/scenarios";
|
||||
import { FlightParametersStore, writeLocalStorage, ScenarioStore, SavedScenarioStore } from "$lib/stores";
|
||||
import SelectSearchable from "$lib/components/SelectSearchable.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { addToast } from "./Toast.svelte";
|
||||
import ScenarioEditor from "./ScenarioEditor.svelte";
|
||||
import { PREDICTION_MODE_MAP, PPREDICTION_MODE_NAMES } from "$lib/types";
|
||||
import type { SavedScenario } from "$lib/types";
|
||||
import { getSavedScenarios, updateScenario, saveScenario } from "$lib/api/scenarios";
|
||||
import { FlightParametersStore, writeLocalStorage, ScenarioStore, SavedScenarioStore } from "$lib/stores";
|
||||
import SelectSearchable from "$lib/components/SelectSearchable.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { addToast } from "./Toast.svelte";
|
||||
import ScenarioEditor from "./ScenarioEditor.svelte";
|
||||
|
||||
let isCollapsed = $state(false);
|
||||
let scenarioUnsaved = $derived(checkScenarioUnsaved());
|
||||
let selectedScenarioId = $state(-1);
|
||||
let isCollapsed = $state(false);
|
||||
let scenarioUnsaved = $derived(checkScenarioUnsaved());
|
||||
let selectedScenarioId = $state(-1);
|
||||
|
||||
let scenarioEditorRef: ScenarioEditor | null = null;
|
||||
let scenarioEditorRef: ScenarioEditor | null = null;
|
||||
|
||||
onMount(() => {
|
||||
getSavedScenarios()
|
||||
.then((scenarios) => SavedScenarioStore.set(scenarios))
|
||||
.catch((error) => {
|
||||
addToast({
|
||||
header: "Error Loading Points",
|
||||
body: `Failed to load saved points: ${error.message}`,
|
||||
color: "danger",
|
||||
});
|
||||
return [];
|
||||
});
|
||||
selectedScenarioId = $ScenarioStore.id;
|
||||
});
|
||||
onMount(() => {
|
||||
getSavedScenarios()
|
||||
.then((scenarios) => SavedScenarioStore.set(scenarios))
|
||||
.catch((error) => {
|
||||
addToast({
|
||||
header: "Error Loading Points",
|
||||
body: `Failed to load saved points: ${error.message}`,
|
||||
color: "danger",
|
||||
});
|
||||
return [];
|
||||
});
|
||||
selectedScenarioId = $ScenarioStore.id;
|
||||
});
|
||||
|
||||
function checkScenarioUnsaved() {
|
||||
const savedScenario = $SavedScenarioStore.find((scenario) => scenario.id === $ScenarioStore.id);
|
||||
function checkScenarioUnsaved() {
|
||||
const savedScenario = $SavedScenarioStore.find((scenario) => scenario.id === $ScenarioStore.id);
|
||||
|
||||
if (!savedScenario) {
|
||||
return false; // No saved scenario found
|
||||
}
|
||||
if (!savedScenario) {
|
||||
return false; // No saved scenario found
|
||||
}
|
||||
|
||||
const flightParameters = $FlightParametersStore;
|
||||
const savedData = savedScenario.template_data;
|
||||
const savedFlightParameters = savedData.flight_parameters;
|
||||
const flightParameters = $FlightParametersStore;
|
||||
const savedFlightParameters = savedScenario.flight_parameters;
|
||||
|
||||
// Compare flight parameters excluding launch_datetime
|
||||
console.log("Comparing flight parameters:", flightParameters, savedFlightParameters);
|
||||
return JSON.stringify(flightParameters) !== JSON.stringify(savedFlightParameters);
|
||||
}
|
||||
// Compare flight parameters excluding launch_datetime
|
||||
console.log("Comparing flight parameters:", flightParameters, savedFlightParameters);
|
||||
return JSON.stringify(flightParameters) !== JSON.stringify(savedFlightParameters);
|
||||
}
|
||||
|
||||
function handleSaveCurrentScenario() {
|
||||
console.log("handleSaveCurrentScenario called");
|
||||
const scenario = $SavedScenarioStore.find((s) => s.id === selectedScenarioId);
|
||||
if (selectedScenarioId !== -1 && scenario) {
|
||||
$ScenarioStore.id = selectedScenarioId;
|
||||
updateScenario($ScenarioStore)
|
||||
.then((updatedScenario) => {
|
||||
$SavedScenarioStore = $SavedScenarioStore.map((s) =>
|
||||
s.id === updatedScenario.id ? updatedScenario : s,
|
||||
);
|
||||
SavedScenarioStore.set($SavedScenarioStore);
|
||||
$ScenarioStore = updatedScenario;
|
||||
selectedScenarioId = updatedScenario.id;
|
||||
addToast({
|
||||
header: "Сценарий обновлен",
|
||||
body: `Сценарий "${updatedScenario.name}" успешно обновлен.`,
|
||||
color: "success",
|
||||
});
|
||||
scenarioUnsaved = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
addToast({
|
||||
header: "Ошибка обновления сценария",
|
||||
body: `Ошибка при обновлении сценария: ${error.message}`,
|
||||
color: "danger",
|
||||
});
|
||||
console.error("Ошибка при обновлении сценария:", error);
|
||||
});
|
||||
} else {
|
||||
scenarioEditorRef?.openModalAndCreate(
|
||||
null,
|
||||
{
|
||||
id: 0,
|
||||
name: "",
|
||||
template_data: {
|
||||
flight_parameters: $FlightParametersStore,
|
||||
description: "test",
|
||||
model: "test",
|
||||
dataset: "test",
|
||||
prediction_mode: $ScenarioStore.template_data.prediction_mode
|
||||
},
|
||||
},
|
||||
true,
|
||||
handleModalSave,
|
||||
);
|
||||
}
|
||||
}
|
||||
function handleSaveCurrentScenario() {
|
||||
console.log("handleSaveCurrentScenario called");
|
||||
const scenario = $SavedScenarioStore.find((s) => s.id === selectedScenarioId);
|
||||
if (selectedScenarioId !== -1 && scenario) {
|
||||
$ScenarioStore.id = selectedScenarioId;
|
||||
updateScenario($ScenarioStore)
|
||||
.then((updatedScenario) => {
|
||||
$SavedScenarioStore = $SavedScenarioStore.map((s) =>
|
||||
s.id === updatedScenario.id ? updatedScenario : s,
|
||||
);
|
||||
SavedScenarioStore.set($SavedScenarioStore);
|
||||
$ScenarioStore = updatedScenario;
|
||||
selectedScenarioId = updatedScenario.id;
|
||||
addToast({
|
||||
header: "Сценарий обновлен",
|
||||
body: `Сценарий "${updatedScenario.name}" успешно обновлен.`,
|
||||
color: "success",
|
||||
});
|
||||
scenarioUnsaved = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
addToast({
|
||||
header: "Ошибка обновления сценария",
|
||||
body: `Ошибка при обновлении сценария: ${error.message}`,
|
||||
color: "danger",
|
||||
});
|
||||
console.error("Ошибка при обновлении сценария:", error);
|
||||
});
|
||||
} else {
|
||||
scenarioEditorRef?.openModalAndCreate(
|
||||
null,
|
||||
{
|
||||
id: 0,
|
||||
name: "",
|
||||
flight_parameters: $FlightParametersStore,
|
||||
description: "test",
|
||||
model: "test",
|
||||
dataset: "test",
|
||||
prediction_mode: $ScenarioStore.prediction_mode,
|
||||
},
|
||||
true,
|
||||
handleModalSave,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleApplySelectedScenario(showToast = true) {
|
||||
const selectedScenario = $SavedScenarioStore.find((scenario) => scenario.id === selectedScenarioId);
|
||||
if (selectedScenario) {
|
||||
$ScenarioStore = selectedScenario;
|
||||
$FlightParametersStore = selectedScenario.template_data.flight_parameters;
|
||||
scenarioUnsaved = false;
|
||||
writeLocalStorage("scenario", $ScenarioStore);
|
||||
writeLocalStorage("flightParameters", $FlightParametersStore);
|
||||
if (showToast) {
|
||||
addToast({
|
||||
header: "Сценарий применен",
|
||||
body: `Сценарий "${selectedScenario.name}" успешно применен.`,
|
||||
color: "success",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (showToast)
|
||||
addToast({
|
||||
header: "Сценарий не найден",
|
||||
body: "Выбранный сценарий не существует.",
|
||||
color: "warning",
|
||||
});
|
||||
console.warn("Selected scenario not found:", selectedScenarioId);
|
||||
}
|
||||
}
|
||||
function handleApplySelectedScenario(showToast = true) {
|
||||
const selectedScenario = $SavedScenarioStore.find((scenario) => scenario.id === selectedScenarioId);
|
||||
if (selectedScenario) {
|
||||
$ScenarioStore = selectedScenario;
|
||||
$FlightParametersStore = selectedScenario.flight_parameters;
|
||||
scenarioUnsaved = false;
|
||||
writeLocalStorage("scenario", $ScenarioStore);
|
||||
writeLocalStorage("flightParameters", $FlightParametersStore);
|
||||
if (showToast) {
|
||||
addToast({
|
||||
header: "Сценарий применен",
|
||||
body: `Сценарий "${selectedScenario.name}" успешно применен.`,
|
||||
color: "success",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (showToast)
|
||||
addToast({
|
||||
header: "Сценарий не найден",
|
||||
body: "Выбранный сценарий не существует.",
|
||||
color: "warning",
|
||||
});
|
||||
console.warn("Selected scenario not found:", selectedScenarioId);
|
||||
}
|
||||
}
|
||||
|
||||
function handleModalSave(savedScenario: SavedScenario) {
|
||||
if (savedScenario) {
|
||||
$ScenarioStore = savedScenario;
|
||||
selectedScenarioId = savedScenario.id;
|
||||
scenarioUnsaved = false;
|
||||
}
|
||||
}
|
||||
function handleModalSave(savedScenario: SavedScenario) {
|
||||
if (savedScenario) {
|
||||
$ScenarioStore = savedScenario;
|
||||
selectedScenarioId = savedScenario.id;
|
||||
scenarioUnsaved = false;
|
||||
}
|
||||
}
|
||||
|
||||
export const collapsePanel = () => {
|
||||
isCollapsed = true;
|
||||
};
|
||||
export const collapsePanel = () => {
|
||||
isCollapsed = true;
|
||||
};
|
||||
|
||||
export const expandPanel = () => {
|
||||
isCollapsed = false;
|
||||
};
|
||||
export const expandPanel = () => {
|
||||
isCollapsed = false;
|
||||
};
|
||||
|
||||
export const togglePanel = () => {
|
||||
isCollapsed = !isCollapsed;
|
||||
};
|
||||
export const togglePanel = () => {
|
||||
isCollapsed = !isCollapsed;
|
||||
};
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<CardHeader
|
||||
class="bg-primary text-white d-flex justify-content-between align-items-center card-header p-1 px-3"
|
||||
style="cursor:pointer;"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="d-flex w-100 justify-content-between align-items-center bg-transparent border-0 p-0"
|
||||
style="width:100%;"
|
||||
aria-label="Свернуть/развернуть параметры прогнозирования"
|
||||
onclick={() => (isCollapsed = !isCollapsed)}
|
||||
>
|
||||
<b class="card-title mb-0 text-white p-0">Сценарий прогнозирования</b>
|
||||
<Button class="p-0" size="sm" color="primary" onclick={() => (isCollapsed = !isCollapsed)}>
|
||||
{#if isCollapsed}
|
||||
<Icon name="caret-left-fill" class="text-white" />
|
||||
{:else}
|
||||
<Icon name="caret-down-fill" class="text-white" />
|
||||
{/if}
|
||||
</Button>
|
||||
</button>
|
||||
</CardHeader>
|
||||
{#if !isCollapsed}
|
||||
<CardBody>
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="scenarioName" class="form-label">Cценарий:</Label>
|
||||
<InputGroup size="sm">
|
||||
<div class="position-relative flex-grow-1">
|
||||
<SelectSearchable
|
||||
style="min-height: calc(1.5em + .5rem + 2px); padding: .25rem .5rem; font-size: .875rem;"
|
||||
id="cp-start-point"
|
||||
options={$SavedScenarioStore.map((scenario) => ({
|
||||
value: scenario.id,
|
||||
label:
|
||||
scenario.name +
|
||||
`${scenario.id == $ScenarioStore.id && scenarioUnsaved ? " (изменено)" : ""}`,
|
||||
}))}
|
||||
bind:selected={selectedScenarioId}
|
||||
placeholder="Новый сценарий..."
|
||||
searchPlaceholder="Поиск сценариев..."
|
||||
on:change={() => {
|
||||
if (!scenarioUnsaved) {
|
||||
handleApplySelectedScenario(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
color="white"
|
||||
class="position-absolute top-50 end-0 translate-middle-y rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 16px; height: 16px; border: none; background: var(--bs-secondary); color: var(--bs-white); z-index: 10; margin-right: 2rem;"
|
||||
on:click={() => {
|
||||
selectedScenarioId = -1;
|
||||
}}
|
||||
disabled={selectedScenarioId === -1}
|
||||
>
|
||||
<Icon name="x" style="font-size: 16px;" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button color="success" title="Применить сценарий" onclick={() => {handleApplySelectedScenario(true)}}>
|
||||
<span>✓</span>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
<CardHeader
|
||||
class="bg-primary text-white d-flex justify-content-between align-items-center card-header p-1 px-3"
|
||||
style="cursor:pointer;">
|
||||
<button
|
||||
type="button"
|
||||
class="d-flex w-100 justify-content-between align-items-center bg-transparent border-0 p-0"
|
||||
style="width:100%;"
|
||||
aria-label="Свернуть/развернуть параметры прогнозирования"
|
||||
onclick={() => (isCollapsed = !isCollapsed)}>
|
||||
<b class="card-title mb-0 text-white p-0">Сценарий прогнозирования</b>
|
||||
<Button class="p-0" size="sm" color="primary" onclick={() => (isCollapsed = !isCollapsed)}>
|
||||
{#if isCollapsed}
|
||||
<Icon name="caret-left-fill" class="text-white" />
|
||||
{:else}
|
||||
<Icon name="caret-down-fill" class="text-white" />
|
||||
{/if}
|
||||
</Button>
|
||||
</button>
|
||||
</CardHeader>
|
||||
{#if !isCollapsed}
|
||||
<CardBody>
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="scenarioName" class="form-label">Cценарий:</Label>
|
||||
<InputGroup size="sm">
|
||||
<div class="position-relative flex-grow-1">
|
||||
<SelectSearchable
|
||||
style="min-height: calc(1.5em + .5rem + 2px); padding: .25rem .5rem; font-size: .875rem;"
|
||||
id="cp-start-point"
|
||||
options={$SavedScenarioStore.map((scenario) => ({
|
||||
value: scenario.id,
|
||||
label:
|
||||
scenario.name +
|
||||
`${scenario.id == $ScenarioStore.id && scenarioUnsaved ? " (изменено)" : ""}`,
|
||||
}))}
|
||||
bind:selected={selectedScenarioId}
|
||||
placeholder="Новый сценарий..."
|
||||
searchPlaceholder="Поиск сценариев..."
|
||||
on:change={() => {
|
||||
if (!scenarioUnsaved) {
|
||||
handleApplySelectedScenario(false);
|
||||
}
|
||||
}} />
|
||||
<Button
|
||||
size="sm"
|
||||
color="white"
|
||||
class="position-absolute top-50 end-0 translate-middle-y rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 16px; height: 16px; border: none; background: var(--bs-secondary); color: var(--bs-white); z-index: 10; margin-right: 2rem;"
|
||||
on:click={() => {
|
||||
selectedScenarioId = -1;
|
||||
}}
|
||||
disabled={selectedScenarioId === -1}>
|
||||
<Icon name="x" style="font-size: 16px;" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
color="success"
|
||||
title="Применить сценарий"
|
||||
onclick={() => {
|
||||
handleApplySelectedScenario(true);
|
||||
}}>
|
||||
<span>✓</span>
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<Button color="secondary flex-fill" size="sm" title="Открыть список сценариев">
|
||||
Все сценарии
|
||||
<Icon name="journal-bookmark-fill" />
|
||||
</Button>
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<Button color="secondary flex-fill" size="sm" title="Открыть список сценариев">
|
||||
Все сценарии
|
||||
<Icon name="journal-bookmark-fill" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="primary flex-fill"
|
||||
size="sm"
|
||||
title="Сохранить текущие условия как сценарий"
|
||||
onclick={handleSaveCurrentScenario}
|
||||
disabled={!scenarioUnsaved && selectedScenarioId !== -1}
|
||||
>
|
||||
{selectedScenarioId !== -1 ? "Обновить сценарий" : "Сохранить сценарий"}
|
||||
<Icon name="floppy2-fill" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
color="primary flex-fill"
|
||||
size="sm"
|
||||
title="Сохранить текущие условия как сценарий"
|
||||
onclick={handleSaveCurrentScenario}
|
||||
disabled={!scenarioUnsaved && selectedScenarioId !== -1}>
|
||||
{selectedScenarioId !== -1 ? "Обновить сценарий" : "Сохранить сценарий"}
|
||||
<Icon name="floppy2-fill" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<hr />
|
||||
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="scenarioMode" class="form-label">Режим сценария:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input type="select" id="scenarioMode" bind:value={$ScenarioStore.template_data.prediction_mode}
|
||||
on:change={() => {
|
||||
scenarioUnsaved = true;
|
||||
}}>
|
||||
{#each Object.entries(PREDICTION_MODE_MAP) as [key, value]}
|
||||
<option {value}
|
||||
>{PPREDICTION_MODE_NAMES[key as keyof typeof PPREDICTION_MODE_NAMES]}
|
||||
{key}
|
||||
</option>
|
||||
{/each}
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="scenarioMode" class="form-label">Режим сценария:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input
|
||||
type="select"
|
||||
id="scenarioMode"
|
||||
bind:value={$ScenarioStore.prediction_mode}
|
||||
on:change={() => {
|
||||
scenarioUnsaved = true;
|
||||
}}>
|
||||
{#each Object.entries(PREDICTION_MODE_MAP) as [key, value]}
|
||||
<option {value}>
|
||||
{PPREDICTION_MODE_NAMES[key as keyof typeof PPREDICTION_MODE_NAMES]}
|
||||
{key}
|
||||
</option>
|
||||
{/each}
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="scenarioMode" class="form-label">Модель атмосферы:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input type="select" id="scenarioMode">
|
||||
<option>GFS (0.25°)</option>
|
||||
<option>GFS (0.5°)</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="scenarioMode" class="form-label">Модель атмосферы:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input type="select" id="scenarioMode">
|
||||
<option>GFS (0.25°)</option>
|
||||
<option>GFS (0.5°)</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="scenarioMode" class="form-label">Набор данных:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input type="select" id="scenarioMode">
|
||||
<option>Выбрать автоматически</option>
|
||||
<!-- TODO ручка апи для доступных наборов -->
|
||||
<option>20250701-00</option>
|
||||
<option>20250701-06</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
<FormGroup spacing="mb-2">
|
||||
<Label for="scenarioMode" class="form-label">Набор данных:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input type="select" id="scenarioMode">
|
||||
<option>Выбрать автоматически</option>
|
||||
<!-- TODO ручка апи для доступных наборов -->
|
||||
<option>20250701-00</option>
|
||||
<option>20250701-06</option>
|
||||
</Input>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
<hr />
|
||||
<hr />
|
||||
|
||||
<FormGroup spacing="mb-0">
|
||||
<Label for="export" class="form-label">Экспортировать результат:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input type="select" id="export">
|
||||
<option>JSON</option>
|
||||
<option>CSV</option>
|
||||
<option>KML</option>
|
||||
</Input>
|
||||
<Button
|
||||
color="primary"
|
||||
title="Edit Saved Locations"
|
||||
onclick={() => console.log("Not implemented yet")}
|
||||
>
|
||||
<span>Экспорт</span>
|
||||
<Icon name="file-earmark-arrow-down" />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
</CardBody>
|
||||
{/if}
|
||||
<FormGroup spacing="mb-0">
|
||||
<Label for="export" class="form-label">Экспортировать результат:</Label>
|
||||
<InputGroup size="sm">
|
||||
<Input type="select" id="export">
|
||||
<option>JSON</option>
|
||||
<option>CSV</option>
|
||||
<option>KML</option>
|
||||
</Input>
|
||||
<Button
|
||||
color="primary"
|
||||
title="Edit Saved Locations"
|
||||
onclick={() => console.log("Not implemented yet")}>
|
||||
<span>Экспорт</span>
|
||||
<Icon name="file-earmark-arrow-down" />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
</CardBody>
|
||||
{/if}
|
||||
</Card>
|
||||
<ScenarioEditor bind:this={scenarioEditorRef} onSave={handleModalSave} />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue