diff --git a/CONTEXT.md b/CONTEXT.md index 3c118b63c..a43586823 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -67,22 +67,14 @@ A key-value pair on a Vertex or Edge. UI label varies by query language: "Proper _Avoid_: Attribute (legacy code term being phased out) **Styles**: -Display customizations per Vertex Type and Edge Type — shape, color, icon, line style, and display labels. Resolved through a **Styles Cascade** (highest precedence first): User Custom Styles → Shared Styles → App Default Styles. Persisted and merged with Schema-discovered metadata to produce the final rendering. The two specific instances are **Vertex Styles** and **Edge Styles**, each stored in IndexedDB as its own type-keyed Map per layer. Storage keys follow a `--styles` convention. -_Avoid_: User Preferences, preferences, user styling, user settings; "customization" as a standalone noun +Display customizations per Vertex Type and Edge Type — shape, color, icon, line style, and display labels. A type's style resolves as the **User Style** if the user has set one, otherwise the **App Default Style**. Persisted and merged with Schema-discovered metadata to produce the final rendering. The two specific instances are **Vertex Styles** and **Edge Styles**, each stored in IndexedDB as its own type-keyed Map. Storage keys follow a `user--styles` convention. The verb is **customize**; the UI shorthand is **your styles**. +_Avoid_: User Preferences, preferences, user styling, user settings; "customization" as a standalone noun; Effective styles (no separate atom), style overrides -**Styles Cascade**: -The precedence stack that resolves which style value a vertex or edge type displays. From highest to lowest priority: (3) **User Custom Styles** — per-type edits made in the style dialogs, (2) **Shared Styles** — loaded from a file via Settings, (1) **App Default Styles** — hardcoded in the codebase (`appDefaultVertexStyle` / `appDefaultEdgeStyle`). The layers below User Custom Styles (1–2) collectively are **Default Styles** — what a per-type "Reset to Default" restores to (it clears the user's edit, revealing the Shared or App Default beneath). The verb is **customize**; the UI shorthand is **custom styles** / **shared styles**. -_Avoid_: Imported Default Styles, imported defaults (the layer is **Shared Styles**); Effective styles (no separate atom), style overrides - -**User Custom Styles**: -The highest-precedence layer in the Styles Cascade. Per-type edits a user makes in the vertex/edge style dialogs. Stored in `userVertexStylesAtom` (`Map`, key `"user-vertex-styles"`) and `userEdgeStylesAtom` (`Map`, key `"user-edge-styles"`). Clearing these ("Reset Custom Styles" in Settings) reveals Shared Styles beneath. - -**Shared Styles**: -The second-highest-precedence layer in the Styles Cascade (below User Custom, above App Default). Loaded from a styling file via the Settings → Styles screen. Stored in `sharedVertexStylesAtom` (key `"shared-vertex-styles"`) and `sharedEdgeStylesAtom` (key `"shared-edge-styles"`). Non-destructive to User Custom Styles — loading writes only this layer. Clearing these ("Reset Shared Styles" in Settings) falls through to App Default Styles. Named for their purpose: a user **saves** their styles to a file and others **load** it to get the same look. -_Avoid_: Imported Default Styles, imported defaults (renamed — "shared" names the purpose and reads better) +**User Styles**: +The user's own styles, taking precedence over the app defaults. Per-type edits a user makes in the vertex/edge style dialogs, plus styles loaded from a file. Stored in `userVertexStylesAtom` (`Map`, key `"user-vertex-styles"`) and `userEdgeStylesAtom` (`Map`, key `"user-edge-styles"`). Clearing these ("Reset to Defaults" in Settings) falls back to App Default Styles. A user **saves** their styles to a file and others **load** it to get the same look. **App Default Styles**: -The lowest-precedence layer in the Styles Cascade. Hardcoded in the codebase as `appDefaultVertexStyle` and `appDefaultEdgeStyle`. Not persisted — always available as the final fallback. +The built-in fallback style, used for any type the user hasn't styled. Hardcoded in the codebase as `appDefaultVertexStyle` and `appDefaultEdgeStyle`. Not persisted — always available as the final fallback. In UI copy this is just the **default style** (the "app" qualifier is internal only). **Schema Sync**: The process that queries the database to discover vertex types, edge types, and their attributes. Required before a user can explore a new Connection. diff --git a/docs/adr/20260623-type-keyed-map-atoms-for-user-preferences.md b/docs/adr/20260623-type-keyed-map-atoms-for-user-preferences.md index dced1f621..c58c761b5 100644 --- a/docs/adr/20260623-type-keyed-map-atoms-for-user-preferences.md +++ b/docs/adr/20260623-type-keyed-map-atoms-for-user-preferences.md @@ -3,6 +3,7 @@ - **Status:** Accepted - **Date:** 2026-06-23 - **Updated:** 2026-07-06 — the styling vocabulary was renamed to the Styles convention (#1866). The decision below is unchanged; only the identifiers drifted. Read it with these substitutions: `VertexPreferencesStorageModel`→`VertexStyleStorage`, `EdgePreferencesStorageModel`→`EdgeStyleStorage`, `vertexPreferencesAtom`→`vertexStyleAtom`, `edgePreferencesAtom`→`edgeStyleAtom`, `LegacyUserStylingStorageModel`→`LegacyUserStylingStorage`, `userPreferences.ts`→`graphStyles.ts`. +- **Updated:** 2026-07-20 — the multi-layer roadmap was abandoned (#1974). Styles resolve as a single user layer over the app defaults; the never-shipped shared layer was removed. The type-keyed Map decision below is unchanged. - **Related:** ADR `per-key-diff-merge-cross-tab-reconciliation` — the Map-keyed shape is the prerequisite for per-type merge. ADR `storage-layer-owns-persistence-failure` — the migration's "catch and degrade to defaults on failure" posture follows that ADR's established startup-failure stance. Issue #1864 (this change). Issues #1820 / #1831 (cross-tab merge, separate follow-up). ## Context @@ -37,7 +38,7 @@ userEdgeStylesAtom: Map; // key: "user-ed Splitting vertex styles from edge styles (rather than a single `Map`) keeps the types precise and avoids a heterogeneous map that mixes `VertexPreferencesStorageModel` and `EdgePreferencesStorageModel` values under untyped string keys. -The keys follow a `--styles` convention. Only the user-defined layer (`user-`) exists today; planned later work adds imported (UI file import), server (fetched from server config), and base (hard-coded codebase defaults) layers, which merge by precedence to produce the final rendering. Naming the user layer `user-vertex-styles` now — rather than a bare `vertex-styles` — means those sibling keys slot in without renaming or re-migrating this one. +The keys are `user-vertex-styles` and `user-edge-styles`. The `user-` prefix is a historical artifact of a since-abandoned multi-layer roadmap (see the 2026-07-20 note); styles now resolve as this single user layer over the hard-coded app defaults. Maps persist natively through localForage/IndexedDB via the structured-clone algorithm — no serialization work is needed. diff --git a/docs/adr/20260624-styling-file-format.md b/docs/adr/20260624-styling-file-format.md index 19d8b3ff0..06c2ec362 100644 --- a/docs/adr/20260624-styling-file-format.md +++ b/docs/adr/20260624-styling-file-format.md @@ -3,7 +3,8 @@ - **Status:** Accepted - **Date:** 2026-06-24 - **Updated:** 2026-07-06 — the Preferences → Styles rename (#1866) has since shipped. The decision below is unchanged; read `VertexPreferencesStorageModel`→`VertexStyleStorage` and `EdgePreferencesStorageModel`→`EdgeStyleStorage`. -- **Related:** ADR `shared-file-envelope` (the outer envelope this payload lives inside). ADR `type-keyed-map-atoms-for-user-preferences` (the Map storage shape that the shared layer mirrors). Issue #1866 (Preferences → Styles rename). +- **Updated:** 2026-07-20 — the styles cascade collapsed to a single user layer (#1974). Import and export now target the user styles (`userVertexStylesAtom` / `userEdgeStylesAtom`) directly; the never-shipped shared layer was removed with no data migration. The file format itself is unchanged. +- **Related:** ADR `shared-file-envelope` (the outer envelope this payload lives inside). ADR `type-keyed-map-atoms-for-user-preferences` (the Map storage shape the user styles mirror). Issue #1866 (Preferences → Styles rename). ## Context @@ -82,13 +83,13 @@ type EntryImportIssue = { ### Export semantics -Export produces the **effective merged view**: for each type that has either user or shared styling, the output is `{ ...sharedMap.get(type), ...userMap.get(type) }` with user fields winning on conflict. Re-importing this file on a fresh machine reproduces the full visual appearance. +Export produces each type's stored **user style** verbatim (`userVertexStylesAtom` / `userEdgeStylesAtom`). Re-importing this file on a fresh machine reproduces the full visual appearance. ### Import semantics -Import writes to the **shared layer** (`sharedVertexStylesAtom` / `sharedEdgeStylesAtom`) — it is **non-destructive** to user customizations. The user layer is untouched. If the user has customized a field that the import also specifies, the user's value wins in the cascade (user layer has higher precedence). +Import writes to the **user styles** (`userVertexStylesAtom` / `userEdgeStylesAtom`). -Import **merges** into the existing shared layer rather than replacing it: each type in the file is set onto the current shared map, and types not present in the file are retained. When the file specifies a type that already has a shared style, that overlap is surfaced via `getStylingConflicts` and the user confirms before the overwrite proceeds. This lets a user assemble shared styles from several files while still being warned before any existing shared style is overwritten. +Import **merges** into the existing user styles rather than replacing them: each type in the file is set onto the current map, and types not present in the file are retained. When the file specifies a type that already has a user style, that overlap is surfaced via `getStylingConflicts` and the user confirms before the overwrite proceeds. This lets a user assemble styles from several files while still being warned before any existing style is overwritten. ## Consequences diff --git a/docs/adr/20260709-read-time-transform-for-persisted-values.md b/docs/adr/20260709-read-time-transform-for-persisted-values.md index e87f90e55..00d3cc2af 100644 --- a/docs/adr/20260709-read-time-transform-for-persisted-values.md +++ b/docs/adr/20260709-read-time-transform-for-persisted-values.md @@ -12,7 +12,7 @@ Persisted state in IndexedDB (via `atomWithLocalForage`) reloads in its stored s Reshape the value **on read**, via a `transform` option on `atomWithLocalForage`: `transform: (loaded: T) => T` runs on the preloaded value before it seeds the atom. The transform lives beside its type (`transformGraphViewLayout` / `transformSchemaViewLayout`, sharing `transformLegacySidebarItem`) and is wired onto the atom in `storageAtoms.ts`. -- **Updated 2026-07-10:** `transformVertexStyles` (in `vertexStylesTransform.ts`) is a second consumer, applied to both `user-vertex-styles` and `shared-vertex-styles`. It coerces retired round-polygon shapes to their non-round counterpart (see ADR `coerce-retired-round-polygon-shapes`). Values arriving through file import are stored verbatim — the same ReadTransform coerces them on the next load, so both entry points (persisted storage and imported files) converge on the same coercion without the import path needing its own transform. +- **Updated 2026-07-10:** `transformVertexStyles` (in `vertexStylesTransform.ts`) is a second consumer, applied to `user-vertex-styles`. It coerces retired round-polygon shapes to their non-round counterpart (see ADR `coerce-retired-round-polygon-shapes`). Values arriving through file import are stored verbatim — the same ReadTransform coerces them on the next load, so both entry points (persisted storage and imported files) converge on the same coercion without the import path needing its own transform. Two decisions here are not obvious from the code: diff --git a/docs/adr/20260710-coerce-retired-round-polygon-shapes.md b/docs/adr/20260710-coerce-retired-round-polygon-shapes.md index 2ee0d77f2..816264a42 100644 --- a/docs/adr/20260710-coerce-retired-round-polygon-shapes.md +++ b/docs/adr/20260710-coerce-retired-round-polygon-shapes.md @@ -8,7 +8,7 @@ Cytoscape's round-polygon canvas renderer degenerates at 24px (our node size) for six shapes: `round-triangle`, `round-pentagon`, `round-hexagon`, `round-heptagon`, `round-octagon`, `round-tag`. The corner computation collapses, rendering each as a formless blob and producing invalid edge-endpoint coordinates that cause connected edges to disappear. `round-rectangle` and `round-diamond` are unaffected (they use a different code path). No upstream fix exists; the one related issue (cytoscape/cytoscape.js#3282) describes a crash, not this visual collapse. -The shapes became selectable in the UI via #1886, so users may have stored them in IndexedDB (user-vertex-styles, shared-vertex-styles) or in exported styling files. We must handle that existing data. +The shapes became selectable in the UI via #1886, so users may have stored them in IndexedDB (user-vertex-styles) or in exported styling files. We must handle that existing data. ## Decision diff --git a/docs/features/settings.md b/docs/features/settings.md index 831f1a640..ba2820858 100644 --- a/docs/features/settings.md +++ b/docs/features/settings.md @@ -10,33 +10,23 @@ ## Styles -The Styles settings page lets you manage your styling configuration — the colors, shapes, icons, and other visual properties applied to vertex and edge types in the graph view. +The Styles settings page lets you manage your styling configuration: the colors, shapes, icons, and other visual properties applied to vertex and edge types in the graph view. -Graph Explorer uses a three-layer cascade to determine how each type looks: - -1. **Custom styles** (highest priority) — per-type edits you make in the style dialogs -2. **Shared styles** — loaded from a file via this settings page -3. **App defaults** — the built-in fallback styles - -When you customize a type in the style dialog, that change only affects your custom layer. Shared styles fill in where you haven't customized. +Each type's appearance comes from your styles when you've set them, falling back to the built-in defaults otherwise. You set a type's style by editing it in the style dialog or by loading a styling file. ### Save styles to share -Saves your current effective styles (custom and shared, merged) to `graph-explorer.styles.json`. Loading this file on another machine or browser reproduces your full visual configuration. +Saves your current styles to `graph-explorer.styles.json`. Loading this file on another machine or browser reproduces your full visual configuration. -### Load shared styles +### Load styles -Loads a styling file and merges it into your shared styles. Your custom styles are not affected — they continue to take priority. If there are no conflicts the file is applied immediately; if the file contains types that already have shared styles, you'll see a confirmation listing the types that will be replaced before anything changes. Types not in the file are left unchanged. The page shows an indicator of how many types currently have shared styles. +Loads a styling file and merges it into your styles. If there are no conflicts the file is applied immediately; if the file contains types you've already styled, you'll see a confirmation listing the types that will be replaced before anything changes. Types not in the file are left unchanged. Loading is all-or-nothing: if the file contains any invalid value, nothing is loaded and you'll see a report listing every offending type and field so you can fix the file and load it again. Icons must be Lucide references (`lucide:`) or base64-encoded data URIs; remote URLs are rejected. Unrecognized or extra fields are ignored silently (this is how a file from a newer version still loads), so a file whose entries contain only unrecognized fields loads nothing and reports that no styles were found. -### Reset custom styles - -Clears all your per-type style customizations. After this, types will show their shared styles (if any) or the app defaults. This cannot be undone — consider saving first. - -### Reset shared styles +### Reset your styles -Removes all shared styles. Your custom styles remain unaffected. +Clears all your node and edge styles, returning every type to the defaults. This cannot be undone, so consider saving first. ## About diff --git a/packages/graph-explorer/src/core/StateProvider/graphStyles.ts b/packages/graph-explorer/src/core/StateProvider/graphStyles.ts index 3ce752246..6575793b4 100644 --- a/packages/graph-explorer/src/core/StateProvider/graphStyles.ts +++ b/packages/graph-explorer/src/core/StateProvider/graphStyles.ts @@ -10,12 +10,7 @@ import DEFAULT_ICON_URL from "@/utils/defaultIconUrl"; import type { EdgeType, VertexType } from "../entities"; import { useActiveSchema } from "./schema"; -import { - sharedEdgeStylesAtom, - sharedVertexStylesAtom, - userEdgeStylesAtom, - userVertexStylesAtom, -} from "./storageAtoms"; +import { userEdgeStylesAtom, userVertexStylesAtom } from "./storageAtoms"; export const SHAPE_STYLES = [ "rectangle", @@ -177,80 +172,53 @@ export type LegacyUserStylingStorage = { }; /** - * The styles cascade — the single place vertex/edge appearance is assembled. - * - * Three layers, lowest precedence first: - * 1. App defaults — {@link appDefaultVertexStyle} / {@link appDefaultEdgeStyle}. - * 2. Shared styles — `sharedVertexStylesAtom` / `sharedEdgeStylesAtom` - * (storage keys `shared-vertex-styles` / `shared-edge-styles`), loaded - * from a styling file on the Settings → Styles page. - * 3. User customizations — `userVertexStylesAtom` / `userEdgeStylesAtom` - * (storage keys `user-vertex-styles` / `user-edge-styles`), edits made in the - * style dialogs. - * - * Higher layers override lower ones per field (see {@link resolveVertexStyle} - * / {@link resolveEdgeStyle}). Related modules: `core/styling` (the styling - * import parser and import/export hooks) and `core/fileEnvelope` (the file - * wrapper). The `icon`↔`iconUrl` rename happens at the file-format seam in the - * styling parser, not here. + * The single place vertex/edge appearance is assembled: a type's stored user + * style overrides the app defaults per field, falling back to the defaults for + * anything unset. User styles come from the style dialogs and from loading a + * file (`core/styling`); the `icon`↔`iconUrl` rename happens at the file-format + * seam in that parser, not here. */ -/** Vertex styles indexed by type for O(1) lookup with cascade fallback. - * Cascade: user > shared > app defaults. */ +/** Vertex styles indexed by type for O(1) lookup, resolved against defaults. */ export const vertexStyleAtom = atom(get => { const userStyles = get(userVertexStylesAtom); - const sharedStyles = get(sharedVertexStylesAtom); return { get(type: VertexType) { - return resolveVertexStyle( - type, - sharedStyles.get(type), - userStyles.get(type), - ); + return resolveVertexStyle(type, userStyles.get(type)); }, }; }); -/** Edge styles indexed by type for O(1) lookup with cascade fallback. - * Cascade: user > shared > app defaults. */ +/** Edge styles indexed by type for O(1) lookup, resolved against defaults. */ export const edgeStyleAtom = atom(get => { const userStyles = get(userEdgeStylesAtom); - const sharedStyles = get(sharedEdgeStylesAtom); return { get(type: EdgeType) { - return resolveEdgeStyle( - type, - sharedStyles.get(type), - userStyles.get(type), - ); + return resolveEdgeStyle(type, userStyles.get(type)); }, }; }); -/** Combines the cascade layers: app defaults < shared < user. */ +/** The user's vertex style overlaid on the app defaults. */ export function resolveVertexStyle( type: VertexType, - shared?: VertexStyleStorage, user?: VertexStyleStorage, ): VertexStyle { return { type, ...appDefaultVertexStyle, - ...shared, ...user, } as const; } -/** Combines the cascade layers: app defaults < shared < user. */ +/** The user's edge style overlaid on the app defaults. */ export function resolveEdgeStyle( type: EdgeType, - shared?: EdgeStyleStorage, user?: EdgeStyleStorage, ): EdgeStyle { return { type, ...appDefaultEdgeStyle, - ...shared, ...user, } as const; } diff --git a/packages/graph-explorer/src/core/StateProvider/migrateUserStyling.test.ts b/packages/graph-explorer/src/core/StateProvider/migrateUserStyling.test.ts index 906803367..c67b219cf 100644 --- a/packages/graph-explorer/src/core/StateProvider/migrateUserStyling.test.ts +++ b/packages/graph-explorer/src/core/StateProvider/migrateUserStyling.test.ts @@ -22,9 +22,8 @@ import { persistenceStatusStore } from "./persistence"; * Older versions stored all styling under a single `"user-styling"` key as a * `LegacyUserStylingStorage` object: `{ vertices: VertexStyleStorage[], edges: * EdgeStyleStorage[] }`. Styling is now stored as two type-keyed maps - * under `"user-vertex-styles"` and `"user-edge-styles"` — the user-defined layer of the - * planned `--styles` set. This migration converts the old shape - * to the new one on startup. + * under `"user-vertex-styles"` and `"user-edge-styles"`. This migration converts + * the old shape to the new one on startup. * * The old `"user-styling"` key is intentionally left untouched as a rollback * escape hatch; deleting it is a separate follow-up. diff --git a/packages/graph-explorer/src/core/StateProvider/storageAtoms.ts b/packages/graph-explorer/src/core/StateProvider/storageAtoms.ts index 1162f0e0d..433c489b4 100644 --- a/packages/graph-explorer/src/core/StateProvider/storageAtoms.ts +++ b/packages/graph-explorer/src/core/StateProvider/storageAtoms.ts @@ -68,8 +68,6 @@ const [ schemaAtom, userVertexStylesAtom, userEdgeStylesAtom, - sharedVertexStylesAtom, - sharedEdgeStylesAtom, graphViewLayoutAtom, schemaViewLayoutAtom, allGraphSessionsAtom, @@ -89,10 +87,7 @@ const [ atomWithLocalForage("schema", new Map(), { reconcile: reconcileMapByKey, }), - /** - * User-defined vertex style overrides, keyed by type. The `user-` prefix - * marks the user-defined layer of the `--styles` set. - */ + /** User-defined vertex style overrides, keyed by type. */ atomWithLocalForage( "user-vertex-styles", new Map(), @@ -104,22 +99,6 @@ const [ new Map(), { reconcile: reconcileMapByKey }, ), - /** - * Shared vertex styles, loaded from a styling file. The `shared-` prefix marks - * the shared layer of the `--styles` set — the fallback beneath - * a user's own edits. - */ - atomWithLocalForage( - "shared-vertex-styles", - new Map(), - { reconcile: reconcileMapByKey, transform: transformVertexStyles }, - ), - /** Shared edge styles, loaded from a styling file. */ - atomWithLocalForage( - "shared-edge-styles", - new Map(), - { reconcile: reconcileMapByKey }, - ), atomWithLocalForage("graph-view-layout", defaultGraphViewLayout, { transform: transformGraphViewLayout, }), @@ -153,8 +132,6 @@ export { schemaAtom, userVertexStylesAtom, userEdgeStylesAtom, - sharedVertexStylesAtom, - sharedEdgeStylesAtom, graphViewLayoutAtom, schemaViewLayoutAtom, allGraphSessionsAtom, diff --git a/packages/graph-explorer/src/core/styling/roundTrip.test.ts b/packages/graph-explorer/src/core/styling/roundTrip.test.ts index a2e46372b..fe4bd3ee8 100644 --- a/packages/graph-explorer/src/core/styling/roundTrip.test.ts +++ b/packages/graph-explorer/src/core/styling/roundTrip.test.ts @@ -11,8 +11,6 @@ import { getAppStore } from "@/core"; import { createEdgeType, createVertexType } from "@/core/entities"; import { createFileEnvelope } from "@/core/fileEnvelope"; import { - sharedEdgeStylesAtom, - sharedVertexStylesAtom, userEdgeStylesAtom, userVertexStylesAtom, } from "@/core/StateProvider/storageAtoms"; @@ -104,21 +102,21 @@ describe("round-trip: export then import", () => { importResult.current(parseOut); - const sharedVertices = store.get(sharedVertexStylesAtom); - expect(sharedVertices.get(createVertexType("airport"))).toStrictEqual({ + const importedVertices = store.get(userVertexStylesAtom); + expect(importedVertices.get(createVertexType("airport"))).toStrictEqual({ type: createVertexType("airport"), displayNameAttribute: "code", iconUrl: "lucide:anchor", iconImageType: "image/svg+xml", color: "#e66412", }); - expect(sharedVertices.get(createVertexType("country"))).toStrictEqual({ + expect(importedVertices.get(createVertexType("country"))).toStrictEqual({ type: createVertexType("country"), color: "#e612b8", }); - const sharedEdges = store.get(sharedEdgeStylesAtom); - expect(sharedEdges.get(createEdgeType("route"))).toStrictEqual({ + const importedEdges = store.get(userEdgeStylesAtom); + expect(importedEdges.get(createEdgeType("route"))).toStrictEqual({ type: createEdgeType("route"), lineThickness: 1, labelColor: "#eef4ff", @@ -185,9 +183,9 @@ describe("round-trip: export then import", () => { importResult.current(parseOut); - const shared = store.get(sharedVertexStylesAtom); + const imported = store.get(userVertexStylesAtom); expect( - shared.get( + imported.get( createVertexType("http://data.nobelprize.org/terms/LaureateAward"), ), ).toStrictEqual({ @@ -197,7 +195,9 @@ describe("round-trip: export then import", () => { color: "#1229e6", }); expect( - shared.get(createVertexType("http://data.nobelprize.org/terms/Laureate")), + imported.get( + createVertexType("http://data.nobelprize.org/terms/Laureate"), + ), ).toStrictEqual({ type: createVertexType("http://data.nobelprize.org/terms/Laureate"), iconUrl: "lucide:angry", @@ -205,14 +205,14 @@ describe("round-trip: export then import", () => { color: "#ba12e6", }); expect( - shared.get(createVertexType("http://www.w3.org/ns/dcat#Catalog")), + imported.get(createVertexType("http://www.w3.org/ns/dcat#Catalog")), ).toStrictEqual({ type: createVertexType("http://www.w3.org/ns/dcat#Catalog"), iconUrl: "lucide:alert-octagon", iconImageType: "image/svg+xml", }); expect( - shared.get(createVertexType("http://dbpedia.org/ontology/Award")), + imported.get(createVertexType("http://dbpedia.org/ontology/Award")), ).toStrictEqual({ type: createVertexType("http://dbpedia.org/ontology/Award"), displayNameAttribute: "http://www.w3.org/2000/01/rdf-schema#label", @@ -256,8 +256,8 @@ describe("round-trip: export then import", () => { importResult.current(parseOut); - const shared = store.get(sharedVertexStylesAtom); - expect(shared.get(createVertexType("CustomNode"))!.iconUrl).toBe( + const imported = store.get(userVertexStylesAtom); + expect(imported.get(createVertexType("CustomNode"))!.iconUrl).toBe( svgDataUri, ); }); @@ -306,14 +306,14 @@ describe("round-trip: export then import", () => { importResult.current(parseOut); - const shared = store.get(sharedVertexStylesAtom); - expect(shared.get(createVertexType("PngNode"))).toStrictEqual({ + const imported = store.get(userVertexStylesAtom); + expect(imported.get(createVertexType("PngNode"))).toStrictEqual({ type: createVertexType("PngNode"), iconUrl: pngDataUri, iconImageType: "image/png", color: "#111", }); - expect(shared.get(createVertexType("JpegNode"))).toStrictEqual({ + expect(imported.get(createVertexType("JpegNode"))).toStrictEqual({ type: createVertexType("JpegNode"), iconUrl: jpegDataUri, iconImageType: "image/jpeg", @@ -362,8 +362,8 @@ describe("round-trip: export then import", () => { importResult.current(parseOut); - const shared = store.get(sharedVertexStylesAtom); - expect(shared.get(createVertexType("BmpNode"))).toStrictEqual({ + const imported = store.get(userVertexStylesAtom); + expect(imported.get(createVertexType("BmpNode"))).toStrictEqual({ type: createVertexType("BmpNode"), iconUrl: bmpDataUri, iconImageType: "image/bmp", @@ -416,9 +416,9 @@ describe("round-trip: export then import", () => { ]), }); - const shared = store.get(sharedVertexStylesAtom); - expect(shared.has(createVertexType("HttpsNode"))).toBe(false); - expect(shared.has(createVertexType("HttpNode"))).toBe(false); + const imported = store.get(userVertexStylesAtom); + expect(imported.has(createVertexType("HttpsNode"))).toBe(false); + expect(imported.has(createVertexType("HttpNode"))).toBe(false); }); test("mixed icon types in a single export all survive round-trip", async () => { @@ -476,17 +476,17 @@ describe("round-trip: export then import", () => { importResult.current(parseOut); - const shared = store.get(sharedVertexStylesAtom); - expect(shared.get(createVertexType("LucideType"))!.iconUrl).toBe( + const imported = store.get(userVertexStylesAtom); + expect(imported.get(createVertexType("LucideType"))!.iconUrl).toBe( "lucide:user", ); - expect(shared.get(createVertexType("SvgDataType"))!.iconUrl).toBe( + expect(imported.get(createVertexType("SvgDataType"))!.iconUrl).toBe( "data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=", ); - expect(shared.get(createVertexType("PngDataType"))!.iconUrl).toBe( + expect(imported.get(createVertexType("PngDataType"))!.iconUrl).toBe( "data:image/png;base64,iVBORw0KGgo=", ); - expect(shared.get(createVertexType("NoIconType"))).toStrictEqual({ + expect(imported.get(createVertexType("NoIconType"))).toStrictEqual({ type: createVertexType("NoIconType"), color: "#abc", shape: "diamond", @@ -550,10 +550,10 @@ describe("round-trip: export then import", () => { result.current(parseOut); const store = getAppStore(); - const shared = store.get(sharedVertexStylesAtom); + const imported = store.get(userVertexStylesAtom); expect( - shared.get( + imported.get( createVertexType("http://data.nobelprize.org/terms/LaureateAward"), ), ).toStrictEqual({ @@ -563,7 +563,7 @@ describe("round-trip: export then import", () => { color: "#1229e6", }); - expect(shared.get(createVertexType("airport"))).toStrictEqual({ + expect(imported.get(createVertexType("airport"))).toStrictEqual({ type: createVertexType("airport"), displayNameAttribute: "code", iconUrl: "lucide:anchor", @@ -571,13 +571,13 @@ describe("round-trip: export then import", () => { color: "#e66412", }); - expect(shared.get(createVertexType("country"))).toStrictEqual({ + expect(imported.get(createVertexType("country"))).toStrictEqual({ type: createVertexType("country"), color: "#e612b8", }); - const sharedEdges = store.get(sharedEdgeStylesAtom); - expect(sharedEdges.get(createEdgeType("route"))).toStrictEqual({ + const importedEdges = store.get(userEdgeStylesAtom); + expect(importedEdges.get(createEdgeType("route"))).toStrictEqual({ type: createEdgeType("route"), lineThickness: 1, labelColor: "#eef4ff", diff --git a/packages/graph-explorer/src/core/styling/useStylingImportExport.test.ts b/packages/graph-explorer/src/core/styling/useStylingImportExport.test.ts index 14dff0b2a..d1db979ff 100644 --- a/packages/graph-explorer/src/core/styling/useStylingImportExport.test.ts +++ b/packages/graph-explorer/src/core/styling/useStylingImportExport.test.ts @@ -10,8 +10,7 @@ import type { import { getAppStore } from "@/core"; import { createEdgeType, createVertexType } from "@/core/entities"; import { - sharedVertexStylesAtom, - sharedEdgeStylesAtom, + userEdgeStylesAtom, userVertexStylesAtom, } from "@/core/StateProvider/storageAtoms"; import { renderHookWithJotai } from "@/utils/testing"; @@ -30,7 +29,7 @@ vi.mock("@/utils/fileData", () => ({ })); describe("styling import", () => { - test("parseStylingFile + applyImport writes styles to shared atoms", async () => { + test("parseStylingFile + applyImport writes styles to user atoms", async () => { const { result } = renderHookWithJotai(() => useApplyStylingImport()); const file = createStylingFile({ @@ -47,7 +46,7 @@ describe("styling import", () => { result.current(parsed); const store = getAppStore(); - const vertexStyles = store.get(sharedVertexStylesAtom); + const vertexStyles = store.get(userVertexStylesAtom); expect(vertexStyles.get(createVertexType("Person"))).toStrictEqual({ type: createVertexType("Person"), color: "#ff0000", @@ -58,43 +57,17 @@ describe("styling import", () => { color: "#00ff00", }); - const edgeStyles = store.get(sharedEdgeStylesAtom); + const edgeStyles = store.get(userEdgeStylesAtom); expect(edgeStyles.get(createEdgeType("route"))).toStrictEqual({ type: createEdgeType("route"), lineColor: "#0000ff", }); }); - test("does not modify user atoms on import", async () => { + test("merges new import with existing user styles", async () => { const store = getAppStore(); store.set( userVertexStylesAtom, - new Map([ - [ - createVertexType("Person"), - { type: createVertexType("Person"), color: "#111" }, - ], - ]), - ); - - const { result } = renderHookWithJotai(() => useApplyStylingImport()); - - const file = createStylingFile({ - vertices: { Person: { color: "#999" } }, - edges: {}, - }); - - const parsed = await parseStylingFile(file); - result.current(parsed); - - const userStyles = store.get(userVertexStylesAtom); - expect(userStyles.get(createVertexType("Person"))?.color).toBe("#111"); - }); - - test("merges new import with existing shared styles", async () => { - const store = getAppStore(); - store.set( - sharedVertexStylesAtom, new Map([ [ createVertexType("OldType"), @@ -113,19 +86,19 @@ describe("styling import", () => { const parsed = await parseStylingFile(file); result.current(parsed); - const vertexStyles = store.get(sharedVertexStylesAtom); + const vertexStyles = store.get(userVertexStylesAtom); expect(vertexStyles.has(createVertexType("OldType"))).toBe(true); expect(vertexStyles.has(createVertexType("NewType"))).toBe(true); }); test("getStylingConflicts identifies overlapping keys", () => { - const sharedVertexStyles = new Map([ + const userVertexStyles = new Map([ [ createVertexType("Person"), { type: createVertexType("Person"), color: "#existing" }, ], ]); - const sharedEdgeStyles = new Map([ + const userEdgeStyles = new Map([ [ createEdgeType("knows"), { type: createEdgeType("knows"), lineColor: "#old" }, @@ -153,8 +126,8 @@ describe("styling import", () => { const conflicts = getStylingConflicts( parsed, - sharedVertexStyles, - sharedEdgeStyles, + userVertexStyles, + userEdgeStyles, ); expect(conflicts).toStrictEqual({ vertices: ["Person"], @@ -233,36 +206,27 @@ describe("styling import", () => { }); describe("useExportStylingFile", () => { - test("exports merged user+shared styles with user winning", () => { + test("exports user styles", () => { const store = getAppStore(); store.set( - sharedVertexStylesAtom, + userVertexStylesAtom, new Map([ [ createVertexType("Person"), { type: createVertexType("Person"), - color: "#shared", + color: "#user", shape: "star", }, ], ]), ); store.set( - userVertexStylesAtom, - new Map([ - [ - createVertexType("Person"), - { type: createVertexType("Person"), color: "#user" }, - ], - ]), - ); - store.set( - sharedEdgeStylesAtom, + userEdgeStylesAtom, new Map([ [ createEdgeType("route"), - { type: createEdgeType("route"), lineColor: "#edge-shared" }, + { type: createEdgeType("route"), lineColor: "#edge-user" }, ], ]), ); @@ -275,48 +239,10 @@ describe("useExportStylingFile", () => { Person: { color: "#user", shape: "star" }, }); expect(payload.edges).toStrictEqual({ - route: { lineColor: "#edge-shared" }, + route: { lineColor: "#edge-user" }, }); }); - test("exports types only present in user layer", () => { - const store = getAppStore(); - store.set( - userVertexStylesAtom, - new Map([ - [ - createVertexType("City"), - { type: createVertexType("City"), color: "#city" }, - ], - ]), - ); - - const { result } = renderHookWithJotai(() => useExportStylingFile()); - const payload = result.current.getExportPayload(); - - expect(payload.vertices).toStrictEqual({ City: { color: "#city" } }); - expect(payload.edges).toStrictEqual({}); - }); - - test("exports types only present in shared layer", () => { - const store = getAppStore(); - store.set( - sharedEdgeStylesAtom, - new Map([ - [ - createEdgeType("likes"), - { type: createEdgeType("likes"), lineStyle: "dashed" }, - ], - ]), - ); - - const { result } = renderHookWithJotai(() => useExportStylingFile()); - const payload = result.current.getExportPayload(); - - expect(payload.vertices).toStrictEqual({}); - expect(payload.edges).toStrictEqual({ likes: { lineStyle: "dashed" } }); - }); - test("returns empty payload when no styles exist", () => { const { result } = renderHookWithJotai(() => useExportStylingFile()); const payload = result.current.getExportPayload(); diff --git a/packages/graph-explorer/src/core/styling/useStylingImportExport.ts b/packages/graph-explorer/src/core/styling/useStylingImportExport.ts index 820cc8a23..f90c6cdb6 100644 --- a/packages/graph-explorer/src/core/styling/useStylingImportExport.ts +++ b/packages/graph-explorer/src/core/styling/useStylingImportExport.ts @@ -8,8 +8,6 @@ import type { import { parseFileEnvelope } from "@/core/fileEnvelope"; import { - sharedEdgeStylesAtom, - sharedVertexStylesAtom, userEdgeStylesAtom, userVertexStylesAtom, } from "@/core/StateProvider/storageAtoms"; @@ -52,46 +50,43 @@ export async function parseStylingFile( } /** - * The types in `parsed` that already have a shared style. These are the entries + * The types in `parsed` that already have a user style. These are the entries * a load would overwrite, so the caller can warn before applying. */ export function getStylingConflicts( parsed: StylingParseResult, - sharedVertexStyles: Map, - sharedEdgeStyles: Map, + userVertexStyles: Map, + userEdgeStyles: Map, ): ImportConflicts { const vertices: string[] = []; const edges: string[] = []; for (const type of parsed.vertexStyles.keys()) { - if (sharedVertexStyles.has(type)) { + if (userVertexStyles.has(type)) { vertices.push(type); } } for (const type of parsed.edgeStyles.keys()) { - if (sharedEdgeStyles.has(type)) { + if (userEdgeStyles.has(type)) { edges.push(type); } } return { vertices, edges }; } -/** - * Merges a parsed styling file into the shared-styles layer, leaving user - * customizations untouched. - */ +/** Merges a parsed styling file into the user styles. */ export function useApplyStylingImport() { - const setSharedVertexStyles = useSetAtom(sharedVertexStylesAtom); - const setSharedEdgeStyles = useSetAtom(sharedEdgeStylesAtom); + const setUserVertexStyles = useSetAtom(userVertexStylesAtom); + const setUserEdgeStyles = useSetAtom(userEdgeStylesAtom); return function applyImport(parsed: StylingParseResult): void { - setSharedVertexStyles(prev => { + setUserVertexStyles(prev => { const merged = new Map(prev); for (const [type, style] of parsed.vertexStyles) { merged.set(type, style); } return merged; }); - setSharedEdgeStyles(prev => { + setUserEdgeStyles(prev => { const merged = new Map(prev); for (const [type, style] of parsed.edgeStyles) { merged.set(type, style); @@ -104,32 +99,16 @@ export function useApplyStylingImport() { export function useExportStylingFile() { const userVertexStyles = useAtomValue(userVertexStylesAtom); const userEdgeStyles = useAtomValue(userEdgeStylesAtom); - const sharedVertexStyles = useAtomValue(sharedVertexStylesAtom); - const sharedEdgeStyles = useAtomValue(sharedEdgeStylesAtom); function getExportPayload(): StylingExportPayload { const vertices: Record = {}; - - const allVertexTypes = new Set([ - ...sharedVertexStyles.keys(), - ...userVertexStyles.keys(), - ]); - for (const type of allVertexTypes) { - const shared = sharedVertexStyles.get(type); - const user = userVertexStyles.get(type); - vertices[type] = toVertexFileEntry({ type, ...shared, ...user }); + for (const [type, style] of userVertexStyles) { + vertices[type] = toVertexFileEntry(style); } const edges: Record = {}; - - const allEdgeTypes = new Set([ - ...sharedEdgeStyles.keys(), - ...userEdgeStyles.keys(), - ]); - for (const type of allEdgeTypes) { - const shared = sharedEdgeStyles.get(type); - const user = userEdgeStyles.get(type); - edges[type] = toEdgeFileEntry({ type, ...shared, ...user }); + for (const [type, style] of userEdgeStyles) { + edges[type] = toEdgeFileEntry(style); } return { vertices, edges }; diff --git a/packages/graph-explorer/src/modules/EdgesStyling/EdgeStyleDialog.tsx b/packages/graph-explorer/src/modules/EdgesStyling/EdgeStyleDialog.tsx index 77c83ba9c..3a1f1e445 100644 --- a/packages/graph-explorer/src/modules/EdgesStyling/EdgeStyleDialog.tsx +++ b/packages/graph-explorer/src/modules/EdgesStyling/EdgeStyleDialog.tsx @@ -99,7 +99,7 @@ function Content({ edgeType }: { edgeType: EdgeType }) { Customize Your {t("edge")} Style - Changes here become your custom style and override shared styles. + Changes here override the default style for this {t("edge-type")}. diff --git a/packages/graph-explorer/src/modules/NodesStyling/NodeStyleDialog.tsx b/packages/graph-explorer/src/modules/NodesStyling/NodeStyleDialog.tsx index 0ccb57966..833e3b13d 100644 --- a/packages/graph-explorer/src/modules/NodesStyling/NodeStyleDialog.tsx +++ b/packages/graph-explorer/src/modules/NodesStyling/NodeStyleDialog.tsx @@ -140,7 +140,7 @@ function Content({ vertexType }: { vertexType: VertexType }) { Customize Your {t("node")} Style - Changes here become your custom style and override shared styles. + Changes here override the default style for this {t("node-type")}. diff --git a/packages/graph-explorer/src/routes/Settings/LoadStylesButton.test.tsx b/packages/graph-explorer/src/routes/Settings/LoadStylesButton.test.tsx index 23ab3de28..e68623438 100644 --- a/packages/graph-explorer/src/routes/Settings/LoadStylesButton.test.tsx +++ b/packages/graph-explorer/src/routes/Settings/LoadStylesButton.test.tsx @@ -11,7 +11,7 @@ import { type AppStore, getAppStore } from "@/core"; import { createVertexType, type VertexType } from "@/core/entities"; import { createFileEnvelope } from "@/core/fileEnvelope"; import { createQueryClient } from "@/core/queryClient"; -import { sharedVertexStylesAtom } from "@/core/StateProvider/storageAtoms"; +import { userVertexStylesAtom } from "@/core/StateProvider/storageAtoms"; import { TestProvider } from "@/utils/testing"; import LoadStylesButton from "./LoadStylesButton"; @@ -23,7 +23,7 @@ vi.mock("@/utils/fileData", () => ({ })); /** - * Renders the button, optionally seeding the shared-styles atoms first. The + * Renders the button, optionally seeding the user-styles atoms first. The * seed happens before render so the component's first render already reflects * it — seeding after render leaves the load action closing over the stale * initial value until an async re-render flushes, which races the upload. @@ -73,7 +73,7 @@ describe("LoadStylesButton", () => { // Node-only load omits the zero edge side. expect(screen.getByText("Loaded 2 node styles.")).toBeInTheDocument(); expect( - store.get(sharedVertexStylesAtom).get(createVertexType("Person")), + store.get(userVertexStylesAtom).get(createVertexType("Person")), ).toStrictEqual({ type: createVertexType("Person"), color: "#abc" }); }); @@ -95,11 +95,11 @@ describe("LoadStylesButton", () => { ).toBeInTheDocument(); }); - test("prompts before overwriting an existing shared style, then completes on confirm", async () => { + test("prompts before overwriting an existing style, then completes on confirm", async () => { const user = userEvent.setup(); const store = renderButton(store => store.set( - sharedVertexStylesAtom, + userVertexStylesAtom, new Map([ [ createVertexType("Person"), @@ -116,17 +116,17 @@ describe("LoadStylesButton", () => { // Conflict prompt first — nothing applied yet. expect( - await screen.findByText("Replace 1 existing shared style?"), + await screen.findByText("Replace 1 existing style?"), ).toBeInTheDocument(); expect( - store.get(sharedVertexStylesAtom).get(createVertexType("Person"))?.color, + store.get(userVertexStylesAtom).get(createVertexType("Person"))?.color, ).toBe("#old"); await user.click(screen.getByRole("button", { name: "Load & Replace" })); expect(await screen.findByText("Styles Loaded")).toBeInTheDocument(); expect( - store.get(sharedVertexStylesAtom).get(createVertexType("Person"))?.color, + store.get(userVertexStylesAtom).get(createVertexType("Person"))?.color, ).toBe("#new"); }); @@ -142,7 +142,7 @@ describe("LoadStylesButton", () => { expect(await screen.findByText("Load Failed")).toBeInTheDocument(); expect(screen.getByText("Person")).toBeInTheDocument(); // Nothing persisted. - expect(store.get(sharedVertexStylesAtom).size).toBe(0); + expect(store.get(userVertexStylesAtom).size).toBe(0); }); test("reports when a valid file contains no recognized styles", async () => { diff --git a/packages/graph-explorer/src/routes/Settings/LoadStylesButton.tsx b/packages/graph-explorer/src/routes/Settings/LoadStylesButton.tsx index 37878fb38..35c808649 100644 --- a/packages/graph-explorer/src/routes/Settings/LoadStylesButton.tsx +++ b/packages/graph-explorer/src/routes/Settings/LoadStylesButton.tsx @@ -1,5 +1,10 @@ import { useAtomValue } from "jotai"; -import { AlertTriangleIcon, CheckCircleIcon, UploadIcon } from "lucide-react"; +import { + AlertTriangleIcon, + CheckCircleIcon, + FolderOpenIcon, + UploadIcon, +} from "lucide-react"; import { startTransition, useActionState } from "react"; import type { @@ -29,8 +34,8 @@ import { GroupTitle, } from "@/components"; import { - sharedEdgeStylesAtom, - sharedVertexStylesAtom, + userEdgeStylesAtom, + userVertexStylesAtom, } from "@/core/StateProvider/storageAtoms"; import { getStylingConflicts, @@ -75,8 +80,8 @@ type LoadAction = export default function LoadStylesButton() { const applyImport = useApplyStylingImport(); - const sharedVertexStyles = useAtomValue(sharedVertexStylesAtom); - const sharedEdgeStyles = useAtomValue(sharedEdgeStylesAtom); + const userVertexStyles = useAtomValue(userVertexStylesAtom); + const userEdgeStyles = useAtomValue(userEdgeStylesAtom); // The action is a reducer that may await and perform the load side effect, // since it runs as an event rather than as a pure reducer. React tracks @@ -92,8 +97,8 @@ export default function LoadStylesButton() { const parsed = await parseStylingFile(action.file); const conflicts = getStylingConflicts( parsed, - sharedVertexStyles, - sharedEdgeStyles, + userVertexStyles, + userEdgeStyles, ); // Conflicts must be confirmed before overwriting; otherwise apply now. if (hasConflicts(conflicts)) { @@ -166,8 +171,8 @@ export default function LoadStylesButton() { asChild > @@ -216,12 +221,12 @@ function ConflictContent({ - {`Replace ${conflictCount} existing shared ${conflictCount === 1 ? "style" : "styles"}?`} + {`Replace ${conflictCount} existing ${conflictCount === 1 ? "style" : "styles"}?`} - The loaded file will overwrite these existing shared styles. New types - will be added alongside them. This cannot be undone, so consider - saving first. + The loaded file will overwrite these existing styles. New types will + be added alongside them. This cannot be undone, so consider saving + first. diff --git a/packages/graph-explorer/src/routes/Settings/ResetAllStylesButton.tsx b/packages/graph-explorer/src/routes/Settings/ResetAllStylesButton.tsx deleted file mode 100644 index 3a47a27cf..000000000 --- a/packages/graph-explorer/src/routes/Settings/ResetAllStylesButton.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { useSetAtom } from "jotai"; -import { Trash2Icon } from "lucide-react"; - -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogMedia, - AlertDialogTitle, - AlertDialogTrigger, - Button, -} from "@/components"; -import { - sharedEdgeStylesAtom, - sharedVertexStylesAtom, - userEdgeStylesAtom, - userVertexStylesAtom, -} from "@/core/StateProvider/storageAtoms"; - -export default function ResetAllStylesButton() { - const setUserVertexStyles = useSetAtom(userVertexStylesAtom); - const setUserEdgeStyles = useSetAtom(userEdgeStylesAtom); - const setSharedVertexStyles = useSetAtom(sharedVertexStylesAtom); - const setSharedEdgeStyles = useSetAtom(sharedEdgeStylesAtom); - - function resetAllStyles() { - setUserVertexStyles(new Map()); - setUserEdgeStyles(new Map()); - setSharedVertexStyles(new Map()); - setSharedEdgeStyles(new Map()); - } - - return ( - - - - - - - - - - Reset All Styles - - - This will clear all your styles and all shared styles, returning - everything to defaults. Consider saving first. - - This cannot be undone. - - - - Cancel - - Reset All Styles - - - - - ); -} diff --git a/packages/graph-explorer/src/routes/Settings/ResetSharedStylesButton.tsx b/packages/graph-explorer/src/routes/Settings/ResetSharedStylesButton.tsx deleted file mode 100644 index 6ef82584a..000000000 --- a/packages/graph-explorer/src/routes/Settings/ResetSharedStylesButton.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useSetAtom } from "jotai"; -import { Trash2Icon } from "lucide-react"; - -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogMedia, - AlertDialogTitle, - AlertDialogTrigger, - Button, -} from "@/components"; -import { - sharedEdgeStylesAtom, - sharedVertexStylesAtom, -} from "@/core/StateProvider/storageAtoms"; - -export default function ResetSharedStylesButton() { - const setSharedVertexStyles = useSetAtom(sharedVertexStylesAtom); - const setSharedEdgeStyles = useSetAtom(sharedEdgeStylesAtom); - - function resetSharedStyles() { - setSharedVertexStyles(new Map()); - setSharedEdgeStyles(new Map()); - } - - return ( - - - - - - - - - - Reset Shared Styles - - - This will remove all shared styles. Your styles will remain. You - can load a file again at any time; consider saving first. - - This cannot be undone. - - - - Cancel - - Reset Shared Styles - - - - - ); -} diff --git a/packages/graph-explorer/src/routes/Settings/ResetAllStylesButton.test.tsx b/packages/graph-explorer/src/routes/Settings/ResetStylesButton.test.tsx similarity index 57% rename from packages/graph-explorer/src/routes/Settings/ResetAllStylesButton.test.tsx rename to packages/graph-explorer/src/routes/Settings/ResetStylesButton.test.tsx index 7b71854b6..58d2dd85b 100644 --- a/packages/graph-explorer/src/routes/Settings/ResetAllStylesButton.test.tsx +++ b/packages/graph-explorer/src/routes/Settings/ResetStylesButton.test.tsx @@ -4,17 +4,11 @@ import userEvent from "@testing-library/user-event"; import { describe, expect, test } from "vitest"; import { TooltipProvider } from "@/components"; -import { - getAppStore, - sharedEdgeStylesAtom, - sharedVertexStylesAtom, - userEdgeStylesAtom, - userVertexStylesAtom, -} from "@/core"; +import { getAppStore, userEdgeStylesAtom, userVertexStylesAtom } from "@/core"; import { createQueryClient } from "@/core/queryClient"; import { DbState, TestProvider } from "@/utils/testing"; -import ResetAllStylesButton from "./ResetAllStylesButton"; +import ResetStylesButton from "./ResetStylesButton"; function renderButton(state = new DbState()) { const store = getAppStore(); @@ -23,7 +17,7 @@ function renderButton(state = new DbState()) { render( - + , ); @@ -31,10 +25,10 @@ function renderButton(state = new DbState()) { } function triggerButton() { - return screen.getByRole("button", { name: "Reset All Styles" }); + return screen.getByRole("button", { name: "Reset" }); } -describe("ResetAllStylesButton", () => { +describe("ResetStylesButton", () => { test("renders a trigger button", () => { renderButton(); expect(triggerButton()).toBeInTheDocument(); @@ -47,41 +41,30 @@ describe("ResetAllStylesButton", () => { await user.click(triggerButton()); expect(screen.getByRole("alertdialog")).toBeInTheDocument(); - expect( - screen.getByText(/clear all your styles and all shared styles/), - ).toBeInTheDocument(); }); - test("resets all user and shared styles when confirmed", async () => { + test("clears all styles when confirmed", async () => { const user = userEvent.setup(); const store = renderButton(); await user.click(triggerButton()); - await user.click(screen.getByRole("button", { name: "Reset All Styles" })); + await user.click(screen.getByRole("button", { name: "Reset to Defaults" })); expect(store.get(userVertexStylesAtom)).toStrictEqual(new Map()); expect(store.get(userEdgeStylesAtom)).toStrictEqual(new Map()); - expect(store.get(sharedVertexStylesAtom)).toStrictEqual(new Map()); - expect(store.get(sharedEdgeStylesAtom)).toStrictEqual(new Map()); }); test("does nothing when cancelled", async () => { const user = userEvent.setup(); const store = renderButton(); - const expectedUserVertex = new Map(store.get(userVertexStylesAtom)); - const expectedUserEdge = new Map(store.get(userEdgeStylesAtom)); - const expectedSharedVertex = new Map(store.get(sharedVertexStylesAtom)); - const expectedSharedEdge = new Map(store.get(sharedEdgeStylesAtom)); + const expectedVertex = new Map(store.get(userVertexStylesAtom)); + const expectedEdge = new Map(store.get(userEdgeStylesAtom)); await user.click(triggerButton()); await user.click(screen.getByRole("button", { name: "Cancel" })); - expect(store.get(userVertexStylesAtom)).toStrictEqual(expectedUserVertex); - expect(store.get(userEdgeStylesAtom)).toStrictEqual(expectedUserEdge); - expect(store.get(sharedVertexStylesAtom)).toStrictEqual( - expectedSharedVertex, - ); - expect(store.get(sharedEdgeStylesAtom)).toStrictEqual(expectedSharedEdge); + expect(store.get(userVertexStylesAtom)).toStrictEqual(expectedVertex); + expect(store.get(userEdgeStylesAtom)).toStrictEqual(expectedEdge); }); }); diff --git a/packages/graph-explorer/src/routes/Settings/ResetCustomStylesButton.tsx b/packages/graph-explorer/src/routes/Settings/ResetStylesButton.tsx similarity index 75% rename from packages/graph-explorer/src/routes/Settings/ResetCustomStylesButton.tsx rename to packages/graph-explorer/src/routes/Settings/ResetStylesButton.tsx index e2d45300d..7c81de55c 100644 --- a/packages/graph-explorer/src/routes/Settings/ResetCustomStylesButton.tsx +++ b/packages/graph-explorer/src/routes/Settings/ResetStylesButton.tsx @@ -19,11 +19,11 @@ import { userVertexStylesAtom, } from "@/core/StateProvider/storageAtoms"; -export default function ResetCustomStylesButton() { +export default function ResetStylesButton() { const setUserVertexStyles = useSetAtom(userVertexStylesAtom); const setUserEdgeStyles = useSetAtom(userEdgeStylesAtom); - function resetCustomStyles() { + function resetStyles() { setUserVertexStyles(new Map()); setUserEdgeStyles(new Map()); } @@ -33,7 +33,7 @@ export default function ResetCustomStylesButton() { @@ -41,22 +41,19 @@ export default function ResetCustomStylesButton() { - Reset Your Styles + Reset to Defaults - This will clear all the styles you've set yourself. Shared - styles will remain. Consider saving first. + This will clear all your node and edge styles, returning + everything to the defaults. Consider saving first. This cannot be undone. Cancel - - Reset Your Styles + + Reset to Defaults diff --git a/packages/graph-explorer/src/routes/Settings/SaveStylesButton.test.tsx b/packages/graph-explorer/src/routes/Settings/SaveStylesButton.test.tsx index 199e22494..a59830f01 100644 --- a/packages/graph-explorer/src/routes/Settings/SaveStylesButton.test.tsx +++ b/packages/graph-explorer/src/routes/Settings/SaveStylesButton.test.tsx @@ -32,7 +32,7 @@ function renderButton() { } function saveButton() { - return screen.getByRole("button", { name: "Save to File" }); + return screen.getByRole("button", { name: "Save" }); } describe("SaveStylesButton", () => { diff --git a/packages/graph-explorer/src/routes/Settings/SaveStylesButton.tsx b/packages/graph-explorer/src/routes/Settings/SaveStylesButton.tsx index d5033ad18..a76b1bac0 100644 --- a/packages/graph-explorer/src/routes/Settings/SaveStylesButton.tsx +++ b/packages/graph-explorer/src/routes/Settings/SaveStylesButton.tsx @@ -1,4 +1,4 @@ -import { DownloadIcon, TriangleAlertIcon } from "lucide-react"; +import { SaveAllIcon, TriangleAlertIcon } from "lucide-react"; import { startTransition, useActionState } from "react"; import { @@ -93,8 +93,8 @@ export default function SaveStylesButton() { onClick={() => dispatch({ type: "save" })} disabled={isPending} > - - Save to File + + Save @@ -38,7 +28,7 @@ export default function SettingsStyles() { Styles Share your node and edge styles with others, or reset them back to - defaults. Your styles and shared styles are tracked separately. + defaults. @@ -49,22 +39,18 @@ export default function SettingsStyles() { - + - @@ -78,59 +64,12 @@ export default function SettingsStyles() { - - - - - - - - - - - + ); } - -function SharedStylesStatus({ - vertexCount, - edgeCount, -}: { - vertexCount: number; - edgeCount: number; -}) { - if (vertexCount === 0 && edgeCount === 0) return null; - - const parts: string[] = []; - if (vertexCount > 0) { - parts.push(`${vertexCount} node`); - } - if (edgeCount > 0) { - parts.push(`${edgeCount} edge`); - } - - return ( -
-
- -
-

- {parts.join(" and ")}{" "} - {vertexCount + edgeCount === 1 ? "type has" : "types have"} shared - styles -

-
- ); -} diff --git a/packages/graph-explorer/src/utils/testing/DbState.ts b/packages/graph-explorer/src/utils/testing/DbState.ts index e13a77ae7..54ad06c76 100644 --- a/packages/graph-explorer/src/utils/testing/DbState.ts +++ b/packages/graph-explorer/src/utils/testing/DbState.ts @@ -25,8 +25,6 @@ import { schemaAtom, type SchemaStorageModel, schemaViewLayoutAtom, - sharedEdgeStylesAtom, - sharedVertexStylesAtom, toEdgeMap, toNodeMap, type Vertex, @@ -59,8 +57,6 @@ export class DbState { activeConfig: RawConfiguration; vertexStyles: Map; edgeStyles: Map; - sharedVertexStyles: Map; - sharedEdgeStyles: Map; graphViewLayout: GraphViewLayout; schemaViewLayout: SchemaViewLayout; @@ -81,8 +77,6 @@ export class DbState { this.vertexStyles = createRandomVertexStyles(); this.edgeStyles = createRandomEdgeStyles(); - this.sharedVertexStyles = createRandomVertexStyles(); - this.sharedEdgeStyles = createRandomEdgeStyles(); this.graphViewLayout = createRandomGraphViewLayout(); this.schemaViewLayout = createRandomSchemaViewLayout(); @@ -199,38 +193,6 @@ export class DbState { return composedStyle; } - /* Shared Styles Helpers */ - - /** - * Adds a style configuration for the vertex type to the shared styles. - * @param vertexType The type of the vertex to add the style to. - * @param style The style configuration to add. - * @returns The fully composed style configuration. - */ - addSharedVertexStyle( - vertexType: VertexType, - style: Omit, - ): VertexStyleStorage { - const composedStyle = { ...style, type: vertexType }; - this.sharedVertexStyles.set(vertexType, composedStyle); - return composedStyle; - } - - /** - * Adds a style configuration for the edge type to the shared styles. - * @param edgeType The type of the edge to add the style to. - * @param style The style configuration to add. - * @returns The fully composed style configuration. - */ - addSharedEdgeStyle( - edgeType: EdgeType, - style: Omit, - ): EdgeStyleStorage { - const composedStyle = { ...style, type: edgeType }; - this.sharedEdgeStyles.set(edgeType, composedStyle); - return composedStyle; - } - /* View Layout Helpers */ /** @@ -273,8 +235,6 @@ export class DbState { // Styling store.set(userVertexStylesAtom, this.vertexStyles); store.set(userEdgeStylesAtom, this.edgeStyles); - store.set(sharedVertexStylesAtom, this.sharedVertexStyles); - store.set(sharedEdgeStylesAtom, this.sharedEdgeStyles); // View Layout store.set(graphViewLayoutAtom, this.graphViewLayout);