Commit 9a412925 authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

feat: redesign UI to premium shadcn/ui with Canifa warm red/orange accents and...

feat: redesign UI to premium shadcn/ui with Canifa warm red/orange accents and translate company terms to Vietnamese departments (Phòng ban)
parent 5a7791d1
......@@ -41,34 +41,34 @@ export function CompanySettingsSidebar() {
className="flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
>
<ChevronLeft className="h-3.5 w-3.5 shrink-0" />
<span className="truncate">{selectedCompany?.name ?? "Company"}</span>
<span className="truncate">{selectedCompany?.name ?? "Phòng ban"}</span>
</Link>
<div className="flex items-center gap-2 px-2 py-1">
<Settings className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="flex-1 truncate text-sm font-bold text-foreground">
Company Settings
Thiết lập phòng ban
</span>
</div>
</div>
<nav className="flex-1 min-h-0 overflow-y-auto scrollbar-auto-hide px-3 py-2">
<div className="flex flex-col gap-0.5">
<SidebarNavItem to="/company/settings" label="General" icon={SlidersHorizontal} end />
<SidebarNavItem to="/company/settings" label="Chung" icon={SlidersHorizontal} end />
<SidebarNavItem
to="/company/settings/environments"
label="Environments"
label="Môi trường"
icon={MonitorCog}
end
/>
<SidebarNavItem
to="/company/settings/access"
label="Access"
label="Truy cập"
icon={Shield}
badge={badges?.joinRequests ?? 0}
end
/>
<SidebarNavItem to="/company/settings/invites" label="Invites" icon={MailPlus} end />
<SidebarNavItem to="/company/settings/secrets" label="Secrets" icon={KeyRound} end />
<SidebarNavItem to="/company/settings/invites" label="Lời mời" icon={MailPlus} end />
<SidebarNavItem to="/company/settings/secrets" label="Bí mật" icon={KeyRound} end />
</div>
</nav>
</aside>
......
......@@ -79,12 +79,12 @@ export function Sidebar() {
className="flex items-center gap-2.5 px-3 py-2 text-[13px] font-medium text-muted-foreground hover:bg-accent/50 hover:text-foreground transition-colors"
>
<SquarePen className="h-4 w-4 shrink-0" />
<span className="truncate">New Issue</span>
<span className="truncate">Vấn đề mới</span>
</button>
<SidebarNavItem to="/dashboard" label="Dashboard" icon={LayoutDashboard} liveCount={liveRunCount} />
<SidebarNavItem to="/dashboard" label="Bảng điều khiển" icon={LayoutDashboard} liveCount={liveRunCount} />
<SidebarNavItem
to="/inbox"
label="Inbox"
label="Hộp thư"
icon={Inbox}
badge={inboxBadge.inbox}
badgeTone={inboxBadge.failedRuns > 0 ? "danger" : "default"}
......@@ -99,18 +99,18 @@ export function Sidebar() {
/>
</div>
<SidebarSection label="Work">
<SidebarNavItem to="/issues" label="Issues" icon={CircleDot} />
<SidebarNavItem to="/routines" label="Routines" icon={Repeat} />
<SidebarSection label="Công việc">
<SidebarNavItem to="/issues" label="Vấn đề" icon={CircleDot} />
<SidebarNavItem to="/routines" label="Quy trình" icon={Repeat} />
<PluginLauncherOutlet
placementZones={["sidebar"]}
context={pluginContext}
className="flex flex-col gap-0.5"
itemClassName="text-[13px] font-medium"
/>
<SidebarNavItem to="/goals" label="Goals" icon={Target} />
<SidebarNavItem to="/goals" label="Mục tiêu" icon={Target} />
{showWorkspacesLink ? (
<SidebarNavItem to="/workspaces" label="Workspaces" icon={GitBranch} />
<SidebarNavItem to="/workspaces" label="Không gian làm việc" icon={GitBranch} />
) : null}
</SidebarSection>
......@@ -118,12 +118,12 @@ export function Sidebar() {
<SidebarAgents />
<SidebarSection label="Company">
<SidebarNavItem to="/org" label="Org" icon={Network} />
<SidebarNavItem to="/skills" label="Skills" icon={Boxes} />
<SidebarNavItem to="/costs" label="Costs" icon={DollarSign} />
<SidebarNavItem to="/activity" label="Activity" icon={History} />
<SidebarNavItem to="/company/settings" label="Settings" icon={Settings} />
<SidebarSection label="Phòng ban">
<SidebarNavItem to="/org" label="Sơ đồ" icon={Network} />
<SidebarNavItem to="/skills" label="Kỹ năng" icon={Boxes} />
<SidebarNavItem to="/costs" label="Chi phí" icon={DollarSign} />
<SidebarNavItem to="/activity" label="Hoạt động" icon={History} />
<SidebarNavItem to="/company/settings" label="Thiết lập" icon={Settings} />
</SidebarSection>
<PluginSlotOutlet
......
......@@ -225,12 +225,12 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
<Button
variant="ghost"
className="h-9 flex-1 justify-start gap-2 px-2 text-left"
aria-label={selectedCompany ? `Open ${selectedCompany.name} workspace switcher` : "Open workspace switcher"}
aria-label={selectedCompany ? `Mở menu chuyển phòng ban của ${selectedCompany.name}` : "Mở menu chuyển phòng ban"}
>
<span className="flex min-w-0 flex-1 items-center gap-2">
{selectedCompany ? <WorkspaceIcon company={selectedCompany} /> : null}
<span className="truncate text-sm font-bold text-foreground">
{selectedCompany?.name ?? "Select workspace"}
{selectedCompany?.name ?? "Chọn phòng ban"}
</span>
</span>
<ChevronsUpDown className="size-3.5 shrink-0 text-muted-foreground" />
......@@ -239,7 +239,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
<DropdownMenuContent align="start" sideOffset={8} className="w-64 p-1">
<div className="flex items-center justify-between gap-2 px-2 py-1.5">
<DropdownMenuLabel className="p-0 text-[11px] font-semibold uppercase text-muted-foreground">
Switch workspace
Chuyển phòng ban
</DropdownMenuLabel>
<button
type="button"
......@@ -250,7 +250,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
}}
className="rounded px-1.5 py-0.5 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
{isEditingOrder ? "Done" : "Edit"}
{isEditingOrder ? "Xong" : "Sắp xếp"}
</button>
</div>
<div className="max-h-96 overflow-y-auto">
......@@ -275,7 +275,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
</SortableContext>
</DndContext>
{orderedCompanies.length === 0 ? (
<DropdownMenuItem disabled>No workspaces</DropdownMenuItem>
<DropdownMenuItem disabled>Không có phòng ban</DropdownMenuItem>
) : null}
</div>
<DropdownMenuSeparator />
......@@ -285,7 +285,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
disabled={isEditingOrder}
>
<Plus className="size-4" />
<span>Add company...</span>
<span>Thêm phòng ban...</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild disabled={isEditingOrder}>
......@@ -301,7 +301,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
>
<UserPlus className="size-4" />
<span className="truncate">
{selectedCompany ? `Invite people to ${selectedCompany.name}` : "Invite people"}
{selectedCompany ? `Mời nhân sự vào ${selectedCompany.name}` : "Mời nhân sự"}
</span>
</Link>
</DropdownMenuItem>
......@@ -317,7 +317,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
}}
>
<Settings className="size-4" />
<span>Company settings</span>
<span>Thiết lập phòng ban</span>
</Link>
</DropdownMenuItem>
{session?.session ? (
......@@ -329,7 +329,7 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb
disabled={isEditingOrder || signOutMutation.isPending}
>
<LogOut className="size-4" />
<span>{signOutMutation.isPending ? "Signing out..." : "Sign out"}</span>
<span>{signOutMutation.isPending ? "Đang đăng xuất..." : "Đăng xuất"}</span>
</DropdownMenuItem>
</>
) : null}
......
{
"app": {
"noCompanies": {
"title": "Create your first company",
"description": "Get started by creating a company.",
"newCompany": "New Company"
"title": "Create your first department",
"description": "Get started by creating a department.",
"newCompany": "New Department"
}
}
}
{
"app": {
"noCompanies": {
"title": "Tạo công ty đầu tiên của bạn",
"description": "Bắt đầu bằng cách tạo một công ty.",
"newCompany": "Công ty mới"
"title": "Tạo phòng ban đầu tiên của bạn",
"description": "Bắt đầu bằng cách tạo một phòng ban.",
"newCompany": "Phòng ban mới"
}
}
}
......@@ -38,45 +38,45 @@
--color-sidebar-ring: var(--sidebar-ring);
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0px;
--radius-xl: 0px;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
}
:root {
color-scheme: light;
--radius: 0;
--radius: 0.5rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary: oklch(0.53 0.22 25.4);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--accent: oklch(0.97 0.01 25.4);
--accent-foreground: oklch(0.205 0.01 25.4);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--ring: oklch(0.53 0.22 25.4);
--chart-1: oklch(0.53 0.22 25.4);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary: oklch(0.53 0.22 25.4);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--sidebar-ring: oklch(0.53 0.22 25.4);
--chip-match-title-bg: oklch(0.97 0.02 265);
--chip-match-title-fg: oklch(0.4 0.13 265);
--chip-match-title-border: oklch(0.85 0.05 265);
......@@ -92,38 +92,38 @@
}
.dark {
--background: oklch(0.145 0 0);
--background: oklch(0.13 0.01 25.4);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card: oklch(0.17 0.01 25.4);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover: oklch(0.17 0.01 25.4);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--primary: oklch(0.58 0.23 25.4);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.22 0.01 25.4);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted: oklch(0.22 0.01 25.4);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent: oklch(0.22 0.02 25.4);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.637 0.237 25.331);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--border: oklch(0.22 0.01 25.4);
--input: oklch(0.22 0.01 25.4);
--ring: oklch(0.58 0.23 25.4);
--chart-1: oklch(0.58 0.23 25.4);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.145 0 0);
--sidebar: oklch(0.13 0.01 25.4);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary: oklch(0.58 0.23 25.4);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent: oklch(0.22 0.02 25.4);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
--sidebar-border: oklch(0.22 0.01 25.4);
--sidebar-ring: oklch(0.58 0.23 25.4);
--chip-match-title-bg: oklch(0.27 0.04 265 / 0.5);
--chip-match-title-fg: oklch(0.78 0.10 265);
--chip-match-title-border: oklch(0.4 0.06 265);
......
......@@ -49,7 +49,7 @@ export function Activity() {
const [filter, setFilter] = useState("all");
useEffect(() => {
setBreadcrumbs([{ label: "Activity" }]);
setBreadcrumbs([{ label: "Hoạt động" }]);
}, [setBreadcrumbs]);
const { data, isLoading, error } = useQuery({
......@@ -101,7 +101,7 @@ export function Activity() {
}, [data]);
if (!selectedCompanyId) {
return <EmptyState icon={History} message="Select a company to view activity." />;
return <EmptyState icon={History} message="Chọn phòng ban để xem hoạt động." />;
}
if (isLoading) {
......@@ -122,10 +122,10 @@ export function Activity() {
<div className="flex items-center justify-end">
<Select value={filter} onValueChange={setFilter}>
<SelectTrigger className="w-[140px] h-8 text-xs">
<SelectValue placeholder="Filter by type" />
<SelectValue placeholder="Lọc theo loại" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All types</SelectItem>
<SelectItem value="all">Tất cả</SelectItem>
{entityTypes.map((type) => (
<SelectItem key={type} value={type}>
{type.charAt(0).toUpperCase() + type.slice(1)}
......@@ -138,7 +138,7 @@ export function Activity() {
{error && <p className="text-sm text-destructive">{error.message}</p>}
{filtered && filtered.length === 0 && (
<EmptyState icon={History} message="No activity yet." />
<EmptyState icon={History} message="Chưa có hoạt động nào." />
)}
{filtered && filtered.length > 0 && (
......
......@@ -266,7 +266,7 @@ export function AdapterManager() {
useEffect(() => {
setBreadcrumbs([
{ label: selectedCompany?.name ?? "Company", href: "/dashboard" },
{ label: selectedCompany?.name ?? "Phòng ban", href: "/dashboard" },
{ label: "Settings", href: "/instance/settings/general" },
{ label: "Adapters" },
]);
......
......@@ -116,11 +116,11 @@ export function Agents() {
}, [agents]);
useEffect(() => {
setBreadcrumbs([{ label: "Agents" }]);
setBreadcrumbs([{ label: "Tác nhân" }]);
}, [setBreadcrumbs]);
if (!selectedCompanyId) {
return <EmptyState icon={Bot} message="Select a company to view agents." />;
return <EmptyState icon={Bot} message="Chọn phòng ban để xem danh sách tác nhân." />;
}
if (isLoading) {
......@@ -136,10 +136,10 @@ export function Agents() {
<Tabs value={tab} onValueChange={(v) => navigate(`/agents/${v}`)}>
<PageTabBar
items={[
{ value: "all", label: "All" },
{ value: "active", label: "Active" },
{ value: "paused", label: "Paused" },
{ value: "error", label: "Error" },
{ value: "all", label: "Tất cả" },
{ value: "active", label: "Đang hoạt động" },
{ value: "paused", label: "Tạm dừng" },
{ value: "error", label: "Lỗi" },
]}
value={tab}
onValueChange={(v) => navigate(`/agents/${v}`)}
......@@ -171,7 +171,7 @@ export function Agents() {
)}>
{showTerminated && <span className="text-background text-[10px] leading-none">&#10003;</span>}
</span>
Show terminated
Hiển thị đã chấm dứt
</button>
</div>
)}
......@@ -201,13 +201,13 @@ export function Agents() {
)}
<Button size="sm" variant="outline" onClick={openNewAgent}>
<Plus className="h-3.5 w-3.5 mr-1.5" />
New Agent
Tác nhân mới
</Button>
</div>
</div>
{filtered.length > 0 && (
<p className="text-xs text-muted-foreground">{filtered.length} agent{filtered.length !== 1 ? "s" : ""}</p>
<p className="text-xs text-muted-foreground">{filtered.length} tác nhân</p>
)}
{error && <p className="text-sm text-destructive">{error.message}</p>}
......@@ -215,8 +215,8 @@ export function Agents() {
{agents && agents.length === 0 && (
<EmptyState
icon={Bot}
message="Create your first agent to get started."
action="New Agent"
message="Hãy tạo tác nhân đầu tiên của bạn để bắt đầu."
action="Tác nhân mới"
onAction={openNewAgent}
/>
)}
......@@ -286,7 +286,7 @@ export function Agents() {
{effectiveView === "list" && agents && agents.length > 0 && filtered.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-8">
No agents match the selected filter.
Không có tác nhân nào khớp với bộ lọc đã chọn.
</p>
)}
......@@ -301,13 +301,13 @@ export function Agents() {
{effectiveView === "org" && orgTree && orgTree.length > 0 && filteredOrg.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-8">
No agents match the selected filter.
Không có tác nhân nào khớp với bộ lọc đã chọn.
</p>
)}
{effectiveView === "org" && orgTree && orgTree.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-8">
No organizational hierarchy defined.
Chưa định nghĩa sơ đồ tổ chức.
</p>
)}
</div>
......
......@@ -26,7 +26,7 @@ export function Approvals() {
const [actionError, setActionError] = useState<string | null>(null);
useEffect(() => {
setBreadcrumbs([{ label: "Approvals" }]);
setBreadcrumbs([{ label: "Phê duyệt" }]);
}, [setBreadcrumbs]);
const { data, isLoading, error } = useQuery({
......@@ -75,7 +75,7 @@ export function Approvals() {
).length;
if (!selectedCompanyId) {
return <p className="text-sm text-muted-foreground">Select a company first.</p>;
return <p className="text-sm text-muted-foreground">Vui lòng chọn phòng ban trước.</p>;
}
if (isLoading) {
......@@ -87,7 +87,7 @@ export function Approvals() {
<div className="flex items-center justify-between">
<Tabs value={statusFilter} onValueChange={(v) => navigate(`/approvals/${v}`)}>
<PageTabBar items={[
{ value: "pending", label: <>Pending{pendingCount > 0 && (
{ value: "pending", label: <>Đang chờ{pendingCount > 0 && (
<span className={cn(
"ml-1.5 rounded-full px-1.5 py-0.5 text-[10px] font-medium",
"bg-yellow-500/20 text-yellow-500"
......@@ -95,7 +95,7 @@ export function Approvals() {
{pendingCount}
</span>
)}</> },
{ value: "all", label: "All" },
{ value: "all", label: "Tất cả" },
]} />
</Tabs>
</div>
......@@ -107,7 +107,7 @@ export function Approvals() {
<div className="flex flex-col items-center justify-center py-16 text-center">
<ShieldCheck className="h-8 w-8 text-muted-foreground/30 mb-3" />
<p className="text-sm text-muted-foreground">
{statusFilter === "pending" ? "No pending approvals." : "No approvals yet."}
{statusFilter === "pending" ? "Không có phê duyệt nào đang chờ." : "Chưa có phê duyệt nào."}
</p>
</div>
)}
......
......@@ -69,7 +69,7 @@ export function Companies() {
});
useEffect(() => {
setBreadcrumbs([{ label: "Companies" }]);
setBreadcrumbs([{ label: "Phòng ban" }]);
}, [setBreadcrumbs]);
function startEdit(companyId: string, currentName: string) {
......@@ -92,12 +92,12 @@ export function Companies() {
<div className="flex items-center justify-end">
<Button size="sm" onClick={() => openOnboarding()}>
<Plus className="h-3.5 w-3.5 mr-1.5" />
New Company
Thêm phòng ban
</Button>
</div>
<div className="h-6">
{loading && <p className="text-sm text-muted-foreground">Loading companies...</p>}
{loading && <p className="text-sm text-muted-foreground">Đang tải danh sách phòng ban...</p>}
{error && <p className="text-sm text-destructive">{error.message}</p>}
</div>
......@@ -215,7 +215,7 @@ export function Companies() {
onClick={() => startEdit(company.id, company.name)}
>
<Pencil className="h-3.5 w-3.5" />
Rename
Đổi tên
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
......@@ -223,7 +223,7 @@ export function Companies() {
onClick={() => setConfirmDeleteId(company.id)}
>
<Trash2 className="h-3.5 w-3.5" />
Delete Company
Xóa phòng ban
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
......@@ -235,13 +235,13 @@ export function Companies() {
<div className="flex items-center gap-1.5">
<Users className="h-3.5 w-3.5" />
<span>
{agentCount} {agentCount === 1 ? "agent" : "agents"}
{agentCount} {agentCount === 1 ? "tác vụ/agent" : "tác vụ/agent"}
</span>
</div>
<div className="flex items-center gap-1.5">
<CircleDot className="h-3.5 w-3.5" />
<span>
{issueCount} {issueCount === 1 ? "issue" : "issues"}
{issueCount} vấn đề
</span>
</div>
<div className="flex items-center gap-1.5 tabular-nums">
......@@ -250,12 +250,12 @@ export function Companies() {
{formatCents(company.spentMonthlyCents)}
{company.budgetMonthlyCents > 0
? <> / {formatCents(company.budgetMonthlyCents)} <span className="text-xs">({budgetPct}%)</span></>
: <span className="text-xs ml-1">Unlimited budget</span>}
: <span className="text-xs ml-1">Không giới hạn ngân sách</span>}
</span>
</div>
<div className="flex items-center gap-1.5 ml-auto">
<Calendar className="h-3.5 w-3.5" />
<span>Created {relativeTime(company.createdAt)}</span>
<span>Đã tạo {relativeTime(company.createdAt)}</span>
</div>
</div>
......@@ -266,7 +266,7 @@ export function Companies() {
onClick={(e) => e.stopPropagation()}
>
<p className="text-sm text-destructive font-medium">
Delete this company and all its data? This cannot be undone.
Xóa phòng ban này và toàn bộ dữ liệu? Thao tác này không thể hoàn tác.
</p>
<div className="flex items-center gap-2 ml-4 shrink-0">
<Button
......@@ -275,7 +275,7 @@ export function Companies() {
onClick={() => setConfirmDeleteId(null)}
disabled={deleteMutation.isPending}
>
Cancel
Hủy bỏ
</Button>
<Button
variant="destructive"
......@@ -283,7 +283,7 @@ export function Companies() {
onClick={() => deleteMutation.mutate(company.id)}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? "Deleting…" : "Delete"}
{deleteMutation.isPending ? "Đang xóa…" : "Xóa"}
</Button>
</div>
</div>
......
This diff is collapsed.
......@@ -169,9 +169,9 @@ export function CompanyEnvironments() {
useEffect(() => {
setBreadcrumbs([
{ label: selectedCompany?.name ?? "Company", href: "/dashboard" },
{ label: "Settings", href: "/company/settings" },
{ label: "Environments" },
{ label: selectedCompany?.name ?? "Phòng ban", href: "/dashboard" },
{ label: "Thiết lập", href: "/company/settings" },
{ label: "Môi trường" },
]);
}, [selectedCompany?.name, setBreadcrumbs]);
......@@ -397,7 +397,7 @@ export function CompanyEnvironments() {
Object.keys(sandboxConfigErrors).length === 0);
if (!selectedCompanyId) {
return <div className="text-sm text-muted-foreground">Select a company to manage environments.</div>;
return <div className="text-sm text-muted-foreground">Vui lòng chọn phòng ban để quản lý môi trường.</div>;
}
if (!environmentsEnabled) {
......@@ -405,10 +405,10 @@ export function CompanyEnvironments() {
<div className="max-w-3xl space-y-4">
<div className="flex items-center gap-2">
<Settings className="h-5 w-5 text-muted-foreground" />
<h1 className="text-lg font-semibold">Company Environments</h1>
<h1 className="text-lg font-semibold">Môi trường phòng ban</h1>
</div>
<div className="rounded-md border border-border px-4 py-4 text-sm text-muted-foreground">
Enable Environments in instance experimental settings to manage company execution targets.
Kích hoạt môi trường trong thiết lập thử nghiệm của hệ thống để quản lý các mục tiêu thực thi của phòng ban.
</div>
</div>
);
......@@ -419,10 +419,10 @@ export function CompanyEnvironments() {
<div className="space-y-2">
<div className="flex items-center gap-2">
<Settings className="h-5 w-5 text-muted-foreground" />
<h1 className="text-lg font-semibold">Company Environments</h1>
<h1 className="text-lg font-semibold">Môi trường phòng ban</h1>
</div>
<p className="max-w-3xl text-sm text-muted-foreground">
Define reusable execution targets for projects, issue workspaces, and remote-capable adapters.
Định nghĩa các mục tiêu thực thi tái sử dụng cho dự án, không gian làm việc và các adapter hỗ trợ điều khiển từ xa.
</p>
</div>
......@@ -486,7 +486,7 @@ export function CompanyEnvironments() {
<div className="space-y-3">
{(environments ?? []).length === 0 ? (
<div className="text-sm text-muted-foreground">No environments saved for this company yet.</div>
<div className="text-sm text-muted-foreground">Chưa có môi trường nào được lưu cho phòng ban này.</div>
) : (
(environments ?? []).map((environment) => {
const probe = probeResults[environment.id] ?? null;
......
......@@ -500,7 +500,7 @@ function ExportPreviewPane({
}) {
if (!selectedFile || content === null) {
return (
<EmptyState icon={Package} message="Select a file to preview its contents." />
<EmptyState icon={Package} message="Chọn một tệp để xem trước nội dung." />
);
}
......@@ -546,7 +546,7 @@ function ExportPreviewPane({
</pre>
) : (
<div className="rounded-lg border border-border bg-accent/10 px-4 py-3 text-sm text-muted-foreground">
Binary asset preview is not available for this file type.
Không hỗ trợ xem trước tệp nhị phân loại này.
</div>
)}
</div>
......@@ -911,7 +911,7 @@ export function CompanyExport() {
}
if (!selectedCompanyId) {
return <EmptyState icon={Package} message="Select a company to export." />;
return <EmptyState icon={Package} message="Chọn phòng ban để xuất dữ liệu." />;
}
if (exportPreviewMutation.isPending && !exportData) {
......@@ -919,7 +919,7 @@ export function CompanyExport() {
}
if (!exportData) {
return <EmptyState icon={Package} message="Loading export data..." />;
return <EmptyState icon={Package} message="Đang tải dữ liệu xuất..." />;
}
const previewContent = selectedFile
......@@ -935,14 +935,14 @@ export function CompanyExport() {
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-4 text-sm">
<span className="font-medium">
{selectedCompany?.name ?? "Company"} export
Xuất phòng ban {selectedCompany?.name ?? "phòng ban"}
</span>
<span className="text-muted-foreground">
{selectedCount} / {totalFiles} file{totalFiles === 1 ? "" : "s"} selected
Đã chọn {selectedCount} / {totalFiles} tệp
</span>
{warnings.length > 0 && (
<span className="text-amber-500">
{warnings.length} warning{warnings.length === 1 ? "" : "s"}
{warnings.length} cảnh báo
</span>
)}
</div>
......@@ -953,8 +953,8 @@ export function CompanyExport() {
>
<Download className="mr-1.5 h-3.5 w-3.5" />
{downloadMutation.isPending
? "Building export..."
: `Export ${selectedCount} file${selectedCount === 1 ? "" : "s"}`}
? "Đang tạo tệp xuất..."
: `Xuất ${selectedCount} tệp`}
</Button>
</div>
</div>
......@@ -972,7 +972,7 @@ export function CompanyExport() {
<div className="grid h-[calc(100vh-12rem)] gap-0 xl:grid-cols-[19rem_minmax(0,1fr)]">
<aside className="flex flex-col border-r border-border overflow-hidden">
<div className="border-b border-border px-4 py-3 shrink-0">
<h2 className="text-base font-semibold">Package files</h2>
<h2 className="text-base font-semibold">Các tệp trong gói</h2>
</div>
<div className="border-b border-border px-3 py-2 shrink-0">
<div className="flex items-center gap-2 rounded-md border border-border px-2 py-1">
......@@ -981,7 +981,7 @@ export function CompanyExport() {
type="text"
value={treeSearch}
onChange={(e) => handleSearchChange(e.target.value)}
placeholder="Search files..."
placeholder="Tìm kiếm tệp..."
className="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
data-page-search-target="true"
/>
......@@ -1005,7 +1005,7 @@ export function CompanyExport() {
onClick={() => setTaskLimit((prev) => prev + TASKS_PAGE_SIZE)}
className="w-full rounded-md border border-border px-3 py-1.5 text-xs text-muted-foreground hover:bg-accent/30 hover:text-foreground transition-colors"
>
Show more issues ({visibleTaskChildren} of {totalTaskChildren})
Hiển thị thêm vấn đề ({visibleTaskChildren} trên {totalTaskChildren})
</button>
</div>
)}
......
This diff is collapsed.
......@@ -13,9 +13,9 @@ import { queryKeys } from "@/lib/queryKeys";
const inviteRoleOptions = [
{
value: "viewer",
label: "Viewer",
description: "Can view company work and follow along without operational permissions.",
gets: "No built-in grants.",
label: "Người quan sát (Viewer)",
description: "Có thể xem công việc và theo dõi hoạt động mà không có quyền vận hành.",
gets: "Không có quyền mặc định.",
},
{
value: "operator",
......@@ -31,9 +31,9 @@ const inviteRoleOptions = [
},
{
value: "owner",
label: "Owner",
description: "Full company access, including membership and permission management.",
gets: "Everything in Admin, plus managing members and permission grants.",
label: "Chủ sở hữu (Owner)",
description: "Toàn quyền truy cập phòng ban, bao gồm quản lý thành viên và phân quyền.",
gets: "Mọi quyền của Quản trị viên, cộng thêm quyền quản lý thành viên và phân quyền cụ thể.",
},
] as const;
......@@ -81,9 +81,9 @@ export function CompanyInvites() {
useEffect(() => {
setBreadcrumbs([
{ label: selectedCompany?.name ?? "Company", href: "/dashboard" },
{ label: "Settings", href: "/company/settings" },
{ label: "Invites" },
{ label: selectedCompany?.name ?? "Phòng ban", href: "/dashboard" },
{ label: "Thiết lập", href: "/company/settings" },
{ label: "Lời mời" },
]);
}, [selectedCompany?.name, setBreadcrumbs]);
......@@ -151,20 +151,20 @@ export function CompanyInvites() {
});
if (!selectedCompanyId) {
return <div className="text-sm text-muted-foreground">Select a company to manage invites.</div>;
return <div className="text-sm text-muted-foreground">Vui lòng chọn phòng ban để quản lý lời mời.</div>;
}
if (invitesQuery.isLoading) {
return <div className="text-sm text-muted-foreground">Loading invites…</div>;
return <div className="text-sm text-muted-foreground">Đang tải danh sách lời mời…</div>;
}
if (invitesQuery.error) {
const message =
invitesQuery.error instanceof ApiError && invitesQuery.error.status === 403
? "You do not have permission to manage company invites."
? "Bạn không có quyền quản lý lời mời gia nhập phòng ban."
: invitesQuery.error instanceof Error
? invitesQuery.error.message
: "Failed to load invites.";
: "Không thể tải danh sách lời mời.";
return <div className="text-sm text-destructive">{message}</div>;
}
......@@ -173,10 +173,10 @@ export function CompanyInvites() {
<div className="space-y-3">
<div className="flex items-center gap-2">
<MailPlus className="h-5 w-5 text-muted-foreground" />
<h1 className="text-lg font-semibold">Company Invites</h1>
<h1 className="text-lg font-semibold">Lời mời gia nhập phòng ban</h1>
</div>
<p className="max-w-3xl text-sm text-muted-foreground">
Create human invite links for company access. New invite links are copied to your clipboard when they are generated.
Tạo liên kết mời người dùng tham gia phòng ban. Liên kết mời mới sẽ được tự động sao chép sau khi được tạo.
</p>
</div>
......@@ -288,7 +288,7 @@ export function CompanyInvites() {
{inviteHistory.length === 0 ? (
<div className="border-t border-border px-5 py-8 text-sm text-muted-foreground">
No invites have been created for this company yet.
Chưa có lời mời nào được tạo cho phòng ban này.
</div>
) : (
<div className="border-t border-border">
......
This diff is collapsed.
......@@ -797,8 +797,8 @@ export function CompanySkills() {
useEffect(() => {
setBreadcrumbs([
{ label: "Skills", href: "/skills" },
...(routeSkillId ? [{ label: "Detail" }] : []),
{ label: "Kỹ năng", href: "/skills" },
...(routeSkillId ? [{ label: "Chi tiết" }] : []),
]);
}, [routeSkillId, setBreadcrumbs]);
......@@ -1080,7 +1080,7 @@ export function CompanySkills() {
});
if (!selectedCompanyId) {
return <EmptyState icon={Boxes} message="Select a company to manage skills." />;
return <EmptyState icon={Boxes} message="Vui lòng chọn phòng ban để quản lý kỹ năng." />;
}
function handleAddSkillSource() {
......@@ -1097,9 +1097,9 @@ export function CompanySkills() {
<Dialog open={deleteOpen} onOpenChange={closeDeleteDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Remove skill</DialogTitle>
<DialogTitle>Xóa kỹ năng</DialogTitle>
<DialogDescription>
Remove this skill from the company library. If any agents still use it, removal will be blocked until it is detached.
Xóa kỹ năng này khỏi thư viện của phòng ban. Nếu có agent nào đang sử dụng kỹ năng này, việc xóa sẽ bị chặn cho đến khi được gỡ liên kết.
</DialogDescription>
</DialogHeader>
<div className="space-y-3 text-sm">
......@@ -1189,9 +1189,9 @@ export function CompanySkills() {
<div className="border-b border-border px-4 py-3">
<div className="flex items-center justify-between gap-2">
<div>
<h1 className="text-base font-semibold">Skills</h1>
<h1 className="text-base font-semibold">Kỹ năng</h1>
<p className="text-xs text-muted-foreground">
{skillsQuery.data?.length ?? 0} available
{skillsQuery.data?.length ?? 0} kỹ năng
</p>
</div>
<div className="flex items-center gap-1">
......@@ -1215,7 +1215,7 @@ export function CompanySkills() {
<input
value={skillFilter}
onChange={(event) => setSkillFilter(event.target.value)}
placeholder="Filter skills"
placeholder="Lọc kỹ năng..."
className="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
/>
</div>
......
......@@ -168,7 +168,7 @@ export function Costs() {
} = useDateRange();
useEffect(() => {
setBreadcrumbs([{ label: "Costs" }]);
setBreadcrumbs([{ label: "Chi phí" }]);
}, [setBreadcrumbs]);
const [today, setToday] = useState(() => new Date().toDateString());
......@@ -529,7 +529,7 @@ export function Costs() {
}), [budgetPolicies]);
if (!selectedCompanyId) {
return <EmptyState icon={DollarSign} message="Select a company to view costs." />;
return <EmptyState icon={DollarSign} message="Chọn phòng ban để xem chi phí." />;
}
const showCustomPrompt = preset === "custom" && !customReady;
......
......@@ -50,7 +50,7 @@ export function Dashboard() {
});
useEffect(() => {
setBreadcrumbs([{ label: "Dashboard" }]);
setBreadcrumbs([{ label: "Bảng điều khiển" }]);
}, [setBreadcrumbs]);
const { data, isLoading, error } = useQuery({
......@@ -176,14 +176,14 @@ export function Dashboard() {
return (
<EmptyState
icon={LayoutDashboard}
message="Welcome to Paperclip. Set up your first company and agent to get started."
action="Get Started"
message="Chào mừng đến với Canifa Agent. Hãy thiết lập phòng ban và tác nhân đầu tiên của bạn để bắt đầu."
action="Bắt đầu"
onAction={openOnboarding}
/>
);
}
return (
<EmptyState icon={LayoutDashboard} message="Create or select a company to view the dashboard." />
<EmptyState icon={LayoutDashboard} message="Vui lòng tạo hoặc chọn phòng ban để xem bảng điều khiển." />
);
}
......@@ -202,14 +202,14 @@ export function Dashboard() {
<div className="flex items-center gap-2.5">
<Bot className="h-4 w-4 text-amber-600 dark:text-amber-400 shrink-0" />
<p className="text-sm text-amber-900 dark:text-amber-100">
You have no agents.
Bạn chưa có tác nhân nào.
</p>
</div>
<button
onClick={() => openOnboarding({ initialStep: 2, companyId: selectedCompanyId! })}
className="text-sm font-medium text-amber-700 hover:text-amber-900 dark:text-amber-300 dark:hover:text-amber-100 underline underline-offset-2 shrink-0"
>
Create one here
Tạo tác nhân tại đây
</button>
</div>
)}
......@@ -241,7 +241,7 @@ export function Dashboard() {
<MetricCard
icon={Bot}
value={data.agents.active + data.agents.running + data.agents.paused + data.agents.error}
label="Agents Enabled"
label="Tác nhân hoạt động"
to="/agents"
description={
<span>
......@@ -254,7 +254,7 @@ export function Dashboard() {
<MetricCard
icon={CircleDot}
value={data.tasks.inProgress}
label="Tasks In Progress"
label="Nhiệm vụ đang thực hiện"
to="/issues"
description={
<span>
......@@ -266,7 +266,7 @@ export function Dashboard() {
<MetricCard
icon={DollarSign}
value={formatCents(data.costs.monthSpendCents)}
label="Month Spend"
label="Chi tiêu trong tháng"
to="/costs"
description={
<span>
......@@ -279,7 +279,7 @@ export function Dashboard() {
<MetricCard
icon={ShieldCheck}
value={data.pendingApprovals + data.budgets.pendingApprovals}
label="Pending Approvals"
label="Đang chờ duyệt"
to="/approvals"
description={
<span>
......@@ -318,7 +318,7 @@ export function Dashboard() {
{recentActivity.length > 0 && (
<div className="min-w-0">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
Recent Activity
Hoạt động gần đây
</h3>
<div className="border border-border divide-y divide-border overflow-hidden">
{recentActivity.map((event) => (
......@@ -339,7 +339,7 @@ export function Dashboard() {
{/* Recent Tasks */}
<div className="min-w-0">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
Recent Tasks
Nhiệm vụ gần đây
</h3>
{recentIssues.length === 0 ? (
<div className="border border-border p-4">
......
......@@ -14,8 +14,8 @@ export function DashboardLive() {
useEffect(() => {
setBreadcrumbs([
{ label: "Dashboard", href: "/dashboard" },
{ label: "Live runs" },
{ label: "Bảng điều khiển", href: "/dashboard" },
{ label: "Chạy trực tiếp" },
]);
}, [setBreadcrumbs]);
......@@ -23,7 +23,7 @@ export function DashboardLive() {
return (
<EmptyState
icon={RadioTower}
message={companies.length === 0 ? "Create a company to view live runs." : "Select a company to view live runs."}
message={companies.length === 0 ? "Vui lòng tạo phòng ban để xem các phiên chạy trực tiếp." : "Chọn phòng ban để xem các phiên chạy trực tiếp."}
/>
);
}
......@@ -37,14 +37,14 @@ export function DashboardLive() {
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
>
<ArrowLeft className="h-3.5 w-3.5" />
Dashboard
Bảng điều khiển
</Link>
<h1 className="mt-2 text-2xl font-semibold tracking-normal text-foreground">Live agent runs</h1>
<h1 className="mt-2 text-2xl font-semibold tracking-normal text-foreground">Tác nhân chạy trực tiếp</h1>
<p className="mt-1 text-sm text-muted-foreground">
Active runs first, followed by the most recent completed runs.
Các phiên đang hoạt động hiển thị trước, theo sau là các phiên hoàn thành gần đây nhất.
</p>
</div>
<div className="text-sm text-muted-foreground">Showing up to {DASHBOARD_LIVE_RUN_LIMIT}</div>
<div className="text-sm text-muted-foreground">Hiển thị tối đa {DASHBOARD_LIVE_RUN_LIMIT}</div>
</div>
<ActiveAgentsPanel
......@@ -55,7 +55,7 @@ export function DashboardLive() {
cardLimit={DASHBOARD_LIVE_RUN_LIMIT}
gridClassName="gap-3 md:grid-cols-2 2xl:grid-cols-3"
cardClassName="h-[420px]"
emptyMessage="No active or recent agent runs."
emptyMessage="Không có phiên chạy tác nhân nào đang hoạt động hoặc gần đây."
queryScope="dashboard-live"
showMoreLink={false}
/>
......
......@@ -17,7 +17,7 @@ export function Goals() {
const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => {
setBreadcrumbs([{ label: "Goals" }]);
setBreadcrumbs([{ label: "Mục tiêu" }]);
}, [setBreadcrumbs]);
const { data: goals, isLoading, error } = useQuery({
......@@ -27,7 +27,7 @@ export function Goals() {
});
if (!selectedCompanyId) {
return <EmptyState icon={Target} message="Select a company to view goals." />;
return <EmptyState icon={Target} message="Chọn phòng ban để xem mục tiêu." />;
}
if (isLoading) {
......@@ -41,8 +41,8 @@ export function Goals() {
{goals && goals.length === 0 && (
<EmptyState
icon={Target}
message="No goals yet."
action="Add Goal"
message="Chưa có mục tiêu nào."
action="Thêm mục tiêu"
onAction={() => openNewGoal()}
/>
)}
......@@ -52,7 +52,7 @@ export function Goals() {
<div className="flex items-center justify-start">
<Button size="sm" variant="outline" onClick={() => openNewGoal()}>
<Plus className="h-3.5 w-3.5 mr-1.5" />
New Goal
Mục tiêu mới
</Button>
</div>
<GoalTree goals={goals} goalLink={(goal) => `/goals/${goal.id}`} />
......
......@@ -1866,7 +1866,7 @@ export function Inbox() {
}, [selectedIndex]);
if (!selectedCompanyId) {
return <EmptyState icon={InboxIcon} message="Select a company to view inbox." />;
return <EmptyState icon={InboxIcon} message="Chọn phòng ban để xem hộp thư." />;
}
const hasRunFailures = failedRuns.length > 0;
......@@ -1954,15 +1954,15 @@ export function Inbox() {
items={[
{
value: "mine",
label: "Mine",
label: "Của tôi",
},
{
value: "recent",
label: "Recent",
label: "Gần đây",
},
{ value: "unread", label: "Unread" },
{ value: "blocked", label: "Blocked" },
{ value: "all", label: "All" },
{ value: "unread", label: "Chưa đọc" },
{ value: "blocked", label: "Bị chặn" },
{ value: "all", label: "Tất cả" },
]}
/>
</Tabs>
......
......@@ -108,7 +108,7 @@ export function Issues() {
const issueLinkState = useMemo(
() =>
createIssueDetailLocationState(
"Issues",
"Vấn đề",
`${location.pathname}${location.search}${location.hash}`,
"issues",
),
......@@ -116,7 +116,7 @@ export function Issues() {
);
useEffect(() => {
setBreadcrumbs([{ label: "Issues" }]);
setBreadcrumbs([{ label: "Vấn đề" }]);
}, [setBreadcrumbs]);
const issuePageSize = workspaceIdFilter ? WORKSPACE_FILTER_ISSUE_LIMIT : ISSUES_PAGE_SIZE;
......@@ -173,7 +173,7 @@ export function Issues() {
});
if (!selectedCompanyId) {
return <EmptyState icon={CircleDot} message="Select a company to view issues." />;
return <EmptyState icon={CircleDot} message="Chọn phòng ban để xem danh sách vấn đề." />;
}
return (
......
......@@ -20,9 +20,9 @@ export function JoinRequestQueue() {
useEffect(() => {
setBreadcrumbs([
{ label: selectedCompany?.name ?? "Company", href: "/dashboard" },
{ label: "Inbox", href: "/inbox" },
{ label: "Join Requests" },
{ label: selectedCompany?.name ?? "Phòng ban", href: "/dashboard" },
{ label: "Hộp thư", href: "/inbox" },
{ label: "Yêu cầu tham gia" },
]);
}, [selectedCompany?.name, setBreadcrumbs]);
......@@ -56,11 +56,11 @@ export function JoinRequestQueue() {
});
if (!selectedCompanyId) {
return <div className="text-sm text-muted-foreground">Select a company to review join requests.</div>;
return <div className="text-sm text-muted-foreground">Chọn phòng ban để xem yêu cầu tham gia.</div>;
}
if (requestsQuery.isLoading) {
return <div className="text-sm text-muted-foreground">Loading join requests…</div>;
return <div className="text-sm text-muted-foreground">Đang tải yêu cầu tham gia...</div>;
}
if (requestsQuery.error) {
......@@ -78,16 +78,16 @@ export function JoinRequestQueue() {
<div className="space-y-3">
<div className="flex items-center gap-2">
<UserPlus2 className="h-5 w-5 text-muted-foreground" />
<h1 className="text-lg font-semibold">Join Request Queue</h1>
<h1 className="text-lg font-semibold">Hàng đợi yêu cầu tham gia</h1>
</div>
<p className="max-w-3xl text-sm text-muted-foreground">
Review human and agent join requests outside the mixed inbox feed. This queue uses the same approval mutations as the inline inbox cards.
Xem xét các yêu cầu tham gia của con người và tác nhân bên ngoài nguồn cấp dữ liệu hộp thư hỗn hợp. Hàng đợi này sử dụng các đột biến phê duyệt giống như các thẻ hộp thư nội dòng.
</p>
</div>
<div className="flex flex-wrap gap-3 rounded-xl border border-border bg-card p-4">
<label className="space-y-2 text-sm">
<span className="font-medium">Status</span>
<span className="font-medium">Trạng thái</span>
<select
className="rounded-md border border-border bg-background px-3 py-2"
value={status}
......@@ -95,13 +95,13 @@ export function JoinRequestQueue() {
setStatus(event.target.value as "pending_approval" | "approved" | "rejected")
}
>
<option value="pending_approval">Pending approval</option>
<option value="approved">Approved</option>
<option value="rejected">Rejected</option>
<option value="pending_approval">Đang chờ duyệt</option>
<option value="approved">Đã duyệt</option>
<option value="rejected">Đã từ chối</option>
</select>
</label>
<label className="space-y-2 text-sm">
<span className="font-medium">Request type</span>
<span className="font-medium">Loại yêu cầu</span>
<select
className="rounded-md border border-border bg-background px-3 py-2"
value={requestType}
......@@ -109,9 +109,9 @@ export function JoinRequestQueue() {
setRequestType(event.target.value as "all" | "human" | "agent")
}
>
<option value="all">All</option>
<option value="human">Human</option>
<option value="agent">Agent</option>
<option value="all">Tất cả</option>
<option value="human">Người</option>
<option value="agent">Tác nhân</option>
</select>
</label>
</div>
......@@ -119,7 +119,7 @@ export function JoinRequestQueue() {
<div className="space-y-4">
{(requestsQuery.data ?? []).length === 0 ? (
<div className="rounded-xl border border-dashed border-border px-4 py-8 text-sm text-muted-foreground">
No join requests match the current filters.
Không có yêu cầu tham gia nào khớp với bộ lọc hiện tại.
</div>
) : (
requestsQuery.data!.map((request) => (
......@@ -154,13 +154,13 @@ export function JoinRequestQueue() {
onClick={() => rejectMutation.mutate(request.id)}
disabled={rejectMutation.isPending}
>
Reject
Từ chối
</Button>
<Button
onClick={() => approveMutation.mutate(request.id)}
disabled={approveMutation.isPending}
>
Approve
Phê duyệt
</Button>
</div>
) : null}
......
......@@ -17,7 +17,7 @@ export function MyIssues() {
const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => {
setBreadcrumbs([{ label: "My Issues" }]);
setBreadcrumbs([{ label: "Vấn đề của tôi" }]);
}, [setBreadcrumbs]);
const { data: issues, isLoading, error } = useQuery({
......@@ -27,7 +27,7 @@ export function MyIssues() {
});
if (!selectedCompanyId) {
return <EmptyState icon={ListTodo} message="Select a company to view your issues." />;
return <EmptyState icon={ListTodo} message="Chọn phòng ban để xem vấn đề của bạn." />;
}
if (isLoading) {
......@@ -44,7 +44,7 @@ export function MyIssues() {
{error && <p className="text-sm text-destructive">{error.message}</p>}
{myIssues.length === 0 && (
<EmptyState icon={ListTodo} message="No issues assigned to you." />
<EmptyState icon={ListTodo} message="Không có vấn đề nào được giao cho bạn." />
)}
{myIssues.length > 0 && (
......
......@@ -94,7 +94,7 @@ export function Org() {
const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => {
setBreadcrumbs([{ label: "Org Chart" }]);
setBreadcrumbs([{ label: "Sơ đồ tổ chức" }]);
}, [setBreadcrumbs]);
const { data, isLoading, error } = useQuery({
......@@ -104,7 +104,7 @@ export function Org() {
});
if (!selectedCompanyId) {
return <EmptyState icon={GitBranch} message="Select a company to view org chart." />;
return <EmptyState icon={GitBranch} message="Chọn phòng ban để xem sơ đồ tổ chức." />;
}
if (isLoading) {
......@@ -118,7 +118,7 @@ export function Org() {
{data && data.length === 0 && (
<EmptyState
icon={GitBranch}
message="No agents in the organization. Create agents to build your org chart."
message="Không có tác nhân nào trong phòng ban. Hãy tạo các tác nhân để xây dựng sơ đồ tổ chức."
/>
)}
......
......@@ -194,7 +194,7 @@ export function OrgChart() {
}, [agents]);
useEffect(() => {
setBreadcrumbs([{ label: "Org Chart" }]);
setBreadcrumbs([{ label: "Sơ đồ tổ chức" }]);
}, [setBreadcrumbs]);
// Layout computation
......@@ -429,7 +429,7 @@ export function OrgChart() {
}, [pan, zoom]);
if (!selectedCompanyId) {
return <EmptyState icon={Network} message="Select a company to view the org chart." />;
return <EmptyState icon={Network} message="Chọn phòng ban để xem sơ đồ tổ chức." />;
}
if (isLoading) {
......@@ -437,7 +437,7 @@ export function OrgChart() {
}
if (orgTree && orgTree.length === 0) {
return <EmptyState icon={Network} message="No organizational hierarchy defined." />;
return <EmptyState icon={Network} message="Chưa định nghĩa sơ đồ tổ chức." />;
}
return (
......@@ -446,13 +446,13 @@ export function OrgChart() {
<Link to="/company/import">
<Button variant="outline" size="sm">
<Upload className="mr-1.5 h-3.5 w-3.5" />
Import company
Nhập phòng ban
</Button>
</Link>
<Link to="/company/export">
<Button variant="outline" size="sm">
<Download className="mr-1.5 h-3.5 w-3.5" />
Export company
Xuất phòng ban
</Button>
</Link>
</div>
......
......@@ -130,13 +130,13 @@ export function PluginPage() {
}
return (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">Select a company to view this page.</p>
<p className="text-sm text-muted-foreground">Vui lòng chọn phòng ban để xem trang này.</p>
</div>
);
}
if (!contributions) {
return <div className="text-sm text-muted-foreground">Loading…</div>;
return <div className="text-sm text-muted-foreground">Đang tải...</div>;
}
if (!pluginId && pluginRoutePath) {
......@@ -168,7 +168,7 @@ export function PluginPage() {
<Button variant="ghost" size="sm" asChild>
<Link to={companyPrefix ? `/${companyPrefix}/dashboard` : "/dashboard"}>
<ArrowLeft className="h-4 w-4 mr-1" />
Back
Quay lại
</Link>
</Button>
</div>
......
......@@ -19,7 +19,7 @@ export function Projects() {
const { setBreadcrumbs } = useBreadcrumbs();
useEffect(() => {
setBreadcrumbs([{ label: "Projects" }]);
setBreadcrumbs([{ label: "Dự án" }]);
}, [setBreadcrumbs]);
const { data: allProjects, isLoading, error } = useQuery({
......@@ -33,7 +33,7 @@ export function Projects() {
);
if (!selectedCompanyId) {
return <EmptyState icon={Hexagon} message="Select a company to view projects." />;
return <EmptyState icon={Hexagon} message="Chọn phòng ban để xem danh sách dự án." />;
}
if (isLoading) {
......@@ -45,7 +45,7 @@ export function Projects() {
<div className="flex items-center justify-end">
<Button size="sm" variant="outline" onClick={openNewProject}>
<Plus className="h-4 w-4 mr-1" />
Add Project
Thêm dự án
</Button>
</div>
......@@ -54,8 +54,8 @@ export function Projects() {
{!isLoading && projects.length === 0 && (
<EmptyState
icon={Hexagon}
message="No projects yet."
action="Add Project"
message="Chưa có dự án nào."
action="Thêm dự án"
onAction={openNewProject}
/>
)}
......
......@@ -241,7 +241,7 @@ export function Routines() {
const [routineViewState, setRoutineViewState] = useState<RoutineViewState>(() => getRoutineViewState(routineViewStateKey));
useEffect(() => {
setBreadcrumbs([{ label: "Routines" }]);
setBreadcrumbs([{ label: "Quy trình" }]);
}, [setBreadcrumbs]);
useEffect(() => {
......@@ -479,7 +479,7 @@ export function Routines() {
}
if (!selectedCompanyId) {
return <EmptyState icon={Repeat} message="Select a company to view routines." />;
return <EmptyState icon={Repeat} message="Chọn phòng ban để xem các quy trình." />;
}
if (isLoading) {
......@@ -491,7 +491,7 @@ export function Routines() {
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight">
Routines
Quy trình
</h1>
<p className="text-sm text-muted-foreground">
Recurring work definitions that materialize into auditable execution issues.
......@@ -509,8 +509,8 @@ export function Routines() {
value={activeTab}
onValueChange={handleTabChange}
items={[
{ value: "routines", label: "Routines" },
{ value: "runs", label: "Recent Runs" },
{ value: "routines", label: "Quy trình" },
{ value: "runs", label: "Phiên chạy gần đây" },
]}
/>
<TabsContent value="routines" className="space-y-4">
......@@ -877,7 +877,7 @@ export function Routines() {
<div className="py-12">
<EmptyState
icon={Repeat}
message="No routines yet. Use Create routine to define the first recurring workflow."
message="Chưa có quy trình nào. Hãy sử dụng Tạo quy trình để tạo quy trình làm việc định kỳ đầu tiên."
/>
</div>
) : (
......
......@@ -376,7 +376,7 @@ export function Secrets() {
const [vaultError, setVaultError] = useState<string | null>(null);
useEffect(() => {
setBreadcrumbs([{ label: "Secrets" }]);
setBreadcrumbs([{ label: "Bí mật" }]);
}, [setBreadcrumbs]);
const secretsQuery = useQuery({
......@@ -747,7 +747,7 @@ export function Secrets() {
if (!selectedCompanyId) {
return (
<div className="p-6 text-sm text-muted-foreground">Select a company to manage secrets.</div>
<div className="p-6 text-sm text-muted-foreground">Vui lòng chọn phòng ban để quản lý bí mật.</div>
);
}
......@@ -755,7 +755,7 @@ export function Secrets() {
<div className="flex h-full min-h-0 flex-col gap-4">
<div className="flex items-center gap-2">
<KeyRound className="h-5 w-5 text-muted-foreground" />
<h1 className="text-lg font-semibold">Secrets</h1>
<h1 className="text-lg font-semibold">Bí mật</h1>
</div>
<Tabs
......@@ -765,8 +765,8 @@ export function Secrets() {
>
<PageTabBar
items={[
{ value: "secrets", label: "Secrets" },
{ value: "vaults", label: "Provider vaults" },
{ value: "secrets", label: "Bí mật phòng ban" },
{ value: "vaults", label: "Kho khóa (Vaults)" },
]}
align="start"
value={activeTab}
......@@ -781,9 +781,9 @@ export function Secrets() {
<Input
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="Search by name, key, ref"
placeholder="Tìm kiếm theo tên, khóa, tham chiếu..."
className="pl-7 text-xs sm:text-sm"
aria-label="Search secrets"
aria-label="Tìm kiếm bí mật"
data-page-search-target="true"
/>
</div>
......@@ -802,32 +802,32 @@ export function Secrets() {
className="ml-auto"
/>
<Button onClick={() => setCreateOpen(true)} size="sm">
<Plus className="h-3.5 w-3.5 mr-1" /> New secret
<Plus className="h-3.5 w-3.5 mr-1" /> Thêm bí mật
</Button>
</div>
<div className="min-h-0 flex-1 overflow-y-auto">
{secretsQuery.isError ? (
<div className="text-sm text-destructive flex items-center gap-2 py-4">
<AlertCircle className="h-4 w-4" /> Failed to load secrets:{" "}
<AlertCircle className="h-4 w-4" /> Không thể tải bí mật:{" "}
{(secretsQuery.error as Error).message}
<Button variant="ghost" size="sm" onClick={() => secretsQuery.refetch()}>
Retry
Thử lại
</Button>
</div>
) : secrets.length === 0 && !secretsQuery.isPending ? (
<EmptyState
icon={KeyRound}
message="No secrets yet. Create your first managed secret or link an external reference."
action="New secret"
message="Chưa có bí mật nào. Hãy tạo bí mật được quản lý đầu tiên hoặc liên kết tới một tham chiếu bên ngoài."
action="Thêm bí mật"
onAction={() => setCreateOpen(true)}
/>
) : filtered.length === 0 ? (
<EmptyState icon={Search} message="No secrets match your filters." />
<EmptyState icon={Search} message="Không tìm thấy bí mật nào khớp với bộ lọc." />
) : (
<table className="w-full text-sm">
<thead className="bg-muted/40 text-xs uppercase tracking-wide text-muted-foreground">
<tr>
<th className="px-3 py-2 text-left font-medium">Name</th>
<th className="px-3 py-2 text-left font-medium">Tên</th>
<th className="px-2 py-2 text-left font-medium">Mode</th>
<th className="px-2 py-2 text-left font-medium">Provider</th>
<th className="px-2 py-2 text-left font-medium">Status</th>
......
......@@ -207,7 +207,7 @@ export function UserProfile() {
});
useEffect(() => {
setBreadcrumbs([{ label: "Users" }, { label: data?.user.name ?? userSlug }]);
setBreadcrumbs([{ label: "Thành viên" }, { label: data?.user.name ?? userSlug }]);
}, [data?.user.name, setBreadcrumbs, userSlug]);
const allTime = data?.stats.find((entry) => entry.key === "all");
......@@ -243,7 +243,7 @@ export function UserProfile() {
);
if (!selectedCompanyId) {
return <EmptyState icon={UserRound} message="Select a company to view user profiles." />;
return <EmptyState icon={UserRound} message="Chọn phòng ban để xem thông tin cá nhân." />;
}
if (isLoading) {
......@@ -251,7 +251,7 @@ export function UserProfile() {
}
if (error || !data) {
return <EmptyState icon={AlertCircle} message="User profile not found for this company." />;
return <EmptyState icon={AlertCircle} message="Không tìm thấy thông tin thành viên trong phòng ban này." />;
}
const allTimeTokens = allTime ? totalTokens(allTime) : 0;
......
......@@ -17,5 +17,6 @@
"lexical": ["./node_modules/lexical/index.d.ts"]
}
},
"include": ["src", "storybook"]
"include": ["src", "storybook"],
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/**/*.spec.ts", "src/**/*.spec.tsx"]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment