-
Notifications
You must be signed in to change notification settings - Fork 20
Nav editor #1630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: canary
Are you sure you want to change the base?
Nav editor #1630
Conversation
…e patterns Remove redundant state tracking (localSpaces, renamedItems, orderChanged) and make localNavigation the single source of truth for staged changes. Simplify change detection by directly comparing localNavigation vs remoteNavigation. Remove unused spaceName parameter, add proper TypeScript type for navPage registration, and replace manual tabOrder commit logic with commitAllSpaceChanges to follow existing spaceStore patterns and reduce code duplication.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughThe changes introduce admin-level navigation editing capabilities with client-side staging, a local state store for navigation items, an API endpoint for persisting navigation configuration, and fallback logic to reconstruct navigation data client-side when server-provided data is unavailable. Additionally, event handling improvements are made to editable text inputs to prevent unintended navigation during editing. Changes
Sequence DiagramsequenceDiagram
participant Admin as Admin User
participant NavUI as Navigation UI
participant Store as Local Store
participant API as API Endpoint
participant DB as Database
rect rgb(200, 220, 240)
Note over Admin,DB: Navigation Editing & Commit Flow
Admin->>NavUI: Toggle Edit Mode
Admin->>NavUI: Create/Rename/Delete/Reorder Items
NavUI->>Store: Update local navigation state
Store-->>NavUI: Reflect changes (uncommitted)
Admin->>NavUI: Click Commit
NavUI->>API: POST signed update request
API->>DB: Validate admin, update navigation_config
DB-->>API: Success
API->>Store: Return response (new spaceIds if created)
Store-->>NavUI: Mark changes as committed
NavUI-->>Admin: Show success toast
end
rect rgb(240, 220, 200)
Note over NavUI,Store: Alternative: Cancel Flow
Admin->>NavUI: Click Cancel
NavUI->>Store: Reset to remote state
Store-->>NavUI: Restore original items
NavUI-->>Admin: Show cancelled toast
end
sequenceDiagram
participant Server as Server (page.tsx)
participant DB as Database
participant Client as NavPageSpace (Client)
participant LocalStore as Local Stores
rect rgb(220, 240, 220)
Note over Server,LocalStore: Space Data Fallback Flow
Server->>DB: Query spaceData by navSlug
alt Space Data Found
DB-->>Server: Return spaceData
Server->>Client: Pass spaceData to component
else Space Data Missing
DB-->>Server: No data
Server->>Server: Load adminIdentityPublicKeys
Server->>Client: Pass null spaceData + adminIdentityPublicKeys
Client->>LocalStore: Query localNavItem & localSpace
LocalStore-->>Client: Return local navigation/space items
Client->>Client: Construct NavPageSpacePageData
Client->>Client: Render with local data + isEditable
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Areas requiring close attention:
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/common/components/organisms/Sidebar.tsx (1)
63-81:navEditModeis not destructured, causing incorrect conditional logic.On line 79,
navEditModeis referenced but it's not included in the destructuring on line 64. This meansnavEditModewill beundefined, and the conditioneditMode && !navEditModewill effectively beeditMode && true, which defeats the purpose of the navigation edit mode check.🔎 Proposed fix
- const { editMode, setEditMode, sidebarEditable, portalRef } = + const { editMode, setEditMode, navEditMode, sidebarEditable, portalRef } = useSidebarContext();
🧹 Nitpick comments (10)
src/common/data/stores/app/index.tsx (1)
70-78: Consider clearing navigation state on logout.The
logoutfunction resets account, homebase, space, checkpoints, and chat stores, but doesn't reset navigation state. If users make local navigation edits, those could persist after logout and potentially be visible to another user on the same device.🔎 Proposed fix
logout: () => { get().account.reset(); get().homebase.clearHomebase(); get().homebase.clearHomebaseTabOrder(); get().space.clear(); get().checkpoints.clearCheckpoints(); get().chat.clearMessages(); + get().navigation.resetNavigationChanges(); localStorage.removeItem(LOCAL_STORAGE_LOCATION); },src/common/components/organisms/NavigationManagement.tsx (3)
54-56: Simplify the nested ternary logic for readability.The current logic is hard to follow with nested ternaries. Consider simplifying:
🔎 Proposed refactor
- // Use local navigation items if available, otherwise fall back to props - // Only use localNavigation if it has items (not empty array from initial state) - const items = localNavigation.length > 0 || navigationItems.length === 0 - ? localNavigation.length > 0 ? localNavigation : navigationItems - : navigationItems; + // Use local navigation items if available, otherwise fall back to props + const items = localNavigation.length > 0 ? localNavigation : navigationItems;The simplified version achieves the same result: use
localNavigationwhen it has items, otherwise usenavigationItems.
58-63: Add cleanup for the debounced function.The debounced function should be cancelled on unmount to prevent potential memory leaks or state updates on unmounted components.
🔎 Proposed fix using useMemo with cleanup
+import { useEffect, useMemo } from "react"; + - const debouncedUpdateOrder = useCallback( - debounce((newOrder: NavigationItem[]) => { + const debouncedUpdateOrder = useMemo( + () => debounce((newOrder: NavigationItem[]) => { updateNavigationOrder(newOrder); }, 300), [updateNavigationOrder] ); + + useEffect(() => { + return () => { + debouncedUpdateOrder.cancel(); + }; + }, [debouncedUpdateOrder]);
107-115: Consider removing the 404-as-success fallback before production.Treating 404 errors as success is useful for local testing, but this should be removed or feature-flagged before production deployment to avoid masking real API issues.
🔎 Suggestion
Consider wrapping this logic in an environment check:
} catch (error: any) { console.error("Failed to commit navigation changes:", error); - // For local testing, still show success if it's just a 404 (API not implemented) - if (error?.response?.status === 404) { + // TODO: Remove this fallback before production + if (process.env.NODE_ENV === 'development' && error?.response?.status === 404) { toast.success("Navigation changes staged locally (API endpoints not yet implemented)"); onCommit?.(); } else { toast.error("Failed to commit navigation changes"); }src/common/components/organisms/Navigation.tsx (2)
559-566: Click handler relies on DOM inspection which can be fragile.Using
target.closest('button[aria-label="Delete item"]')or checkingtagName === 'INPUT'is fragile if the DOM structure changes. Consider usingevent.stopPropagation()directly on the delete button and EditableText wrapper instead.
613-630:hasUncommittedChanges()is called during render.Calling a function that reads state during render (in the JSX condition) could cause unnecessary re-renders if the function implementation changes. Consider storing the result in a variable or using
useMemo.🔎 Proposed fix
+ // Memoize uncommitted changes check + const showCommitButtons = React.useMemo( + () => hasUncommittedChanges(), + [localNavigation, hasUncommittedChanges] + ); {/* Commit/Cancel buttons */} - {hasUncommittedChanges() && !shrunk && ( + {showCommitButtons && !shrunk && ( <div className="flex gap-2 mt-4 pt-4 border-t">src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx (2)
96-107: Consider using a more descriptivespaceNameinstead of rawnavSlug.Currently
spaceNameis set tonavSlugdirectly. For better UX, consider using the navigation item's label if available:🔎 Proposed fix
return { spaceId: localNavItem.spaceId, - spaceName: navSlug, + spaceName: localNavItem.label || navSlug, spaceType: SPACE_TYPES.NAV_PAGE,
110-122: 404 UI is basic but functional.For a client component, this is an appropriate fallback. Consider adding a link back to home for better UX, or using
useRouterto enable programmatic navigation.src/pages/api/navigation/config.ts (1)
25-38: Type guard could be more thorough.The type guard validates the shape but doesn't check if
navigationConfig.itemsarray elements have the required structure when present. Consider adding validation for item structure.🔎 Proposed fix
function isUpdateNavigationConfigRequest( thing: unknown, ): thing is UpdateNavigationConfigRequest { + const isValidNavigationItem = (item: unknown): boolean => { + if (typeof item !== 'object' || item === null) return false; + const i = item as Record<string, unknown>; + return typeof i.id === 'string' && + typeof i.label === 'string' && + typeof i.href === 'string'; + }; + return ( isSignable(thing) && typeof thing["communityId"] === "string" && typeof thing["publicKey"] === "string" && typeof thing["timestamp"] === "string" && thing["navigationConfig"] !== undefined && typeof thing["navigationConfig"] === "object" && (thing["navigationConfig"]["items"] === undefined || - isArray(thing["navigationConfig"]["items"])) + (isArray(thing["navigationConfig"]["items"]) && + thing["navigationConfig"]["items"].every(isValidNavigationItem))) ); }src/common/data/stores/app/navigation/navigationStore.ts (1)
252-263: TODO for space deletion left unimplemented.Deleting navigation items with associated spaces doesn't clean up the space data. This could lead to orphaned space records in the database over time.
Would you like me to open an issue to track implementing the space deletion endpoint?
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
scripts/seed.ts(2 hunks)src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx(4 hunks)src/app/[navSlug]/[[...tabName]]/page.tsx(1 hunks)src/common/components/atoms/editable-text.tsx(1 hunks)src/common/components/organisms/Navigation.tsx(6 hunks)src/common/components/organisms/NavigationManagement.tsx(1 hunks)src/common/components/organisms/Sidebar.tsx(3 hunks)src/common/data/stores/app/index.tsx(3 hunks)src/common/data/stores/app/navigation/navigationStore.ts(1 hunks)src/pages/api/navigation/config.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/pages/api/navigation/config.ts (4)
src/config/systemConfig.ts (1)
NavigationConfig(155-160)src/common/lib/signedFiles.ts (3)
Signable(20-23)isSignable(29-41)validateSignable(43-52)src/common/data/api/requestHandler.ts (2)
NounspaceResponse(9-16)requestHandler(31-51)src/common/data/database/supabase/clients/server.ts (1)
createSupabaseServerClient(7-7)
src/common/data/stores/app/index.tsx (1)
src/common/data/stores/app/navigation/navigationStore.ts (2)
NavigationStore(51-51)createNavigationStoreFunc(58-292)
src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx (2)
src/common/types/spaceData.ts (2)
NavPageSpacePageData(77-83)SPACE_TYPES(7-13)src/common/lib/hooks/useCurrentSpaceIdentityPublicKey.ts (1)
useCurrentSpaceIdentityPublicKey(3-5)
src/common/data/stores/app/navigation/navigationStore.ts (6)
src/common/types/spaceData.ts (1)
SPACE_TYPES(7-13)src/config/systemConfig.ts (2)
NavigationItem(167-175)NavigationConfig(155-160)src/common/data/stores/createStore.ts (2)
StoreSet(51-51)StoreGet(52-52)src/common/data/stores/app/index.tsx (1)
AppStore(44-57)src/common/lib/signedFiles.ts (1)
signSignable(54-62)src/pages/api/space/registry/index.ts (1)
RegisterNewSpaceResponse(114-114)
🔇 Additional comments (9)
src/common/components/atoms/editable-text.tsx (1)
42-56: LGTM!The event propagation handling is correctly implemented. Stopping propagation on both the input click and the display div double-click prevents unintended navigation when editing inline text within navigation items.
src/app/[navSlug]/[[...tabName]]/page.tsx (1)
30-43: LGTM with a minor observation.The client-side fallback pattern is implemented correctly. The dynamic import of
loadSystemConfigand the defensive default foradminIdentityPublicKeysare appropriate.Consider moving the dynamic import to the top of the file as a static import if
loadSystemConfigis always needed, which would improve code predictability and potentially bundle optimization. However, this is a minor point and the current approach works.src/common/data/stores/app/index.tsx (1)
37-41:partializedNavigationStoreis imported but not used in persistence.The
partializedNavigationStoreis imported on line 40, but it's not included in thepartializefunction (lines 109-115). This means navigation state won't persist to localStorage across sessions.If navigation changes should persist locally (for the local-first editing flow), add it to the partialize function. If navigation state is intentionally ephemeral, remove the unused import.
🔎 Proposed fix (if persistence is needed)
partialize: (state: AppStore) => ({ account: partializedAccountStore(state), homebase: partializedHomebaseStore(state), space: partializedSpaceStore(state), checkpoints: partializedCheckpointStore(state), chat: partializedChatStore(state), + navigation: partializedNavigationStore(state), }),Also applies to: 109-115
src/common/components/organisms/NavigationManagement.tsx (1)
147-180: LGTM!The Reorder.Group implementation is correct. Keys are properly set using
item.id, and the drag-and-drop reordering integrates well with the debounced update callback.src/common/components/organisms/Navigation.tsx (1)
634-655: JSX structure mismatch:<ul>tag opened but items conditionally rendered.Lines 634-655 map navigation items inside a
<ul>that starts at line 634, but the closing</ul>is at line 698, after the conditional edit button and otherNavButton/NavItemcomponents. This is correct structurally, but ensure the edit button (lines 688-697) should be inside the<ul>as it usesNavButtonwhich renders<li>.Also applies to: 656-698
src/app/[navSlug]/[[...tabName]]/NavPageSpace.tsx (1)
69-108: Fallback logic looks correct for uncommitted navigation items.The fallback mechanism properly checks local stores when server data is unavailable, enabling preview of uncommitted navigation items. The dependency array is comprehensive.
src/pages/api/navigation/config.ts (1)
87-96: Admin key check is correct and secure.The authorization properly validates that the signer's public key is in the admin list before allowing updates.
src/common/data/stores/app/navigation/navigationStore.ts (2)
72-104: Good separation of concerns for navigation item creation.The implementation correctly handles the optional space creation alongside navigation item creation, maintaining consistency between the navigation and space stores.
142-159:hasUncommittedChangescomparison is correct but could be simplified.The logic correctly compares length, order, and key properties. Consider using
isEqualfrom lodash for a more maintainable comparison, though the current explicit approach has the benefit of being clear about which fields matter.
| * Requires: | ||
| * - NEXT_PUBLIC_SUPABASE_URL | ||
| * - SUPABASE_SERVICE_ROLE_KEY | ||
| * - SUPABASE_SERVICE_KEY |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for references to both variable names
echo "=== Searching for SUPABASE_SERVICE_ROLE_KEY ==="
rg "SUPABASE_SERVICE_ROLE_KEY" -n --type-list | head -20
echo -e "\n=== Searching for SUPABASE_SERVICE_KEY ==="
rg "SUPABASE_SERVICE_KEY" -n --type-list | head -20
# Look for .env files
echo -e "\n=== Checking for .env files ==="
fd -e env -o -name ".env*" | head -20
# Check the seed.ts file to understand context
echo -e "\n=== Content of scripts/seed.ts (lines 15-75) ==="
wc -l scripts/seed.tsRepository: blankdotspace/space-system
Length of output: 1292
🏁 Script executed:
#!/bin/bash
# Search for references to both variable names
echo "=== Searching for SUPABASE_SERVICE_ROLE_KEY ==="
rg "SUPABASE_SERVICE_ROLE_KEY" --type ts --type js --type md -n 2>/dev/null | head -30
echo -e "\n=== Searching for SUPABASE_SERVICE_KEY ==="
rg "SUPABASE_SERVICE_KEY" --type ts --type js --type md -n 2>/dev/null | head -30
# Look for .env files
echo -e "\n=== Checking for .env files ==="
fd "\.env" --type f 2>/dev/null
# Check the seed.ts file content around the changes
echo -e "\n=== Content of scripts/seed.ts (lines 15-75) ==="
sed -n '15,75p' scripts/seed.tsRepository: blankdotspace/space-system
Length of output: 3106
Incomplete environment variable migration: update src/config/loaders/runtimeLoader.ts and documentation.
The rename from SUPABASE_SERVICE_ROLE_KEY to SUPABASE_SERVICE_KEY is inconsistently applied. The seed script, tests, and production code (src/common/data/database/supabase/clients/server.ts) use the new name, but src/config/loaders/runtimeLoader.ts still references the old name. This will cause runtime failures.
Update:
src/config/loaders/runtimeLoader.ts:17to useSUPABASE_SERVICE_KEYdocs/GETTING_STARTED.md:72anddocs/SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md:466to reflect the new variable name
🤖 Prompt for AI Agents
In scripts/seed.ts around line 20 and specifically in
src/config/loaders/runtimeLoader.ts at line 17, the environment variable rename
was applied inconsistently: runtimeLoader.ts still references
SUPABASE_SERVICE_ROLE_KEY while the rest of the code uses SUPABASE_SERVICE_KEY;
update runtimeLoader.ts to read SUPABASE_SERVICE_KEY instead of
SUPABASE_SERVICE_ROLE_KEY. Also update docs/GETTING_STARTED.md at line 72 and
docs/SYSTEMS/CONFIGURATION/ARCHITECTURE_OVERVIEW.md at line 466 to replace any
occurrences of SUPABASE_SERVICE_ROLE_KEY with SUPABASE_SERVICE_KEY so
documentation matches runtime usage.
| React.useEffect(() => { | ||
| loadNavigation(systemConfig.navigation); | ||
| }, [systemConfig.navigation, loadNavigation]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find Navigation component and check how it receives systemConfig
find . -name "Navigation.tsx" -type f | head -5Repository: blankdotspace/space-system
Length of output: 118
🏁 Script executed:
#!/bin/bash
# Read the Navigation.tsx file to understand the useEffect and how systemConfig is used
cat -n ./src/common/components/organisms/Navigation.tsx | head -200Repository: blankdotspace/space-system
Length of output: 9026
🏁 Script executed:
#!/bin/bash
# Continue reading to understand how systemConfig is passed to Navigation component
cat -n ./src/common/components/organisms/Navigation.tsx | sed -n '200,300p'Repository: blankdotspace/space-system
Length of output: 3882
🏁 Script executed:
#!/bin/bash
# Find where Navigation component is used and how systemConfig is passed to it
rg -n "Navigation" --type=tsx -B 2 -A 2 | grep -A 5 -B 5 "systemConfig" | head -50Repository: blankdotspace/space-system
Length of output: 97
🏁 Script executed:
#!/bin/bash
# Find where Navigation component is used and how systemConfig is passed to it
rg -n "<Navigation" -A 5 | head -60Repository: blankdotspace/space-system
Length of output: 2659
🏁 Script executed:
#!/bin/bash
# Check the Sidebar component to see where systemConfig comes from
cat -n ./src/common/components/organisms/Sidebar.tsx | head -150Repository: blankdotspace/space-system
Length of output: 3217
🏁 Script executed:
#!/bin/bash
# Find where Sidebar is used and how systemConfig is obtained
rg -n "<Sidebar" -A 3 | head -50Repository: blankdotspace/space-system
Length of output: 2032
🏁 Script executed:
#!/bin/bash
# Check ClientSidebarWrapper to see how systemConfig is obtained
cat -n ./src/common/components/organisms/ClientSidebarWrapper.tsxRepository: blankdotspace/space-system
Length of output: 704
🏁 Script executed:
#!/bin/bash
# Find where ClientSidebarWrapper is used and how systemConfig is passed
rg -n "ClientSidebarWrapper" -B 5 -A 5Repository: blankdotspace/space-system
Length of output: 5342
🏁 Script executed:
#!/bin/bash
# Check layout.tsx to see how systemConfig is loaded
cat -n ./src/app/layout.tsx | head -160Repository: blankdotspace/space-system
Length of output: 5726
🏁 Script executed:
#!/bin/bash
# Check how loadSystemConfig is defined
rg -n "loadSystemConfig" -A 10 | head -80Repository: blankdotspace/space-system
Length of output: 4541
🏁 Script executed:
#!/bin/bash
# Check the actual loadSystemConfig implementation
cat -n ./src/config/index.ts | head -100Repository: blankdotspace/space-system
Length of output: 4224
🏁 Script executed:
#!/bin/bash
# Check the navigationStore to understand what loadNavigation does
cat -n ./src/common/data/stores/app/navigation/navigationStore.ts | head -100Repository: blankdotspace/space-system
Length of output: 4118
🏁 Script executed:
#!/bin/bash
# Check the RuntimeConfigLoader to understand if it returns a new object each time
cat -n ./src/config/loaders/runtimeLoader.ts | head -150Repository: blankdotspace/space-system
Length of output: 3238
🏁 Script executed:
#!/bin/bash
# Check if there are any other places where Navigation might receive a newly created systemConfig
rg -n "systemConfig\." ./src/app/layout.tsx | head -20Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Check how many times systemConfig could be regenerated during a page lifecycle
# Look for any callbacks or effects that might reload config
rg -n "loadSystemConfig\|revalidate" ./src/app/layout.tsxRepository: blankdotspace/space-system
Length of output: 52
Object reference dependency in useEffect may cause unnecessary re-runs.
The effect depends on systemConfig.navigation, which could be a new object reference even when its contents are identical. This causes the effect to re-run and reset localNavigation, potentially discarding uncommitted user changes.
Consider depending on the actual navigation items instead of the object itself, or memoize the navigation config to ensure referential stability:
React.useEffect(() => {
loadNavigation(systemConfig.navigation);
}, [systemConfig.navigation?.items, loadNavigation]);Or use useMemo to stabilize the reference:
const stableNavigation = React.useMemo(
() => systemConfig.navigation,
[systemConfig.navigation?.items]
);
React.useEffect(() => {
loadNavigation(stableNavigation);
}, [stableNavigation, loadNavigation]);🤖 Prompt for AI Agents
In src/common/components/organisms/Navigation.tsx around lines 146-148, the
useEffect depends on systemConfig.navigation (an object) which may get a new
reference and cause unnecessary re-runs and reset localNavigation; update the
dependency to a stable value by either depending on the actual navigation items
(e.g., systemConfig.navigation?.items) or memoizing the navigation config with
useMemo keyed by navigation items and then use that stable value in the effect;
adjust the useEffect dependency array to use the chosen stable reference and
ensure loadNavigation receives the memoized/stable navigation.
| const debouncedUpdateOrder = React.useCallback( | ||
| debounce((newOrder: NavigationItem[]) => { | ||
| updateNavigationOrder(newOrder); | ||
| }, 300), | ||
| [updateNavigationOrder] | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing cleanup for debounced function.
The debounce function creates a timer that should be cancelled on unmount to prevent memory leaks and stale updates. Additionally, the dependency array is missing from React.useCallback.
🔎 Proposed fix
- // Debounced reorder handler
- const debouncedUpdateOrder = React.useCallback(
- debounce((newOrder: NavigationItem[]) => {
- updateNavigationOrder(newOrder);
- }, 300),
- [updateNavigationOrder]
- );
+ // Debounced reorder handler
+ const debouncedUpdateOrder = React.useMemo(
+ () => debounce((newOrder: NavigationItem[]) => {
+ updateNavigationOrder(newOrder);
+ }, 300),
+ [updateNavigationOrder]
+ );
+
+ // Cleanup debounce on unmount
+ React.useEffect(() => {
+ return () => {
+ debouncedUpdateOrder.cancel();
+ };
+ }, [debouncedUpdateOrder]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const debouncedUpdateOrder = React.useCallback( | |
| debounce((newOrder: NavigationItem[]) => { | |
| updateNavigationOrder(newOrder); | |
| }, 300), | |
| [updateNavigationOrder] | |
| ); | |
| // Debounced reorder handler | |
| const debouncedUpdateOrder = React.useMemo( | |
| () => debounce((newOrder: NavigationItem[]) => { | |
| updateNavigationOrder(newOrder); | |
| }, 300), | |
| [updateNavigationOrder] | |
| ); | |
| // Cleanup debounce on unmount | |
| React.useEffect(() => { | |
| return () => { | |
| debouncedUpdateOrder.cancel(); | |
| }; | |
| }, [debouncedUpdateOrder]); |
🤖 Prompt for AI Agents
In src/common/components/organisms/Navigation.tsx around lines 159-164, the
debouncedUpdateOrder created via debounce needs proper cleanup and stable
dependencies: move the debounced function into a ref (e.g. const debouncedRef =
useRef(debounce(...))) or create it inside useCallback but ensure debounce is
created only once, then add a useEffect that on unmount calls
debouncedRef.current.cancel() (or the debounce library's cancel/clear method) to
avoid timers leaking; also ensure the useCallback/useRef setup includes
updateNavigationOrder in its dependency handling (either rebuild the debounced
function when updateNavigationOrder changes or use a ref to the latest
updateNavigationOrder) so the dependency array is correct and linter-safe.
| // TODO: For local testing, skip space registration if API fails | ||
| let actualSpaceId = space.spaceId; | ||
| try { | ||
| const { data: registrationResponse } = await axiosBackend.post<RegisterNewSpaceResponse>("/api/space/registry", signedRegistration); | ||
|
|
||
| if (registrationResponse.result !== "success" || !registrationResponse.value?.spaceId) { | ||
| throw new Error(`Failed to register space for nav item ${space.itemId}`); | ||
| } | ||
|
|
||
| actualSpaceId = registrationResponse.value!.spaceId; | ||
|
|
||
| // Update the navigation item and space store with the actual spaceId from the API | ||
| set((draft) => { | ||
| const itemIndex = draft.navigation.localNavigation.findIndex( | ||
| (item) => item.id === space.itemId | ||
| ); | ||
| if (itemIndex !== -1) { | ||
| draft.navigation.localNavigation[itemIndex].spaceId = actualSpaceId; | ||
| } | ||
| // Move the localSpaces entry to the new spaceId (if spaceId changed) | ||
| if (space.spaceId !== actualSpaceId && draft.space.localSpaces[space.spaceId]) { | ||
| draft.space.localSpaces[actualSpaceId] = draft.space.localSpaces[space.spaceId]; | ||
| draft.space.localSpaces[actualSpaceId].id = actualSpaceId; | ||
| delete draft.space.localSpaces[space.spaceId]; | ||
| } else if (space.spaceId === actualSpaceId) { | ||
| // Ensure the spaceId is set correctly | ||
| draft.space.localSpaces[actualSpaceId].id = actualSpaceId; | ||
| } | ||
| }, "commitNavigationChanges-updateSpaceId"); | ||
|
|
||
| // Use spaceStore's commitAllSpaceChanges to commit the tab order and sync state | ||
| // This handles tab commits, deletions, and order updates, and syncs remoteSpaces | ||
| await get().space.commitAllSpaceChanges(actualSpaceId); | ||
| } catch (spaceError: any) { | ||
| // For local testing, continue even if space registration fails | ||
| console.warn(`Space registration failed for nav item ${space.itemId} - continuing with local spaceId:`, spaceError); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silent failure mode could lead to inconsistent state.
When space registration fails (lines 246-249), the code logs a warning and continues. However, the navigation item still has the local spaceId which won't exist in the database. On page reload, the navigation will reference a non-existent space.
Consider either:
- Removing the
spaceIdfrom the navigation item if registration fails - Not committing the navigation config if any space registration fails
- Clearly documenting this as "local-only" preview behavior
🔎 Proposed fix (option 1)
} catch (spaceError: any) {
// For local testing, continue even if space registration fails
console.warn(`Space registration failed for nav item ${space.itemId} - continuing with local spaceId:`, spaceError);
+ // Remove spaceId from navigation item since it won't exist in database
+ set((draft) => {
+ const itemIndex = draft.navigation.localNavigation.findIndex(
+ (item) => item.id === space.itemId
+ );
+ if (itemIndex !== -1) {
+ delete draft.navigation.localNavigation[itemIndex].spaceId;
+ }
+ // Clean up local space entry
+ delete draft.space.localSpaces[space.spaceId];
+ }, "commitNavigationChanges-cleanupFailedSpace");
}🤖 Prompt for AI Agents
In src/common/data/stores/app/navigation/navigationStore.ts around lines
213-249, the catch block currently logs and continues which leaves the nav item
pointing to a local-only spaceId that won't exist remotely; update the catch to
remove the stale spaceId from the navigation item and clean up localSpaces
instead of silently continuing: find the navigation item by space.itemId and set
its spaceId to undefined/null (or remove the property), delete any
draft.space.localSpaces[space.spaceId] entry, and avoid calling
space.commitAllSpaceChanges for that failed space; optionally add a clear
"local-only" flag or comment to the entry so UI can show preview-only state.
| // Step 3: Update navigation config in database | ||
| const unsignedRequest = { | ||
| communityId, | ||
| navigationConfig: { | ||
| items: state.localNavigation, | ||
| }, | ||
| publicKey, | ||
| timestamp: moment().toISOString(), | ||
| }; | ||
|
|
||
| const signedRequest = signSignable( | ||
| unsignedRequest, | ||
| currentIdentity.rootKeys.privateKey | ||
| ); | ||
|
|
||
| // Update navigation config in database | ||
| await axiosBackend.put("/api/navigation/config", signedRequest); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Navigation config update doesn't include all NavigationConfig fields.
The update request only sends items but NavigationConfig also has logoTooltip, showMusicPlayer, and showSocials. If these exist in the current config, they'll be overwritten with undefined.
Based on the type definition in systemConfig.ts (lines 154-159), ensure the full config is preserved:
🔎 Proposed fix
// Step 3: Update navigation config in database
+ // Preserve other navigation config fields (logoTooltip, showMusicPlayer, showSocials)
+ // by only updating the items array
const unsignedRequest = {
communityId,
navigationConfig: {
items: state.localNavigation,
+ // TODO: Include existing config values or fetch them from server
+ // For now, only items are updated - other fields should be preserved server-side
},
publicKey,
timestamp: moment().toISOString(),
};Alternatively, modify the API to merge rather than replace the entire config.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/common/data/stores/app/navigation/navigationStore.ts around lines 265 to
281, the unsignedRequest only includes navigationConfig.items which will cause
other NavigationConfig fields (logoTooltip, showMusicPlayer, showSocials) to be
lost; fix by building navigationConfig for the request from the full current
config (merge existing config fields with the updated items) so the
unsignedRequest includes items plus logoTooltip, showMusicPlayer, and
showSocials (or any other fields present) before signing and sending;
alternatively, if preferred, change the API to accept a partial update and merge
server-side rather than replacing the entire config.
| export function partializedNavigationStore(state: AppStore) { | ||
| return { | ||
| // Only persist minimal state if needed | ||
| // Navigation config is loaded from SystemConfig on app start | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for partializedNavigationStore usage and verify the store configuration
rg -n "partializedNavigationStore" -A 10 -B 10Repository: blankdotspace/space-system
Length of output: 2980
🏁 Script executed:
#!/bin/bash
# Check the navigationStore file structure and imports
fd -e ts -e tsx navigationStore | head -5 | xargs -I {} sh -c 'echo "File: {}"; wc -l {}; echo "---"'Repository: blankdotspace/space-system
Length of output: 197
🏁 Script executed:
#!/bin/bash
# Find how partializedNavigationStore is used in the persist configuration
rg -n "partializedNavigationStore\|persist.*navigation" src/common/data/stores/app/index.tsx -A 3 -B 3Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Look for the full persist middleware configuration
grep -n "persist\|partialize" src/common/data/stores/app/index.tsx | head -20Repository: blankdotspace/space-system
Length of output: 985
🏁 Script executed:
#!/bin/bash
# Get the exact persist configuration around lines 109-116
sed -n '109,116p' src/common/data/stores/app/index.tsxRepository: blankdotspace/space-system
Length of output: 408
🏁 Script executed:
#!/bin/bash
# Check if navigationStore is included in the partialize config
sed -n '100,125p' src/common/data/stores/app/index.tsxRepository: blankdotspace/space-system
Length of output: 835
Remove unused partializedNavigationStore function.
The function is imported in the app store configuration but never used in the persist middleware's partialize function (lines 109-114). Since navigation state is intentionally not persisted (loaded from SystemConfig on app start), either remove the unused partializedNavigationStore function and its import, or add navigation: partializedNavigationStore(state) to the partialize object if persistence is intended.
🤖 Prompt for AI Agents
In src/common/data/stores/app/navigation/navigationStore.ts around lines
294-299, the exported partializedNavigationStore function is unused (imported in
app store config but not included in the persist partialize) and navigation
state is intentionally not persisted; either delete this function and remove its
import from the app store configuration, or if you intended to persist
navigation add navigation: partializedNavigationStore(state) to the persist
partialize object (lines ~109-114) and keep the export — choose one and update
the corresponding import/usage accordingly.
| export type UnsignedUpdateNavigationConfigRequest = { | ||
| communityId: string; | ||
| navigationConfig: NavigationConfig; | ||
| publicKey: string; | ||
| timestamp: string; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing timestamp validation to prevent replay attacks.
The request includes a timestamp field but it's never validated. An attacker could replay a previously captured signed request indefinitely. Consider adding timestamp freshness validation.
🔎 Proposed fix
Add timestamp validation after signature verification:
if (!validateSignable(updateRequest)) {
res.status(400).json({
result: "error",
error: {
message: "Invalid signature",
},
});
return;
}
+ // Validate timestamp freshness (e.g., within 5 minutes)
+ const requestTime = new Date(updateRequest.timestamp).getTime();
+ const now = Date.now();
+ const maxAge = 5 * 60 * 1000; // 5 minutes
+ if (isNaN(requestTime) || Math.abs(now - requestTime) > maxAge) {
+ res.status(400).json({
+ result: "error",
+ error: {
+ message: "Request timestamp is invalid or expired",
+ },
+ });
+ return;
+ }Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/pages/api/navigation/config.ts around lines 14 to 19 the
UnsignedUpdateNavigationConfigRequest includes a timestamp but there is no
freshness check, enabling replay attacks; after verifying the signature parse
and validate the timestamp (accept ISO or epoch), compute the age against server
time, reject requests older or too-future skew beyond a configured window (e.g.,
5 minutes) with an appropriate 4xx response, centralize the allowed skew value
as a constant and add robust parsing/error handling for malformed timestamps so
only fresh, well-formed requests proceed.
| const { error: updateError } = await supabase | ||
| .from("community_configs") | ||
| .update({ | ||
| navigation_config: updateRequest.navigationConfig, | ||
| updated_at: new Date().toISOString(), | ||
| }) | ||
| .eq("community_id", updateRequest.communityId) | ||
| .eq("is_published", true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update operation lacks row-level check for successful update.
The update query doesn't verify that a row was actually updated. If the community_id and is_published combination doesn't match any row (race condition where config was unpublished), the update silently succeeds without modifying anything.
🔎 Proposed fix
// Update the navigation_config column
- const { error: updateError } = await supabase
+ const { error: updateError, count } = await supabase
.from("community_configs")
.update({
navigation_config: updateRequest.navigationConfig,
updated_at: new Date().toISOString(),
})
.eq("community_id", updateRequest.communityId)
- .eq("is_published", true);
+ .eq("is_published", true)
+ .select();
- if (updateError) {
+ if (updateError || count === 0) {
console.error("Error updating navigation config:", updateError);
res.status(500).json({
result: "error",
error: {
- message: updateError.message,
+ message: updateError?.message || "No matching config found to update",
},
});
return;
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/pages/api/navigation/config.ts around lines 99 to 106, the UPDATE call
doesn't verify that a row was actually modified; change the code to request the
updated rows (e.g. include a .select() or use Supabase's returning behavior) and
then check the response: if updateError is set or the returned data array is
empty (no rows updated), treat this as a failure (log and return an error/409 or
throw) instead of proceeding silently; ensure the handler surface returns a
clear error when zero rows are affected so callers can handle the
race/unpublished case.
Summary by CodeRabbit
New Features
Bug Fixes
Chores
✏️ Tip: You can customize this high-level summary in your review settings.