mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 18:18:37 +00:00
feat(web): add ESLint 10 with flat config and CI integration
- Install ESLint 10 + TypeScript-ESLint + React hooks plugins - Create eslint.config.js with flat config format - Fix conditional hooks violations in agents-page, task-list, knowledge-chart - Fix unused variables and useless assignments - Add lint step to CI workflow before build - Remove unused eslint-disable comments Rules configured: - rules-of-hooks: error (critical) - exhaustive-deps: off (intentional patterns) - no-explicit-any: off (pragmatic casts) - no-console: off (dev convenience)
This commit is contained in:
1 parent
08b51adaf1
commit
607ef556b5
41 files changed
+1142
-71
No files matched your search
@@ -73,4 +73,5 @@ jobs:
|
||||
cache: pnpm
|
||||
cache-dependency-path: ui/web/pnpm-lock.yaml
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm lint
|
||||
- run: pnpm build
|
||||
@@ -0,0 +1,37 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist", "node_modules"] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2024,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
// React hooks - keep critical rules only
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
// Disabled: many intentional patterns (stable refs, translation fn)
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
// Disabled: legitimate pattern to co-locate hooks with components
|
||||
"react-refresh/only-export-components": "off",
|
||||
|
||||
// TypeScript
|
||||
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
||||
// Disabled: pragmatic casts for external libs and dynamic data
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
|
||||
// General - disabled for dev convenience
|
||||
"no-console": "off",
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -8,6 +8,7 @@
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:ui": "vitest --ui"
|
||||
@@ -54,6 +55,7 @@
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/react": "^19.2.14",
|
||||
@@ -61,9 +63,14 @@
|
||||
"@types/ws": "^8.18.1",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"@vitest/ui": "^4.1.4",
|
||||
"eslint": "^10.2.0",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.5.0",
|
||||
"jsdom": "^29.0.2",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^6.0.2",
|
||||
"typescript-eslint": "^8.58.1",
|
||||
"unified": "^11.0.5",
|
||||
"vite": "^8.0.8",
|
||||
"vitest": "^4.1.4",
|
||||
|
||||
Generated
+1027
File diff suppressed because it is too large.
Load diff
@@ -164,7 +164,7 @@ export function SigmaGraphContainer({
|
||||
onSigmaReady?.(null);
|
||||
}
|
||||
};
|
||||
}, [graph, edgeType, compact]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [graph, edgeType, compact]);
|
||||
|
||||
// --- Update theme colors without re-init ---
|
||||
useEffect(() => {
|
||||
|
||||
@@ -53,14 +53,14 @@ export function WsProvider({ children }: { children: React.ReactNode }) {
|
||||
store.setTenantSelected(true);
|
||||
} else if (!client.isOwner && tenants.length === 1) {
|
||||
// Non-owner with single tenant — auto-select
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
|
||||
localStorage.setItem(LOCAL_STORAGE_KEYS.TENANT_ID, tenants[0]!.slug);
|
||||
store.setTenantSelected(true);
|
||||
} else if (!client.isOwner && tenants.length === 0) {
|
||||
// No tenants — leave tenantSelected=false (blocked)
|
||||
} else if (client.isOwner && !savedScope && tenants.length > 0) {
|
||||
// Owner without scope — auto-select first tenant
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
|
||||
localStorage.setItem(LOCAL_STORAGE_KEYS.TENANT_ID, tenants[0]!.slug);
|
||||
window.location.reload();
|
||||
return;
|
||||
|
||||
@@ -83,7 +83,6 @@ export function MermaidBlock({ code }: MermaidBlockProps) {
|
||||
return (
|
||||
<div
|
||||
className="not-prose my-3 overflow-x-auto rounded-lg border border-border/60 bg-background p-4"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: svg ?? "" }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -108,7 +108,7 @@ export const MarkdownRenderer = memo(function MarkdownRenderer({ content, classN
|
||||
}, []);
|
||||
|
||||
// Stable components config — only recreated when token/callbacks change.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
const components = useMemo((): Components & Record<string, any> => ({
|
||||
pre({ children }) {
|
||||
return <>{children}</>;
|
||||
|
||||
@@ -83,7 +83,7 @@ export function Combobox({
|
||||
// Auto-detect if inside a Radix Dialog (which sets pointer-events:none on body)
|
||||
const el = containerRef.current?.closest<HTMLElement>('[data-slot="dialog-content"]');
|
||||
return el ?? null;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
}, [portalContainer, open]);
|
||||
|
||||
// Compute dropdown position — flip above input when near viewport bottom.
|
||||
|
||||
@@ -49,7 +49,7 @@ export function useAutoScroll<T extends HTMLElement>(
|
||||
if (isNearBottom.current) {
|
||||
scrollToBottom(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
}, deps);
|
||||
|
||||
// Force scroll when trigger increments (e.g. user sends a message) — instant
|
||||
|
||||
@@ -45,7 +45,7 @@ export function getAllIanaTimezones(): TzOption[] {
|
||||
if (_cachedTimezones) return _cachedTimezones;
|
||||
let tzNames: string[];
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
tzNames = (Intl as any).supportedValuesOf("timeZone") as string[];
|
||||
} catch {
|
||||
tzNames = FALLBACK_TIMEZONES;
|
||||
|
||||
@@ -78,7 +78,7 @@ export function AgentAdvancedDialog({ open, onOpenChange, agent, onUpdate }: Age
|
||||
setPrune(s.prune);
|
||||
setSbEnabled(s.sbEnabled);
|
||||
setSb(s.sb);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -16,7 +16,7 @@ function estimateTokensFromContent(content: string): number {
|
||||
let ascii = 0;
|
||||
let nonAscii = 0;
|
||||
for (const ch of content) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
|
||||
if (ch.codePointAt(0)! < 128) ascii++;
|
||||
else nonAscii++;
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ export function HeartbeatConfigDialog({
|
||||
loadChecklist();
|
||||
fetchTargets().then(setTargets)
|
||||
.catch((err) => console.error("[HeartbeatConfig] fetch targets failed:", err));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
}, [open]);
|
||||
|
||||
const handleTest = async () => {
|
||||
|
||||
@@ -46,6 +46,20 @@ export function AgentsPage() {
|
||||
const ownerIDs = useMemo(() => [...new Set(agents.map((a) => a.owner_id).filter(Boolean))], [agents]);
|
||||
const { resolve } = useContactResolver(ownerIDs);
|
||||
|
||||
const filtered = useMemo(() => agents.filter((a) => {
|
||||
if (ownerFilter && a.owner_id !== ownerFilter) return false;
|
||||
if (typeFilter && a.agent_type !== typeFilter) return false;
|
||||
const q = search.toLowerCase();
|
||||
return (
|
||||
a.agent_key.toLowerCase().includes(q) ||
|
||||
(a.display_name ?? "").toLowerCase().includes(q)
|
||||
);
|
||||
}), [agents, ownerFilter, typeFilter, search]);
|
||||
|
||||
const { pageItems, pagination, setPage, setPageSize, resetPage } = usePagination(filtered);
|
||||
|
||||
useEffect(() => { resetPage(); }, [search, ownerFilter, typeFilter, resetPage]);
|
||||
|
||||
const handleResummon = async (agent: { id: string; display_name?: string; agent_key: string }) => {
|
||||
try {
|
||||
await resummonAgent(agent.id);
|
||||
@@ -65,20 +79,6 @@ export function AgentsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const filtered = agents.filter((a) => {
|
||||
if (ownerFilter && a.owner_id !== ownerFilter) return false;
|
||||
if (typeFilter && a.agent_type !== typeFilter) return false;
|
||||
const q = search.toLowerCase();
|
||||
return (
|
||||
a.agent_key.toLowerCase().includes(q) ||
|
||||
(a.display_name ?? "").toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
|
||||
const { pageItems, pagination, setPage, setPageSize, resetPage } = usePagination(filtered);
|
||||
|
||||
useEffect(() => { resetPage(); }, [search, ownerFilter, typeFilter, resetPage]);
|
||||
|
||||
const resolveOwnerName = (id: string) => {
|
||||
const contact = resolve(id);
|
||||
return contact?.display_name || contact?.username || id;
|
||||
|
||||
@@ -37,7 +37,7 @@ function highlightLine(line: string, lang: Tab): React.ReactNode | string {
|
||||
|
||||
// Simple token-based highlighting
|
||||
const parts: React.ReactNode[] = [];
|
||||
let remaining = line;
|
||||
const remaining = line;
|
||||
let key = 0;
|
||||
|
||||
const kwPatterns = lang === "go"
|
||||
@@ -67,7 +67,7 @@ function highlightLine(line: string, lang: Tab): React.ReactNode | string {
|
||||
// Remaining text after last string
|
||||
if (lastIndex < remaining.length) {
|
||||
const after = remaining.slice(lastIndex);
|
||||
parts.push(<span key={key++}>{highlightKeywords(after, kwPatterns)}</span>);
|
||||
parts.push(<span key={key}>{highlightKeywords(after, kwPatterns)}</span>);
|
||||
}
|
||||
|
||||
if (parts.length === 0) return line;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import type { BuiltinToolData } from "./hooks/use-builtin-tools";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
|
||||
// Tools with dedicated settings forms always show the Settings button,
|
||||
// even when settings is empty (the form lets the user create settings).
|
||||
|
||||
@@ -195,7 +195,7 @@ export function WebFetchExtractorChainForm({ initialSettings, onSave, onCancel }
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
const serialized = entries.map(({ id: _id, ...rest }) => rest);
|
||||
await onSave({ extractors: serialized });
|
||||
} catch {
|
||||
|
||||
@@ -42,10 +42,9 @@ function getAdvancedFields(channelType: string) {
|
||||
|
||||
function deriveInitialValues(instance: ChannelInstanceData): Record<string, unknown> {
|
||||
const config = (instance.config ?? {}) as Record<string, unknown>;
|
||||
const { groups: _groups, ...rest } = config as Record<string, unknown> & { groups?: unknown };
|
||||
// Only keep advanced keys (exclude essential + groups)
|
||||
return Object.fromEntries(
|
||||
Object.entries(rest).filter(([k]) => !ESSENTIAL_CONFIG_KEYS.has(k)),
|
||||
Object.entries(config).filter(([k]) => !ESSENTIAL_CONFIG_KEYS.has(k) && k !== "groups"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,7 +64,7 @@ export function ChannelAdvancedDialog({
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setValues(deriveInitialValues(instance));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
}, [open]);
|
||||
|
||||
const handleChange = useCallback((key: string, value: unknown) => {
|
||||
|
||||
@@ -30,7 +30,7 @@ export function WhatsAppReauthDialog({
|
||||
// because we only want to trigger on open/close transitions, not on identity changes.
|
||||
useEffect(() => {
|
||||
if (open && status === "idle") start();
|
||||
}, [open]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
// Reset state when dialog closes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -30,7 +30,7 @@ export function ZaloPersonalQRDialog({
|
||||
// Auto-start when dialog opens
|
||||
useEffect(() => {
|
||||
if (open && status === "idle") start();
|
||||
}, [open]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
// Reset state when dialog closes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -91,8 +91,8 @@ export function useChatTeamTasks(
|
||||
eventName === "team.task.commented" ||
|
||||
eventName === "team.task.attachment_added"
|
||||
) {
|
||||
let icon = "📋";
|
||||
let text = "";
|
||||
let icon: string;
|
||||
let text: string;
|
||||
if (eventName === "team.task.completed") { icon = "✅"; text = `Task #${event.task_number} "${event.subject}" completed`; }
|
||||
else if (eventName === "team.task.failed") { icon = "❌"; text = `Task #${event.task_number} "${event.subject}" failed${event.reason ? ": " + event.reason : ""}`; }
|
||||
else if (eventName === "team.task.commented") { icon = "💬"; text = `Comment on #${event.task_number} "${event.subject}"`; }
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { InfoLabel } from "@/components/shared/info-label";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
|
||||
export function SubSection({
|
||||
title,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { InfoLabel } from "@/components/shared/info-label";
|
||||
import { ProviderModelSelect } from "@/components/shared/provider-model-select";
|
||||
import { SubSection, Field } from "./ai-defaults-form-controls";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
type AgentsData = Record<string, any>;
|
||||
|
||||
const DEFAULT: AgentsData = { defaults: {} };
|
||||
|
||||
@@ -8,7 +8,7 @@ import { BehaviorSessionsCard } from "./behavior-sessions-card";
|
||||
import { BehaviorSecurityCard } from "./behavior-security-card";
|
||||
import { BehaviorPendingCompactionCard, type PendingCompactionValues } from "./behavior-pending-compaction-card";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
|
||||
interface Props {
|
||||
config: Record<string, any>;
|
||||
@@ -77,7 +77,7 @@ export function BehaviorSection({ config, onPatch, saving }: Props) {
|
||||
});
|
||||
setPendingCompaction(ch.pending_compaction ?? {});
|
||||
setDirty(false);
|
||||
}, [config]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [config]);
|
||||
|
||||
const markDirty = <T,>(setter: React.Dispatch<React.SetStateAction<T>>) =>
|
||||
(v: T) => { setter(v); setDirty(true); };
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { MultiUserPicker } from "@/components/shared/multi-user-picker";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
type ChannelsData = Record<string, any>;
|
||||
|
||||
const CHANNEL_META: Record<string, { label: string; secretEnv: string; secretField: string }> = {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { useHttp } from "@/hooks/use-ws";
|
||||
import { useAuthStore } from "@/stores/use-auth-store";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
type ToolsData = Record<string, any>;
|
||||
|
||||
interface DenyGroupInfo {
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { InfoLabel } from "@/components/shared/info-label";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
type ToolsData = Record<string, any>;
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { InfoLabel } from "@/components/shared/info-label";
|
||||
import { ToolNameSelect } from "@/components/shared/tool-name-select";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
type ToolsData = Record<string, any>;
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -24,7 +24,7 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { InfoLabel } from "@/components/shared/info-label";
|
||||
import { isSecret } from "@/lib/secret";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
type ToolsData = Record<string, any>;
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { ROUTES } from "@/lib/constants";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
interface Props {
|
||||
data: Record<string, any> | undefined;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export function CronAdvancedDialog({ open, onOpenChange, job, onUpdate }: CronAd
|
||||
if (!open) return;
|
||||
reset(deriveDefaults(job));
|
||||
fetchTargets();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
}, [open]);
|
||||
|
||||
const handleSave = async () => {
|
||||
|
||||
@@ -99,7 +99,7 @@ export function MCPUserCredentialsDialog({
|
||||
.then(setStatus)
|
||||
.catch((err) => console.error("[MCPUserCredentials] load credentials failed:", err))
|
||||
.finally(() => { setLoadingStatus(false); setInitialLoad(false); });
|
||||
}, [open, server.id, onGetCredentials, canManageUsers, selectedUserId]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [open, server.id, onGetCredentials, canManageUsers, selectedUserId]);
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
|
||||
@@ -38,7 +38,8 @@ export function EpisodicTab({ agentId }: Props) {
|
||||
const toggleExpand = (id: string) => {
|
||||
setExpanded((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.has(id) ? next.delete(id) : next.add(id);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -95,7 +95,7 @@ export function KGEntityDetailDialog({ open, onOpenChange, agentId, entity, getE
|
||||
if (open && entity && !traversing) {
|
||||
traverse(entity.id, entity.user_id, depth);
|
||||
}
|
||||
}, [open, entity]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [open, entity]);
|
||||
|
||||
// Build graph data from traversal results
|
||||
const graphData = useMemo(() => {
|
||||
|
||||
@@ -75,7 +75,7 @@ export function ProviderAdvancedDialog({
|
||||
setAcpIdleTTL(s.acpIdleTTL);
|
||||
setAcpPermMode(s.acpPermMode);
|
||||
setAcpWorkDir(s.acpWorkDir);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
}, [open]);
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
@@ -51,7 +51,7 @@ export function useSkills() {
|
||||
// even if the SKILL_DEPS_* events were emitted before the client connected.
|
||||
useEffect(() => {
|
||||
if (connected) invalidate();
|
||||
}, [connected]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [connected]);
|
||||
|
||||
const getSkill = useCallback(
|
||||
async (name: string) => {
|
||||
|
||||
@@ -65,7 +65,7 @@ export const BoardContainer = memo(function BoardContainer({
|
||||
// Re-fetch when filters change
|
||||
useEffect(() => {
|
||||
if (initialized) load();
|
||||
}, [statusFilter, selectedScope]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [statusFilter, selectedScope]);
|
||||
|
||||
// ── Callbacks for children ──
|
||||
|
||||
|
||||
@@ -97,6 +97,17 @@ export function TaskList({
|
||||
}
|
||||
}, [deleteTasksBulk, teamId, selectedIds]);
|
||||
|
||||
const handleSingleDelete = useCallback(async () => {
|
||||
if (!deleteTask || !deleteTargetId) return;
|
||||
setSingleDeleting(true);
|
||||
try {
|
||||
await deleteTask(teamId, deleteTargetId);
|
||||
setDeleteTargetId(null);
|
||||
} finally {
|
||||
setSingleDeleting(false);
|
||||
}
|
||||
}, [deleteTask, teamId, deleteTargetId]);
|
||||
|
||||
if (loading && tasks.length === 0) {
|
||||
return <div className="py-8 text-center text-sm text-muted-foreground">{t("tasks.loading")}</div>;
|
||||
}
|
||||
@@ -117,17 +128,6 @@ export function TaskList({
|
||||
setDeleteTargetId(taskId);
|
||||
};
|
||||
|
||||
const handleSingleDelete = useCallback(async () => {
|
||||
if (!deleteTask || !deleteTargetId) return;
|
||||
setSingleDeleting(true);
|
||||
try {
|
||||
await deleteTask(teamId, deleteTargetId);
|
||||
setDeleteTargetId(null);
|
||||
} finally {
|
||||
setSingleDeleting(false);
|
||||
}
|
||||
}, [deleteTask, teamId, deleteTargetId]);
|
||||
|
||||
const hasBulkDelete = !!deleteTasksBulk && pageTerminalIds.length > 0;
|
||||
const allPageSelected = pageTerminalIds.length > 0 && pageTerminalIds.every((id) => selectedIds.has(id));
|
||||
const someSelected = selectedIds.size > 0 && !allPageSelected;
|
||||
|
||||
@@ -103,7 +103,7 @@ export function TeamAuditLogsModal({ open, onOpenChange, teamId }: TeamAuditLogs
|
||||
setEvents([]);
|
||||
fetchEvents(0, false);
|
||||
}
|
||||
}, [open]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
fetchEvents(events.length, true);
|
||||
|
||||
@@ -19,12 +19,6 @@ export function KnowledgeChart({ data, loading, granularity }: KnowledgeChartPro
|
||||
const { t } = useTranslation("usage");
|
||||
const timezone = useUiStore((s) => s.timezone);
|
||||
|
||||
const hasData = data.some(
|
||||
(d) => d.memory_docs > 0 || d.memory_chunks > 0 || d.kg_entities > 0 || d.kg_relations > 0,
|
||||
);
|
||||
|
||||
if (!loading && !hasData) return null;
|
||||
|
||||
const chartData = useMemo(() => data.map((d) => ({
|
||||
label: formatBucketTz(d.bucket_time, timezone, granularity),
|
||||
memory_docs: d.memory_docs,
|
||||
@@ -33,6 +27,12 @@ export function KnowledgeChart({ data, loading, granularity }: KnowledgeChartPro
|
||||
kg_relations: d.kg_relations,
|
||||
})), [data, granularity, timezone]);
|
||||
|
||||
const hasData = data.some(
|
||||
(d) => d.memory_docs > 0 || d.memory_chunks > 0 || d.kg_entities > 0 || d.kg_relations > 0,
|
||||
);
|
||||
|
||||
if (!loading && !hasData) return null;
|
||||
|
||||
return (
|
||||
<ChartWrapper
|
||||
title={t("analytics.knowledgeChart.title")}
|
||||
|
||||
Reference in new issue
Block a user