diff --git a/package-lock.json b/package-lock.json index 04f8ed5..2ec5c85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-slot": "^1.2.5", "@tailwindcss/vite": "^4.3.0", "@tanstack/react-query": "^5.101.0", "axios": "^1.17.0", @@ -1531,6 +1531,24 @@ } } }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-menu": { "version": "2.1.16", "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", @@ -1742,12 +1760,12 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", - "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.5.tgz", + "integrity": "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", @@ -1759,6 +1777,21 @@ } } }, + "node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", + "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", diff --git a/package.json b/package.json index 403d289..1b1fbb4 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", - "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-slot": "^1.2.5", "@tailwindcss/vite": "^4.3.0", "@tanstack/react-query": "^5.101.0", "axios": "^1.17.0", diff --git a/src/api/generated/api.schemas.ts b/src/api/generated/api.schemas.ts index 61bd4fb..48a6d25 100644 --- a/src/api/generated/api.schemas.ts +++ b/src/api/generated/api.schemas.ts @@ -11,12 +11,12 @@ export interface ProjectRequest { } export interface ProjectResponse { - id?: string; - name?: string; + id: string; + workspaceId: string; + /** @minLength 1 */ + name: string; description?: string; - workspaceId?: string; - updatedAt?: string; - createdAt?: string; + updatedAt: string; } export interface WorkspaceRequest { @@ -40,3 +40,10 @@ export type GetWorkspacesParams = { */ name?: string; }; + +export type GetProjectsParams = { + /** + * Filter projects whose name contains this value (case-insensitive) + */ + name?: string; +}; diff --git a/src/api/generated/project/project.ts b/src/api/generated/project/project.ts index 81ed54a..b941398 100644 --- a/src/api/generated/project/project.ts +++ b/src/api/generated/project/project.ts @@ -20,7 +20,7 @@ import type { UseQueryResult, } from "@tanstack/react-query"; -import type { ProjectRequest, ProjectResponse } from "../api.schemas"; +import type { GetProjectsParams, ProjectRequest, ProjectResponse } from "../api.schemas"; import { customInstance } from "../../api"; @@ -188,11 +188,12 @@ export const useDeleteProject = ( return useMutation(getDeleteProjectMutationOptions(options), queryClient); }; /** - * Retrieve all projects belonging to a workspace. + * Retrieve all projects belonging to a workspace, optionally filtered by name. * @summary List projects of a workspace */ export const getProjects = ( workspaceId: string, + params?: GetProjectsParams, options?: SecondParameter, signal?: AbortSignal ) => { @@ -200,14 +201,15 @@ export const getProjects = ( { url: `/workspaces/${encodeURIComponent(String(workspaceId))}/projects`, method: "GET", + params, signal, }, options ); }; -export const getGetProjectsQueryKey = (workspaceId: string) => { - return [`/workspaces/${workspaceId}/projects`] as const; +export const getGetProjectsQueryKey = (workspaceId: string, params?: GetProjectsParams) => { + return [`/workspaces/${workspaceId}/projects`, ...(params ? [params] : [])] as const; }; export const getGetProjectsQueryOptions = < @@ -215,6 +217,7 @@ export const getGetProjectsQueryOptions = < TError = unknown, >( workspaceId: string, + params?: GetProjectsParams, options?: { query?: Partial>, TError, TData>>; request?: SecondParameter; @@ -222,10 +225,10 @@ export const getGetProjectsQueryOptions = < ) => { const { query: queryOptions, request: requestOptions } = options ?? {}; - const queryKey = queryOptions?.queryKey ?? getGetProjectsQueryKey(workspaceId); + const queryKey = queryOptions?.queryKey ?? getGetProjectsQueryKey(workspaceId, params); const queryFn: QueryFunction>> = ({ signal }) => - getProjects(workspaceId, requestOptions, signal); + getProjects(workspaceId, params, requestOptions, signal); return { queryKey, @@ -242,6 +245,7 @@ export type GetProjectsQueryError = unknown; export function useGetProjects>, TError = unknown>( workspaceId: string, + params: undefined | GetProjectsParams, options: { query: Partial>, TError, TData>> & Pick< @@ -258,6 +262,7 @@ export function useGetProjects>, ): DefinedUseQueryResult & { queryKey: DataTag }; export function useGetProjects>, TError = unknown>( workspaceId: string, + params?: GetProjectsParams, options?: { query?: Partial>, TError, TData>> & Pick< @@ -274,6 +279,7 @@ export function useGetProjects>, ): UseQueryResult & { queryKey: DataTag }; export function useGetProjects>, TError = unknown>( workspaceId: string, + params?: GetProjectsParams, options?: { query?: Partial>, TError, TData>>; request?: SecondParameter; @@ -286,13 +292,14 @@ export function useGetProjects>, export function useGetProjects>, TError = unknown>( workspaceId: string, + params?: GetProjectsParams, options?: { query?: Partial>, TError, TData>>; request?: SecondParameter; }, queryClient?: QueryClient ): UseQueryResult & { queryKey: DataTag } { - const queryOptions = getGetProjectsQueryOptions(workspaceId, options); + const queryOptions = getGetProjectsQueryOptions(workspaceId, params, options); const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag; diff --git a/src/api/generated/workspace/workspace.ts b/src/api/generated/workspace/workspace.ts index a7cc0d0..d5825f5 100644 --- a/src/api/generated/workspace/workspace.ts +++ b/src/api/generated/workspace/workspace.ts @@ -26,6 +26,116 @@ import { customInstance } from "../../api"; type SecondParameter unknown> = Parameters[1]; +/** + * Retrieve a single workspace by its unique identifier. + * @summary Get a workspace by ID + */ +export const getWorkspace = ( + id: string, + options?: SecondParameter, + signal?: AbortSignal +) => { + return customInstance( + { url: `/workspaces/${encodeURIComponent(String(id))}`, method: "GET", signal }, + options + ); +}; + +export const getGetWorkspaceQueryKey = (id: string) => { + return [`/workspaces/${id}`] as const; +}; + +export const getGetWorkspaceQueryOptions = < + TData = Awaited>, + TError = unknown, +>( + id: string, + options?: { + query?: Partial>, TError, TData>>; + request?: SecondParameter; + } +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetWorkspaceQueryKey(id); + + const queryFn: QueryFunction>> = ({ signal }) => + getWorkspace(id, requestOptions, signal); + + return { + queryKey, + queryFn, + enabled: id !== null && id !== undefined, + ...queryOptions, + } as UseQueryOptions>, TError, TData> & { + queryKey: DataTag; + }; +}; + +export type GetWorkspaceQueryResult = NonNullable>>; +export type GetWorkspaceQueryError = unknown; + +export function useGetWorkspace>, TError = unknown>( + id: string, + options: { + query: Partial>, TError, TData>> & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): DefinedUseQueryResult & { queryKey: DataTag }; +export function useGetWorkspace>, TError = unknown>( + id: string, + options?: { + query?: Partial>, TError, TData>> & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +export function useGetWorkspace>, TError = unknown>( + id: string, + options?: { + query?: Partial>, TError, TData>>; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +/** + * @summary Get a workspace by ID + */ + +export function useGetWorkspace>, TError = unknown>( + id: string, + options?: { + query?: Partial>, TError, TData>>; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag } { + const queryOptions = getGetWorkspaceQueryOptions(id, options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { + queryKey: DataTag; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + /** * Update a workspace * @summary Update a workspace diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx new file mode 100644 index 0000000..83d5734 --- /dev/null +++ b/src/components/Breadcrumbs.tsx @@ -0,0 +1,50 @@ +import { Fragment } from "react"; +import { Link } from "react-router"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb.tsx"; +import { Home } from "lucide-react"; + +interface Breadcrumb { + label: string; + href?: string; +} + +interface BreadcrumbsProps { + items?: Breadcrumb[]; +} + +export const Breadcrumbs = ({ items = [] }: BreadcrumbsProps) => { + return ( + + + + + + + + + + + {items.map((item, index, array) => ( + + + {item.href ? ( + + {item.label} + + ) : ( + item.label + )} + + {index !== array.length - 1 && } + + ))} + + + ); +}; diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..5175f3d --- /dev/null +++ b/src/components/ui/breadcrumb.tsx @@ -0,0 +1,101 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode; + } +>(({ ...props }, ref) =>