mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 10:15:48 +00:00
fix(vault): skip goclaw_gen media in enrichment, improve graph UX
Backend: - Skip goclaw_gen_* files in Handle, EnqueueUnenriched, gatherCandidates - Skip in RescanWorkspace PendingEvents (progress count) - Reduces noisy auto-links by ~59% Frontend: - Theme-aware node colors (Tailwind -600/-400 matching sidebar) - Neutral gray edges (zinc-500/300), lighter inactive edges - Disable hover label white box (defaultDrawNodeHover) - Auto-expand + load first-level folders - Group hover text colors for tree items - Edge density based on camera zoom
This commit is contained in:
1 parent
0546850df8
commit
2c2e2cb536
7 files changed
+138
-49
No files matched your search
@@ -6,6 +6,7 @@ import (
|
||||
"log/slog"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
@@ -153,6 +154,10 @@ func (w *EnrichWorker) gatherCandidates(ctx context.Context, tenantID, _ string,
|
||||
if n.Score < enrichSimilarityMin || n.Document.Summary == "" {
|
||||
continue
|
||||
}
|
||||
// Skip auto-generated media files as link targets — they create noise.
|
||||
if strings.HasPrefix(n.Document.PathBasename, "goclaw_gen_") {
|
||||
continue
|
||||
}
|
||||
// Bidirectional dedup: only process each pair once.
|
||||
a, b := src.DocID, n.Document.ID
|
||||
if a > b {
|
||||
|
||||
@@ -125,7 +125,12 @@ func (w *EnrichWorker) EnqueueUnenriched(ctx context.Context, tenantID, workspac
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, doc := range docs {
|
||||
// Skip auto-generated media files — they create noise links.
|
||||
if strings.HasPrefix(filepath.Base(doc.Path), "goclaw_gen_") {
|
||||
continue
|
||||
}
|
||||
agentID := ""
|
||||
if doc.AgentID != nil {
|
||||
agentID = *doc.AgentID
|
||||
@@ -147,10 +152,11 @@ func (w *EnrichWorker) EnqueueUnenriched(ctx context.Context, tenantID, workspac
|
||||
},
|
||||
}
|
||||
bus.Publish(event)
|
||||
count++
|
||||
}
|
||||
|
||||
slog.Info("vault.enrich: enqueued unenriched", "tenant", tenantID, "count", len(docs))
|
||||
return len(docs), nil
|
||||
slog.Info("vault.enrich: enqueued unenriched", "tenant", tenantID, "count", count)
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// enrichTaskSiblingCap bounds the number of auto-linked siblings per
|
||||
@@ -172,6 +178,13 @@ func (w *EnrichWorker) Handle(ctx context.Context, event eventbus.DomainEvent) e
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip auto-generated media files (goclaw_gen_*) — they create excessive
|
||||
// noise links due to similar embeddings. These are typically image outputs
|
||||
// that don't benefit from semantic linking.
|
||||
if basename := filepath.Base(payload.Path); strings.HasPrefix(basename, "goclaw_gen_") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Dedup: skip if same hash already processed.
|
||||
w.dedupMu.Lock()
|
||||
if prev, exists := w.dedup[payload.DocID]; exists && prev == payload.ContentHash {
|
||||
|
||||
@@ -115,7 +115,9 @@ func RescanWorkspace(ctx context.Context, params RescanParams, vs store.VaultSto
|
||||
|
||||
// Collect enrichment events — published AFTER the loop so the caller
|
||||
// can call progress.Start(total) before workers receive events.
|
||||
if bus != nil {
|
||||
// Skip auto-generated media files (goclaw_gen_*) — they create excessive
|
||||
// noise links and shouldn't be counted in progress tracking.
|
||||
if bus != nil && !strings.HasPrefix(filepath.Base(relPath), "goclaw_gen_") {
|
||||
result.PendingEvents = append(result.PendingEvents, eventbus.DomainEvent{
|
||||
ID: uuid.Must(uuid.NewV7()).String(),
|
||||
Type: eventbus.EventVaultDocUpserted,
|
||||
|
||||
@@ -2,18 +2,36 @@ import Graph from "graphology";
|
||||
import type { VaultDocument, VaultLink } from "@/types/vault";
|
||||
import { getNodeSize, truncateMiddle } from "@/components/graph/graph-utils";
|
||||
|
||||
// Colors per vault document type — muted pastels that work well on both
|
||||
// light and dark backgrounds. Avoid harsh neons on dark mode.
|
||||
export const VAULT_TYPE_COLORS: Record<string, string> = {
|
||||
context: "#818cf8", // indigo-400 (softer)
|
||||
memory: "#a78bfa", // violet-400
|
||||
note: "#5eead4", // teal-300 (soft, readable)
|
||||
skill: "#86efac", // green-300
|
||||
episodic: "#fcd34d", // amber-300
|
||||
media: "#f9a8d4", // pink-300 (soft, not harsh)
|
||||
document: "#67e8f9", // cyan-300
|
||||
// Colors per vault document type — matches DOC_TYPE_ICONS in vault-tree.tsx
|
||||
// Light mode: Tailwind -600 variants for contrast on white/light backgrounds
|
||||
// Dark mode: Tailwind -400 variants for visibility on dark backgrounds
|
||||
export const VAULT_TYPE_COLORS_LIGHT: Record<string, string> = {
|
||||
context: "#2563eb", // blue-600 (matches text-blue-600)
|
||||
memory: "#9333ea", // purple-600 (matches text-purple-600)
|
||||
note: "#d97706", // amber-600 (matches text-amber-600)
|
||||
skill: "#059669", // emerald-600 (matches text-emerald-600)
|
||||
episodic: "#ea580c", // orange-600 (matches text-orange-600)
|
||||
media: "#e11d48", // rose-600 (matches text-rose-600)
|
||||
document: "#0891b2", // cyan-600 (matches text-cyan-600)
|
||||
};
|
||||
const DEFAULT_COLOR = "#a1a1aa";
|
||||
export const VAULT_TYPE_COLORS_DARK: Record<string, string> = {
|
||||
context: "#60a5fa", // blue-400 (matches dark:text-blue-400)
|
||||
memory: "#c084fc", // purple-400 (matches dark:text-purple-400)
|
||||
note: "#fbbf24", // amber-400 (matches dark:text-amber-400)
|
||||
skill: "#34d399", // emerald-400 (matches dark:text-emerald-400)
|
||||
episodic: "#fb923c", // orange-400 (matches dark:text-orange-400)
|
||||
media: "#fb7185", // rose-400 (matches dark:text-rose-400)
|
||||
document: "#22d3ee", // cyan-400 (matches dark:text-cyan-400)
|
||||
};
|
||||
const DEFAULT_COLOR_LIGHT = "#475569"; // slate-600
|
||||
const DEFAULT_COLOR_DARK = "#94a3b8"; // slate-400
|
||||
|
||||
/** Get node color based on doc type and theme */
|
||||
export function getVaultNodeColor(docType: string, isDark: boolean): string {
|
||||
const colors = isDark ? VAULT_TYPE_COLORS_DARK : VAULT_TYPE_COLORS_LIGHT;
|
||||
const fallback = isDark ? DEFAULT_COLOR_DARK : DEFAULT_COLOR_LIGHT;
|
||||
return colors[docType] ?? fallback;
|
||||
}
|
||||
|
||||
/** Limit documents by degree centrality (highest-connected first). */
|
||||
export function limitVaultDocsByDegree(
|
||||
@@ -57,7 +75,8 @@ export function buildVaultGraph(
|
||||
x: 0,
|
||||
y: 0,
|
||||
size: getNodeSize(degree),
|
||||
color: VAULT_TYPE_COLORS[doc.doc_type] ?? DEFAULT_COLOR,
|
||||
// Color set by nodeReducer based on theme; use light as initial fallback
|
||||
color: VAULT_TYPE_COLORS_LIGHT[doc.doc_type] ?? DEFAULT_COLOR_LIGHT,
|
||||
docType: doc.doc_type,
|
||||
});
|
||||
}
|
||||
@@ -70,6 +89,8 @@ export function buildVaultGraph(
|
||||
graph.addEdgeWithKey(link.id, link.from_doc_id, link.to_doc_id, {
|
||||
label: link.link_type,
|
||||
type: "curvedArrow",
|
||||
color: "#a1a1aa", // zinc-400, lighter gray
|
||||
size: 0.4,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import noverlap from "graphology-layout-noverlap";
|
||||
import type Graph from "graphology";
|
||||
import { useUiStore } from "@/stores/use-ui-store";
|
||||
import { SIGMA_SETTINGS } from "./graph-utils";
|
||||
import { getVaultNodeColor } from "@/adapters/vault-graph-adapter";
|
||||
|
||||
export type EdgeType = "curvedArrow" | "arrow";
|
||||
|
||||
@@ -24,22 +25,19 @@ export interface SigmaGraphContainerProps {
|
||||
hiddenTypes?: Set<string>;
|
||||
}
|
||||
|
||||
/** Theme-aware colors */
|
||||
/** Theme-aware colors - uses DOM class check for reliability */
|
||||
function useThemeColors() {
|
||||
const theme = useUiStore((s) => s.theme);
|
||||
const isDark =
|
||||
theme === "dark" ||
|
||||
(theme === "system" &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
// Subscribe to store to trigger re-render when theme changes
|
||||
useUiStore((s) => s.theme);
|
||||
// Check DOM class for actual dark state (handles system mode correctly)
|
||||
const isDark = typeof document !== "undefined" && document.documentElement.classList.contains("dark");
|
||||
return {
|
||||
isDark,
|
||||
labelColor: isDark ? "#e2e8f0" : "#1e293b",
|
||||
// Base edge color — subtle, not dominant
|
||||
edgeColor: isDark ? "#47556944" : "#94a3b8aa",
|
||||
// Dim color for non-active edges
|
||||
dimEdgeColor: isDark ? "#47556922" : "#cbd5e155",
|
||||
// Base edge color — subtle gray
|
||||
edgeColor: isDark ? "#71717a" : "#d4d4d8", // zinc-500 dark, zinc-300 light
|
||||
// Highlight for active neighborhood edges
|
||||
highlightEdgeColor: isDark ? "#a1a1aa" : "#64748b",
|
||||
highlightEdgeColor: isDark ? "#d4d4d8" : "#3f3f46",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,7 +58,9 @@ export function SigmaGraphContainer({
|
||||
const [hoveredNode, setHoveredNode] = useState<string | null>(null);
|
||||
// Pulse phase for animated highlighted edges (0..1, cycles)
|
||||
const [pulsePhase, setPulsePhase] = useState(0);
|
||||
const { labelColor, edgeColor, dimEdgeColor, highlightEdgeColor } = useThemeColors();
|
||||
// Camera ratio for edge density control (lower = more zoomed out)
|
||||
const [cameraRatio, setCameraRatio] = useState(1);
|
||||
const { isDark, labelColor, edgeColor, highlightEdgeColor } = useThemeColors();
|
||||
|
||||
const setSigmaRef = useCallback(
|
||||
(instance: Sigma | null) => {
|
||||
@@ -147,6 +147,8 @@ export function SigmaGraphContainer({
|
||||
zoomingRatio: 1.3,
|
||||
// Enable z-index sorting — required for edge/node `zIndex` attr to affect render order
|
||||
zIndex: true,
|
||||
// Disable default hover label box (white background) — we use forceLabel instead
|
||||
defaultDrawNodeHover: () => undefined,
|
||||
});
|
||||
|
||||
setSigmaRef(sigma);
|
||||
@@ -214,27 +216,32 @@ export function SigmaGraphContainer({
|
||||
(attrs.docType || attrs.entityType || "other") as string;
|
||||
|
||||
sigma.setSetting("nodeReducer", (node, data) => {
|
||||
// Apply theme-aware color based on docType
|
||||
const docType = getNodeType(data);
|
||||
const themedColor = getVaultNodeColor(docType, isDark);
|
||||
const themedData = { ...data, color: themedColor };
|
||||
|
||||
// Filter: hide nodes of hidden types
|
||||
if (hiddenTypes?.size && hiddenTypes.has(getNodeType(data))) {
|
||||
return { ...data, hidden: true };
|
||||
if (hiddenTypes?.size && hiddenTypes.has(docType)) {
|
||||
return { ...themedData, hidden: true };
|
||||
}
|
||||
|
||||
const activeNode = selectedNodeId || hoveredNode;
|
||||
if (!activeNode) return data;
|
||||
if (!activeNode) return themedData;
|
||||
|
||||
const hood = neighborhoodRef.current;
|
||||
if (!hood) return data;
|
||||
if (!hood) return themedData;
|
||||
|
||||
if (node === activeNode) {
|
||||
// Active node: bring to top, show label
|
||||
return { ...data, zIndex: 3, forceLabel: true };
|
||||
return { ...themedData, zIndex: 3, forceLabel: true };
|
||||
}
|
||||
if (hood.nodes.has(node)) {
|
||||
// Within 2-hop neighborhood: show label, slightly raised z-index
|
||||
return { ...data, zIndex: 2, forceLabel: true };
|
||||
return { ...themedData, zIndex: 2, forceLabel: true };
|
||||
}
|
||||
// Outside neighborhood: keep color/size, no dimming — just lower z-index
|
||||
return { ...data, zIndex: 0 };
|
||||
return { ...themedData, zIndex: 0 };
|
||||
});
|
||||
|
||||
sigma.setSetting("edgeReducer", (edge, data) => {
|
||||
@@ -248,7 +255,23 @@ export function SigmaGraphContainer({
|
||||
}
|
||||
|
||||
const activeNode = selectedNodeId || hoveredNode;
|
||||
if (!activeNode) return data;
|
||||
|
||||
// Zoom-based edge density: when zoomed out, hide edges from low-degree nodes
|
||||
// cameraRatio < 1 means zoomed out; < 0.5 means very zoomed out
|
||||
if (!activeNode && cameraRatio < 0.6) {
|
||||
const srcDegree = graph.degree(graph.source(edge));
|
||||
const tgtDegree = graph.degree(graph.target(edge));
|
||||
// Progressive threshold: more zoomed out → hide more edges
|
||||
const minDegree = cameraRatio < 0.3 ? 8 : cameraRatio < 0.45 ? 5 : 3;
|
||||
if (srcDegree < minDegree && tgtDegree < minDegree) {
|
||||
return { ...data, hidden: true };
|
||||
}
|
||||
}
|
||||
|
||||
// No active node: use default edge color from graph attributes
|
||||
if (!activeNode) {
|
||||
return { ...data, color: edgeColor, size: 0.5 };
|
||||
}
|
||||
|
||||
const hood = neighborhoodRef.current;
|
||||
if (!hood) return data;
|
||||
@@ -265,7 +288,8 @@ export function SigmaGraphContainer({
|
||||
});
|
||||
|
||||
sigma.refresh();
|
||||
}, [selectedNodeId, hoveredNode, graph, highlightEdgeColor, dimEdgeColor, hiddenTypes]);
|
||||
// sigmaVersion ensures this runs after sigma is created
|
||||
}, [selectedNodeId, hoveredNode, graph, highlightEdgeColor, edgeColor, hiddenTypes, isDark, cameraRatio, sigmaVersion]);
|
||||
|
||||
// --- Pulse animation for highlighted edges (only runs when a node is active) ---
|
||||
// Respects prefers-reduced-motion — skips animation entirely for accessibility
|
||||
@@ -333,17 +357,25 @@ export function SigmaGraphContainer({
|
||||
onNodeSelectRef.current?.(null);
|
||||
};
|
||||
|
||||
// Track camera ratio for edge density control
|
||||
const handleCameraUpdate = () => {
|
||||
const ratio = sigma.getCamera().ratio;
|
||||
setCameraRatio(ratio);
|
||||
};
|
||||
|
||||
sigma.on("enterNode", handleEnterNode);
|
||||
sigma.on("leaveNode", handleLeaveNode);
|
||||
sigma.on("clickNode", handleClickNode);
|
||||
sigma.on("doubleClickNode", handleDoubleClickNode);
|
||||
sigma.on("clickStage", handleClickStage);
|
||||
sigma.getCamera().on("updated", handleCameraUpdate);
|
||||
|
||||
return () => {
|
||||
sigma.off("enterNode", handleEnterNode);
|
||||
sigma.off("leaveNode", handleLeaveNode);
|
||||
sigma.off("clickNode", handleClickNode);
|
||||
sigma.off("doubleClickNode", handleDoubleClickNode);
|
||||
sigma.getCamera().off("updated", handleCameraUpdate);
|
||||
sigma.off("clickStage", handleClickStage);
|
||||
};
|
||||
}, [sigmaVersion]); // re-register only when sigma instance changes
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import {
|
||||
ChevronRight, Folder, FolderOpen, Loader2,
|
||||
FileText, Brain, StickyNote, Sparkles, Clock, Image, FileType,
|
||||
@@ -49,7 +49,17 @@ function VaultTreeNode({
|
||||
activePath: string | null; onSelect: (path: string) => void; onLoadMore: (path: string) => void;
|
||||
}) {
|
||||
// Auto-expand first level folders (depth=0) by default
|
||||
const [expanded, setExpanded] = useState(depth === 0 && node.isDir);
|
||||
const shouldAutoExpand = depth === 0 && node.isDir;
|
||||
const [expanded, setExpanded] = useState(shouldAutoExpand);
|
||||
const [didAutoLoad, setDidAutoLoad] = useState(false);
|
||||
|
||||
// Auto-load children when auto-expanded
|
||||
useEffect(() => {
|
||||
if (shouldAutoExpand && !didAutoLoad && node.hasChildren && node.children.length === 0 && !node.loading) {
|
||||
setDidAutoLoad(true);
|
||||
onLoadMore(node.path);
|
||||
}
|
||||
}, [shouldAutoExpand, didAutoLoad, node.hasChildren, node.children.length, node.loading, node.path, onLoadMore]);
|
||||
const entry = meta.get(node.path);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
@@ -65,15 +75,15 @@ function VaultTreeNode({
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="flex w-full items-center gap-1 rounded px-2 py-1 text-left text-sm cursor-pointer hover:bg-accent"
|
||||
className="group flex w-full items-center gap-1 rounded px-2 py-1 text-left text-sm cursor-pointer hover:bg-accent"
|
||||
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<ChevronRight className={`h-3 w-3 shrink-0 transition-transform ${expanded ? "rotate-90" : ""}`} />
|
||||
<ChevronRight className={`h-3 w-3 shrink-0 transition-transform text-muted-foreground group-hover:text-foreground ${expanded ? "rotate-90" : ""}`} />
|
||||
{expanded
|
||||
? <FolderOpen className="h-4 w-4 shrink-0 text-yellow-600" />
|
||||
: <Folder className="h-4 w-4 shrink-0 text-yellow-600" />}
|
||||
<span className="truncate text-xs">{node.name}</span>
|
||||
? <FolderOpen className="h-4 w-4 shrink-0 text-yellow-600 dark:text-yellow-500" />
|
||||
: <Folder className="h-4 w-4 shrink-0 text-yellow-600 dark:text-yellow-500" />}
|
||||
<span className="truncate text-xs text-foreground/80 group-hover:text-foreground">{node.name}</span>
|
||||
{node.loading && <Loader2 className="h-3 w-3 shrink-0 animate-spin text-muted-foreground ml-auto" />}
|
||||
{!node.loading && node.children.length > 0 && (
|
||||
<span className="ml-auto shrink-0 rounded-full bg-muted px-1 text-2xs tabular-nums text-muted-foreground">
|
||||
@@ -111,14 +121,14 @@ function VaultTreeNode({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={`flex w-full items-center gap-1.5 rounded px-2 py-1 text-left cursor-pointer ${
|
||||
isActive ? "bg-accent text-accent-foreground" : "hover:bg-accent/50"
|
||||
className={`group flex w-full items-center gap-1.5 rounded px-2 py-1 text-left cursor-pointer ${
|
||||
isActive ? "bg-accent" : "hover:bg-accent/50"
|
||||
}`}
|
||||
style={{ paddingLeft: `${depth * 16 + 20}px` }}
|
||||
onClick={() => onSelect(node.path)}
|
||||
>
|
||||
<Icon className={`h-3.5 w-3.5 shrink-0 ${color}`} />
|
||||
<span className="truncate text-xs">{truncateMiddle(fileName)}</span>
|
||||
<span className={`truncate text-xs ${isActive ? "text-accent-foreground" : "text-foreground/80 group-hover:text-foreground"}`}>{truncateMiddle(fileName)}</span>
|
||||
{scopeDot && <span className={`ml-auto h-1.5 w-1.5 rounded-full shrink-0 ${scopeDot}`} />}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useRef, useMemo, useState, useCallback } from "react";
|
||||
import type Sigma from "sigma";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { buildVaultGraph, limitVaultDocsByDegree, VAULT_TYPE_COLORS } from "@/adapters/vault-graph-adapter";
|
||||
import { buildVaultGraph, limitVaultDocsByDegree, VAULT_TYPE_COLORS_LIGHT, VAULT_TYPE_COLORS_DARK } from "@/adapters/vault-graph-adapter";
|
||||
import { useUiStore } from "@/stores/use-ui-store";
|
||||
import { SigmaGraphContainer } from "@/components/graph/sigma-graph-container";
|
||||
import { SigmaGraphControls } from "@/components/graph/sigma-graph-controls";
|
||||
import { SigmaGraphSearch } from "@/components/graph/sigma-graph-search";
|
||||
@@ -31,6 +32,11 @@ export function VaultGraphView({ agentId, teamId, selectedDocId, onNodeSelect, o
|
||||
const [hiddenTypes, setHiddenTypes] = useState<Set<string>>(new Set());
|
||||
const [filtersOpen, setFiltersOpen] = useState(false);
|
||||
|
||||
// Theme-aware node colors - subscribe to store for re-render, check DOM for actual state
|
||||
useUiStore((s) => s.theme);
|
||||
const isDark = typeof document !== "undefined" && document.documentElement.classList.contains("dark");
|
||||
const typeColors = isDark ? VAULT_TYPE_COLORS_DARK : VAULT_TYPE_COLORS_LIGHT;
|
||||
|
||||
const { documents: allDocs, links, loading } = useVaultGraphData(agentId, { teamId });
|
||||
|
||||
const totalCount = allDocs.length;
|
||||
@@ -71,7 +77,7 @@ export function VaultGraphView({ agentId, teamId, selectedDocId, onNodeSelect, o
|
||||
{/* Top bar — responsive: stacks on narrow screens */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 px-3 py-1 border-b shrink-0">
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-0.5 text-xs text-muted-foreground flex-1 min-w-0">
|
||||
{Object.entries(VAULT_TYPE_COLORS).map(([type, color]) => (
|
||||
{Object.entries(typeColors).map(([type, color]) => (
|
||||
<span key={type} className="flex items-center gap-1">
|
||||
<span className="inline-block h-2.5 w-2.5 rounded-full" style={{ backgroundColor: color }} />
|
||||
{type}
|
||||
@@ -88,7 +94,7 @@ export function VaultGraphView({ agentId, teamId, selectedDocId, onNodeSelect, o
|
||||
/>
|
||||
<SigmaGraphFilters
|
||||
graph={graph}
|
||||
typeColors={VAULT_TYPE_COLORS}
|
||||
typeColors={typeColors}
|
||||
hiddenTypes={hiddenTypes}
|
||||
onHiddenTypesChange={setHiddenTypes}
|
||||
collapsed={!filtersOpen}
|
||||
|
||||
Reference in new issue
Block a user