diff --git a/src/api/generated/analysis/analysis.ts b/src/api/generated/analysis/analysis.ts index 1cff535..23c6bb1 100644 --- a/src/api/generated/analysis/analysis.ts +++ b/src/api/generated/analysis/analysis.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.15.0 🍺 + * Generated by orval v8.20.0 🍺 * Do not edit manually. * OpenAPI definition * OpenAPI spec version: v0 @@ -33,6 +33,21 @@ import { customInstance } from "../../api"; type SecondParameter unknown> = Parameters[1]; +const withQueryKey = (query: T, queryKey: K): T & { queryKey: K } => { + const result = { queryKey } as T & { queryKey: K }; + for (const key of Object.keys(query)) { + // The explicit queryKey always wins, matching the previous + // `{ ...query, queryKey }` spread where it was set last. + if (key === "queryKey") continue; + Object.defineProperty(result, key, { + enumerable: true, + configurable: true, + get: () => (query as Record)[key], + }); + } + return result; +}; + /** * Retrieve all static analyses performed on a binary. * @summary List static analyses for a binary @@ -140,7 +155,7 @@ export function useListAnalyses> queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -250,7 +265,7 @@ export function useGetAnalysis>, queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -376,7 +391,7 @@ export function useListStructures< queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -486,7 +501,7 @@ export function useListStrings>, queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -596,7 +611,7 @@ export function useListSegments> queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -706,7 +721,7 @@ export function useListImports>, queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -832,7 +847,7 @@ export function useListGlobalVars< queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -965,7 +980,7 @@ export function useListFunctions< queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -1075,5 +1090,5 @@ export function useListEnums>, TErr queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } diff --git a/src/api/generated/api.schemas.ts b/src/api/generated/api.schemas.ts index 9654e0d..a08aeaf 100644 --- a/src/api/generated/api.schemas.ts +++ b/src/api/generated/api.schemas.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.15.0 🍺 + * Generated by orval v8.20.0 🍺 * Do not edit manually. * OpenAPI definition * OpenAPI spec version: v0 @@ -84,6 +84,23 @@ export interface LibraryResponse { createdAt?: string; } +export interface SymbolEntry { + moduleName?: string; + className?: string; + symbolName?: string; + ordinal?: number; + symbolType?: string; + parameters?: string; + category?: string; +} + +export interface Library { + name?: string; + description?: string; + aliases?: string[]; + symbols?: SymbolEntry[]; +} + export type JobResponseType = (typeof JobResponseType)[keyof typeof JobResponseType]; export const JobResponseType = { @@ -160,8 +177,8 @@ export interface PageableObject { paged?: boolean; pageNumber?: number; pageSize?: number; - unpaged?: boolean; sort?: SortObject; + unpaged?: boolean; } export interface PageLibrarySymbolResponse { @@ -171,10 +188,10 @@ export interface PageLibrarySymbolResponse { content?: LibrarySymbolResponse[]; number?: number; pageable?: PageableObject; - numberOfElements?: number; - sort?: SortObject; first?: boolean; last?: boolean; + sort?: SortObject; + numberOfElements?: number; empty?: boolean; } @@ -357,10 +374,10 @@ export interface PageFunctionSummaryResponse { content?: FunctionSummaryResponse[]; number?: number; pageable?: PageableObject; - numberOfElements?: number; - sort?: SortObject; first?: boolean; last?: boolean; + sort?: SortObject; + numberOfElements?: number; empty?: boolean; } diff --git a/src/api/generated/binary/binary.ts b/src/api/generated/binary/binary.ts index 73805c8..b902fbd 100644 --- a/src/api/generated/binary/binary.ts +++ b/src/api/generated/binary/binary.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.15.0 🍺 + * Generated by orval v8.20.0 🍺 * Do not edit manually. * OpenAPI definition * OpenAPI spec version: v0 @@ -35,6 +35,21 @@ import { customInstance } from "../../api"; type SecondParameter unknown> = Parameters[1]; +const withQueryKey = (query: T, queryKey: K): T & { queryKey: K } => { + const result = { queryKey } as T & { queryKey: K }; + for (const key of Object.keys(query)) { + // The explicit queryKey always wins, matching the previous + // `{ ...query, queryKey }` spread where it was set last. + if (key === "queryKey") continue; + Object.defineProperty(result, key, { + enumerable: true, + configurable: true, + get: () => (query as Record)[key], + }); + } + return result; +}; + /** * Retrieve all binaries belonging to a project, optionally filtered by filename. * @summary List binaries of a project @@ -153,7 +168,7 @@ export function useGetBinaries>, queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -370,7 +385,7 @@ export function useGetDependencies< queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -651,7 +666,7 @@ export function useGetBinary>, TErr queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -852,7 +867,7 @@ export function useDownloadBinary< queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** diff --git a/src/api/generated/chat/chat.ts b/src/api/generated/chat/chat.ts index 07b536b..da9c7de 100644 --- a/src/api/generated/chat/chat.ts +++ b/src/api/generated/chat/chat.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.15.0 🍺 + * Generated by orval v8.20.0 🍺 * Do not edit manually. * OpenAPI definition * OpenAPI spec version: v0 @@ -26,6 +26,21 @@ import { customInstance } from "../../api"; type SecondParameter unknown> = Parameters[1]; +const withQueryKey = (query: T, queryKey: K): T & { queryKey: K } => { + const result = { queryKey } as T & { queryKey: K }; + for (const key of Object.keys(query)) { + // The explicit queryKey always wins, matching the previous + // `{ ...query, queryKey }` spread where it was set last. + if (key === "queryKey") continue; + Object.defineProperty(result, key, { + enumerable: true, + configurable: true, + get: () => (query as Record)[key], + }); + } + return result; +}; + /** * @summary List chat sessions for a binary */ @@ -136,7 +151,7 @@ export function useListSessions> queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -324,7 +339,7 @@ export function useGetSession>, TE queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -510,5 +525,5 @@ export function useGetMessages>, queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } diff --git a/src/api/generated/engine/engine.ts b/src/api/generated/engine/engine.ts index 6b76587..76ce891 100644 --- a/src/api/generated/engine/engine.ts +++ b/src/api/generated/engine/engine.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.15.0 🍺 + * Generated by orval v8.20.0 🍺 * Do not edit manually. * OpenAPI definition * OpenAPI spec version: v0 @@ -23,6 +23,21 @@ import { customInstance } from "../../api"; type SecondParameter unknown> = Parameters[1]; +const withQueryKey = (query: T, queryKey: K): T & { queryKey: K } => { + const result = { queryKey } as T & { queryKey: K }; + for (const key of Object.keys(query)) { + // The explicit queryKey always wins, matching the previous + // `{ ...query, queryKey }` spread where it was set last. + if (key === "queryKey") continue; + Object.defineProperty(result, key, { + enumerable: true, + configurable: true, + get: () => (query as Record)[key], + }); + } + return result; +}; + /** * Retrieve all registered static analysis engines with their availability status. * @summary List available analysis engines @@ -116,5 +131,5 @@ export function useListEngines>, queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } diff --git a/src/api/generated/function/function.ts b/src/api/generated/function/function.ts index aaf359f..b28739e 100644 --- a/src/api/generated/function/function.ts +++ b/src/api/generated/function/function.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.15.0 🍺 + * Generated by orval v8.20.0 🍺 * Do not edit manually. * OpenAPI definition * OpenAPI spec version: v0 @@ -23,6 +23,21 @@ import { customInstance } from "../../api"; type SecondParameter unknown> = Parameters[1]; +const withQueryKey = (query: T, queryKey: K): T & { queryKey: K } => { + const result = { queryKey } as T & { queryKey: K }; + for (const key of Object.keys(query)) { + // The explicit queryKey always wins, matching the previous + // `{ ...query, queryKey }` spread where it was set last. + if (key === "queryKey") continue; + Object.defineProperty(result, key, { + enumerable: true, + configurable: true, + get: () => (query as Record)[key], + }); + } + return result; +}; + /** * Retrieve full details of a function including assembly, decompiled code, and xref counts. * @summary Get function details @@ -130,7 +145,7 @@ export function useGetFunction>, queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -240,7 +255,7 @@ export function useGetCallers>, TE queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -350,5 +365,5 @@ export function useGetCallees>, TE queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } diff --git a/src/api/generated/job/job.ts b/src/api/generated/job/job.ts index 411c73f..708ff83 100644 --- a/src/api/generated/job/job.ts +++ b/src/api/generated/job/job.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.15.0 🍺 + * Generated by orval v8.20.0 🍺 * Do not edit manually. * OpenAPI definition * OpenAPI spec version: v0 @@ -26,6 +26,21 @@ import { customInstance } from "../../api"; type SecondParameter unknown> = Parameters[1]; +const withQueryKey = (query: T, queryKey: K): T & { queryKey: K } => { + const result = { queryKey } as T & { queryKey: K }; + for (const key of Object.keys(query)) { + // The explicit queryKey always wins, matching the previous + // `{ ...query, queryKey }` spread where it was set last. + if (key === "queryKey") continue; + Object.defineProperty(result, key, { + enumerable: true, + configurable: true, + get: () => (query as Record)[key], + }); + } + return result; +}; + /** * Cancel a job that is enqueued, started, or in progress. Returns 409 if the job is already in a final state. * @summary Cancel a job @@ -200,7 +215,7 @@ export function useGetJobs>, TError = queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -307,5 +322,5 @@ export function useGetJob>, TError = u queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } diff --git a/src/api/generated/library-symbols/library-symbols.ts b/src/api/generated/library-symbols/library-symbols.ts index d647724..27bbcae 100644 --- a/src/api/generated/library-symbols/library-symbols.ts +++ b/src/api/generated/library-symbols/library-symbols.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.15.0 🍺 + * Generated by orval v8.20.0 🍺 * Do not edit manually. * OpenAPI definition * OpenAPI spec version: v0 @@ -21,6 +21,7 @@ import type { } from "@tanstack/react-query"; import type { + Library, LibraryResponse, ListLibrarySymbolsParams, PageLibrarySymbolResponse, @@ -34,6 +35,21 @@ import { customInstance } from "../../api"; type SecondParameter unknown> = Parameters[1]; +const withQueryKey = (query: T, queryKey: K): T & { queryKey: K } => { + const result = { queryKey } as T & { queryKey: K }; + for (const key of Object.keys(query)) { + // The explicit queryKey always wins, matching the previous + // `{ ...query, queryKey }` spread where it was set last. + if (key === "queryKey") continue; + Object.defineProperty(result, key, { + enumerable: true, + configurable: true, + get: () => (query as Record)[key], + }); + } + return result; +}; + /** * Parse and store symbols from a library dump file (e.g., TurboDump output). * @summary Upload a library symbol dump @@ -184,6 +200,86 @@ export const useResolveAllSymbols = ( ): UseMutationResult>, TError, void, TContext> => { return useMutation(getResolveAllSymbolsMutationOptions(options), queryClient); }; +/** + * Create a new library from a previously exported JSON backup. The new library gets its own IDs — existing libraries are not affected. + * @summary Import a library from JSON export + */ +export const importLibrary = ( + library: Library, + options?: SecondParameter, + signal?: AbortSignal +) => { + return customInstance( + { + url: `/libraries/import`, + method: "POST", + headers: { "Content-Type": "application/json" }, + data: library, + signal, + }, + options + ); +}; + +export const getImportLibraryMutationOptions = (options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: Library }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: Library }, + TContext +> => { + const mutationKey = ["importLibrary"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && "mutationKey" in options.mutation && options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: Library } + > = (props) => { + const { data } = props ?? {}; + + return importLibrary(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type ImportLibraryMutationResult = NonNullable>>; +export type ImportLibraryMutationBody = Library; +export type ImportLibraryMutationError = unknown; + +/** + * @summary Import a library from JSON export + */ +export const useImportLibrary = ( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: Library }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseMutationResult< + Awaited>, + TError, + { data: Library }, + TContext +> => { + return useMutation(getImportLibraryMutationOptions(options), queryClient); +}; /** * Cross-reference imports from a specific analysis against all loaded library symbol databases. * @summary Resolve symbols for an analysis @@ -367,7 +463,7 @@ export function useListLibraries< queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -477,7 +573,7 @@ export function useGetLibrary>, TE queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -691,7 +787,129 @@ export function useListLibrarySymbols< queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); +} + +/** + * Download the complete library (name, description, aliases, and all symbols) as a JSON file for backup or transfer. + * @summary Export a library as JSON + */ +export const exportLibrary = ( + libraryId: string, + options?: SecondParameter, + signal?: AbortSignal +) => { + return customInstance( + { url: `/libraries/${encodeURIComponent(String(libraryId))}/export`, method: "GET", signal }, + options + ); +}; + +export const getExportLibraryQueryKey = (libraryId: string) => { + return [`/libraries/${libraryId}/export`] as const; +}; + +export const getExportLibraryQueryOptions = < + TData = Awaited>, + TError = unknown, +>( + libraryId: string, + options?: { + query?: Partial>, TError, TData>>; + request?: SecondParameter; + } +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getExportLibraryQueryKey(libraryId); + + const queryFn: QueryFunction>> = ({ signal }) => + exportLibrary(libraryId, requestOptions, signal); + + return { + queryKey, + queryFn, + enabled: libraryId !== null && libraryId !== undefined, + ...queryOptions, + } as UseQueryOptions>, TError, TData> & { + queryKey: DataTag; + }; +}; + +export type ExportLibraryQueryResult = NonNullable>>; +export type ExportLibraryQueryError = unknown; + +export function useExportLibrary< + TData = Awaited>, + TError = unknown, +>( + libraryId: string, + options: { + query: Partial>, TError, TData>> & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): DefinedUseQueryResult & { queryKey: DataTag }; +export function useExportLibrary< + TData = Awaited>, + TError = unknown, +>( + libraryId: string, + options?: { + query?: Partial>, TError, TData>> & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +export function useExportLibrary< + TData = Awaited>, + TError = unknown, +>( + libraryId: string, + options?: { + query?: Partial>, TError, TData>>; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +/** + * @summary Export a library as JSON + */ + +export function useExportLibrary< + TData = Awaited>, + TError = unknown, +>( + libraryId: string, + options?: { + query?: Partial>, TError, TData>>; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag } { + const queryOptions = getExportLibraryQueryOptions(libraryId, options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { + queryKey: DataTag; + }; + + return withQueryKey(query, queryOptions.queryKey); } /** @@ -787,5 +1005,5 @@ export function useListParsers>, queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } diff --git a/src/api/generated/mutation/mutation.ts b/src/api/generated/mutation/mutation.ts index 2682421..40f5f11 100644 --- a/src/api/generated/mutation/mutation.ts +++ b/src/api/generated/mutation/mutation.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.15.0 🍺 + * Generated by orval v8.20.0 🍺 * Do not edit manually. * OpenAPI definition * OpenAPI spec version: v0 @@ -26,6 +26,21 @@ import { customInstance } from "../../api"; type SecondParameter unknown> = Parameters[1]; +const withQueryKey = (query: T, queryKey: K): T & { queryKey: K } => { + const result = { queryKey } as T & { queryKey: K }; + for (const key of Object.keys(query)) { + // The explicit queryKey always wins, matching the previous + // `{ ...query, queryKey }` spread where it was set last. + if (key === "queryKey") continue; + Object.defineProperty(result, key, { + enumerable: true, + configurable: true, + get: () => (query as Record)[key], + }); + } + return result; +}; + /** * Reject the suggested mutation without applying it. * @summary Reject a mutation suggestion @@ -306,5 +321,5 @@ export function useListMutations< queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } diff --git a/src/api/generated/project/project.ts b/src/api/generated/project/project.ts index cd71149..3f70ed5 100644 --- a/src/api/generated/project/project.ts +++ b/src/api/generated/project/project.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.15.0 🍺 + * Generated by orval v8.20.0 🍺 * Do not edit manually. * OpenAPI definition * OpenAPI spec version: v0 @@ -26,6 +26,21 @@ import { customInstance } from "../../api"; type SecondParameter unknown> = Parameters[1]; +const withQueryKey = (query: T, queryKey: K): T & { queryKey: K } => { + const result = { queryKey } as T & { queryKey: K }; + for (const key of Object.keys(query)) { + // The explicit queryKey always wins, matching the previous + // `{ ...query, queryKey }` spread where it was set last. + if (key === "queryKey") continue; + Object.defineProperty(result, key, { + enumerable: true, + configurable: true, + get: () => (query as Record)[key], + }); + } + return result; +}; + /** * Retrieve a single project by ID. * @summary Get a project @@ -133,7 +148,7 @@ export function useGetProject>, TE queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -409,7 +424,7 @@ export function useGetProjects>, queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** diff --git a/src/api/generated/workspace/workspace.ts b/src/api/generated/workspace/workspace.ts index d5825f5..9b4278b 100644 --- a/src/api/generated/workspace/workspace.ts +++ b/src/api/generated/workspace/workspace.ts @@ -1,5 +1,5 @@ /** - * Generated by orval v8.15.0 🍺 + * Generated by orval v8.20.0 🍺 * Do not edit manually. * OpenAPI definition * OpenAPI spec version: v0 @@ -26,6 +26,21 @@ import { customInstance } from "../../api"; type SecondParameter unknown> = Parameters[1]; +const withQueryKey = (query: T, queryKey: K): T & { queryKey: K } => { + const result = { queryKey } as T & { queryKey: K }; + for (const key of Object.keys(query)) { + // The explicit queryKey always wins, matching the previous + // `{ ...query, queryKey }` spread where it was set last. + if (key === "queryKey") continue; + Object.defineProperty(result, key, { + enumerable: true, + configurable: true, + get: () => (query as Record)[key], + }); + } + return result; +}; + /** * Retrieve a single workspace by its unique identifier. * @summary Get a workspace by ID @@ -133,7 +148,7 @@ export function useGetWorkspace> queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** @@ -411,7 +426,7 @@ export function useGetWorkspaces< queryKey: DataTag; }; - return { ...query, queryKey: queryOptions.queryKey }; + return withQueryKey(query, queryOptions.queryKey); } /** diff --git a/src/features/libraries/ImportLibraryDialog.tsx b/src/features/libraries/ImportLibraryDialog.tsx new file mode 100644 index 0000000..7d74929 --- /dev/null +++ b/src/features/libraries/ImportLibraryDialog.tsx @@ -0,0 +1,166 @@ +import { useState, useRef } from "react"; +import { Upload, Loader2, BookOpen, Hash, Tag } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog.tsx"; +import { Button } from "@/components/ui/button.tsx"; +import { Badge } from "@/components/ui/badge.tsx"; +import type { Library } from "@/api/generated/api.schemas.ts"; + +const MAX_FILE_SIZE = 50 * 1024 * 1024; + +interface ImportLibraryDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + isImporting: boolean; + onImport: (data: Library) => void; +} + +export const ImportLibraryDialog = ({ + open, + onOpenChange, + isImporting, + onImport, +}: ImportLibraryDialogProps) => { + const fileInputRef = useRef(null); + const [parsedData, setParsedData] = useState(null); + const [parseError, setParseError] = useState(null); + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] ?? null; + setParseError(null); + setParsedData(null); + + if (!file) return; + + if (file.size > MAX_FILE_SIZE) { + setParseError("File must be under 50MB"); + return; + } + + if (!file.name.endsWith(".json")) { + setParseError("Only .json files are accepted"); + return; + } + + const reader = new FileReader(); + reader.onload = () => { + try { + const json = JSON.parse(reader.result as string); + if (!json.name || typeof json.name !== "string" || json.name.trim() === "") { + setParseError('Invalid format: "name" field is required'); + return; + } + setParsedData(json); + } catch { + setParseError("Invalid JSON file"); + } + }; + reader.onerror = () => { + setParseError("Failed to read file"); + }; + reader.readAsText(file); + }; + + const handleImport = () => { + if (!parsedData) return; + onImport(parsedData); + }; + + const handleClose = () => { + if (!isImporting) { + setParsedData(null); + setParseError(null); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + } + onOpenChange(false); + }; + + const symbolCount = parsedData?.symbols?.length ?? 0; + const aliasCount = parsedData?.aliases?.length ?? 0; + + return ( + + + + Import Library + + Restore a library from a previously exported JSON backup. + + + +
+
+ + + {parseError &&

{parseError}

} +
+ + {parsedData && ( +
+
+ + {parsedData.name} +
+ + {parsedData.description && ( +

+ {parsedData.description} +

+ )} + +
+ + + {symbolCount} symbols + + {aliasCount > 0 && ( + + + {aliasCount} aliases + + )} +
+
+ )} + + + + + +
+
+
+ ); +}; diff --git a/src/features/libraries/LibraryCard.tsx b/src/features/libraries/LibraryCard.tsx index d8c5785..766c782 100644 --- a/src/features/libraries/LibraryCard.tsx +++ b/src/features/libraries/LibraryCard.tsx @@ -1,4 +1,4 @@ -import { Eye, Trash2, BookOpen } from "lucide-react"; +import { Eye, Trash2, BookOpen, Download } from "lucide-react"; import { Card, CardContent, CardFooter } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -9,9 +9,10 @@ interface LibraryCardProps { library: LibraryResponse; onDelete: (library: LibraryResponse) => void; onViewSymbols: (library: LibraryResponse) => void; + onExport: (library: LibraryResponse) => void; } -export const LibraryCard = ({ library, onDelete, onViewSymbols }: LibraryCardProps) => { +export const LibraryCard = ({ library, onDelete, onViewSymbols, onExport }: LibraryCardProps) => { return ( @@ -45,6 +46,9 @@ export const LibraryCard = ({ library, onDelete, onViewSymbols }: LibraryCardPro + {libraries.length > 0 && (