From 50d72511f8783ed69638e826f082e25b5e9bb125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Ziel?= Date: Mon, 13 Jul 2026 14:27:14 +0200 Subject: [PATCH 1/3] Fix primary vertex type selection to skip the generic vertex label createVertex() used to take types[0] as the primary type driving styling and naming. Gremlin/TinkerPop's default base label, "vertex", commonly appears first alongside a more specific label (e.g. ["vertex", "sqsqueue"]), which meant multi-labeled nodes were styled and named after the meaningless generic label instead of their specific one. Skip the generic label (case-insensitively) when a more specific label is present, falling back to it only when it's the sole label. --- .../src/core/entities/vertex.test.ts | 27 +++++++++++++++++ .../src/core/entities/vertex.ts | 29 +++++++++++++++---- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/packages/graph-explorer/src/core/entities/vertex.test.ts b/packages/graph-explorer/src/core/entities/vertex.test.ts index 4844ee96c..bddb6c2bc 100644 --- a/packages/graph-explorer/src/core/entities/vertex.test.ts +++ b/packages/graph-explorer/src/core/entities/vertex.test.ts @@ -43,6 +43,33 @@ describe("createVertex", () => { }); }); + it("should style by the specific label when the generic vertex label is present", () => { + const vertex = createVertex({ + id: "1", + types: ["vertex", "sqsqueue"], + }); + + expect(vertex.type).toBe("sqsqueue"); + expect(vertex.types).toStrictEqual(["vertex", "sqsqueue"]); + }); + + it("should ignore the generic vertex label regardless of case or position", () => { + expect(createVertex({ id: "1", types: ["Vertex", "sqsqueue"] }).type).toBe( + "sqsqueue", + ); + expect(createVertex({ id: "1", types: ["VERTEX", "sqsqueue"] }).type).toBe( + "sqsqueue", + ); + expect( + createVertex({ id: "1", types: ["vertex", "sqsqueue", "resource"] }).type, + ).toBe("sqsqueue"); + }); + + it("should keep the generic vertex label when it is the only label", () => { + expect(createVertex({ id: "1", types: ["vertex"] }).type).toBe("vertex"); + expect(createVertex({ id: "1", types: ["Vertex"] }).type).toBe("Vertex"); + }); + it("should create a vertex with missing types", () => { const vertex = createVertex({ id: "1", diff --git a/packages/graph-explorer/src/core/entities/vertex.ts b/packages/graph-explorer/src/core/entities/vertex.ts index fd3f592f3..1801da091 100644 --- a/packages/graph-explorer/src/core/entities/vertex.ts +++ b/packages/graph-explorer/src/core/entities/vertex.ts @@ -30,9 +30,12 @@ export type Vertex = { id: VertexId; /** - * Single vertex type. + * The primary vertex type, used to drive styling and naming. * - For PG, the node label * - For RDF, the resource class + * + * When a node carries several labels, this is the most specific one: the + * generic `vertex` base label is skipped unless it is the only label. */ type: VertexType; @@ -63,9 +66,10 @@ export function createVertex(options: { id: EntityRawId; /** - * The primary type (used for styling) will be the first type in the array. If - * no types are provided, then types will be an empty array and the primary - * type will be empty string. + * The primary type (used for styling and naming) is the first specific label + * in the array, skipping the generic `vertex` base label unless it is the + * only label. If no types are provided, the vertex falls back to a single + * "missing type" label. */ types?: string[]; @@ -83,13 +87,28 @@ export function createVertex(options: { ) as VertexType[]; return { id: createVertexId(options.id), - type: types[0], + type: selectPrimaryType(types), types, attributes: options.attributes != null ? options.attributes : {}, isBlankNode: options.isBlankNode ?? false, }; } +/** The default label Gremlin/TinkerPop assigns to a vertex created without one. */ +const GENERIC_VERTEX_LABEL = "vertex"; + +/** + * Picks the type that drives a vertex's styling and naming. The generic + * `vertex` base label (matched case-insensitively) is skipped when a more + * specific label exists, so a node labeled `["vertex", "sqsqueue"]` is styled + * and named as `sqsqueue`. A vertex whose only label is `vertex` keeps it. + */ +function selectPrimaryType(types: VertexType[]): VertexType { + return ( + types.find(type => type.toLowerCase() !== GENERIC_VERTEX_LABEL) ?? types[0] + ); +} + /** Creates a VertexType from a string */ export function createVertexType(type: string): VertexType { return type as VertexType; From 29339541c19cc7f2e76002301fc6857b6389a4d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Ziel?= Date: Mon, 13 Jul 2026 14:27:25 +0200 Subject: [PATCH 2/3] Add Schema-view vertex type hiding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Schema view rendered every discovered vertex type with no way to exclude noisy ones, e.g. a database's own generic base label showing up as a node type on every schema graph. Add a per-connection hidden-types store (mirroring the existing schema atom) and an eye toggle on each row of the Styles panel's Nodes tab when opened from the Schema view; hiding a type also drops its schema-graph edges since they lose an endpoint. Hidden types stay listed, dimmed, so they can be restored, and the hidden set is scoped to the Schema view only — the main Graph view, canvas filtering, and discovery are untouched. The Styles panel is now shared between the Graph and Schema views, so the toggle is gated behind a showVertexVisibilityToggle prop passed only from the Schema view's sidebar. --- docs/features/schema-view.md | 2 +- .../StateProvider/hiddenSchemaTypes.test.ts | 72 +++++++++++++++++++ .../core/StateProvider/hiddenSchemaTypes.ts | 56 +++++++++++++++ .../src/core/StateProvider/index.ts | 1 + .../src/core/StateProvider/storageAtoms.ts | 8 +++ .../Sidebar/SchemaExplorerSidebar.test.tsx | 20 ++++++ .../Sidebar/SchemaExplorerSidebar.tsx | 6 +- .../SchemaGraph/useSchemaGraphData.test.ts | 72 +++++++++++++++++++ .../modules/SchemaGraph/useSchemaGraphData.ts | 3 + .../src/modules/Styles/Styles.test.tsx | 12 ++++ .../src/modules/Styles/Styles.tsx | 10 ++- .../src/modules/Styles/VertexStyleRow.tsx | 36 +++++++++- .../src/modules/Styles/VertexStyles.tsx | 8 ++- 13 files changed, 298 insertions(+), 8 deletions(-) create mode 100644 packages/graph-explorer/src/core/StateProvider/hiddenSchemaTypes.test.ts create mode 100644 packages/graph-explorer/src/core/StateProvider/hiddenSchemaTypes.ts create mode 100644 packages/graph-explorer/src/modules/SchemaGraph/useSchemaGraphData.test.ts diff --git a/docs/features/schema-view.md b/docs/features/schema-view.md index 8cac23e0f..f9ef37359 100644 --- a/docs/features/schema-view.md +++ b/docs/features/schema-view.md @@ -17,7 +17,7 @@ You can open the Schema view by clicking "Schema" in the navigation bar. The sidebar has two panels: - **Details** — shows properties and connections for the selected node type or edge connection -- **Styles** — customize colors and icons for node types and edge types, split across a Nodes tab and an Edges tab +- **Styles** — customize colors and icons for node types and edge types, split across a Nodes tab and an Edges tab. On the Nodes tab, the eye icon next to each node type hides it (and its connections) from the Schema view only — the main Graph view and data discovery are unaffected. Hidden types stay listed, dimmed, so they can be shown again; the hidden set persists per connection. Click the active tab icon to collapse the sidebar to just the icon strip. Click any tab icon to reopen it. Both the active tab and sidebar width are remembered across sessions. diff --git a/packages/graph-explorer/src/core/StateProvider/hiddenSchemaTypes.test.ts b/packages/graph-explorer/src/core/StateProvider/hiddenSchemaTypes.test.ts new file mode 100644 index 000000000..9cbc5d3f8 --- /dev/null +++ b/packages/graph-explorer/src/core/StateProvider/hiddenSchemaTypes.test.ts @@ -0,0 +1,72 @@ +// @vitest-environment happy-dom +import { act } from "react"; + +import { createVertexType, getAppStore } from "@/core"; +import { DbState, renderHookWithState } from "@/utils/testing"; + +import { useHiddenSchemaTypes } from "./hiddenSchemaTypes"; +import { hiddenSchemaTypesAtom } from "./storageAtoms"; + +describe("useHiddenSchemaTypes", () => { + it("starts with no hidden types", () => { + const dbState = new DbState(); + const { result } = renderHookWithState( + () => useHiddenSchemaTypes(), + dbState, + ); + + expect(result.current.isHidden(createVertexType("Vertex"))).toBe(false); + expect(result.current.hiddenTypes.size).toBe(0); + }); + + it("toggles a type on and off for the active connection", () => { + const dbState = new DbState(); + const type = createVertexType("Vertex"); + const { result } = renderHookWithState( + () => useHiddenSchemaTypes(), + dbState, + ); + + act(() => result.current.toggleType(type)); + expect(result.current.isHidden(type)).toBe(true); + + act(() => result.current.toggleType(type)); + expect(result.current.isHidden(type)).toBe(false); + }); + + it("removes the connection's map entry once its set empties", () => { + const dbState = new DbState(); + const type = createVertexType("Vertex"); + const store = getAppStore(); + const { result } = renderHookWithState( + () => useHiddenSchemaTypes(), + dbState, + ); + + act(() => result.current.toggleType(type)); + expect(store.get(hiddenSchemaTypesAtom).has(dbState.activeConfig.id)).toBe( + true, + ); + + act(() => result.current.toggleType(type)); + expect(store.get(hiddenSchemaTypesAtom).has(dbState.activeConfig.id)).toBe( + false, + ); + }); + + it("isolates hidden types per connection", () => { + const dbState = new DbState(); + const type = createVertexType("Vertex"); + const store = getAppStore(); + const { result } = renderHookWithState( + () => useHiddenSchemaTypes(), + dbState, + ); + + act(() => result.current.toggleType(type)); + + const map = store.get(hiddenSchemaTypesAtom); + expect(map.size).toBe(1); + expect([...map.keys()]).toEqual([dbState.activeConfig.id]); + }); +}); diff --git a/packages/graph-explorer/src/core/StateProvider/hiddenSchemaTypes.ts b/packages/graph-explorer/src/core/StateProvider/hiddenSchemaTypes.ts new file mode 100644 index 000000000..e69c3a479 --- /dev/null +++ b/packages/graph-explorer/src/core/StateProvider/hiddenSchemaTypes.ts @@ -0,0 +1,56 @@ +import { atom, useAtomValue, useSetAtom } from "jotai"; + +import type { VertexType } from "../entities"; + +import { activeConfigurationAtom, hiddenSchemaTypesAtom } from "./storageAtoms"; + +const EMPTY_HIDDEN_TYPES: ReadonlySet = new Set(); + +/** + * The active connection's hidden Schema-view vertex types. Reads fall back to a + * shared empty set for referential stability; writes delete the connection's + * entry once its set empties, keeping storage clean. + */ +export const activeHiddenSchemaTypesAtom = atom( + get => { + const id = get(activeConfigurationAtom); + return (id && get(hiddenSchemaTypesAtom).get(id)) ?? EMPTY_HIDDEN_TYPES; + }, + (get, set, update: (prev: ReadonlySet) => Set) => { + const id = get(activeConfigurationAtom); + if (!id) { + return; + } + set(hiddenSchemaTypesAtom, prev => { + const current = prev.get(id) ?? EMPTY_HIDDEN_TYPES; + const next = update(current); + const map = new Map(prev); + if (next.size === 0) { + map.delete(id); + } else { + map.set(id, next); + } + return map; + }); + }, +); + +/** Read the active connection's hidden vertex types plus hide/show/toggle actions. */ +export function useHiddenSchemaTypes() { + const hiddenTypes = useAtomValue(activeHiddenSchemaTypesAtom); + const setHidden = useSetAtom(activeHiddenSchemaTypesAtom); + return { + hiddenTypes, + isHidden: (type: VertexType) => hiddenTypes.has(type), + toggleType: (type: VertexType) => + setHidden(prev => { + const next = new Set(prev); + if (next.has(type)) { + next.delete(type); + } else { + next.add(type); + } + return next; + }), + }; +} diff --git a/packages/graph-explorer/src/core/StateProvider/index.ts b/packages/graph-explorer/src/core/StateProvider/index.ts index a215ca8c7..2521e1d47 100644 --- a/packages/graph-explorer/src/core/StateProvider/index.ts +++ b/packages/graph-explorer/src/core/StateProvider/index.ts @@ -11,6 +11,7 @@ export * from "./neighbors"; export * from "./nodes"; export * from "./renderedEntities"; export * from "./graphStyles"; +export * from "./hiddenSchemaTypes"; export * from "./schema"; export * from "./storageAtoms"; export * from "./graphSession"; diff --git a/packages/graph-explorer/src/core/StateProvider/storageAtoms.ts b/packages/graph-explorer/src/core/StateProvider/storageAtoms.ts index 97243c7b1..2d3da9f37 100644 --- a/packages/graph-explorer/src/core/StateProvider/storageAtoms.ts +++ b/packages/graph-explorer/src/core/StateProvider/storageAtoms.ts @@ -77,6 +77,7 @@ const [ defaultNeighborExpansionLimitEnabledAtom, defaultNeighborExpansionLimitAtom, diagnosticLoggingAtom, + hiddenSchemaTypesAtom, ] = await Promise.all([ createActiveConfigurationAtom(), atomWithLocalForage>( @@ -144,6 +145,12 @@ const [ atomWithLocalForage("defaultNeighborExpansionLimit", 10), /** Enables verbose diagnostic logging to the browser console. */ atomWithLocalForage("diagnosticLogging", false), + /** Vertex types hidden from the Schema view, per connection. */ + atomWithLocalForage( + "hidden-schema-types", + new Map>(), + { reconcile: reconcileMapByKey }, + ), ]); export { @@ -162,4 +169,5 @@ export { defaultNeighborExpansionLimitEnabledAtom, defaultNeighborExpansionLimitAtom, diagnosticLoggingAtom, + hiddenSchemaTypesAtom, }; diff --git a/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaExplorerSidebar.test.tsx b/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaExplorerSidebar.test.tsx index 232f2719e..40a45d764 100644 --- a/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaExplorerSidebar.test.tsx +++ b/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaExplorerSidebar.test.tsx @@ -114,6 +114,26 @@ describe("SchemaExplorerSidebar", () => { expect(screen.getByText("locatedIn")).toBeInTheDocument(); }); + test("toggles a vertex type's visibility from the styles tab", async () => { + const user = userEvent.setup(); + const state = stateWithDetailsTab(); + const vertex = createTestableVertex().with({ types: ["Airport"] }); + state.addTestableVertexToGraph(vertex); + + renderSidebar(state); + + await user.click(screen.getByRole("tab", { name: "Styles" })); + + const toggle = screen.getByRole("button", { + name: "Hide Airport from schema view", + }); + await user.click(toggle); + + expect( + screen.getByRole("button", { name: "Show Airport in schema view" }), + ).toBeInTheDocument(); + }); + test("collapses sidebar when clicking the active tab", async () => { const user = userEvent.setup(); renderSidebar(stateWithDetailsTab()); diff --git a/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaExplorerSidebar.tsx b/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaExplorerSidebar.tsx index f42d380b0..70d04f44e 100644 --- a/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaExplorerSidebar.tsx +++ b/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaExplorerSidebar.tsx @@ -65,7 +65,11 @@ export function SchemaExplorerSidebar({ /> - + diff --git a/packages/graph-explorer/src/modules/SchemaGraph/useSchemaGraphData.test.ts b/packages/graph-explorer/src/modules/SchemaGraph/useSchemaGraphData.test.ts new file mode 100644 index 000000000..9b26c4bb0 --- /dev/null +++ b/packages/graph-explorer/src/modules/SchemaGraph/useSchemaGraphData.test.ts @@ -0,0 +1,72 @@ +// @vitest-environment happy-dom +import { act } from "react"; + +import { createEdgeType, createVertexType, useHiddenSchemaTypes } from "@/core"; +import { + createTestableVertex, + DbState, + renderHookWithState, +} from "@/utils/testing"; + +import { useSchemaGraphData } from "./useSchemaGraphData"; + +describe("useSchemaGraphData", () => { + it("excludes hidden vertex types from the nodes", () => { + const state = new DbState(); + state.addTestableVertexToGraph( + createTestableVertex().with({ types: ["Airport"] }), + ); + state.addTestableVertexToGraph( + createTestableVertex().with({ types: ["City"] }), + ); + + const { result } = renderHookWithState( + () => ({ + data: useSchemaGraphData(), + hidden: useHiddenSchemaTypes(), + }), + state, + ); + + expect(result.current.data.nodes.map(n => n.data.type)).toContain( + createVertexType("Airport"), + ); + + act(() => result.current.hidden.toggleType(createVertexType("Airport"))); + + const types = result.current.data.nodes.map(n => n.data.type); + expect(types).not.toContain(createVertexType("Airport")); + expect(types).toContain(createVertexType("City")); + }); + + it("drops edges whose endpoint vertex type is hidden", () => { + const state = new DbState(); + state.addTestableVertexToGraph( + createTestableVertex().with({ types: ["Airport"] }), + ); + state.addTestableVertexToGraph( + createTestableVertex().with({ types: ["City"] }), + ); + state.activeSchema.edgeConnections = [ + { + edgeType: createEdgeType("locatedIn"), + sourceVertexType: createVertexType("Airport"), + targetVertexType: createVertexType("City"), + }, + ]; + + const { result } = renderHookWithState( + () => ({ + data: useSchemaGraphData(), + hidden: useHiddenSchemaTypes(), + }), + state, + ); + + expect(result.current.data.edges.length).toBeGreaterThan(0); + + act(() => result.current.hidden.toggleType(createVertexType("City"))); + + expect(result.current.data.edges).toHaveLength(0); + }); +}); diff --git a/packages/graph-explorer/src/modules/SchemaGraph/useSchemaGraphData.ts b/packages/graph-explorer/src/modules/SchemaGraph/useSchemaGraphData.ts index 782fd5882..5b18ce081 100644 --- a/packages/graph-explorer/src/modules/SchemaGraph/useSchemaGraphData.ts +++ b/packages/graph-explorer/src/modules/SchemaGraph/useSchemaGraphData.ts @@ -7,6 +7,7 @@ import { useActiveSchema, useDisplayEdgeTypeConfigs, useDisplayVertexTypeConfigs, + useHiddenSchemaTypes, type VertexType, } from "@/core"; @@ -44,10 +45,12 @@ export function useSchemaGraphData() { /** Transforms vertex type configs into schema graph nodes. */ function useSchemaGraphNodes(): SchemaGraphNode[] { const vtConfigs = useDisplayVertexTypeConfigs(); + const { isHidden } = useHiddenSchemaTypes(); const nodes: SchemaGraphNode[] = []; for (const config of vtConfigs.values()) { + if (isHidden(config.type)) continue; nodes.push({ data: { id: config.type, diff --git a/packages/graph-explorer/src/modules/Styles/Styles.test.tsx b/packages/graph-explorer/src/modules/Styles/Styles.test.tsx index cb27ed6e9..eab21087a 100644 --- a/packages/graph-explorer/src/modules/Styles/Styles.test.tsx +++ b/packages/graph-explorer/src/modules/Styles/Styles.test.tsx @@ -73,4 +73,16 @@ describe("Styles", () => { expect(screen.getByText("locatedIn")).toBeInTheDocument(); }); + + test("does not show a vertex visibility toggle for the main graph view", () => { + const state = new DbState(); + const vertex = createTestableVertex().with({ types: ["Airport"] }); + state.addTestableVertexToGraph(vertex); + + renderStyles(state); + + expect( + screen.queryByRole("button", { name: /schema view/ }), + ).not.toBeInTheDocument(); + }); }); diff --git a/packages/graph-explorer/src/modules/Styles/Styles.tsx b/packages/graph-explorer/src/modules/Styles/Styles.tsx index ae9b2768a..717385f60 100644 --- a/packages/graph-explorer/src/modules/Styles/Styles.tsx +++ b/packages/graph-explorer/src/modules/Styles/Styles.tsx @@ -34,13 +34,19 @@ export const schemaViewStylesTabAtom = atom("nodes"); export type StylesProps = { onClose: () => void; tabAtom: PrimitiveAtom; + /** Shows a per-row eye toggle to hide vertex types from the Schema view. */ + showVertexVisibilityToggle?: boolean; }; /** * Combined node and edge styling sidebar panel. The two style lists live behind * an internal Nodes/Edges tab switch so both share one styling entry point. */ -export function Styles({ onClose, tabAtom }: StylesProps) { +export function Styles({ + onClose, + tabAtom, + showVertexVisibilityToggle, +}: StylesProps) { const t = useTranslations(); const [selectedTab, setSelectedTab] = useAtom(tabAtom); @@ -63,7 +69,7 @@ export function Styles({ onClose, tabAtom }: StylesProps) { {t("edges")} - + diff --git a/packages/graph-explorer/src/modules/Styles/VertexStyleRow.tsx b/packages/graph-explorer/src/modules/Styles/VertexStyleRow.tsx index 4920ec68a..a2aeecdf3 100644 --- a/packages/graph-explorer/src/modules/Styles/VertexStyleRow.tsx +++ b/packages/graph-explorer/src/modules/Styles/VertexStyleRow.tsx @@ -1,19 +1,33 @@ +import { EyeIcon, EyeOffIcon } from "lucide-react"; import { type ComponentPropsWithRef, useEffect, useState } from "react"; import { Button, FormItem, InputField, Label, StylingIcon } from "@/components"; -import { useDisplayVertexTypeConfig, type VertexType } from "@/core"; +import { + useDisplayVertexTypeConfig, + useHiddenSchemaTypes, + type VertexType, +} from "@/core"; import { useVertexStyling } from "@/core/StateProvider/graphStyles"; import { useDebounceValue, usePrevious } from "@/hooks"; import { useOpenNodeStyleDialog } from "@/modules/NodesStyling"; +import { cn } from "@/utils"; import { LABELS } from "@/utils/constants"; export type VertexStyleRowProps = { vertexType: VertexType; + /** Shows an eye toggle to hide this vertex type from the Schema view. */ + showVisibilityToggle?: boolean; } & ComponentPropsWithRef; -export function VertexStyleRow({ vertexType, ...rest }: VertexStyleRowProps) { +export function VertexStyleRow({ + vertexType, + showVisibilityToggle, + className, + ...rest +}: VertexStyleRowProps) { const { setVertexStyle } = useVertexStyling(vertexType); const displayConfig = useDisplayVertexTypeConfig(vertexType); + const { isHidden, toggleType } = useHiddenSchemaTypes(); const [displayAs, setDisplayAs] = useState(displayConfig.displayLabel); @@ -30,8 +44,10 @@ export function VertexStyleRow({ vertexType, ...rest }: VertexStyleRowProps) { setVertexStyle({ displayLabel: debouncedDisplayAs }); }, [debouncedDisplayAs, prevDisplayAs, setVertexStyle]); + const hidden = showVisibilityToggle && isHidden(vertexType); + return ( - + {vertexType ? ( ) : ( @@ -50,6 +66,20 @@ export function VertexStyleRow({ vertexType, ...rest }: VertexStyleRowProps) { Customize + {showVisibilityToggle ? ( + + ) : null} ); diff --git a/packages/graph-explorer/src/modules/Styles/VertexStyles.tsx b/packages/graph-explorer/src/modules/Styles/VertexStyles.tsx index a3fe8a723..bd395a5d8 100644 --- a/packages/graph-explorer/src/modules/Styles/VertexStyles.tsx +++ b/packages/graph-explorer/src/modules/Styles/VertexStyles.tsx @@ -6,8 +6,13 @@ import { useDisplayVertexTypeConfigs } from "@/core"; import { VertexStyleRow } from "./VertexStyleRow"; +export type VertexStylesProps = { + /** Shows a per-row eye toggle to hide vertex types from the Schema view. */ + showVisibilityToggle?: boolean; +}; + /** Styling list for every vertex type, shown on the Nodes tab. */ -export function VertexStyles() { +export function VertexStyles({ showVisibilityToggle }: VertexStylesProps) { const vtConfigs = useDisplayVertexTypeConfigs().values().toArray(); if (vtConfigs.length === 0) { @@ -23,6 +28,7 @@ export function VertexStyles() { {index !== 0 ? : null} From ca45e58bfea81ab9ae2ae1cb560bf24a937cc5a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Ziel?= Date: Mon, 13 Jul 2026 14:35:13 +0200 Subject: [PATCH 3/3] Stop threading the visibility toggle through the shared VertexStyleRow VertexStyleRow is also rendered by the main Graph view's Styles panel, so a showVisibilityToggle prop and an unconditional useHiddenSchemaTypes() subscription inside it coupled the shared row to a Schema-view-only feature. Wrap it from the outside instead (SchemaVertexStyleRow in VertexStyles.tsx), leaving VertexStyleRow itself untouched. Also tighten two new tests to assert full expected values with toStrictEqual instead of toContain/toEqual, per docs/agents/testing.md. --- .../StateProvider/hiddenSchemaTypes.test.ts | 2 +- .../SchemaGraph/useSchemaGraphData.test.ts | 15 +++-- .../src/modules/Styles/VertexStyleRow.tsx | 36 +--------- .../src/modules/Styles/VertexStyles.tsx | 66 +++++++++++++++++-- 4 files changed, 71 insertions(+), 48 deletions(-) diff --git a/packages/graph-explorer/src/core/StateProvider/hiddenSchemaTypes.test.ts b/packages/graph-explorer/src/core/StateProvider/hiddenSchemaTypes.test.ts index 9cbc5d3f8..c6671c849 100644 --- a/packages/graph-explorer/src/core/StateProvider/hiddenSchemaTypes.test.ts +++ b/packages/graph-explorer/src/core/StateProvider/hiddenSchemaTypes.test.ts @@ -67,6 +67,6 @@ describe("useHiddenSchemaTypes", () => { const map = store.get(hiddenSchemaTypesAtom); expect(map.size).toBe(1); - expect([...map.keys()]).toEqual([dbState.activeConfig.id]); + expect([...map.keys()]).toStrictEqual([dbState.activeConfig.id]); }); }); diff --git a/packages/graph-explorer/src/modules/SchemaGraph/useSchemaGraphData.test.ts b/packages/graph-explorer/src/modules/SchemaGraph/useSchemaGraphData.test.ts index 9b26c4bb0..bdd477f39 100644 --- a/packages/graph-explorer/src/modules/SchemaGraph/useSchemaGraphData.test.ts +++ b/packages/graph-explorer/src/modules/SchemaGraph/useSchemaGraphData.test.ts @@ -13,6 +13,7 @@ import { useSchemaGraphData } from "./useSchemaGraphData"; describe("useSchemaGraphData", () => { it("excludes hidden vertex types from the nodes", () => { const state = new DbState(); + state.activeSchema.vertices = []; state.addTestableVertexToGraph( createTestableVertex().with({ types: ["Airport"] }), ); @@ -28,15 +29,15 @@ describe("useSchemaGraphData", () => { state, ); - expect(result.current.data.nodes.map(n => n.data.type)).toContain( - createVertexType("Airport"), - ); + expect( + result.current.data.nodes.map(n => n.data.type).toSorted(), + ).toStrictEqual([createVertexType("Airport"), createVertexType("City")]); act(() => result.current.hidden.toggleType(createVertexType("Airport"))); - const types = result.current.data.nodes.map(n => n.data.type); - expect(types).not.toContain(createVertexType("Airport")); - expect(types).toContain(createVertexType("City")); + expect(result.current.data.nodes.map(n => n.data.type)).toStrictEqual([ + createVertexType("City"), + ]); }); it("drops edges whose endpoint vertex type is hidden", () => { @@ -63,7 +64,7 @@ describe("useSchemaGraphData", () => { state, ); - expect(result.current.data.edges.length).toBeGreaterThan(0); + expect(result.current.data.edges).toHaveLength(1); act(() => result.current.hidden.toggleType(createVertexType("City"))); diff --git a/packages/graph-explorer/src/modules/Styles/VertexStyleRow.tsx b/packages/graph-explorer/src/modules/Styles/VertexStyleRow.tsx index a2aeecdf3..4920ec68a 100644 --- a/packages/graph-explorer/src/modules/Styles/VertexStyleRow.tsx +++ b/packages/graph-explorer/src/modules/Styles/VertexStyleRow.tsx @@ -1,33 +1,19 @@ -import { EyeIcon, EyeOffIcon } from "lucide-react"; import { type ComponentPropsWithRef, useEffect, useState } from "react"; import { Button, FormItem, InputField, Label, StylingIcon } from "@/components"; -import { - useDisplayVertexTypeConfig, - useHiddenSchemaTypes, - type VertexType, -} from "@/core"; +import { useDisplayVertexTypeConfig, type VertexType } from "@/core"; import { useVertexStyling } from "@/core/StateProvider/graphStyles"; import { useDebounceValue, usePrevious } from "@/hooks"; import { useOpenNodeStyleDialog } from "@/modules/NodesStyling"; -import { cn } from "@/utils"; import { LABELS } from "@/utils/constants"; export type VertexStyleRowProps = { vertexType: VertexType; - /** Shows an eye toggle to hide this vertex type from the Schema view. */ - showVisibilityToggle?: boolean; } & ComponentPropsWithRef; -export function VertexStyleRow({ - vertexType, - showVisibilityToggle, - className, - ...rest -}: VertexStyleRowProps) { +export function VertexStyleRow({ vertexType, ...rest }: VertexStyleRowProps) { const { setVertexStyle } = useVertexStyling(vertexType); const displayConfig = useDisplayVertexTypeConfig(vertexType); - const { isHidden, toggleType } = useHiddenSchemaTypes(); const [displayAs, setDisplayAs] = useState(displayConfig.displayLabel); @@ -44,10 +30,8 @@ export function VertexStyleRow({ setVertexStyle({ displayLabel: debouncedDisplayAs }); }, [debouncedDisplayAs, prevDisplayAs, setVertexStyle]); - const hidden = showVisibilityToggle && isHidden(vertexType); - return ( - + {vertexType ? ( ) : ( @@ -66,20 +50,6 @@ export function VertexStyleRow({ Customize - {showVisibilityToggle ? ( - - ) : null} ); diff --git a/packages/graph-explorer/src/modules/Styles/VertexStyles.tsx b/packages/graph-explorer/src/modules/Styles/VertexStyles.tsx index bd395a5d8..89e54d2c9 100644 --- a/packages/graph-explorer/src/modules/Styles/VertexStyles.tsx +++ b/packages/graph-explorer/src/modules/Styles/VertexStyles.tsx @@ -1,8 +1,14 @@ +import { EyeIcon, EyeOffIcon } from "lucide-react"; import { Virtuoso } from "react-virtuoso"; import { Fragment } from "react/jsx-runtime"; -import { Divider, NoNodeTypesEmptyState } from "@/components"; -import { useDisplayVertexTypeConfigs } from "@/core"; +import { Button, Divider, NoNodeTypesEmptyState } from "@/components"; +import { + useDisplayVertexTypeConfigs, + useHiddenSchemaTypes, + type VertexType, +} from "@/core"; +import { cn } from "@/utils"; import { VertexStyleRow } from "./VertexStyleRow"; @@ -26,13 +32,59 @@ export function VertexStyles({ showVisibilityToggle }: VertexStylesProps) { itemContent={(index, vtConfig) => ( {index !== 0 ? : null} - + {showVisibilityToggle ? ( + + ) : ( + + )} )} /> ); } + +/** + * Wraps VertexStyleRow with an eye toggle that hides the vertex type from + * the Schema view, without changing the shared row also rendered by the + * main Graph view's Styles panel. + */ +function SchemaVertexStyleRow({ + vertexType, + className, +}: { + vertexType: VertexType; + className?: string; +}) { + const { isHidden, toggleType } = useHiddenSchemaTypes(); + const hidden = isHidden(vertexType); + + return ( + + ); +}