From 7a85ad64a5f0c3c5741beb8f59580a57273fed06 Mon Sep 17 00:00:00 2001 From: Levi Gomes Date: Thu, 9 Jul 2026 15:05:50 -0300 Subject: [PATCH 1/2] feat(languages): confirm and block deactivation with project usage Add a role-gated deactivate affordance to language cards and a confirmation dialog that lists the projects using the language and blocks deactivation while any project still uses it (matching the backend 409). The trash is shown to platform admins on every language and to managers only on languages they created. - languagesAPI.stats / languagesAPI.delete - LanguageResponse.created_by; LanguageProjectRef / LanguageStatsResponse - confirm dialog lists affected projects and disables Deactivate when in use --- src/components/pages/LanguagesPage.tsx | 103 ++++++++++++++++++++++++- src/services/api.ts | 5 ++ src/types/index.ts | 2 + src/types/language.ts | 12 +++ 4 files changed, 120 insertions(+), 2 deletions(-) diff --git a/src/components/pages/LanguagesPage.tsx b/src/components/pages/LanguagesPage.tsx index da49e1b..0481597 100644 --- a/src/components/pages/LanguagesPage.tsx +++ b/src/components/pages/LanguagesPage.tsx @@ -1,8 +1,10 @@ import { useEffect, useState } from "react" -import { Languages, Plus } from "lucide-react" +import { Languages, Plus, Trash2 } from "lucide-react" import { toast } from "sonner" import { languagesAPI } from "@/services/api" +import type { LanguageResponse, LanguageStatsResponse } from "@/types" import { useLanguagesStore } from "@/stores/languagesStore" +import { useAuth } from "@/contexts/AuthContext" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" @@ -21,18 +23,26 @@ import { InfoTooltip } from "@/components/common/InfoTooltip" import { formatDate } from "@/utils/format" export default function LanguagesPage() { + const { user, isPlatformAdmin, isManager } = useAuth() const { languages, loading: storeLoading, lastFetched, fetch: fetchLanguages } = useLanguagesStore() const loading = storeLoading || (!lastFetched && languages.length === 0) const [dialogOpen, setDialogOpen] = useState(false) const [creating, setCreating] = useState(false) const [name, setName] = useState("") const [code, setCode] = useState("") + const [deleteTarget, setDeleteTarget] = useState(null) + const [deleteStats, setDeleteStats] = useState(null) + const [deleting, setDeleting] = useState(false) useEffect(() => { fetchLanguages() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + function canDeactivate(lang: LanguageResponse) { + return isPlatformAdmin || (isManager && lang.created_by === user?.id) + } + function openDialog() { setName("") setCode("") @@ -60,6 +70,42 @@ export default function LanguagesPage() { } } + async function openDeleteDialog(lang: LanguageResponse) { + setDeleteTarget(lang) + setDeleteStats(null) + try { + const { data } = await languagesAPI.stats(lang.id) + setDeleteStats(data) + } catch { + setDeleteStats(null) + } + } + + const blockedByProjects = (deleteStats?.project_count ?? 0) > 0 + + async function handleDeactivate() { + if (!deleteTarget || deleteStats === null || blockedByProjects) return + setDeleting(true) + try { + await languagesAPI.delete(deleteTarget.id) + toast.success("Language deactivated") + setDeleteTarget(null) + useLanguagesStore.getState().invalidate() + await fetchLanguages() + } catch (err: unknown) { + const status = (err as { response?: { status?: number } })?.response?.status + if (status === 409) { + toast.error("This language is used by projects and cannot be deactivated") + } else if (status === 403) { + toast.error("You can only deactivate languages you created") + } else { + toast.error("Failed to deactivate language") + } + } finally { + setDeleting(false) + } + } + if (loading) { return } @@ -95,8 +141,19 @@ export default function LanguagesPage() { {languages.map((lang) => (
+ {canDeactivate(lang) && ( + + )}
{lang.code} @@ -167,6 +224,48 @@ export default function LanguagesPage() { + + { if (!open) setDeleteTarget(null) }}> + + + Deactivate Language + + {deleteTarget + ? deleteStats === null + ? `Checking which projects use "${deleteTarget.name}"...` + : blockedByProjects + ? `"${deleteTarget.name}" is used by ${deleteStats.project_count} project${deleteStats.project_count !== 1 ? "s" : ""} and cannot be deactivated. Reassign or remove it from these projects first.` + : `Deactivating "${deleteTarget.name}" hides it from new project creation. Existing projects are unaffected and it can be restored later.` + : ""} + + + {blockedByProjects && deleteStats && ( +
    + {deleteStats.projects.map((project) => ( +
  • + {project.name} +
  • + ))} +
+ )} + + + + +
+
) } diff --git a/src/services/api.ts b/src/services/api.ts index d0f3a8c..d48bfb0 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -16,6 +16,7 @@ import type { AppRoleResponse, LanguageResponse, LanguageCreate, + LanguageStatsResponse, OrganizationResponse, OrganizationCreate, OrganizationUpdate, @@ -174,6 +175,10 @@ export const languagesAPI = { api.get(`/languages/${languageId}`), getByCode: (code: string) => api.get(`/languages/code/${code}`), + stats: (languageId: string) => + api.get(`/languages/${languageId}/stats`), + delete: (languageId: string) => + api.delete(`/languages/${languageId}`), } export const orgsAPI = { diff --git a/src/types/index.ts b/src/types/index.ts index e343aa0..7411888 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -25,6 +25,8 @@ export type { export type { LanguageResponse, LanguageCreate, + LanguageProjectRef, + LanguageStatsResponse, } from "./language" export type { diff --git a/src/types/language.ts b/src/types/language.ts index fb5a8bd..daa9cdb 100644 --- a/src/types/language.ts +++ b/src/types/language.ts @@ -2,6 +2,7 @@ export interface LanguageResponse { id: string name: string code: string + created_by: string | null created_at: string } @@ -9,3 +10,14 @@ export interface LanguageCreate { name: string code: string } + +export interface LanguageProjectRef { + id: string + name: string +} + +export interface LanguageStatsResponse { + language_id: string + project_count: number + projects: LanguageProjectRef[] +} From 1586cc11a22042bc43e45134e7dd80abc4b8afb0 Mon Sep 17 00:00:00 2001 From: Levi Gomes Date: Wed, 15 Jul 2026 17:01:16 -0300 Subject: [PATCH 2/2] chore(ci): empty commit to re-trigger CI checks Co-Authored-By: Claude Opus 4.8 (1M context)