feat(export-import): add yaml export/import with id upsert for credentials, snippets, and scenarios

- all entities export a kind field (credential/snippet/scenario) for safe type checking on import
- import upserts by id: overwrites if id exists, creates with explicit id otherwise
- scenario export now includes id and step ids; import deletes old steps before recreating
- add GET /:id/export and POST /import endpoints to credential and snippet controllers
- add UuidBadge ui component: shortens uuid to first+last 4 hex chars, tooltip, clipboard copy with animated ClipboardCheck icon pop
- apply UuidBadge across all entity id display sites (detail pages, card footers, table columns)
- add export/import buttons to CredentialsPage, SnippetsPage, CredentialDetailPage, SnippetDetailPage
This commit is contained in:
2026-04-10 13:09:09 +03:00
parent 1164289173
commit 32be7c0a59
54 changed files with 728 additions and 272 deletions
+43 -3
View File
@@ -8,6 +8,7 @@ import { Repository } from "typeorm";
import { SnippetEntity } from "./snippet.entity";
import { CreateSnippetDto } from "./dto/create-snippet.dto";
import { UpdateSnippetDto } from "./dto/update-snippet.dto";
import { SnippetExportDto } from "./dto/snippet-export.dto";
import {
PaginationQueryDto,
PaginatedResult,
@@ -47,13 +48,13 @@ export class SnippetService {
return { data, total, page, limit };
}
async findOne(id: number): Promise<SnippetEntity> {
async findOne(id: string): Promise<SnippetEntity> {
const snippet = await this.repo.findOneBy({ id });
if (!snippet) throw new NotFoundException(`Snippet ${id} not found`);
return snippet;
}
async update(id: number, dto: UpdateSnippetDto): Promise<SnippetEntity> {
async update(id: string, dto: UpdateSnippetDto): Promise<SnippetEntity> {
const snippet = await this.findOne(id);
if (dto.name && dto.name !== snippet.name) {
const conflict = await this.repo.findOneBy({ name: dto.name });
@@ -63,7 +64,7 @@ export class SnippetService {
return this.repo.save(snippet);
}
async remove(id: number): Promise<void> {
async remove(id: string): Promise<void> {
await this.findOne(id);
await this.repo.delete(id);
}
@@ -73,4 +74,43 @@ export class SnippetService {
const { data } = await this.findAll({ limit: 1000 });
return Object.fromEntries(data.map((s) => [s.name, s.code]));
}
exportSnippet(snippet: SnippetEntity): SnippetExportDto {
return {
kind: "snippet",
id: snippet.id,
name: snippet.name,
description: snippet.description,
code: snippet.code,
};
}
async importSnippet(dto: SnippetExportDto): Promise<SnippetEntity> {
if (dto.id) {
const existing = await this.repo.findOneBy({ id: dto.id });
if (existing) {
Object.assign(existing, {
name: dto.name,
description: dto.description ?? null,
code: dto.code,
});
return this.repo.save(existing);
}
return this.repo.save(
this.repo.create({
id: dto.id,
name: dto.name,
description: dto.description ?? null,
code: dto.code,
}),
);
}
return this.repo.save(
this.repo.create({
name: dto.name,
description: dto.description ?? null,
code: dto.code,
}),
);
}
}