feat(scenarios,environments): add markdown description field

- add optional description to scenario and environment entities
- expose description in create/update DTOs, MCP tools, and export DTOs
- render description as markdown on detail pages
- add description editor (CodeEditor) to create/edit forms for both resources
This commit is contained in:
2026-04-14 23:53:45 +03:00
parent 4dab218b75
commit e6aacc899d
20 changed files with 206 additions and 32 deletions
+6 -6
View File
@@ -133,13 +133,13 @@ export const environments = {
get(id: string): Promise<Environment> { get(id: string): Promise<Environment> {
return request(`/environments/${id}`); return request(`/environments/${id}`);
}, },
create(name: string, data: Environment['data']): Promise<Environment> { create(name: string, data: Environment['data'], description?: string): Promise<Environment> {
return request('/environments', { return request('/environments', {
method: 'POST', method: 'POST',
body: JSON.stringify({ name, data }), body: JSON.stringify({ name, description, data }),
}); });
}, },
update(id: string, patch: Partial<Pick<Environment, 'name' | 'data'>>): Promise<Environment> { update(id: string, patch: Partial<Pick<Environment, 'name' | 'description' | 'data'>>): Promise<Environment> {
return request(`/environments/${id}`, { return request(`/environments/${id}`, {
method: 'PATCH', method: 'PATCH',
body: JSON.stringify(patch), body: JSON.stringify(patch),
@@ -182,13 +182,13 @@ export const scenarios = {
get(id: string): Promise<Scenario & { steps: ScenarioStep[] }> { get(id: string): Promise<Scenario & { steps: ScenarioStep[] }> {
return request(`/scenarios/${id}`); return request(`/scenarios/${id}`);
}, },
create(name: string): Promise<Scenario> { create(name: string, description?: string): Promise<Scenario> {
return request('/scenarios', { return request('/scenarios', {
method: 'POST', method: 'POST',
body: JSON.stringify({ name }), body: JSON.stringify({ name, description }),
}); });
}, },
update(id: string, patch: Partial<Pick<Scenario, 'name'>>): Promise<Scenario> { update(id: string, patch: Partial<Pick<Scenario, 'name' | 'description'>>): Promise<Scenario> {
return request(`/scenarios/${id}`, { return request(`/scenarios/${id}`, {
method: 'PATCH', method: 'PATCH',
body: JSON.stringify(patch), body: JSON.stringify(patch),
+2
View File
@@ -16,6 +16,7 @@ export interface EnvironmentData {
export interface Environment { export interface Environment {
id: string; id: string;
name: string; name: string;
description?: string;
data: EnvironmentData; data: EnvironmentData;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@@ -82,6 +83,7 @@ export interface ScenarioCredential {
export interface Scenario { export interface Scenario {
id: string; id: string;
name: string; name: string;
description?: string;
steps?: ScenarioStep[]; steps?: ScenarioStep[];
scenarioCredentials?: ScenarioCredential[]; scenarioCredentials?: ScenarioCredential[];
createdAt: string; createdAt: string;
+10 -1
View File
@@ -36,6 +36,7 @@
"updated": "Environment updated", "updated": "Environment updated",
"col_id": "ID", "col_id": "ID",
"col_name": "Name", "col_name": "Name",
"col_description": "Description",
"col_data": "Data", "col_data": "Data",
"col_updated": "Updated", "col_updated": "Updated",
"empty": "No environments yet.", "empty": "No environments yet.",
@@ -56,10 +57,13 @@
"form_name": "Name", "form_name": "Name",
"form_name_placeholder": "e.g. staging, production", "form_name_placeholder": "e.g. staging, production",
"form_name_required": "Name is required", "form_name_required": "Name is required",
"form_description": "Description",
"form_description_placeholder": "Describe this environment",
"form_data": "Data (JSON)", "form_data": "Data (JSON)",
"form_data_invalid_json": "Must be a valid JSON object", "form_data_invalid_json": "Must be a valid JSON object",
"back": "Environments", "back": "Environments",
"field_id": "ID", "field_id": "ID",
"field_description": "Description",
"field_created": "Created", "field_created": "Created",
"field_updated": "Updated", "field_updated": "Updated",
"section_data": "Data" "section_data": "Data"
@@ -115,12 +119,14 @@
"field_updated": "Updated", "field_updated": "Updated",
"field_lastUsed": "Last Used" "field_lastUsed": "Last Used"
}, },
"scenarios": { "title": "Scenarios", "scenarios": {
"title": "Scenarios",
"created": "Scenario created", "created": "Scenario created",
"updated": "Scenario updated", "updated": "Scenario updated",
"run_started": "Scenario run started", "run_started": "Scenario run started",
"col_id": "ID", "col_id": "ID",
"col_name": "Name", "col_name": "Name",
"col_description": "Description",
"col_updated": "Updated", "col_updated": "Updated",
"empty": "No scenarios yet.", "empty": "No scenarios yet.",
"loading": "Loading…", "loading": "Loading…",
@@ -128,6 +134,7 @@
"action_delete": "Delete", "action_delete": "Delete",
"field_id": "ID", "field_id": "ID",
"field_name": "Name", "field_name": "Name",
"field_description": "Description",
"field_steps": "Steps", "field_steps": "Steps",
"field_created": "Created", "field_created": "Created",
"field_updated": "Updated", "field_updated": "Updated",
@@ -148,6 +155,8 @@
"action_cancel": "Cancel", "action_cancel": "Cancel",
"create_title": "New Scenario", "create_title": "New Scenario",
"edit_title": "Edit Scenario", "edit_title": "Edit Scenario",
"form_description": "Description",
"form_description_placeholder": "Describe this scenario",
"form_name": "Name", "form_name": "Name",
"form_name_placeholder": "e.g. Login flow", "form_name_placeholder": "e.g. Login flow",
"form_name_required": "Name is required", "form_name_required": "Name is required",
@@ -12,6 +12,7 @@ export function CreateEnvironmentPage() {
const toast = useToast(); const toast = useToast();
const [name, setName] = useState(''); const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [dataJson, setDataJson] = useState('{\n "id": "https://id.example.com"\n}'); const [dataJson, setDataJson] = useState('{\n "id": "https://id.example.com"\n}');
const [nameError, setNameError] = useState(''); const [nameError, setNameError] = useState('');
const [dataError, setDataError] = useState(''); const [dataError, setDataError] = useState('');
@@ -42,7 +43,11 @@ export function CreateEnvironmentPage() {
} }
setSaving(true); setSaving(true);
try { try {
const env = await environments.create(name.trim(), parsedData); const env = await environments.create(
name.trim(),
parsedData,
description.trim() || undefined,
);
toast.success(t('environments.created')); toast.success(t('environments.created'));
navigate(`/environments/${env.id}`); navigate(`/environments/${env.id}`);
} catch (err) { } catch (err) {
@@ -83,6 +88,15 @@ export function CreateEnvironmentPage() {
required required
autoFocus autoFocus
/> />
<div className={styles.formField}>
<label className={styles.fieldLabel}>{t('environments.form_description')}</label>
<CodeEditor
language="markdown"
value={description}
onChange={setDescription}
rows={6}
/>
</div>
<div className={styles.formField}> <div className={styles.formField}>
<label className={styles.fieldLabel}>{t('environments.form_data')}</label> <label className={styles.fieldLabel}>{t('environments.form_data')}</label>
<CodeEditor <CodeEditor
@@ -15,6 +15,7 @@ export function EditEnvironmentPage() {
const [env, setEnv] = useState<Environment | null>(null); const [env, setEnv] = useState<Environment | null>(null);
const [name, setName] = useState(''); const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [dataJson, setDataJson] = useState('{}'); const [dataJson, setDataJson] = useState('{}');
const [nameError, setNameError] = useState(''); const [nameError, setNameError] = useState('');
const [dataError, setDataError] = useState(''); const [dataError, setDataError] = useState('');
@@ -28,6 +29,7 @@ export function EditEnvironmentPage() {
.then((data) => { .then((data) => {
setEnv(data); setEnv(data);
setName(data.name); setName(data.name);
setDescription(data.description || '');
setDataJson(JSON.stringify(data.data ?? {}, null, 2)); setDataJson(JSON.stringify(data.data ?? {}, null, 2));
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -60,6 +62,7 @@ export function EditEnvironmentPage() {
try { try {
await environments.update(id!, { await environments.update(id!, {
name: name.trim(), name: name.trim(),
description: description.trim() || undefined,
data: parsedData, data: parsedData,
}); });
toast.success(t('environments.updated')); toast.success(t('environments.updated'));
@@ -109,6 +112,15 @@ export function EditEnvironmentPage() {
required required
autoFocus autoFocus
/> />
<div className={styles.formField}>
<label className={styles.fieldLabel}>{t('environments.form_description')}</label>
<CodeEditor
language="markdown"
value={description}
onChange={setDescription}
rows={6}
/>
</div>
<div className={styles.formField}> <div className={styles.formField}>
<label className={styles.fieldLabel}>{t('environments.form_data')}</label> <label className={styles.fieldLabel}>{t('environments.form_data')}</label>
<CodeEditor <CodeEditor
@@ -13,6 +13,7 @@ import {
DescriptionList, DescriptionList,
Timestamp, Timestamp,
UuidBadge, UuidBadge,
MarkdownContent,
useToast, useToast,
} from '../../ui'; } from '../../ui';
import styles from '../Page.module.css'; import styles from '../Page.module.css';
@@ -117,6 +118,17 @@ export function EnvironmentDetailPage() {
/> />
</Card> </Card>
{env.description && (
<div className={styles.stepsSection}>
<div className={styles.sectionHeadingRow}>
<h2 className={styles.sectionHeading}>{t('environments.field_description')}</h2>
</div>
<Card>
<MarkdownContent content={env.description} />
</Card>
</div>
)}
<div className={styles.stepsSection}> <div className={styles.stepsSection}>
<h2 className={styles.sectionHeading}>{t('environments.section_data')}</h2> <h2 className={styles.sectionHeading}>{t('environments.section_data')}</h2>
<Card> <Card>
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { X, Save, ClipboardList } from 'lucide-react'; import { X, Save, ClipboardList } from 'lucide-react';
import { scenarios } from '../../api'; import { scenarios } from '../../api';
import { Breadcrumbs, Button, Card, Input, useToast } from '../../ui'; import { Breadcrumbs, Button, Card, Input, CodeEditor, useToast } from '../../ui';
import styles from '../Page.module.css'; import styles from '../Page.module.css';
export function CreateScenarioPage() { export function CreateScenarioPage() {
@@ -12,6 +12,7 @@ export function CreateScenarioPage() {
const toast = useToast(); const toast = useToast();
const [name, setName] = useState(''); const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [nameError, setNameError] = useState(''); const [nameError, setNameError] = useState('');
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@@ -23,7 +24,7 @@ export function CreateScenarioPage() {
} }
setSaving(true); setSaving(true);
try { try {
const scenario = await scenarios.create(name.trim()); const scenario = await scenarios.create(name.trim(), description.trim() || undefined);
toast.success(t('scenarios.created')); toast.success(t('scenarios.created'));
navigate(`/scenarios/${scenario.id}`); navigate(`/scenarios/${scenario.id}`);
} catch (err) { } catch (err) {
@@ -64,6 +65,15 @@ export function CreateScenarioPage() {
required required
autoFocus autoFocus
/> />
<div className={styles.formField}>
<label className={styles.fieldLabel}>{t('scenarios.form_description')}</label>
<CodeEditor
language="markdown"
value={description}
onChange={setDescription}
rows={8}
/>
</div>
</div> </div>
<div className={styles.formActions}> <div className={styles.formActions}>
+16 -2
View File
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { X, Save, ClipboardList } from 'lucide-react'; import { X, Save, ClipboardList } from 'lucide-react';
import { scenarios } from '../../api'; import { scenarios } from '../../api';
import type { Scenario } from '../../api'; import type { Scenario } from '../../api';
import { Breadcrumbs, Button, Card, Input, useToast } from '../../ui'; import { Breadcrumbs, Button, Card, Input, CodeEditor, useToast } from '../../ui';
import styles from '../Page.module.css'; import styles from '../Page.module.css';
export function EditScenarioPage() { export function EditScenarioPage() {
@@ -15,6 +15,7 @@ export function EditScenarioPage() {
const [scenario, setScenario] = useState<Scenario | null>(null); const [scenario, setScenario] = useState<Scenario | null>(null);
const [name, setName] = useState(''); const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [nameError, setNameError] = useState(''); const [nameError, setNameError] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@@ -26,6 +27,7 @@ export function EditScenarioPage() {
.then((data) => { .then((data) => {
setScenario(data); setScenario(data);
setName(data.name); setName(data.name);
setDescription(data.description || '');
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, [id]); }, [id]);
@@ -38,7 +40,10 @@ export function EditScenarioPage() {
} }
setSaving(true); setSaving(true);
try { try {
await scenarios.update(id!, { name: name.trim() }); await scenarios.update(id!, {
name: name.trim(),
description: description.trim() || undefined,
});
toast.success(t('scenarios.updated')); toast.success(t('scenarios.updated'));
navigate(`/scenarios/${id}`); navigate(`/scenarios/${id}`);
} catch (err) { } catch (err) {
@@ -86,6 +91,15 @@ export function EditScenarioPage() {
required required
autoFocus autoFocus
/> />
<div className={styles.formField}>
<label className={styles.fieldLabel}>{t('scenarios.form_description')}</label>
<CodeEditor
language="markdown"
value={description}
onChange={setDescription}
rows={8}
/>
</div>
</div> </div>
<div className={styles.formActions}> <div className={styles.formActions}>
@@ -37,6 +37,7 @@ import {
Table, Table,
Timestamp, Timestamp,
UuidBadge, UuidBadge,
MarkdownContent,
useToast, useToast,
type TableColumn, type TableColumn,
} from '../../ui'; } from '../../ui';
@@ -400,6 +401,17 @@ export function ScenarioDetailPage() {
/> />
</Card> </Card>
{scenario.description && (
<div className={styles.stepsSection}>
<div className={styles.sectionHeadingRow}>
<h2 className={styles.sectionHeading}>{t('scenarios.field_description')}</h2>
</div>
<Card>
<MarkdownContent content={scenario.description} />
</Card>
</div>
)}
{scenario.steps.length > 0 && ( {scenario.steps.length > 0 && (
<div className={styles.stepsSection}> <div className={styles.stepsSection}>
<div className={styles.sectionToolbar}> <div className={styles.sectionToolbar}>
+1 -1
View File
@@ -35,7 +35,7 @@ RUN npm ci --omit=dev -w server
COPY --from=builder /app/server/dist ./dist COPY --from=builder /app/server/dist ./dist
# Runtime directories (keys and SQLite DB mounted via volumes) # Runtime directories (keys and SQLite DB mounted via volumes)
RUN mkdir -p data keys RUN mkdir -p data
EXPOSE 3000 EXPOSE 3000
@@ -1,5 +1,5 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsNotEmpty, IsObject, IsString } from "class-validator"; import { IsNotEmpty, IsObject, IsOptional, IsString } from "class-validator";
import { EnvironmentData } from "../environment.entity"; import { EnvironmentData } from "../environment.entity";
export class CreateEnvironmentDto { export class CreateEnvironmentDto {
@@ -8,6 +8,11 @@ export class CreateEnvironmentDto {
@IsNotEmpty() @IsNotEmpty()
name: string; name: string;
@ApiPropertyOptional({ description: "Optional markdown description" })
@IsOptional()
@IsString()
description?: string;
@ApiProperty({ @ApiProperty({
description: "Generic key-value map for environment metadata", description: "Generic key-value map for environment metadata",
example: { example: {
@@ -25,6 +25,11 @@ export class EnvironmentExportDto {
@IsNotEmpty() @IsNotEmpty()
name: string; name: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
description?: string;
@ApiProperty() @ApiProperty()
@IsObject() @IsObject()
data: EnvironmentData; data: EnvironmentData;
@@ -18,6 +18,9 @@ export class EnvironmentEntity {
@Column({ unique: true }) @Column({ unique: true })
name: string; name: string;
@Column("text", { nullable: true })
description: string | null;
@Column("simple-json") @Column("simple-json")
data: EnvironmentData; data: EnvironmentData;
+23 -4
View File
@@ -71,6 +71,7 @@ export class EnvironmentService {
kind: "environment", kind: "environment",
id: env.id, id: env.id,
name: env.name, name: env.name,
description: env.description ?? undefined,
data: env.data, data: env.data,
}; };
} }
@@ -81,18 +82,36 @@ export class EnvironmentService {
if (dto.id) { if (dto.id) {
const existing = await this.repo.findOneBy({ id: dto.id }); const existing = await this.repo.findOneBy({ id: dto.id });
if (existing) { if (existing) {
Object.assign(existing, { name: dto.name, data: dto.data }); Object.assign(existing, {
name: dto.name,
description: dto.description ?? null,
data: dto.data,
});
return this.repo.save(existing); return this.repo.save(existing);
} }
return this.repo.save( return this.repo.save(
this.repo.create({ id: dto.id, name: dto.name, data: dto.data }), this.repo.create({
id: dto.id,
name: dto.name,
description: dto.description ?? null,
data: dto.data,
}),
); );
} }
const byName = await this.repo.findOneBy({ name: dto.name }); const byName = await this.repo.findOneBy({ name: dto.name });
if (byName) { if (byName) {
Object.assign(byName, { data: dto.data }); Object.assign(byName, {
description: dto.description ?? null,
data: dto.data,
});
return this.repo.save(byName); return this.repo.save(byName);
} }
return this.repo.save(this.repo.create({ name: dto.name, data: dto.data })); return this.repo.save(
this.repo.create({
name: dto.name,
description: dto.description ?? null,
data: dto.data,
}),
);
} }
} }
+39 -9
View File
@@ -181,15 +181,20 @@ export class McpService {
name: z name: z
.string() .string()
.describe("Unique environment name, e.g. liquio-diia-stg"), .describe("Unique environment name, e.g. liquio-diia-stg"),
description: z
.string()
.optional()
.describe("Optional markdown description"),
data: z data: z
.record(z.string(), z.string()) .record(z.string(), z.string())
.describe("Map of string keys to string values"), .describe("Map of string keys to string values"),
}, },
}, },
async ({ name, data }) => { async ({ name, description, data }) => {
try { try {
const env = await this.environmentService.create({ const env = await this.environmentService.create({
name, name,
description,
data: data as EnvironmentData, data: data as EnvironmentData,
}); });
return { return {
@@ -207,20 +212,26 @@ export class McpService {
server.registerTool( server.registerTool(
"update_environment", "update_environment",
{ {
description: "Update an existing environment (name and/or data)", description:
"Update an existing environment (name, description, and/or data)",
inputSchema: { inputSchema: {
id: z.uuid().describe("Environment ID to update"), id: z.uuid().describe("Environment ID to update"),
name: z.string().optional().describe("New name"), name: z.string().optional().describe("New name"),
description: z
.string()
.optional()
.describe("New markdown description"),
data: z data: z
.record(z.string(), z.string()) .record(z.string(), z.string())
.optional() .optional()
.describe("New data map"), .describe("New data map"),
}, },
}, },
async ({ id, name, data }) => { async ({ id, name, description, data }) => {
try { try {
const env = await this.environmentService.update(id, { const env = await this.environmentService.update(id, {
name, name,
description,
data: data as EnvironmentData | undefined, data: data as EnvironmentData | undefined,
}); });
return { return {
@@ -457,11 +468,18 @@ export class McpService {
description: "Create a new scenario", description: "Create a new scenario",
inputSchema: { inputSchema: {
name: z.string().describe("Scenario name"), name: z.string().describe("Scenario name"),
description: z
.string()
.optional()
.describe("Optional markdown description"),
}, },
}, },
async ({ name }) => { async ({ name, description }) => {
try { try {
const scenario = await this.scenarioService.create({ name }); const scenario = await this.scenarioService.create({
name,
description,
});
return { return {
content: [ content: [
{ type: "text" as const, text: JSON.stringify(scenario) }, { type: "text" as const, text: JSON.stringify(scenario) },
@@ -479,15 +497,22 @@ export class McpService {
server.registerTool( server.registerTool(
"update_scenario", "update_scenario",
{ {
description: "Update a scenario name", description: "Update a scenario name and/or description",
inputSchema: { inputSchema: {
id: z.uuid().describe("Scenario ID"), id: z.uuid().describe("Scenario ID"),
name: z.string().optional().describe("New name"), name: z.string().optional().describe("New name"),
description: z
.string()
.optional()
.describe("New markdown description"),
}, },
}, },
async ({ id, name }) => { async ({ id, name, description }) => {
try { try {
const scenario = await this.scenarioService.update(id, { name }); const scenario = await this.scenarioService.update(id, {
name,
description,
});
return { return {
content: [ content: [
{ type: "text" as const, text: JSON.stringify(scenario) }, { type: "text" as const, text: JSON.stringify(scenario) },
@@ -951,6 +976,10 @@ export class McpService {
"Import a scenario from an export payload, creating a new scenario with all its steps", "Import a scenario from an export payload, creating a new scenario with all its steps",
inputSchema: { inputSchema: {
name: z.string().describe("Scenario name"), name: z.string().describe("Scenario name"),
description: z
.string()
.optional()
.describe("Optional markdown description"),
steps: z steps: z
.array( .array(
z.object({ z.object({
@@ -967,10 +996,11 @@ export class McpService {
.describe("Ordered list of steps"), .describe("Ordered list of steps"),
}, },
}, },
async ({ name, steps }) => { async ({ name, description, steps }) => {
try { try {
const scenario = await this.scenarioService.importScenario({ const scenario = await this.scenarioService.importScenario({
name, name,
description,
steps: steps as Parameters< steps: steps as Parameters<
typeof this.scenarioService.importScenario typeof this.scenarioService.importScenario
>[0]["steps"], >[0]["steps"],
@@ -1,9 +1,14 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsNotEmpty, IsString } from "class-validator"; import { IsNotEmpty, IsOptional, IsString } from "class-validator";
export class CreateScenarioDto { export class CreateScenarioDto {
@ApiProperty({ example: "Login and verify cabinet" }) @ApiProperty({ example: "Login and verify cabinet" })
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
name: string; name: string;
@ApiPropertyOptional({ description: "Optional markdown description" })
@IsOptional()
@IsString()
description?: string;
} }
@@ -46,6 +46,11 @@ export class ScenarioExportDto {
@IsNotEmpty() @IsNotEmpty()
name: string; name: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
description?: string;
@ApiProperty({ type: [ScenarioStepExportDto] }) @ApiProperty({ type: [ScenarioStepExportDto] })
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@@ -7,4 +7,9 @@ export class UpdateScenarioDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
name?: string; name?: string;
@ApiPropertyOptional({ description: "Optional markdown description" })
@IsOptional()
@IsString()
description?: string;
} }
+3
View File
@@ -17,6 +17,9 @@ export class ScenarioEntity {
@Column() @Column()
name: string; name: string;
@Column("text", { nullable: true })
description: string | null;
@OneToMany(() => ScenarioStepEntity, (step) => step.scenario, { @OneToMany(() => ScenarioStepEntity, (step) => step.scenario, {
cascade: true, cascade: true,
eager: false, eager: false,
+11 -2
View File
@@ -410,6 +410,7 @@ export class ScenarioService {
kind: "scenario", kind: "scenario",
id: scenario.id, id: scenario.id,
name: scenario.name, name: scenario.name,
description: scenario.description ?? undefined,
steps: scenario.steps.map((s) => ({ steps: scenario.steps.map((s) => ({
title: s.title, title: s.title,
execCode: s.execCode, execCode: s.execCode,
@@ -424,17 +425,25 @@ export class ScenarioService {
const existing = await this.scenarioRepo.findOneBy({ id: dto.id }); const existing = await this.scenarioRepo.findOneBy({ id: dto.id });
if (existing) { if (existing) {
existing.name = dto.name; existing.name = dto.name;
existing.description = dto.description ?? null;
scenario = await this.scenarioRepo.save(existing); scenario = await this.scenarioRepo.save(existing);
// Delete old steps and recreate // Delete old steps and recreate
await this.stepRepo.delete({ scenarioId: scenario.id }); await this.stepRepo.delete({ scenarioId: scenario.id });
} else { } else {
scenario = await this.scenarioRepo.save( scenario = await this.scenarioRepo.save(
this.scenarioRepo.create({ id: dto.id, name: dto.name }), this.scenarioRepo.create({
id: dto.id,
name: dto.name,
description: dto.description ?? null,
}),
); );
} }
} else { } else {
scenario = await this.scenarioRepo.save( scenario = await this.scenarioRepo.save(
this.scenarioRepo.create({ name: dto.name }), this.scenarioRepo.create({
name: dto.name,
description: dto.description ?? null,
}),
); );
} }
if (dto.steps.length > 0) { if (dto.steps.length > 0) {