feat(libraries): add export (JSON download) and import (JSON restore) for library symbols

This commit is contained in:
Rodrigo Verdiani 2026-07-04 16:04:59 -03:00
parent 75637bc265
commit 914005e00e
14 changed files with 650 additions and 50 deletions

View File

@ -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<T extends (...args: never) => unknown> = Parameters<T>[1];
const withQueryKey = <T extends object, K>(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<string, unknown>)[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<TData = Awaited<ReturnType<typeof listAnalyses>>
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -250,7 +265,7 @@ export function useGetAnalysis<TData = Awaited<ReturnType<typeof getAnalysis>>,
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -376,7 +391,7 @@ export function useListStructures<
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -486,7 +501,7 @@ export function useListStrings<TData = Awaited<ReturnType<typeof listStrings>>,
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -596,7 +611,7 @@ export function useListSegments<TData = Awaited<ReturnType<typeof listSegments>>
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -706,7 +721,7 @@ export function useListImports<TData = Awaited<ReturnType<typeof listImports>>,
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -832,7 +847,7 @@ export function useListGlobalVars<
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -965,7 +980,7 @@ export function useListFunctions<
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -1075,5 +1090,5 @@ export function useListEnums<TData = Awaited<ReturnType<typeof listEnums>>, TErr
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}

View File

@ -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;
}

View File

@ -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<T extends (...args: never) => unknown> = Parameters<T>[1];
const withQueryKey = <T extends object, K>(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<string, unknown>)[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<TData = Awaited<ReturnType<typeof getBinaries>>,
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -370,7 +385,7 @@ export function useGetDependencies<
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -651,7 +666,7 @@ export function useGetBinary<TData = Awaited<ReturnType<typeof getBinary>>, TErr
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -852,7 +867,7 @@ export function useDownloadBinary<
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**

View File

@ -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<T extends (...args: never) => unknown> = Parameters<T>[1];
const withQueryKey = <T extends object, K>(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<string, unknown>)[key],
});
}
return result;
};
/**
* @summary List chat sessions for a binary
*/
@ -136,7 +151,7 @@ export function useListSessions<TData = Awaited<ReturnType<typeof listSessions>>
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -324,7 +339,7 @@ export function useGetSession<TData = Awaited<ReturnType<typeof getSession>>, TE
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -510,5 +525,5 @@ export function useGetMessages<TData = Awaited<ReturnType<typeof getMessages>>,
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}

View File

@ -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<T extends (...args: never) => unknown> = Parameters<T>[1];
const withQueryKey = <T extends object, K>(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<string, unknown>)[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<TData = Awaited<ReturnType<typeof listEngines>>,
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}

View File

@ -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<T extends (...args: never) => unknown> = Parameters<T>[1];
const withQueryKey = <T extends object, K>(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<string, unknown>)[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<TData = Awaited<ReturnType<typeof getFunction>>,
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -240,7 +255,7 @@ export function useGetCallers<TData = Awaited<ReturnType<typeof getCallers>>, TE
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -350,5 +365,5 @@ export function useGetCallees<TData = Awaited<ReturnType<typeof getCallees>>, TE
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}

View File

@ -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<T extends (...args: never) => unknown> = Parameters<T>[1];
const withQueryKey = <T extends object, K>(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<string, unknown>)[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<TData = Awaited<ReturnType<typeof getJobs>>, TError =
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -307,5 +322,5 @@ export function useGetJob<TData = Awaited<ReturnType<typeof getJob>>, TError = u
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}

View File

@ -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<T extends (...args: never) => unknown> = Parameters<T>[1];
const withQueryKey = <T extends object, K>(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<string, unknown>)[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 = <TError = unknown, TContext = unknown>(
): UseMutationResult<Awaited<ReturnType<typeof resolveAllSymbols>>, 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<typeof customInstance>,
signal?: AbortSignal
) => {
return customInstance<LibraryResponse>(
{
url: `/libraries/import`,
method: "POST",
headers: { "Content-Type": "application/json" },
data: library,
signal,
},
options
);
};
export const getImportLibraryMutationOptions = <TError = unknown, TContext = unknown>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof importLibrary>>,
TError,
{ data: Library },
TContext
>;
request?: SecondParameter<typeof customInstance>;
}): UseMutationOptions<
Awaited<ReturnType<typeof importLibrary>>,
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<ReturnType<typeof importLibrary>>,
{ data: Library }
> = (props) => {
const { data } = props ?? {};
return importLibrary(data, requestOptions);
};
return { mutationFn, ...mutationOptions };
};
export type ImportLibraryMutationResult = NonNullable<Awaited<ReturnType<typeof importLibrary>>>;
export type ImportLibraryMutationBody = Library;
export type ImportLibraryMutationError = unknown;
/**
* @summary Import a library from JSON export
*/
export const useImportLibrary = <TError = unknown, TContext = unknown>(
options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof importLibrary>>,
TError,
{ data: Library },
TContext
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseMutationResult<
Awaited<ReturnType<typeof importLibrary>>,
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<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -477,7 +573,7 @@ export function useGetLibrary<TData = Awaited<ReturnType<typeof getLibrary>>, TE
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -691,7 +787,129 @@ export function useListLibrarySymbols<
queryKey: DataTag<QueryKey, TData, TError>;
};
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<typeof customInstance>,
signal?: AbortSignal
) => {
return customInstance<Library>(
{ 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<ReturnType<typeof exportLibrary>>,
TError = unknown,
>(
libraryId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof exportLibrary>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
}
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getExportLibraryQueryKey(libraryId);
const queryFn: QueryFunction<Awaited<ReturnType<typeof exportLibrary>>> = ({ signal }) =>
exportLibrary(libraryId, requestOptions, signal);
return {
queryKey,
queryFn,
enabled: libraryId !== null && libraryId !== undefined,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof exportLibrary>>, TError, TData> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
};
export type ExportLibraryQueryResult = NonNullable<Awaited<ReturnType<typeof exportLibrary>>>;
export type ExportLibraryQueryError = unknown;
export function useExportLibrary<
TData = Awaited<ReturnType<typeof exportLibrary>>,
TError = unknown,
>(
libraryId: string,
options: {
query: Partial<UseQueryOptions<Awaited<ReturnType<typeof exportLibrary>>, TError, TData>> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof exportLibrary>>,
TError,
Awaited<ReturnType<typeof exportLibrary>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useExportLibrary<
TData = Awaited<ReturnType<typeof exportLibrary>>,
TError = unknown,
>(
libraryId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof exportLibrary>>, TError, TData>> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof exportLibrary>>,
TError,
Awaited<ReturnType<typeof exportLibrary>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useExportLibrary<
TData = Awaited<ReturnType<typeof exportLibrary>>,
TError = unknown,
>(
libraryId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof exportLibrary>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
/**
* @summary Export a library as JSON
*/
export function useExportLibrary<
TData = Awaited<ReturnType<typeof exportLibrary>>,
TError = unknown,
>(
libraryId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof exportLibrary>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getExportLibraryQueryOptions(libraryId, options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -787,5 +1005,5 @@ export function useListParsers<TData = Awaited<ReturnType<typeof listParsers>>,
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}

View File

@ -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<T extends (...args: never) => unknown> = Parameters<T>[1];
const withQueryKey = <T extends object, K>(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<string, unknown>)[key],
});
}
return result;
};
/**
* Reject the suggested mutation without applying it.
* @summary Reject a mutation suggestion
@ -306,5 +321,5 @@ export function useListMutations<
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}

View File

@ -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<T extends (...args: never) => unknown> = Parameters<T>[1];
const withQueryKey = <T extends object, K>(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<string, unknown>)[key],
});
}
return result;
};
/**
* Retrieve a single project by ID.
* @summary Get a project
@ -133,7 +148,7 @@ export function useGetProject<TData = Awaited<ReturnType<typeof getProject>>, TE
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -409,7 +424,7 @@ export function useGetProjects<TData = Awaited<ReturnType<typeof getProjects>>,
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**

View File

@ -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<T extends (...args: never) => unknown> = Parameters<T>[1];
const withQueryKey = <T extends object, K>(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<string, unknown>)[key],
});
}
return result;
};
/**
* Retrieve a single workspace by its unique identifier.
* @summary Get a workspace by ID
@ -133,7 +148,7 @@ export function useGetWorkspace<TData = Awaited<ReturnType<typeof getWorkspace>>
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**
@ -411,7 +426,7 @@ export function useGetWorkspaces<
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
return withQueryKey(query, queryOptions.queryKey);
}
/**

View File

@ -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<HTMLInputElement>(null);
const [parsedData, setParsedData] = useState<Library | null>(null);
const [parseError, setParseError] = useState<string | null>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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 (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Import Library</DialogTitle>
<DialogDescription>
Restore a library from a previously exported JSON backup.
</DialogDescription>
</DialogHeader>
<div className="space-y-5">
<div className="space-y-2">
<label htmlFor="import-file" className="text-sm font-medium">
JSON backup file *
</label>
<input
ref={fileInputRef}
id="import-file"
type="file"
accept=".json"
onChange={handleFileChange}
disabled={isImporting}
className="file:bg-primary file:text-primary-foreground hover:file:bg-primary/90 block w-full text-sm file:mr-3 file:cursor-pointer file:rounded-md file:border-0 file:px-3 file:py-1.5 file:text-sm file:font-medium"
/>
{parseError && <p className="text-destructive text-xs">{parseError}</p>}
</div>
{parsedData && (
<div className="bg-muted/50 space-y-3 rounded-md p-3">
<div className="flex items-center gap-2">
<BookOpen className="text-muted-foreground h-4 w-4" />
<span className="truncate font-medium">{parsedData.name}</span>
</div>
{parsedData.description && (
<p className="text-muted-foreground line-clamp-2 text-xs">
{parsedData.description}
</p>
)}
<div className="flex items-center gap-2">
<Badge variant="secondary" className="text-xs">
<Hash className="mr-1 h-3 w-3" />
{symbolCount} symbols
</Badge>
{aliasCount > 0 && (
<Badge variant="outline" className="text-xs">
<Tag className="mr-1 h-3 w-3" />
{aliasCount} aliases
</Badge>
)}
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={handleClose} disabled={isImporting}>
Cancel
</Button>
<Button onClick={handleImport} disabled={isImporting || !parsedData}>
{isImporting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Importing...
</>
) : (
<>
<Upload className="mr-2 h-4 w-4" />
Import
</>
)}
</Button>
</DialogFooter>
</div>
</DialogContent>
</Dialog>
);
};

View File

@ -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 (
<Card className="flex flex-col justify-between gap-2 p-4">
<CardContent className="space-y-2 p-0">
@ -45,6 +46,9 @@ export const LibraryCard = ({ library, onDelete, onViewSymbols }: LibraryCardPro
<Button variant="ghost" size="sm" onClick={() => onViewSymbols(library)} className="h-8">
<Eye className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="sm" onClick={() => onExport(library)} className="h-8">
<Download className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="sm"

View File

@ -1,17 +1,22 @@
import { Plus, BookOpen, Wand2 } from "lucide-react";
import { useState } from "react";
import { Plus, BookOpen, Wand2, FileUp } from "lucide-react";
import { Button } from "@/components/ui/button.tsx";
import { EmptyState } from "@/components/EmptyState.tsx";
import { ConfirmDialog } from "@/components/ConfirmDialog.tsx";
import { useLibrariesPage } from "@/features/libraries/hooks/useLibrariesPage.ts";
import { LibraryCard } from "@/features/libraries/LibraryCard.tsx";
import { UploadLibraryDialog } from "@/features/libraries/UploadLibraryDialog.tsx";
import { ImportLibraryDialog } from "@/features/libraries/ImportLibraryDialog.tsx";
import { LibrarySymbolsDialog } from "@/features/libraries/LibrarySymbolsDialog.tsx";
import {
useUploadLibrary,
useImportLibrary,
getListLibrariesQueryKey,
} from "@/api/generated/library-symbols/library-symbols.ts";
import { Api } from "@/api/api.ts";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import type { LibraryResponse, Library } from "@/api/generated/api.schemas.ts";
const Libraries = () => {
const queryClient = useQueryClient();
@ -31,6 +36,45 @@ const Libraries = () => {
handleViewSymbols,
} = useLibrariesPage();
const [importDialogOpen, setImportDialogOpen] = useState(false);
const handleExport = async (library: LibraryResponse) => {
if (!library.id) return;
try {
const response = await Api.get(`/libraries/${encodeURIComponent(library.id)}/export`, {
responseType: "blob",
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a");
link.href = url;
const filename = (library.name ?? "library").replace(/[^a-zA-Z0-9._-]/g, "_");
link.setAttribute("download", `${filename}.json`);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
} catch {
toast.error("Failed to export library");
}
};
const importMutation = useImportLibrary({
mutation: {
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: getListLibrariesQueryKey() });
toast.success(`${data.name} imported — ${data.symbolCount} symbols, ${data.aliasCount} aliases`);
setImportDialogOpen(false);
},
onError: () => {
toast.error("Failed to import library. Make sure the file is a valid export.");
},
},
});
const handleImport = (data: Library) => {
importMutation.mutate({ data });
};
const uploadMutation = useUploadLibrary({
mutation: {
onSuccess: (data) => {
@ -69,6 +113,14 @@ const Libraries = () => {
</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setImportDialogOpen(true)}
>
<FileUp className="mr-1.5 h-4 w-4" />
Import Library
</Button>
{libraries.length > 0 && (
<Button
variant="outline"
@ -107,6 +159,7 @@ const Libraries = () => {
library={lib}
onDelete={setDeleteTarget}
onViewSymbols={handleViewSymbols}
onExport={handleExport}
/>
))}
</div>
@ -119,6 +172,13 @@ const Libraries = () => {
onUpload={handleUpload}
/>
<ImportLibraryDialog
open={importDialogOpen}
onOpenChange={setImportDialogOpen}
isImporting={importMutation.isPending}
onImport={handleImport}
/>
<ConfirmDialog
open={!!deleteTarget}
onOpenChange={(open) => {