Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -3303,6 +3303,8 @@
},
"inviteResent": "Invite resent",
"inviteResendFailed": "Failed to resend invite",
"inviteResendCooldown": "You're resending invites too quickly",
"inviteResendCooldownDetail": "You can resend in {seconds} second | You can resend in {seconds} seconds",
"failedToUpdateWorkspace": "Failed to update workspace",
"failedToCreateWorkspace": "Failed to create workspace",
"failedToDeleteWorkspace": "Failed to delete workspace",
Expand Down
18 changes: 16 additions & 2 deletions src/platform/workspace/api/workspaceApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,22 @@ export class WorkspaceApiError extends Error {
constructor(
message: string,
public readonly status?: number,
public readonly code?: string
public readonly code?: string,
public readonly retryAfter?: number
) {
super(message)
this.name = 'WorkspaceApiError'
}
}

function parseRetryAfterSeconds(value: unknown): number | undefined {
if (typeof value !== 'string' || value.trim() === '') return undefined
const seconds = Number(value)
return Number.isFinite(seconds) && seconds > 0
? Math.ceil(seconds)
: undefined
}

const workspaceApiClient = axios.create({
headers: {
'Content-Type': 'application/json'
Expand All @@ -189,10 +198,15 @@ function handleAxiosError(err: unknown): never {
)
// Callers compare `code` against server-defined values, so the parser's
// "no code reported" sentinel must stay out of that contract.
const retryAfter =
status === 429
? parseRetryAfterSeconds(err.response?.headers?.['retry-after'])
: undefined
throw new WorkspaceApiError(
message,
status,
code === UNKNOWN_ERROR_CODE ? undefined : code
code === UNKNOWN_ERROR_CODE ? undefined : code,
retryAfter
)
}
throw err
Expand Down
19 changes: 18 additions & 1 deletion src/platform/workspace/composables/useMembersPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
import { WorkspaceApiError } from '@/platform/workspace/api/workspaceApi'
import type { WorkspaceRole } from '@/platform/workspace/api/workspaceApi'
import { useTeamPlan } from '@/platform/workspace/composables/useTeamPlan'
import { useWorkspaceUI } from '@/platform/workspace/composables/useWorkspaceUI'
Expand Down Expand Up @@ -349,7 +350,23 @@ export function useMembersPanel() {
summary: t('workspacePanel.toast.inviteResent'),
life: 2000
})
} catch {
} catch (err) {
if (err instanceof WorkspaceApiError && err.status === 429) {
const seconds = err.retryAfter
toast.add({
severity: 'warn',
summary: t('workspacePanel.toast.inviteResendCooldown'),
detail: seconds
? t(
'workspacePanel.toast.inviteResendCooldownDetail',
{ seconds },
seconds
)
: undefined,
life: Math.min((seconds ?? 5) * 1000, 10_000)
})
return
}
toast.add({
severity: 'error',
summary: t('workspacePanel.toast.inviteResendFailed')
Expand Down
Loading