diff --git a/openapi.json b/openapi.json index 4c174ea..8f3efc4 100644 --- a/openapi.json +++ b/openapi.json @@ -1035,6 +1035,48 @@ } } }, + "/api/v1/auth/ws-token": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Issue a short-lived WebSocket token", + "description": "Issue a short-lived JWT (30 minutes) scoped to IM WebSocket access. The token is signed by the appks signing key and can be verified by imks either locally (via cached signing keys) or via RPC. The returned token should be passed as `{ token: }` in the Socket.IO CONNECT auth packet. Requires an authenticated session.", + "operationId": "authWsToken", + "responses": { + "200": { + "description": "Token issued successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse_WsTokenResponse" + } + } + } + }, + "401": { + "description": "The current session is unauthenticated or the login state has expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + }, + "500": { + "description": "Token issuance or Redis write failed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + } + } + } + } + }, "/api/v1/im/workspaces/{workspace_name}/categories": { "get": { "tags": [ @@ -44098,6 +44140,33 @@ } } }, + "ApiResponse_WsTokenResponse": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "description": "Response payload for `POST /auth/ws-token`.", + "required": [ + "token", + "expires_at" + ], + "properties": { + "expires_at": { + "type": "integer", + "format": "int64", + "description": "Unix timestamp (seconds) when the token expires." + }, + "token": { + "type": "string", + "description": "Short-lived JWT prefixed with \"Bearer \" for use in the Socket.IO CONNECT auth packet." + } + } + } + } + }, "ApiResponse_bool": { "type": "object", "required": [ @@ -55495,6 +55564,25 @@ "format": "uuid" } } + }, + "WsTokenResponse": { + "type": "object", + "description": "Response payload for `POST /auth/ws-token`.", + "required": [ + "token", + "expires_at" + ], + "properties": { + "expires_at": { + "type": "integer", + "format": "int64", + "description": "Unix timestamp (seconds) when the token expires." + }, + "token": { + "type": "string", + "description": "Short-lived JWT prefixed with \"Bearer \" for use in the Socket.IO CONNECT auth packet." + } + } } } }, diff --git a/src/client/index.ts b/src/client/index.ts index df9c783..78a73a4 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -214,6 +214,7 @@ export type { ApiResponse_WorkspacePendingApproval } from './models/ApiResponse_ export type { ApiResponse_WorkspaceSettings } from './models/ApiResponse_WorkspaceSettings'; export type { ApiResponse_WorkspaceStats } from './models/ApiResponse_WorkspaceStats'; export type { ApiResponse_WorkspaceWebhook } from './models/ApiResponse_WorkspaceWebhook'; +export type { ApiResponse_WsTokenResponse } from './models/ApiResponse_WsTokenResponse'; export type { AvatarData } from './models/AvatarData'; export type { BlameHunk } from './models/BlameHunk'; export type { BlameLine } from './models/BlameLine'; @@ -496,6 +497,7 @@ export type { WorkspacePendingApproval } from './models/WorkspacePendingApproval export type { WorkspaceSettings } from './models/WorkspaceSettings'; export type { WorkspaceStats } from './models/WorkspaceStats'; export type { WorkspaceWebhook } from './models/WorkspaceWebhook'; +export type { WsTokenResponse } from './models/WsTokenResponse'; export { $AcceptInvitationParams } from './schemas/$AcceptInvitationParams'; export { $AcceptInvitationRequest } from './schemas/$AcceptInvitationRequest'; @@ -704,6 +706,7 @@ export { $ApiResponse_WorkspacePendingApproval } from './schemas/$ApiResponse_Wo export { $ApiResponse_WorkspaceSettings } from './schemas/$ApiResponse_WorkspaceSettings'; export { $ApiResponse_WorkspaceStats } from './schemas/$ApiResponse_WorkspaceStats'; export { $ApiResponse_WorkspaceWebhook } from './schemas/$ApiResponse_WorkspaceWebhook'; +export { $ApiResponse_WsTokenResponse } from './schemas/$ApiResponse_WsTokenResponse'; export { $AvatarData } from './schemas/$AvatarData'; export { $BlameHunk } from './schemas/$BlameHunk'; export { $BlameLine } from './schemas/$BlameLine'; @@ -986,6 +989,7 @@ export { $WorkspacePendingApproval } from './schemas/$WorkspacePendingApproval'; export { $WorkspaceSettings } from './schemas/$WorkspaceSettings'; export { $WorkspaceStats } from './schemas/$WorkspaceStats'; export { $WorkspaceWebhook } from './schemas/$WorkspaceWebhook'; +export { $WsTokenResponse } from './schemas/$WsTokenResponse'; export { AuthService } from './services/AuthService'; export { GitService } from './services/GitService'; diff --git a/src/client/models/ApiResponse_WsTokenResponse.ts b/src/client/models/ApiResponse_WsTokenResponse.ts new file mode 100644 index 0000000..448bf77 --- /dev/null +++ b/src/client/models/ApiResponse_WsTokenResponse.ts @@ -0,0 +1,20 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ApiResponse_WsTokenResponse = { + /** + * Response payload for `POST /auth/ws-token`. + */ + data: { + /** + * Unix timestamp (seconds) when the token expires. + */ + expires_at: number; + /** + * Short-lived JWT prefixed with "Bearer " for use in the Socket.IO CONNECT auth packet. + */ + token: string; + }; +}; + diff --git a/src/client/models/WsTokenResponse.ts b/src/client/models/WsTokenResponse.ts new file mode 100644 index 0000000..e245398 --- /dev/null +++ b/src/client/models/WsTokenResponse.ts @@ -0,0 +1,18 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * Response payload for `POST /auth/ws-token`. + */ +export type WsTokenResponse = { + /** + * Unix timestamp (seconds) when the token expires. + */ + expires_at: number; + /** + * Short-lived JWT prefixed with "Bearer " for use in the Socket.IO CONNECT auth packet. + */ + token: string; +}; + diff --git a/src/client/schemas/$ApiResponse_WsTokenResponse.ts b/src/client/schemas/$ApiResponse_WsTokenResponse.ts new file mode 100644 index 0000000..ce34e5f --- /dev/null +++ b/src/client/schemas/$ApiResponse_WsTokenResponse.ts @@ -0,0 +1,25 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $ApiResponse_WsTokenResponse = { + properties: { + data: { + description: `Response payload for \`POST /auth/ws-token\`.`, + properties: { + expires_at: { + type: 'number', + description: `Unix timestamp (seconds) when the token expires.`, + isRequired: true, + format: 'int64', + }, + token: { + type: 'string', + description: `Short-lived JWT prefixed with "Bearer " for use in the Socket.IO CONNECT auth packet.`, + isRequired: true, + }, + }, + isRequired: true, + }, + }, +} as const; diff --git a/src/client/schemas/$WsTokenResponse.ts b/src/client/schemas/$WsTokenResponse.ts new file mode 100644 index 0000000..4349f13 --- /dev/null +++ b/src/client/schemas/$WsTokenResponse.ts @@ -0,0 +1,20 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $WsTokenResponse = { + description: `Response payload for \`POST /auth/ws-token\`.`, + properties: { + expires_at: { + type: 'number', + description: `Unix timestamp (seconds) when the token expires.`, + isRequired: true, + format: 'int64', + }, + token: { + type: 'string', + description: `Short-lived JWT prefixed with "Bearer " for use in the Socket.IO CONNECT auth packet.`, + isRequired: true, + }, + }, +} as const; diff --git a/src/client/services/AuthService.ts b/src/client/services/AuthService.ts index b064fe4..baf3508 100644 --- a/src/client/services/AuthService.ts +++ b/src/client/services/AuthService.ts @@ -12,6 +12,7 @@ import type { ApiResponse_Regenerate2FABackupCodesResponse } from '../models/Api import type { ApiResponse_RegisterEmailCodeResponse } from '../models/ApiResponse_RegisterEmailCodeResponse'; import type { ApiResponse_RegisterResponse } from '../models/ApiResponse_RegisterResponse'; import type { ApiResponse_RsaResponse } from '../models/ApiResponse_RsaResponse'; +import type { ApiResponse_WsTokenResponse } from '../models/ApiResponse_WsTokenResponse'; import type { ChangePasswordParams } from '../models/ChangePasswordParams'; import type { Disable2FAParams } from '../models/Disable2FAParams'; import type { EmailChangeRequest } from '../models/EmailChangeRequest'; @@ -450,4 +451,20 @@ export class AuthService { }, }); } + /** + * Issue a short-lived WebSocket token + * Issue a short-lived JWT (30 minutes) scoped to IM WebSocket access. The token is signed by the appks signing key and can be verified by imks either locally (via cached signing keys) or via RPC. The returned token should be passed as `{ token: }` in the Socket.IO CONNECT auth packet. Requires an authenticated session. + * @returns ApiResponse_WsTokenResponse Token issued successfully. + * @throws ApiError + */ + public static authWsToken(): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/api/v1/auth/ws-token', + errors: { + 401: `The current session is unauthenticated or the login state has expired.`, + 500: `Token issuance or Redis write failed.`, + }, + }); + } } diff --git a/src/hooks/index.ts b/src/hooks/index.ts index d70655f..ba0858e 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -34,46 +34,74 @@ export { useAssignPr } from './pull-request/useAssignPr'; export { useAssignPrLabel } from './pull-request/useAssignPrLabel'; export { useClosePr } from './pull-request/useClosePr'; export { useCreatePr } from './pull-request/useCreatePr'; +export { useCreateReview } from './pull-request/useCreateReview'; export { useMergePr } from './pull-request/useMergePr'; export { usePrAssignees } from './pull-request/usePrAssignees'; +export { usePrCheckRuns } from './pull-request/usePrCheckRuns'; +export { usePrCommits } from './pull-request/usePrCommits'; export { usePrDetail } from './pull-request/usePrDetail'; +export { usePrEvents } from './pull-request/usePrEvents'; export { usePrFiles } from './pull-request/usePrFiles'; export { usePrLabelRelations } from './pull-request/usePrLabelRelations'; export { usePrLabels } from './pull-request/usePrLabels'; export { usePrList } from './pull-request/usePrList'; export { usePrReactions } from './pull-request/usePrReactions'; export { usePrReviews } from './pull-request/usePrReviews'; +export { usePrStatus } from './pull-request/usePrStatus'; export { useRemovePrReaction } from './pull-request/useRemovePrReaction'; export { useReopenPr } from './pull-request/useReopenPr'; +export { useSubmitReview } from './pull-request/useSubmitReview'; export { useUnassignPr } from './pull-request/useUnassignPr'; export { useUnassignPrLabel } from './pull-request/useUnassignPrLabel'; export { useUpdatePr } from './pull-request/useUpdatePr'; // repo +export { useAddDeployKey } from './repo/useAddDeployKey'; +export { useAddRepoMember } from './repo/useAddRepoMember'; export { useArchiveRepo } from './repo/useArchiveRepo'; export { useCreateBranch } from './repo/useCreateBranch'; export { useCreateProtectionRule } from './repo/useCreateProtectionRule'; +export { useCreateRelease } from './repo/useCreateRelease'; export { useCreateRepo } from './repo/useCreateRepo'; export { useCreateTag } from './repo/useCreateTag'; export { useCreateWebhook } from './repo/useCreateWebhook'; export { useDeleteBranch } from './repo/useDeleteBranch'; +export { useDeleteDeployKey } from './repo/useDeleteDeployKey'; export { useDeleteProtectionRule } from './repo/useDeleteProtectionRule'; +export { useDeleteRelease } from './repo/useDeleteRelease'; export { useDeleteRepo } from './repo/useDeleteRepo'; export { useDeleteTag } from './repo/useDeleteTag'; export { useDeleteWebhook } from './repo/useDeleteWebhook'; export { useForkRepo } from './repo/useForkRepo'; +export { useGitBlame } from './repo/useGitBlame'; +export { useGitBlob } from './repo/useGitBlob'; +export { useGitCommit } from './repo/useGitCommit'; +export { useGitCommits } from './repo/useGitCommits'; +export { useGitDiff } from './repo/useGitDiff'; +export { useGitDiffStats } from './repo/useGitDiffStats'; +export { useGitTree } from './repo/useGitTree'; +export { useRemoveRepoMember } from './repo/useRemoveRepoMember'; export { useRepo } from './repo/useRepo'; export { useRepoBranches } from './repo/useRepoBranches'; +export { useRepoDeployKeys } from './repo/useRepoDeployKeys'; export { useRepoForks } from './repo/useRepoForks'; export { useRepoInvitations } from './repo/useRepoInvitations'; export { useRepoMembers } from './repo/useRepoMembers'; export { useRepoProtectionRules } from './repo/useRepoProtectionRules'; export { useRepoPulls } from './repo/useRepoPulls'; +export { useRepoRelease } from './repo/useRepoRelease'; +export { useRepoReleases } from './repo/useRepoReleases'; +export { useRepoStars } from './repo/useRepoStars'; export { useRepoStats } from './repo/useRepoStats'; export { useRepoTags } from './repo/useRepoTags'; +export { useRepoWatchers } from './repo/useRepoWatchers'; export { useRepoWebhooks } from './repo/useRepoWebhooks'; export { useSetDefaultBranch } from './repo/useSetDefaultBranch'; +export { useStarRepo, useUnstarRepo } from './repo/useStarRepo'; export { useTransferRepo } from './repo/useTransferRepo'; +export { useUpdateRelease } from './repo/useUpdateRelease'; export { useUpdateRepo } from './repo/useUpdateRepo'; +export { useUpdateRepoMemberRole } from './repo/useUpdateRepoMemberRole'; +export { useWatchRepo, useUnwatchRepo } from './repo/useWatchRepo'; // user export { useAccessTokens } from './user/useAccessTokens'; export { useCurrentUser } from './user/useCurrentUser'; @@ -89,6 +117,9 @@ export { useUserNotifications } from './user/useUserNotifications'; export { useUserProfile } from './user/useUserProfile'; export { useUserSessions } from './user/useUserSessions'; // wiki +export { useCreateWikiPage } from './wiki/useCreateWikiPage'; +export { useDeleteWikiPage } from './wiki/useDeleteWikiPage'; +export { useUpdateWikiPage } from './wiki/useUpdateWikiPage'; export { useWikiPage } from './wiki/useWikiPage'; export { useWikiPages } from './wiki/useWikiPages'; export { useWikiRevisions } from './wiki/useWikiRevisions'; diff --git a/src/hooks/pull-request/useCreateReview.ts b/src/hooks/pull-request/useCreateReview.ts new file mode 100644 index 0000000..383685f --- /dev/null +++ b/src/hooks/pull-request/useCreateReview.ts @@ -0,0 +1,25 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useParams } from 'react-router-dom'; + +import { PullRequestsService } from '@/client/services/PullRequestsService'; +import type { CreateReviewParams } from '@/client/models/CreateReviewParams'; + +export function useCreateReview() { + const { workspaceName = '', repoName = '' } = useParams(); + const qc = useQueryClient(); + + return useMutation({ + mutationFn: ({ number, ...params }: CreateReviewParams & { number: number }) => + PullRequestsService.prCreateReview({ + workspaceName, + repoName, + number, + requestBody: params, + }), + onSuccess: (_, vars) => { + qc.invalidateQueries({ + queryKey: ['workspace', workspaceName, 'repos', repoName, 'prs', vars.number], + }); + }, + }); +} diff --git a/src/hooks/pull-request/usePrCheckRuns.ts b/src/hooks/pull-request/usePrCheckRuns.ts new file mode 100644 index 0000000..17c0332 --- /dev/null +++ b/src/hooks/pull-request/usePrCheckRuns.ts @@ -0,0 +1,21 @@ +import { useQuery } from '@tanstack/react-query'; + +import { PullRequestsService } from '@/client/services/PullRequestsService'; + +export function usePrCheckRuns( + workspaceName: string | null, + repoName: string | null, + number: number | null, +) { + return useQuery({ + queryKey: ['workspace', workspaceName, 'repos', repoName, 'prs', number, 'check-runs'], + queryFn: () => { + if (!workspaceName || !repoName || number === null) throw new Error('Missing params'); + return PullRequestsService.prListCheckRuns({ workspaceName, repoName, number }).then( + (r) => r.data, + ); + }, + enabled: !!workspaceName && !!repoName && number !== null, + staleTime: 1000 * 60, + }); +} diff --git a/src/hooks/pull-request/usePrCommits.ts b/src/hooks/pull-request/usePrCommits.ts new file mode 100644 index 0000000..f3bd22d --- /dev/null +++ b/src/hooks/pull-request/usePrCommits.ts @@ -0,0 +1,27 @@ +import { useQuery } from '@tanstack/react-query'; + +import { PullRequestsService } from '@/client/services/PullRequestsService'; + +export function usePrCommits( + workspaceName: string | null, + repoName: string | null, + number: number | null, + limit = 50, + offset = 0, +) { + return useQuery({ + queryKey: ['workspace', workspaceName, 'repos', repoName, 'prs', number, 'commits', { limit, offset }], + queryFn: () => { + if (!workspaceName || !repoName || number === null) throw new Error('Missing params'); + return PullRequestsService.prListCommits({ + workspaceName, + repoName, + number, + limit, + offset, + }).then((r) => r.data); + }, + enabled: !!workspaceName && !!repoName && number !== null, + staleTime: 1000 * 60, + }); +} diff --git a/src/hooks/pull-request/usePrEvents.ts b/src/hooks/pull-request/usePrEvents.ts new file mode 100644 index 0000000..93dc50e --- /dev/null +++ b/src/hooks/pull-request/usePrEvents.ts @@ -0,0 +1,27 @@ +import { useQuery } from '@tanstack/react-query'; + +import { PullRequestsService } from '@/client/services/PullRequestsService'; + +export function usePrEvents( + workspaceName: string | null, + repoName: string | null, + number: number | null, + limit = 50, + offset = 0, +) { + return useQuery({ + queryKey: ['workspace', workspaceName, 'repos', repoName, 'prs', number, 'events', { limit, offset }], + queryFn: () => { + if (!workspaceName || !repoName || number === null) throw new Error('Missing params'); + return PullRequestsService.prListEvents({ + workspaceName, + repoName, + number, + limit, + offset, + }).then((r) => r.data); + }, + enabled: !!workspaceName && !!repoName && number !== null, + staleTime: 1000 * 60, + }); +} diff --git a/src/hooks/pull-request/usePrStatus.ts b/src/hooks/pull-request/usePrStatus.ts new file mode 100644 index 0000000..e7f0877 --- /dev/null +++ b/src/hooks/pull-request/usePrStatus.ts @@ -0,0 +1,21 @@ +import { useQuery } from '@tanstack/react-query'; + +import { PullRequestsService } from '@/client/services/PullRequestsService'; + +export function usePrStatus( + workspaceName: string | null, + repoName: string | null, + number: number | null, +) { + return useQuery({ + queryKey: ['workspace', workspaceName, 'repos', repoName, 'prs', number, 'status'], + queryFn: () => { + if (!workspaceName || !repoName || number === null) throw new Error('Missing params'); + return PullRequestsService.prGetStatus({ workspaceName, repoName, number }).then( + (r) => r.data, + ); + }, + enabled: !!workspaceName && !!repoName && number !== null, + staleTime: 1000 * 60, + }); +} diff --git a/src/hooks/pull-request/useSubmitReview.ts b/src/hooks/pull-request/useSubmitReview.ts new file mode 100644 index 0000000..d103825 --- /dev/null +++ b/src/hooks/pull-request/useSubmitReview.ts @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useParams } from 'react-router-dom'; + +import { PullRequestsService } from '@/client/services/PullRequestsService'; +import type { SubmitReviewParams } from '@/client/models/SubmitReviewParams'; + +export function useSubmitReview() { + const { workspaceName = '', repoName = '' } = useParams(); + const qc = useQueryClient(); + + return useMutation({ + mutationFn: ({ + number, + reviewId, + ...params + }: SubmitReviewParams & { number: number; reviewId: string }) => + PullRequestsService.prSubmitReview({ + workspaceName, + repoName, + number, + reviewId, + requestBody: params, + }), + onSuccess: (_, vars) => { + qc.invalidateQueries({ + queryKey: ['workspace', workspaceName, 'repos', repoName, 'prs', vars.number], + }); + }, + }); +} diff --git a/src/hooks/repo/useAddDeployKey.ts b/src/hooks/repo/useAddDeployKey.ts new file mode 100644 index 0000000..5437afc --- /dev/null +++ b/src/hooks/repo/useAddDeployKey.ts @@ -0,0 +1,18 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useParams } from 'react-router-dom'; + +import type { AddDeployKeyParams } from '@/client/models/AddDeployKeyParams'; +import { ReposService } from '@/client/services/ReposService'; + +export function useAddDeployKey() { + const { workspaceName = '', repoName = '' } = useParams(); + const qc = useQueryClient(); + + return useMutation({ + mutationFn: (params: AddDeployKeyParams) => + ReposService.repoAddDeployKey({ workspaceName, repoName, requestBody: params }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'deploy-keys'] }); + }, + }); +} diff --git a/src/hooks/repo/useAddRepoMember.ts b/src/hooks/repo/useAddRepoMember.ts new file mode 100644 index 0000000..0b6d0e6 --- /dev/null +++ b/src/hooks/repo/useAddRepoMember.ts @@ -0,0 +1,22 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useParams } from 'react-router-dom'; + +import { ReposService } from '@/client/services/ReposService'; +import type { Role } from '@/client/models/Role'; + +export function useAddRepoMember() { + const { workspaceName = '', repoName = '' } = useParams(); + const qc = useQueryClient(); + + return useMutation({ + mutationFn: ({ userId, role }: { userId: string; role?: Role }) => + ReposService.repoAddMember({ + workspaceName, + repoName, + requestBody: { user_id: userId, role }, + }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'members'] }); + }, + }); +} diff --git a/src/hooks/repo/useCreateBranch.ts b/src/hooks/repo/useCreateBranch.ts index f4b4b4e..94c1c78 100644 --- a/src/hooks/repo/useCreateBranch.ts +++ b/src/hooks/repo/useCreateBranch.ts @@ -8,7 +8,7 @@ export function useCreateBranch() { const qc = useQueryClient(); return useMutation({ - mutationFn: (params: { name: string; commit_sha: string }) => + mutationFn: (params: { branch_name: string; start_point: string }) => ReposService.repoCreateBranch({ workspaceName, repoName, requestBody: params }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'branches'] }); diff --git a/src/hooks/repo/useCreateRelease.ts b/src/hooks/repo/useCreateRelease.ts new file mode 100644 index 0000000..89ae825 --- /dev/null +++ b/src/hooks/repo/useCreateRelease.ts @@ -0,0 +1,18 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useParams } from 'react-router-dom'; + +import type { CreateReleaseParams } from '@/client/models/CreateReleaseParams'; +import { ReposService } from '@/client/services/ReposService'; + +export function useCreateRelease() { + const { workspaceName = '', repoName = '' } = useParams(); + const qc = useQueryClient(); + + return useMutation({ + mutationFn: (params: CreateReleaseParams) => + ReposService.repoCreateRelease({ workspaceName, repoName, requestBody: params }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'releases'] }); + }, + }); +} diff --git a/src/hooks/repo/useCreateTag.ts b/src/hooks/repo/useCreateTag.ts index 442b7f8..9ddd29c 100644 --- a/src/hooks/repo/useCreateTag.ts +++ b/src/hooks/repo/useCreateTag.ts @@ -8,7 +8,7 @@ export function useCreateTag() { const qc = useQueryClient(); return useMutation({ - mutationFn: (params: { name: string; target_commit_sha: string; message?: string }) => + mutationFn: (params: { tag_name: string; target: string; message?: string }) => ReposService.repoCreateTag({ workspaceName, repoName, requestBody: params }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'tags'] }); diff --git a/src/hooks/repo/useDeleteBranch.ts b/src/hooks/repo/useDeleteBranch.ts index 1696a21..3f75c40 100644 --- a/src/hooks/repo/useDeleteBranch.ts +++ b/src/hooks/repo/useDeleteBranch.ts @@ -8,8 +8,8 @@ export function useDeleteBranch() { const qc = useQueryClient(); return useMutation({ - mutationFn: (branchId: string) => - ReposService.repoDeleteBranch({ workspaceName, repoName, branchId }), + mutationFn: (branchName: string) => + ReposService.repoDeleteBranch({ workspaceName, repoName, branchName }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'branches'] }); }, diff --git a/src/hooks/repo/useDeleteDeployKey.ts b/src/hooks/repo/useDeleteDeployKey.ts new file mode 100644 index 0000000..2a5da15 --- /dev/null +++ b/src/hooks/repo/useDeleteDeployKey.ts @@ -0,0 +1,17 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useParams } from 'react-router-dom'; + +import { ReposService } from '@/client/services/ReposService'; + +export function useDeleteDeployKey() { + const { workspaceName = '', repoName = '' } = useParams(); + const qc = useQueryClient(); + + return useMutation({ + mutationFn: (keyId: string) => + ReposService.repoDeleteDeployKey({ workspaceName, repoName, keyId }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'deploy-keys'] }); + }, + }); +} diff --git a/src/hooks/repo/useDeleteRelease.ts b/src/hooks/repo/useDeleteRelease.ts new file mode 100644 index 0000000..92b79a0 --- /dev/null +++ b/src/hooks/repo/useDeleteRelease.ts @@ -0,0 +1,17 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useParams } from 'react-router-dom'; + +import { ReposService } from '@/client/services/ReposService'; + +export function useDeleteRelease() { + const { workspaceName = '', repoName = '' } = useParams(); + const qc = useQueryClient(); + + return useMutation({ + mutationFn: (releaseId: string) => + ReposService.repoDeleteRelease({ workspaceName, repoName, releaseId }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'releases'] }); + }, + }); +} diff --git a/src/hooks/repo/useDeleteTag.ts b/src/hooks/repo/useDeleteTag.ts index 91f7d6a..ee9800e 100644 --- a/src/hooks/repo/useDeleteTag.ts +++ b/src/hooks/repo/useDeleteTag.ts @@ -8,7 +8,7 @@ export function useDeleteTag() { const qc = useQueryClient(); return useMutation({ - mutationFn: (tagId: string) => ReposService.repoDeleteTag({ workspaceName, repoName, tagId }), + mutationFn: (tagName: string) => ReposService.repoDeleteTag({ workspaceName, repoName, tagName }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'tags'] }); }, diff --git a/src/hooks/repo/useGitBlame.ts b/src/hooks/repo/useGitBlame.ts new file mode 100644 index 0000000..7f9e6cd --- /dev/null +++ b/src/hooks/repo/useGitBlame.ts @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query'; + +import { GitService } from '@/client/services/GitService'; + +export function useGitBlame( + workspaceName: string | null, + repoName: string | null, + revision: string | null, + path: string | null, +) { + return useQuery({ + queryKey: ['repo', workspaceName, repoName, 'git', 'blame', { revision, path }], + queryFn: () => { + if (!workspaceName || !repoName || !revision || !path) throw new Error('Missing params'); + return GitService.gitBlame({ workspaceName, repoName, revision, path }).then( + (r) => r.data, + ); + }, + enabled: !!workspaceName && !!repoName && !!revision && !!path, + staleTime: 1000 * 60 * 2, + }); +} diff --git a/src/hooks/repo/useGitBlob.ts b/src/hooks/repo/useGitBlob.ts new file mode 100644 index 0000000..21d2335 --- /dev/null +++ b/src/hooks/repo/useGitBlob.ts @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query'; + +import { GitService } from '@/client/services/GitService'; + +export function useGitBlob( + workspaceName: string | null, + repoName: string | null, + revision: string | null, + path: string | null, +) { + return useQuery({ + queryKey: ['repo', workspaceName, repoName, 'git', 'blob', { revision, path }], + queryFn: () => { + if (!workspaceName || !repoName || !revision || !path) throw new Error('Missing params'); + return GitService.gitGetBlob({ workspaceName, repoName, revision, path }).then( + (r) => r.data, + ); + }, + enabled: !!workspaceName && !!repoName && !!revision && !!path, + staleTime: 1000 * 60 * 2, + }); +} diff --git a/src/hooks/repo/useGitCommit.ts b/src/hooks/repo/useGitCommit.ts new file mode 100644 index 0000000..8442b1c --- /dev/null +++ b/src/hooks/repo/useGitCommit.ts @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +import { GitService } from '@/client/services/GitService'; + +export function useGitCommit( + workspaceName: string | null, + repoName: string | null, + revision: string | null, +) { + return useQuery({ + queryKey: ['repo', workspaceName, repoName, 'git', 'commit', revision], + queryFn: () => { + if (!workspaceName || !repoName || !revision) throw new Error('Missing params'); + return GitService.gitGetCommit({ workspaceName, repoName, revision }).then((r) => r.data); + }, + enabled: !!workspaceName && !!repoName && !!revision, + staleTime: 1000 * 60 * 5, + }); +} diff --git a/src/hooks/repo/useGitCommits.ts b/src/hooks/repo/useGitCommits.ts new file mode 100644 index 0000000..89d4d42 --- /dev/null +++ b/src/hooks/repo/useGitCommits.ts @@ -0,0 +1,27 @@ +import { useQuery } from '@tanstack/react-query'; + +import { GitService } from '@/client/services/GitService'; + +export function useGitCommits( + workspaceName: string | null, + repoName: string | null, + revision?: string, + path?: string, + pageSize = 30, +) { + return useQuery({ + queryKey: ['repo', workspaceName, repoName, 'git', 'commits', { revision, path, pageSize }], + queryFn: () => { + if (!workspaceName || !repoName) throw new Error('Missing params'); + return GitService.gitListCommits({ + workspaceName, + repoName, + revision: revision ?? 'HEAD', + path: path ?? undefined, + pageSize, + }).then((r) => r.data); + }, + enabled: !!workspaceName && !!repoName, + staleTime: 1000 * 60, + }); +} diff --git a/src/hooks/repo/useGitDiff.ts b/src/hooks/repo/useGitDiff.ts new file mode 100644 index 0000000..bbac812 --- /dev/null +++ b/src/hooks/repo/useGitDiff.ts @@ -0,0 +1,20 @@ +import { useQuery } from '@tanstack/react-query'; + +import { GitService } from '@/client/services/GitService'; + +export function useGitDiff( + workspaceName: string | null, + repoName: string | null, + base: string | null, + head: string | null, +) { + return useQuery({ + queryKey: ['repo', workspaceName, repoName, 'git', 'diff', { base, head }], + queryFn: () => { + if (!workspaceName || !repoName || !base || !head) throw new Error('Missing params'); + return GitService.gitDiff({ workspaceName, repoName, base, head }).then((r) => r.data); + }, + enabled: !!workspaceName && !!repoName && !!base && !!head, + staleTime: 1000 * 60 * 5, + }); +} diff --git a/src/hooks/repo/useGitDiffStats.ts b/src/hooks/repo/useGitDiffStats.ts new file mode 100644 index 0000000..5350630 --- /dev/null +++ b/src/hooks/repo/useGitDiffStats.ts @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query'; + +import { GitService } from '@/client/services/GitService'; + +export function useGitDiffStats( + workspaceName: string | null, + repoName: string | null, + base: string | null, + head: string | null, +) { + return useQuery({ + queryKey: ['repo', workspaceName, repoName, 'git', 'diff-stats', { base, head }], + queryFn: () => { + if (!workspaceName || !repoName || !base || !head) throw new Error('Missing params'); + return GitService.gitDiffStats({ workspaceName, repoName, base, head }).then( + (r) => r.data, + ); + }, + enabled: !!workspaceName && !!repoName && !!base && !!head, + staleTime: 1000 * 60 * 5, + }); +} diff --git a/src/hooks/repo/useGitTree.ts b/src/hooks/repo/useGitTree.ts new file mode 100644 index 0000000..7bbeb6a --- /dev/null +++ b/src/hooks/repo/useGitTree.ts @@ -0,0 +1,27 @@ +import { useQuery } from '@tanstack/react-query'; + +import { GitService } from '@/client/services/GitService'; + +export function useGitTree( + workspaceName: string | null, + repoName: string | null, + revision?: string, + path?: string, + recursive?: boolean, +) { + return useQuery({ + queryKey: ['repo', workspaceName, repoName, 'git', 'tree', { revision, path, recursive }], + queryFn: () => { + if (!workspaceName || !repoName) throw new Error('Missing params'); + return GitService.gitListTree({ + workspaceName, + repoName, + revision: revision ?? 'HEAD', + path: path ?? undefined, + recursive: recursive ?? undefined, + }).then((r) => r.data); + }, + enabled: !!workspaceName && !!repoName, + staleTime: 1000 * 60, + }); +} diff --git a/src/hooks/repo/useRemoveRepoMember.ts b/src/hooks/repo/useRemoveRepoMember.ts new file mode 100644 index 0000000..60f06cc --- /dev/null +++ b/src/hooks/repo/useRemoveRepoMember.ts @@ -0,0 +1,17 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useParams } from 'react-router-dom'; + +import { ReposService } from '@/client/services/ReposService'; + +export function useRemoveRepoMember() { + const { workspaceName = '', repoName = '' } = useParams(); + const qc = useQueryClient(); + + return useMutation({ + mutationFn: (memberId: string) => + ReposService.repoRemoveMember({ workspaceName, repoName, memberId }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'members'] }); + }, + }); +} diff --git a/src/hooks/repo/useRepoBranches.ts b/src/hooks/repo/useRepoBranches.ts index 6b74626..89d1651 100644 --- a/src/hooks/repo/useRepoBranches.ts +++ b/src/hooks/repo/useRepoBranches.ts @@ -8,7 +8,7 @@ export function useRepoBranches(workspaceName: string | null, repoName: string | queryFn: () => { if (!workspaceName || !repoName) throw new Error('Missing params'); return ReposService.repoListBranches({ workspaceName, repoName, limit: 100 }).then( - (r) => r.data, + (r) => r.data.branches, ); }, enabled: !!workspaceName && !!repoName, diff --git a/src/hooks/repo/useRepoDeployKeys.ts b/src/hooks/repo/useRepoDeployKeys.ts new file mode 100644 index 0000000..aa9cd4c --- /dev/null +++ b/src/hooks/repo/useRepoDeployKeys.ts @@ -0,0 +1,16 @@ +import { useQuery } from '@tanstack/react-query'; + +import { ReposService } from '@/client/services/ReposService'; + +export function useRepoDeployKeys(workspaceName: string | null, repoName: string | null) { + return useQuery({ + queryKey: ['repo', workspaceName, repoName, 'deploy-keys'], + queryFn: () => { + if (!workspaceName || !repoName) throw new Error('Missing params'); + return ReposService.repoListDeployKeys({ workspaceName, repoName }).then( + (r) => r.data, + ); + }, + enabled: !!workspaceName && !!repoName, + }); +} diff --git a/src/hooks/repo/useRepoRelease.ts b/src/hooks/repo/useRepoRelease.ts new file mode 100644 index 0000000..72530d4 --- /dev/null +++ b/src/hooks/repo/useRepoRelease.ts @@ -0,0 +1,21 @@ +import { useQuery } from '@tanstack/react-query'; + +import { ReposService } from '@/client/services/ReposService'; + +export function useRepoRelease( + workspaceName: string | null, + repoName: string | null, + releaseId: string | null, +) { + return useQuery({ + queryKey: ['repo', workspaceName, repoName, 'releases', releaseId], + queryFn: () => { + if (!workspaceName || !repoName || !releaseId) throw new Error('Missing params'); + return ReposService.repoGetRelease({ workspaceName, repoName, releaseId }).then( + (r) => r.data, + ); + }, + enabled: !!workspaceName && !!repoName && !!releaseId, + staleTime: 1000 * 60 * 2, + }); +} diff --git a/src/hooks/repo/useRepoReleases.ts b/src/hooks/repo/useRepoReleases.ts new file mode 100644 index 0000000..1e507c7 --- /dev/null +++ b/src/hooks/repo/useRepoReleases.ts @@ -0,0 +1,21 @@ +import { useQuery } from '@tanstack/react-query'; + +import { ReposService } from '@/client/services/ReposService'; + +export function useRepoReleases( + workspaceName: string | null, + repoName: string | null, + limit = 20, + offset = 0, +) { + return useQuery({ + queryKey: ['repo', workspaceName, repoName, 'releases', { limit, offset }], + queryFn: () => { + if (!workspaceName || !repoName) throw new Error('Missing params'); + return ReposService.repoListReleases({ workspaceName, repoName, limit, offset }).then( + (r) => r.data, + ); + }, + enabled: !!workspaceName && !!repoName, + }); +} diff --git a/src/hooks/repo/useRepoStars.ts b/src/hooks/repo/useRepoStars.ts new file mode 100644 index 0000000..ac5bb18 --- /dev/null +++ b/src/hooks/repo/useRepoStars.ts @@ -0,0 +1,21 @@ +import { useQuery } from '@tanstack/react-query'; + +import { ReposService } from '@/client/services/ReposService'; + +export function useRepoStars( + workspaceName: string | null, + repoName: string | null, + limit = 50, + offset = 0, +) { + return useQuery({ + queryKey: ['repo', workspaceName, repoName, 'stars', { limit, offset }], + queryFn: () => { + if (!workspaceName || !repoName) throw new Error('Missing params'); + return ReposService.repoListStargazers({ workspaceName, repoName, limit, offset }).then( + (r) => r.data, + ); + }, + enabled: !!workspaceName && !!repoName, + }); +} diff --git a/src/hooks/repo/useRepoTags.ts b/src/hooks/repo/useRepoTags.ts index a4f9a0c..7fbd53f 100644 --- a/src/hooks/repo/useRepoTags.ts +++ b/src/hooks/repo/useRepoTags.ts @@ -13,7 +13,7 @@ export function useRepoTags( queryFn: () => { if (!workspaceName || !repoName) throw new Error('Missing params'); return ReposService.repoListTags({ workspaceName, repoName, offset, limit }).then( - (r) => r.data, + (r) => r.data.tags, ); }, enabled: !!workspaceName && !!repoName, diff --git a/src/hooks/repo/useRepoWatchers.ts b/src/hooks/repo/useRepoWatchers.ts new file mode 100644 index 0000000..dfaa562 --- /dev/null +++ b/src/hooks/repo/useRepoWatchers.ts @@ -0,0 +1,21 @@ +import { useQuery } from '@tanstack/react-query'; + +import { ReposService } from '@/client/services/ReposService'; + +export function useRepoWatchers( + workspaceName: string | null, + repoName: string | null, + limit = 50, + offset = 0, +) { + return useQuery({ + queryKey: ['repo', workspaceName, repoName, 'watchers', { limit, offset }], + queryFn: () => { + if (!workspaceName || !repoName) throw new Error('Missing params'); + return ReposService.repoListWatchers({ workspaceName, repoName, limit, offset }).then( + (r) => r.data, + ); + }, + enabled: !!workspaceName && !!repoName, + }); +} diff --git a/src/hooks/repo/useSetDefaultBranch.ts b/src/hooks/repo/useSetDefaultBranch.ts index 5800b3b..5f03306 100644 --- a/src/hooks/repo/useSetDefaultBranch.ts +++ b/src/hooks/repo/useSetDefaultBranch.ts @@ -8,8 +8,8 @@ export function useSetDefaultBranch() { const qc = useQueryClient(); return useMutation({ - mutationFn: (branchId: string) => - ReposService.repoSetDefaultBranch({ workspaceName, repoName, branchId }), + mutationFn: (branchName: string) => + ReposService.repoSetDefaultBranch({ workspaceName, repoName, branchName }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'branches'] }); qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName] }); diff --git a/src/hooks/repo/useStarRepo.ts b/src/hooks/repo/useStarRepo.ts new file mode 100644 index 0000000..9b477f3 --- /dev/null +++ b/src/hooks/repo/useStarRepo.ts @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useParams } from 'react-router-dom'; + +import { ReposService } from '@/client/services/ReposService'; + +export function useStarRepo() { + const { workspaceName = '', repoName = '' } = useParams(); + const qc = useQueryClient(); + + return useMutation({ + mutationFn: () => ReposService.repoStar({ workspaceName, repoName }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'stars'] }); + qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'stats'] }); + }, + }); +} + +export function useUnstarRepo() { + const { workspaceName = '', repoName = '' } = useParams(); + const qc = useQueryClient(); + + return useMutation({ + mutationFn: () => ReposService.repoUnstar({ workspaceName, repoName }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'stars'] }); + qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'stats'] }); + }, + }); +} diff --git a/src/hooks/repo/useUpdateRelease.ts b/src/hooks/repo/useUpdateRelease.ts new file mode 100644 index 0000000..7af46d9 --- /dev/null +++ b/src/hooks/repo/useUpdateRelease.ts @@ -0,0 +1,18 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useParams } from 'react-router-dom'; + +import type { UpdateReleaseParams } from '@/client/models/UpdateReleaseParams'; +import { ReposService } from '@/client/services/ReposService'; + +export function useUpdateRelease() { + const { workspaceName = '', repoName = '' } = useParams(); + const qc = useQueryClient(); + + return useMutation({ + mutationFn: ({ releaseId, ...params }: UpdateReleaseParams & { releaseId: string }) => + ReposService.repoUpdateRelease({ workspaceName, repoName, releaseId, requestBody: params }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'releases'] }); + }, + }); +} diff --git a/src/hooks/repo/useUpdateRepoMemberRole.ts b/src/hooks/repo/useUpdateRepoMemberRole.ts new file mode 100644 index 0000000..db56eaf --- /dev/null +++ b/src/hooks/repo/useUpdateRepoMemberRole.ts @@ -0,0 +1,23 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useParams } from 'react-router-dom'; + +import type { Role } from '@/client/models/Role'; +import { ReposService } from '@/client/services/ReposService'; + +export function useUpdateRepoMemberRole() { + const { workspaceName = '', repoName = '' } = useParams(); + const qc = useQueryClient(); + + return useMutation({ + mutationFn: ({ memberId, role }: { memberId: string; role: Role }) => + ReposService.repoUpdateMemberRole({ + workspaceName, + repoName, + memberId, + requestBody: { role }, + }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'members'] }); + }, + }); +} diff --git a/src/hooks/repo/useWatchRepo.ts b/src/hooks/repo/useWatchRepo.ts new file mode 100644 index 0000000..c07ab04 --- /dev/null +++ b/src/hooks/repo/useWatchRepo.ts @@ -0,0 +1,31 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useParams } from 'react-router-dom'; + +import { ReposService } from '@/client/services/ReposService'; + +export function useWatchRepo() { + const { workspaceName = '', repoName = '' } = useParams(); + const qc = useQueryClient(); + + return useMutation({ + mutationFn: () => + ReposService.repoWatch({ workspaceName, repoName, requestBody: {} }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'watchers'] }); + qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'stats'] }); + }, + }); +} + +export function useUnwatchRepo() { + const { workspaceName = '', repoName = '' } = useParams(); + const qc = useQueryClient(); + + return useMutation({ + mutationFn: () => ReposService.repoUnwatch({ workspaceName, repoName }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'watchers'] }); + qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'stats'] }); + }, + }); +} diff --git a/src/hooks/wiki/useCreateWikiPage.ts b/src/hooks/wiki/useCreateWikiPage.ts new file mode 100644 index 0000000..9b7cde0 --- /dev/null +++ b/src/hooks/wiki/useCreateWikiPage.ts @@ -0,0 +1,20 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { WikiService } from '@/client/services/WikiService'; +import type { CreateWikiPageParams } from '@/client/models/CreateWikiPageParams'; + +export function useCreateWikiPage(workspaceName: string | null, repoName: string | null) { + const qc = useQueryClient(); + + return useMutation({ + mutationFn: (params: CreateWikiPageParams) => { + if (!workspaceName || !repoName) throw new Error('Missing params'); + return WikiService.wikiCreatePage({ workspaceName, repoName, requestBody: params }); + }, + onSuccess: () => { + qc.invalidateQueries({ + queryKey: ['workspace', workspaceName, 'repos', repoName, 'wiki'], + }); + }, + }); +} diff --git a/src/hooks/wiki/useDeleteWikiPage.ts b/src/hooks/wiki/useDeleteWikiPage.ts new file mode 100644 index 0000000..26b609a --- /dev/null +++ b/src/hooks/wiki/useDeleteWikiPage.ts @@ -0,0 +1,19 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { WikiService } from '@/client/services/WikiService'; + +export function useDeleteWikiPage(workspaceName: string | null, repoName: string | null) { + const qc = useQueryClient(); + + return useMutation({ + mutationFn: (slug: string) => { + if (!workspaceName || !repoName) throw new Error('Missing params'); + return WikiService.wikiDeletePage({ workspaceName, repoName, slug }); + }, + onSuccess: () => { + qc.invalidateQueries({ + queryKey: ['workspace', workspaceName, 'repos', repoName, 'wiki'], + }); + }, + }); +} diff --git a/src/hooks/wiki/useUpdateWikiPage.ts b/src/hooks/wiki/useUpdateWikiPage.ts new file mode 100644 index 0000000..45d6c78 --- /dev/null +++ b/src/hooks/wiki/useUpdateWikiPage.ts @@ -0,0 +1,25 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { WikiService } from '@/client/services/WikiService'; +import type { UpdateWikiPageParams } from '@/client/models/UpdateWikiPageParams'; + +export function useUpdateWikiPage(workspaceName: string | null, repoName: string | null) { + const qc = useQueryClient(); + + return useMutation({ + mutationFn: ({ slug, ...params }: UpdateWikiPageParams & { slug: string }) => { + if (!workspaceName || !repoName) throw new Error('Missing params'); + return WikiService.wikiUpdatePage({ + workspaceName, + repoName, + slug, + requestBody: params, + }); + }, + onSuccess: () => { + qc.invalidateQueries({ + queryKey: ['workspace', workspaceName, 'repos', repoName, 'wiki'], + }); + }, + }); +} diff --git a/src/index.tsx b/src/index.tsx index c8805c1..174032a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,6 +5,8 @@ import { RouterProvider } from 'react-router-dom'; import { toast } from 'sonner'; import { UserProvider } from '@/contexts/UserContext'; +import { SocketProvider } from '@/socket'; +import { env } from '@/lib/env'; import { router } from './routes'; import './index.css'; @@ -32,7 +34,9 @@ createRoot(doc).render( - + + + , diff --git a/src/lib/env.ts b/src/lib/env.ts index 5bf5e52..f5face0 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -1,5 +1,6 @@ export const env = { API_BASE_URL: import.meta.env.VITE_API_BASE_URL ?? '/api', + IMKS_URL: import.meta.env.VITE_IMKS_URL ?? 'http://localhost:50048', DEV: import.meta.env.DEV, MODE: import.meta.env.MODE, } as const; diff --git a/src/pages/Workspace/Repo/RepoBlobPage.tsx b/src/pages/Workspace/Repo/RepoBlobPage.tsx new file mode 100644 index 0000000..90f923e --- /dev/null +++ b/src/pages/Workspace/Repo/RepoBlobPage.tsx @@ -0,0 +1,214 @@ +import { ArrowLeft, Code, Copy, Download, GitCommit } from 'lucide-react'; +import { useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { Link, useParams } from 'react-router-dom'; + +import { Button } from '@/components/ui/button'; +import { Spinner } from '@/components/ui/spinner'; +import { useGitBlame } from '@/hooks/repo/useGitBlame'; +import { useGitBlob } from '@/hooks/repo/useGitBlob'; +import { useRepo } from '@/hooks/repo/useRepo'; + +export function RepoBlobPage() { + const { workspaceName = '', repoName = '' } = useParams(); + const [searchParams] = useSearchParams(); + + const branch = searchParams.get('branch') || ''; + const filePath = searchParams.get('blob') || ''; + + const { data: repo } = useRepo(workspaceName, repoName); + const { data: blob, isLoading } = useGitBlob( + workspaceName, + repoName, + branch || (repo?.default_branch ?? null), + filePath || null, + ); + + const [view, setView] = useState<'code' | 'blame'>('code'); + + const effectiveBranch = branch || repo?.default_branch || ''; + const fileName = filePath.split('/').pop() || ''; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (!blob) { + return ( +
+

File not found

+ + Back to code + +
+ ); + } + + const content = blob.binary ? null : new TextDecoder().decode(new Uint8Array(blob.data)); + const lines = content ? content.split('\n') : []; + + const handleCopy = () => { + if (content) navigator.clipboard.writeText(content); + }; + + const handleDownload = () => { + if (!content) return; + const blobObj = new Blob([content], { type: 'text/plain' }); + const url = URL.createObjectURL(blobObj); + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + a.click(); + URL.revokeObjectURL(url); + }; + + return ( +
+
+ + + Back + + / + {filePath} +
+ +
+
+ {lines.length} lines + · + {formatSize(blob.size)} +
+
+ + + + +
+
+ + {blob.binary ? ( +
+

Binary file not shown

+
+ ) : view === 'code' ? ( +
+
+ + + {lines.map((line, idx) => ( + + + + + ))} + +
+ {idx + 1} + +
{line}
+
+
+
+ ) : ( + + )} +
+ ); +} + +function BlameView({ + workspaceName, + repoName, + branch, + path, +}: { + workspaceName: string; + repoName: string; + branch: string; + path: string; +}) { + const { data: blame, isLoading } = useGitBlame(workspaceName, repoName, branch, path); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (!blame) { + return
No blame data
; + } + + return ( +
+
+ {blame.hunks.map((hunk, idx) => { + const commitMsg = hunk.commit?.subject || 'Unknown'; + const commitSha = hunk.commit?.abbreviated_oid || ''; + const lines = hunk.lines || []; + + return ( +
+
+ {commitSha} + {commitMsg} +
+ {lines.map((line, lineIdx) => ( +
+
+ {line.final_line} +
+
+                    {new TextDecoder().decode(new Uint8Array(line.content))}
+                  
+
+ ))} +
+ ); + })} +
+
+ ); +} + +function formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} diff --git a/src/pages/Workspace/Repo/RepoBranchesPage.tsx b/src/pages/Workspace/Repo/RepoBranchesPage.tsx index 9cd6dc1..0640e37 100644 --- a/src/pages/Workspace/Repo/RepoBranchesPage.tsx +++ b/src/pages/Workspace/Repo/RepoBranchesPage.tsx @@ -1,5 +1,5 @@ import { type UseMutationResult, useMutation, useQueryClient } from '@tanstack/react-query'; -import { GitBranch, Plus, Search, Shield, Star, Trash2 } from 'lucide-react'; +import { GitBranch, Plus, Search, Star, Trash2 } from 'lucide-react'; import { useState } from 'react'; import { useParams } from 'react-router-dom'; import { ReposService } from '@/client/services/ReposService'; @@ -7,7 +7,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Spinner } from '@/components/ui/spinner'; import { useRepoBranches } from '@/hooks/repo/useRepoBranches'; -import { cn, timeAgo } from '@/lib/utils'; +import { cn } from '@/lib/utils'; export function RepoBranchesPage() { const { workspaceName = '', repoName = '' } = useParams(); @@ -24,7 +24,7 @@ export function RepoBranchesPage() { ReposService.repoCreateBranch({ workspaceName, repoName, - requestBody: { name: newName, commit_sha: newSource || 'HEAD' }, + requestBody: { branch_name: newName, start_point: newSource || 'HEAD' }, }), onSuccess: () => { setNewName(''); @@ -35,27 +35,28 @@ export function RepoBranchesPage() { }); const deleteBranch = useMutation({ - mutationFn: (branchId: string) => - ReposService.repoDeleteBranch({ workspaceName, repoName, branchId }), + mutationFn: (branchName: string) => + ReposService.repoDeleteBranch({ workspaceName, repoName, branchName }), onSuccess: () => qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'branches'] }), }); const setDefault = useMutation({ - mutationFn: (branchId: string) => - ReposService.repoSetDefaultBranch({ workspaceName, repoName, branchId }), + mutationFn: (branchName: string) => + ReposService.repoSetDefaultBranch({ workspaceName, repoName, branchName }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName, 'branches'] }); qc.invalidateQueries({ queryKey: ['repo', workspaceName, repoName] }); }, }); + const list = branches ?? []; const filtered = search - ? (branches ?? []).filter((b) => b.name.toLowerCase().includes(search.toLowerCase())) - : (branches ?? []); + ? list.filter((b) => b.name.toLowerCase().includes(search.toLowerCase())) + : list; - const defaultBranches = filtered.filter((b) => b.default_branch); - const otherBranches = filtered.filter((b) => !b.default_branch); + const defaultBranches = filtered.filter((b) => b.is_default); + const otherBranches = filtered.filter((b) => !b.is_default); return (
@@ -85,7 +86,7 @@ export function RepoBranchesPage() { /> setNewSource(e.target.value)} /> @@ -111,7 +112,7 @@ export function RepoBranchesPage() {
{defaultBranches.map((b) => ( )} {otherBranches.map((b) => ( - + ))}
) : ( @@ -144,12 +145,10 @@ export function RepoBranchesPage() { interface BranchRowProps { branch: { - id: string; name: string; - commit_sha: string; - default_branch: boolean; - protected: boolean; - last_push_at?: string | null; + commit?: { abbreviated_oid?: string } | null; + is_default?: boolean; + is_merged?: boolean; }; isDefault?: boolean; onDelete: UseMutationResult; @@ -157,37 +156,36 @@ interface BranchRowProps { } function BranchRow({ branch, isDefault, onDelete, onSetDefault }: BranchRowProps) { + const commitSha = branch.commit?.abbreviated_oid ?? ''; + const isDef = isDefault || branch.is_default; + return (
{branch.name} - {isDefault && ( + {isDef && ( Default )} - {branch.protected && ( - - - Protected + {branch.is_merged && ( + + Merged )} - - {branch.commit_sha.slice(0, 7)} - - {branch.last_push_at && ( - {timeAgo(branch.last_push_at)} + {commitSha && ( + {commitSha.slice(0, 7)} )} - {!isDefault && ( + {!isDef && (
+ {showBranchSelector && ( +
+
+ {branches?.map((branch: { name: string; id?: string }) => ( + + ))} +
+
+ )} +
+
+ {breadcrumbs.map((crumb, idx) => ( + + {idx > 0 && /} + + + ))} +
+ + History + +
+ +
+ {isLoading ? ( +
+ +
+ ) : sortedEntries.length > 0 ? ( +
+ {currentPath && ( + + )} + {sortedEntries.map((entry) => ( + + ))} +
+ ) : ( +
+ This directory is empty +
+ )} +
+ + +
+ ); +} + +function FileIcon({ filename }: { filename: string }) { + const ext = filename.split('.').pop()?.toLowerCase(); + if (['md', 'txt', 'rst'].includes(ext || '')) { + return ; + } + if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'].includes(ext || '')) { + return ; + } + if (['ts', 'tsx', 'js', 'jsx', 'py', 'rs', 'go', 'java', 'cpp', 'c', 'h'].includes(ext || '')) { + return ; + } + return ; +} + +function formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +function ReadmePreview({ + workspaceName, + repoName, + branch, + files, +}: { + workspaceName: string; + repoName: string; + branch: string; + files: Array<{ name: string; path: string }>; +}) { + const readmeFile = files.find((f) => + ['README.md', 'readme.md', 'README', 'README.txt'].includes(f.name), + ); + + const { data: blob } = useGitBlob( + readmeFile ? workspaceName : null, + readmeFile ? repoName : null, + readmeFile ? branch : null, + readmeFile ? readmeFile.path : null, + ); + + if (!readmeFile || !blob) return null; + + const content = new TextDecoder().decode(new Uint8Array(blob.data)); + + return ( +
+
+

{readmeFile.name}

+ + View raw + +
+
+
{content}
+
+
+ ); +} diff --git a/src/pages/Workspace/Repo/RepoCommitDetailPage.tsx b/src/pages/Workspace/Repo/RepoCommitDetailPage.tsx new file mode 100644 index 0000000..0f9ac4e --- /dev/null +++ b/src/pages/Workspace/Repo/RepoCommitDetailPage.tsx @@ -0,0 +1,188 @@ +import { ArrowLeft, Calendar, GitCommit, User } from 'lucide-react'; +import { Link, useParams } from 'react-router-dom'; + +import { Spinner } from '@/components/ui/spinner'; +import { useGitCommit } from '@/hooks/repo/useGitCommit'; +import { useGitDiff } from '@/hooks/repo/useGitDiff'; +import { timeAgo } from '@/lib/utils'; + +export function RepoCommitDetailPage() { + const { workspaceName = '', repoName = '', commitSha = '' } = useParams(); + + const { data: commit, isLoading: loadingCommit } = useGitCommit( + workspaceName, + repoName, + commitSha, + ); + + const parentSha = commit?.parent_oids?.[0]?.hex; + const { data: diff, isLoading: loadingDiff } = useGitDiff( + workspaceName, + repoName, + parentSha || null, + commitSha, + ); + + if (loadingCommit) { + return ( +
+ +
+ ); + } + + if (!commit) { + return ( +
+ +

Commit not found

+ + Back to commits + +
+ ); + } + + const author = commit.author?.identity; + const authoredDate = commit.authored_at + ? new Date(commit.authored_at.seconds * 1000).toISOString() + : null; + + return ( +
+ + + Back to commits + + +
+

{commit.subject}

+ {commit.body && ( +
+            {commit.body}
+          
+ )} + +
+ {author && ( + + + {author.name} + {'<'}{author.email}{'>'} + + )} + {authoredDate && ( + + + {timeAgo(authoredDate)} + + )} + + {commit.abbreviated_oid} + + {commit.parent_oids.length > 0 && ( + + parent{' '} + {commit.parent_oids.map((p, i) => ( + + {p.hex.slice(0, 7)} + + ))} + + )} +
+ + {commit.stats && ( +
+ +{commit.stats.additions} + -{commit.stats.deletions} + {commit.stats.changed_files} files changed +
+ )} +
+ + {loadingDiff ? ( +
+ +
+ ) : diff?.files && diff.files.length > 0 ? ( +
+ {diff.stats && ( +
+ {diff.stats.changed_files} files changed + +{diff.stats.additions} + -{diff.stats.deletions} +
+ )} + {diff.files.map((file, fileIdx) => ( +
+
+ {file.new_path} + +{file.additions} + -{file.deletions} + {file.binary && ( + Binary + )} +
+ {!file.binary && ( +
+ {file.hunks.map((hunk, hunkIdx) => ( +
+
+ {hunk.header} +
+ {hunk.lines.map((line, lineIdx) => { + const lineContent = new TextDecoder().decode( + new Uint8Array(line.content), + ); + const isAdd = line.type === 1; + const isDel = line.type === 2; + return ( +
+ + {line.old_line > 0 ? line.old_line : ''} + + + {line.new_line > 0 ? line.new_line : ''} + +
{lineContent}
+
+ ); + })} +
+ ))} +
+ )} +
+ ))} +
+ ) : ( +
+ No file changes in this commit +
+ )} +
+ ); +} diff --git a/src/pages/Workspace/Repo/RepoCommitsPage.tsx b/src/pages/Workspace/Repo/RepoCommitsPage.tsx new file mode 100644 index 0000000..8bda38b --- /dev/null +++ b/src/pages/Workspace/Repo/RepoCommitsPage.tsx @@ -0,0 +1,134 @@ +import { useState } from 'react'; +import { Link, useParams, useSearchParams } from 'react-router-dom'; +import { Clock, GitCommit, Hash, Search } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Spinner } from '@/components/ui/spinner'; +import { useGitCommits } from '@/hooks/repo/useGitCommits'; +import { useRepo } from '@/hooks/repo/useRepo'; +import { useRepoBranches } from '@/hooks/repo/useRepoBranches'; +import { timeAgo } from '@/lib/utils'; + +export function RepoCommitsPage() { + const { workspaceName = '', repoName = '' } = useParams(); + const [searchParams, setSearchParams] = useSearchParams(); + const [search, setSearch] = useState(''); + const [showBranchSelector, setShowBranchSelector] = useState(false); + + const branch = searchParams.get('branch') || ''; + const { data: repo } = useRepo(workspaceName, repoName); + const { data: branches } = useRepoBranches(workspaceName, repoName); + const { data: commitsData, isLoading } = useGitCommits( + workspaceName, + repoName, + branch || repo?.default_branch, + ); + + const effectiveBranch = branch || repo?.default_branch || ''; + const commits = commitsData?.commits ?? []; + const filtered = search + ? commits.filter( + (c) => + c.subject.toLowerCase().includes(search.toLowerCase()) || + c.abbreviated_oid.includes(search), + ) + : commits; + + return ( +
+
+
+ + {showBranchSelector && ( +
+
+ {branches?.map((b: { name: string; id?: string }) => ( + + ))} +
+
+ )} +
+
+ + setSearch(e.target.value)} + /> +
+
+ +
+ {isLoading ? ( +
+ +
+ ) : filtered.length > 0 ? ( +
+ {filtered.map((commit) => ( + + +
+

+ {commit.subject} +

+
+ {commit.author?.identity?.name || 'Unknown'} + {commit.authored_at && ( + + + {timeAgo( + new Date( + commit.authored_at.seconds * 1000, + ).toISOString(), + )} + + )} +
+
+ + + {commit.abbreviated_oid} + + + ))} +
+ ) : ( +
+ +

+ {search ? `No commits matching "${search}"` : 'No commits yet'} +

+
+ )} +
+
+ ); +} diff --git a/src/pages/Workspace/Repo/RepoCreatePrPage.tsx b/src/pages/Workspace/Repo/RepoCreatePrPage.tsx new file mode 100644 index 0000000..c513a1f --- /dev/null +++ b/src/pages/Workspace/Repo/RepoCreatePrPage.tsx @@ -0,0 +1,162 @@ +import { useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { ArrowLeft, GitPullRequest } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { useCreatePr } from '@/hooks/pull-request/useCreatePr'; +import { useRepo } from '@/hooks/repo/useRepo'; +import { useRepoBranches } from '@/hooks/repo/useRepoBranches'; + +export function RepoCreatePrPage() { + const { workspaceName = '', repoName = '' } = useParams(); + const navigate = useNavigate(); + + const { data: repo } = useRepo(workspaceName, repoName); + const { data: branches } = useRepoBranches(workspaceName, repoName); + const createPr = useCreatePr(workspaceName, repoName); + + const [title, setTitle] = useState(''); + const [body, setBody] = useState(''); + const [sourceBranch, setSourceBranch] = useState(''); + const [targetBranch, setTargetBranch] = useState(repo?.default_branch || ''); + const [draft, setDraft] = useState(false); + + const branchNames = (branches ?? []).map((b: { name: string }) => b.name); + + const handleSubmit = () => { + if (!title.trim() || !sourceBranch || !targetBranch) return; + createPr.mutate( + { + title: title.trim(), + body: body.trim() || undefined, + source_branch: sourceBranch, + target_branch: targetBranch, + source_repo_id: repo?.id || '', + head_commit_sha: '', + draft, + }, + { + onSuccess: () => { + navigate(`/${workspaceName}/repos/${repoName}/pulls`); + }, + }, + ); + }; + + return ( +
+
+ +
+ +
+
+ +

Create pull request

+
+
+ +
+
+ + + +
+ +
+ + setTitle(e.target.value)} + /> +
+ +
+ +