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
@@ -0,0 +1,38 @@
.wrapper {
display: inline-flex;
align-items: center;
position: relative;
}
.button {
all: unset;
cursor: pointer;
font: inherit;
color: inherit;
letter-spacing: 0.02em;
}
.button:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
.pop {
position: absolute;
left: calc(100% + 4px);
top: 50%;
transform: translateY(-50%);
display: inline-flex;
align-items: center;
color: var(--color-success-fg);
animation: popFade 1s ease forwards;
pointer-events: none;
}
@keyframes popFade {
0% { opacity: 0; transform: translateY(calc(-50% - 4px)); }
20% { opacity: 1; transform: translateY(-50%); }
70% { opacity: 1; transform: translateY(-50%); }
100% { opacity: 0; transform: translateY(calc(-50% - 4px)); }
}
+36
View File
@@ -0,0 +1,36 @@
import { useState } from 'react';
import { ClipboardCheck } from 'lucide-react';
import { Badge } from '../Badge/Badge';
import styles from './UuidBadge.module.css';
export interface UuidBadgeProps {
id: string;
}
export function UuidBadge({ id }: UuidBadgeProps) {
const [copied, setCopied] = useState(false);
const short = `${id.slice(0, 4)}${id.slice(-4)}`;
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
navigator.clipboard.writeText(id).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1000);
});
};
return (
<span className={styles.wrapper} title={copied ? 'Copied!' : id}>
<Badge variant="neutral">
<button type="button" className={styles.button} onClick={handleClick}>
{short}
</button>
</Badge>
{copied && (
<span className={styles.pop} aria-hidden="true">
<ClipboardCheck size={13} />
</span>
)}
</span>
);
}
+3
View File
@@ -44,3 +44,6 @@ export type { NotificationProps, NotificationVariant } from './Notification/Noti
export { Table } from './Table/Table';
export type { TableProps, TableColumn } from './Table/Table';
export { UuidBadge } from './UuidBadge/UuidBadge';
export type { UuidBadgeProps } from './UuidBadge/UuidBadge';