Skip to content

Commit b4cc802

Browse files
authored
Merge pull request #1616 from FalkorDB/fix-ai-comments
fix: enhance connection storage handling and improve UI accessibility
2 parents ed3b253 + cf7a507 commit b4cc802

File tree

8 files changed

+88
-41
lines changed

8 files changed

+88
-41
lines changed

app/graph/Chat.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { GraphContext, IndicatorContext, QueryLoadingContext, BrowserSettingsCon
1414
import { EventType } from "../api/chat/route";
1515
import ToastButton from "../components/ToastButton";
1616
import { ShineBorder } from "@/components/ui/shine-border";
17-
import { getConnectionItem, setConnectionItem } from "@/lib/connection-storage";
17+
import { getConnectionItem, setConnectionItem, getConnectionPrefix } from "@/lib/connection-storage";
1818

1919
// Function to get the last maxSavedMessages user messages and all messages in between
2020
const getLastUserMessagesWithContext = (allMessages: Message[], maxUserMessages: number) => {
@@ -109,6 +109,7 @@ export default function Chat({ onClose }: Props) {
109109

110110
// Load messages and cypher only preference for current graph on mount
111111
useEffect(() => {
112+
if (!getConnectionPrefix()) return;
112113
const savedMessages = getConnectionItem(`chat-${graphName}`);
113114
const currentMessages = JSON.parse(savedMessages || "[]");
114115
setMessages(currentMessages);

app/graph/graphInfo.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ export default function GraphInfoPanel({ onClose, customizingLabel, setCustomizi
2323
const [edgesSearch, setEdgesSearch] = useState("");
2424
const [propertyKeysSearch, setPropertyKeysSearch] = useState("");
2525

26-
useEffect(() => { setNodesSearch(""); }, [Labels]);
27-
useEffect(() => { setEdgesSearch(""); }, [Relationships]);
28-
useEffect(() => { setPropertyKeysSearch(""); }, [PropertyKeys]);
26+
useEffect(() => { setNodesSearch(""); }, [Labels, maxItemsForSearch]);
27+
useEffect(() => { setEdgesSearch(""); }, [Relationships, maxItemsForSearch]);
28+
useEffect(() => { setPropertyKeysSearch(""); }, [PropertyKeys, maxItemsForSearch]);
2929

3030
return (
3131
<div aria-disabled={nodesCount === undefined || edgesCount === undefined} data-testid="graphInfoPanel" className={cn(`relative h-full w-full p-2 grid grid-rows-[max-content_max-content_max-content_1fr_1fr_1fr] gap-2`)}>
@@ -248,7 +248,7 @@ export default function GraphInfoPanel({ onClose, customizingLabel, setCustomizi
248248
</div>
249249
<ul className="flex flex-wrap gap-2 p-2 overflow-auto">
250250
{
251-
PropertyKeys && PropertyKeys.filter(key => key.toLowerCase().includes(propertyKeysSearch.toLowerCase())).map((key) => (
251+
PropertyKeys && PropertyKeys.filter(key => key.toLowerCase().includes(propertyKeysSearch.toLowerCase())).sort((a, b) => a.localeCompare(b)).map((key) => (
252252
<li key={key} className="max-w-full">
253253
<Button
254254
title={`MATCH (e) WHERE e.${key} IS NOT NULL RETURN e\nUNION\nMATCH ()-[e]-() WHERE e.${key} IS NOT NULL RETURN e`}

app/loginVerification.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { useSession } from "next-auth/react";
44
import { usePathname, useRouter } from "next/navigation";
55
import { useEffect } from "react";
6-
import { removeConnectionItem } from "@/lib/connection-storage";
76

87
export default function LoginVerification({ children }: { children: React.ReactNode }) {
98

@@ -14,9 +13,8 @@ export default function LoginVerification({ children }: { children: React.ReactN
1413

1514
useEffect(() => {
1615
if (data?.user || data === undefined) return;
17-
// Clear both legacy unscoped and connection-scoped savedContent
16+
// Clear legacy unscoped savedContent (scoped cleanup handled by providers.tsx)
1817
localStorage.removeItem("savedContent");
19-
removeConnectionItem("savedContent");
2018
}, [data]);
2119

2220
useEffect(() => {

app/providers.tsx

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useS
66
import dynamic from "next/dynamic";
77
import { cn, fetchOptions, formatName, getDefaultQuery, getQueryWithLimit, getSSEGraphResult, Panel, prepareArg, securedFetch, Tab, getMemoryUsage, GraphRef, ConnectionType, ConnectionInfo, UDFEntry, UDFEntryWithCode, getMetaStats, HistoryQuery, GraphData, Label, Relationship, InfoLabel, Query, Data, MemoryValue } from "@/lib/utils";
88
import { encryptValue, decryptValue, isCryptoAvailable, isEncrypted } from "@/lib/encryption";
9-
import { getConnectionItem, setConnectionItem, setConnectionPrefix, clearConnectionPrefix, migrateToScopedStorage } from "@/lib/connection-storage";
9+
import { getConnectionItem, setConnectionItem, removeConnectionItem, setConnectionPrefix, clearConnectionPrefix, migrateToScopedStorage } from "@/lib/connection-storage";
1010
import { usePathname, useRouter } from "next/navigation";
1111
import { useToast } from "@/components/ui/use-toast";
1212
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
@@ -69,6 +69,9 @@ function ProvidersWithSession({ children }: { children: React.ReactNode }) {
6969
migrateToScopedStorage();
7070
setPrefixReady(true);
7171
} else {
72+
// Clean up scoped data before clearing the prefix,
73+
// so removeConnectionItem can still resolve the scoped key.
74+
removeConnectionItem("savedContent");
7275
clearConnectionPrefix();
7376
setPrefixReady(false);
7477
}
@@ -134,15 +137,15 @@ function ProvidersWithSession({ children }: { children: React.ReactNode }) {
134137
const [cypherOnly, setCypherOnly] = useState<boolean>(false);
135138
const [udfList, setUdfList] = useState<UDFEntry[]>([]);
136139
const [selectedUdf, setSelectedUdf] = useState<UDFEntryWithCode>();
137-
const [columnWidth, setColumnWidth] = useState<number>(0);
138-
const [rowHeight, setRowHeight] = useState<number>(0);
139-
const [newColumnWidth, setNewColumnWidth] = useState<number>(0);
140-
const [newRowHeight, setNewRowHeight] = useState<number>(0);
141-
const [newRowHeightExpandMultiple, setNewRowHeightExpandMultiple] = useState<number>(0);
142-
const [rowHeightExpandMultiple, setRowHeightExpandMultiple] = useState<number>(0);
140+
const [columnWidth, setColumnWidth] = useState<number>(25);
141+
const [rowHeight, setRowHeight] = useState<number>(40);
142+
const [newColumnWidth, setNewColumnWidth] = useState<number>(25);
143+
const [newRowHeight, setNewRowHeight] = useState<number>(40);
144+
const [newRowHeightExpandMultiple, setNewRowHeightExpandMultiple] = useState<number>(3);
145+
const [rowHeightExpandMultiple, setRowHeightExpandMultiple] = useState<number>(3);
143146
const [showUDF, setShowUDF] = useState<boolean>(true);
144-
const [maxItemsForSearch, setMaxItemsForSearch] = useState<number>(0);
145-
const [newMaxItemsForSearch, setNewMaxItemsForSearch] = useState<number>(0);
147+
const [maxItemsForSearch, setMaxItemsForSearch] = useState<number>(20);
148+
const [newMaxItemsForSearch, setNewMaxItemsForSearch] = useState<number>(20);
146149

147150
const replayTutorial = useCallback(() => {
148151
router.push("/graph");
@@ -478,13 +481,15 @@ function ProvidersWithSession({ children }: { children: React.ReactNode }) {
478481
fetchCount(n);
479482
setLastLimit(limit);
480483

481-
if (!tutorialOpen) {
484+
if (!tutorialOpen && prefixReady) {
482485
setConnectionItem("savedContent", JSON.stringify({ graphName: n, query: q }));
483486
}
484487

485488
const newQueries = handelGetNewQueries(newQuery);
486489

487-
setConnectionItem("query history", JSON.stringify(newQueries));
490+
if (prefixReady) {
491+
setConnectionItem("query history", JSON.stringify(newQueries));
492+
}
488493

489494
setHistoryQuery(prev => ({
490495
...prev,
@@ -503,7 +508,7 @@ function ProvidersWithSession({ children }: { children: React.ReactNode }) {
503508
setIsQueryLoading(false);
504509
}
505510
// eslint-disable-next-line react-hooks/exhaustive-deps
506-
}, [graphName, limit, timeout, fetchInfo, fetchCount, handleCooldown, handelGetNewQueries, showMemoryUsage, captionsKeys, showPropertyKeyPrefix, tutorialOpen]);
511+
}, [graphName, limit, timeout, fetchInfo, fetchCount, handleCooldown, handelGetNewQueries, showMemoryUsage, captionsKeys, showPropertyKeyPrefix, tutorialOpen, prefixReady]);
507512

508513
const graphContext = useMemo(() => ({
509514
graph,
@@ -649,7 +654,8 @@ function ProvidersWithSession({ children }: { children: React.ReactNode }) {
649654
setColumnWidth(parseInt(localStorage.getItem("columnWidth") || "25", 10));
650655
setRowHeight(parseInt(localStorage.getItem("rowHeight") || "40", 10));
651656
setRowHeightExpandMultiple(parseInt(localStorage.getItem("rowHeightExpandMultiple") || "3", 10));
652-
setMaxItemsForSearch(parseInt(localStorage.getItem("maxItemsForSearch") || "20", 10));
657+
const parsedMaxItems = parseInt(localStorage.getItem("maxItemsForSearch") || "20", 10);
658+
setMaxItemsForSearch(Number.isFinite(parsedMaxItems) ? Math.min(Math.max(parsedMaxItems, 10), 50) : 20);
653659
// Decrypt secret key if encrypted, or migrate plain text keys to encrypted format
654660
const storedSecretKey = localStorage.getItem("secretKey") || "";
655661
if (storedSecretKey) {

app/settings/browserSettings.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -375,14 +375,15 @@ export default function BrowserSettings() {
375375
<div className="basis-0 grow flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2 p-2 bg-muted/10 rounded-lg">
376376
<div className="flex flex-col gap-2 flex-1">
377377
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
378-
<label htmlFor="refreshInterval" className="text-lg font-semibold">Refresh Interval</label>
378+
<label id="refreshIntervalLabel" htmlFor="refreshInterval" className="text-lg font-semibold">Refresh Interval</label>
379379
<p className="text-sm text-muted-foreground">
380380
Reload graph info data every {newRefreshInterval} seconds
381381
</p>
382382
</div>
383383
<div className="w-full sm:w-64">
384384
<Slider
385385
id="refreshInterval"
386+
aria-labelledby="refreshIntervalLabel"
386387
className="w-full"
387388
min={5}
388389
max={60}
@@ -399,14 +400,15 @@ export default function BrowserSettings() {
399400
<div className="basis-0 grow flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2 p-2 bg-muted/10 rounded-lg">
400401
<div className="flex flex-col gap-2 flex-1">
401402
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
402-
<label htmlFor="maxItemsForSearch" className="text-lg font-semibold">Max Items For Search</label>
403+
<label id="maxItemsForSearchLabel" htmlFor="maxItemsForSearch" className="text-lg font-semibold">Max Items For Search</label>
403404
<p className="text-sm text-muted-foreground">
404405
Set the maximum number of items before search display.
405406
</p>
406407
</div>
407408
<div className="w-full sm:w-64">
408409
<Slider
409410
id="maxItemsForSearch"
411+
aria-labelledby="maxItemsForSearchLabel"
410412
className="w-full"
411413
min={10}
412414
max={50}
@@ -690,14 +692,15 @@ export default function BrowserSettings() {
690692
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2">
691693
<div className="flex flex-col gap-2 flex-1">
692694
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
693-
<label htmlFor="columnWidth" className="font-semibold">Column Width</label>
695+
<label id="columnWidthLabel" htmlFor="columnWidth" className="font-semibold">Column Width</label>
694696
<p className="text-sm text-muted-foreground">
695697
Set the width of the table columns.
696698
</p>
697699
</div>
698700
<div className="w-full sm:w-64">
699701
<Slider
700702
id="columnWidth"
703+
aria-labelledby="columnWidthLabel"
701704
className="w-full"
702705
type="%"
703706
min={20}
@@ -715,14 +718,15 @@ export default function BrowserSettings() {
715718
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2">
716719
<div className="flex flex-col gap-2 flex-1">
717720
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
718-
<label htmlFor="rowHeight" className="font-semibold">Row Height</label>
721+
<label id="rowHeightLabel" htmlFor="rowHeight" className="font-semibold">Row Height</label>
719722
<p className="text-sm text-muted-foreground">
720723
Set the height of the table rows.
721724
</p>
722725
</div>
723726
<div className="w-full sm:w-64">
724727
<Slider
725728
id="rowHeight"
729+
aria-labelledby="rowHeightLabel"
726730
className="w-full"
727731
type="px"
728732
min={40}
@@ -739,14 +743,15 @@ export default function BrowserSettings() {
739743
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-2">
740744
<div className="flex flex-col gap-2 flex-1">
741745
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
742-
<label htmlFor="rowHeightExpandMultiple" className="font-semibold">Row Height Expand Multiplier</label>
746+
<label id="rowHeightExpandMultipleLabel" htmlFor="rowHeightExpandMultiple" className="font-semibold">Row Height Expand Multiplier</label>
743747
<p className="text-sm text-muted-foreground">
744748
Height multiplier for expanded rows.
745749
</p>
746750
</div>
747751
<div className="w-full sm:w-64">
748752
<Slider
749753
id="rowHeightExpandMultiple"
754+
aria-labelledby="rowHeightExpandMultipleLabel"
750755
className="w-full"
751756
type="px"
752757
min={2}

lib/connection-storage.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,30 @@ export function getConnectionPrefix(): string {
3030
return _prefix;
3131
}
3232

33+
// ── SSR guard ──────────────────────────────────────────────────────
34+
35+
function isBrowser(): boolean {
36+
return typeof window !== "undefined";
37+
}
38+
3339
// ── scoped wrappers ────────────────────────────────────────────────
3440

3541
function prefixed(key: string): string {
3642
return `${_prefix}${key}`;
3743
}
3844

3945
export function getConnectionItem(key: string): string | null {
46+
if (!isBrowser()) return null;
4047
return localStorage.getItem(prefixed(key));
4148
}
4249

4350
export function setConnectionItem(key: string, value: string): void {
51+
if (!isBrowser()) return;
4452
localStorage.setItem(prefixed(key), value);
4553
}
4654

4755
export function removeConnectionItem(key: string): void {
56+
if (!isBrowser()) return;
4857
localStorage.removeItem(prefixed(key));
4958
}
5059

@@ -58,8 +67,13 @@ export function removeConnectionItem(key: string): void {
5867
*/
5968
const SCOPED_KEYS = ["query history", "savedContent"];
6069

70+
/** Prefixes used by graph-specific keys stored as `prefix-graphName`. */
71+
const SCOPED_KEY_PREFIXES = ["chat-", "cypherOnly-"];
72+
6173
export function migrateToScopedStorage(): void {
62-
if (!_prefix) return;
74+
if (!isBrowser() || !_prefix) return;
75+
76+
// Migrate exact-match keys
6377
for (const key of SCOPED_KEYS) {
6478
const scopedKey = prefixed(key);
6579
if (localStorage.getItem(scopedKey) !== null) continue;
@@ -69,4 +83,24 @@ export function migrateToScopedStorage(): void {
6983
localStorage.removeItem(key);
7084
}
7185
}
86+
87+
// Migrate graph-specific prefixed keys (e.g. "chat-myGraph", "cypherOnly-myGraph")
88+
for (let i = 0; i < localStorage.length; i++) {
89+
const key = localStorage.key(i);
90+
if (!key) continue;
91+
// Skip keys that are already scoped (start with the connection prefix)
92+
if (key.startsWith(_prefix)) continue;
93+
for (const p of SCOPED_KEY_PREFIXES) {
94+
if (key.startsWith(p)) {
95+
const scopedKey = prefixed(key);
96+
if (localStorage.getItem(scopedKey) === null) {
97+
localStorage.setItem(scopedKey, localStorage.getItem(key)!);
98+
}
99+
localStorage.removeItem(key);
100+
// Removing a key shifts indices, so decrement to re-check the current index
101+
i--;
102+
break;
103+
}
104+
}
105+
}
72106
}

0 commit comments

Comments
 (0)