From 3286fd54c8d79e72dd5a5325bdd1af060604a6d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Tue, 20 Jan 2026 00:47:26 +0000 Subject: [PATCH] feat: settings data loading per page (#51) --- .../web/app/settings/general/editors/page.tsx | 39 +- .../settings/general/notifications/page.tsx | 26 +- apps/web/app/settings/general/page.tsx | 31 +- apps/web/app/settings/layout.tsx | 43 -- apps/web/app/settings/prompts/page.tsx | 22 +- .../settings/notifications-settings.tsx | 31 +- .../settings/settings-app-sidebar.tsx | 408 ++++++++---------- 7 files changed, 303 insertions(+), 297 deletions(-) diff --git a/apps/web/app/settings/general/editors/page.tsx b/apps/web/app/settings/general/editors/page.tsx index d5deab02b7..2ff417ca11 100644 --- a/apps/web/app/settings/general/editors/page.tsx +++ b/apps/web/app/settings/general/editors/page.tsx @@ -1,5 +1,40 @@ import { EditorsSettings } from '@/components/settings/editors-settings'; +import { StateProvider } from '@/components/state-provider'; +import { fetchUserSettings, listEditors } from '@/lib/http'; -export default function GeneralEditorsPage() { - return ; +export default async function GeneralEditorsPage() { + let initialState = {}; + try { + const [editorsResponse, settingsResponse] = await Promise.all([ + listEditors({ cache: 'no-store' }), + fetchUserSettings({ cache: 'no-store' }), + ]); + const settings = settingsResponse.settings; + initialState = { + editors: { + items: editorsResponse.editors ?? [], + loaded: true, + loading: false, + }, + userSettings: settings + ? { + workspaceId: settings.workspace_id ?? null, + boardId: settings.board_id ?? null, + repositoryIds: settings.repository_ids ?? [], + preferredShell: settings.preferred_shell ?? null, + shellOptions: settingsResponse.shell_options ?? [], + defaultEditorId: settings.default_editor_id ?? null, + loaded: true, + } + : undefined, + }; + } catch { + initialState = {}; + } + + return ( + + + + ); } diff --git a/apps/web/app/settings/general/notifications/page.tsx b/apps/web/app/settings/general/notifications/page.tsx index e1b5e192e7..a8a1363283 100644 --- a/apps/web/app/settings/general/notifications/page.tsx +++ b/apps/web/app/settings/general/notifications/page.tsx @@ -1,5 +1,27 @@ import { NotificationsSettings } from '@/components/settings/notifications-settings'; +import { StateProvider } from '@/components/state-provider'; +import { listNotificationProviders } from '@/lib/http'; -export default function GeneralNotificationsPage() { - return ; +export default async function GeneralNotificationsPage() { + let initialState = {}; + try { + const response = await listNotificationProviders({ cache: 'no-store' }); + initialState = { + notificationProviders: { + items: response.providers ?? [], + events: response.events ?? [], + appriseAvailable: response.apprise_available ?? false, + loaded: true, + loading: false, + }, + }; + } catch { + initialState = {}; + } + + return ( + + + + ); } diff --git a/apps/web/app/settings/general/page.tsx b/apps/web/app/settings/general/page.tsx index 95ac3bcde3..c29eb7a1d3 100644 --- a/apps/web/app/settings/general/page.tsx +++ b/apps/web/app/settings/general/page.tsx @@ -1,5 +1,32 @@ import { GeneralSettings } from '@/components/settings/general-settings'; +import { StateProvider } from '@/components/state-provider'; +import { fetchUserSettings } from '@/lib/http'; -export default function GeneralSettingsPage() { - return ; +export default async function GeneralSettingsPage() { + let initialState = {}; + try { + const response = await fetchUserSettings({ cache: 'no-store' }); + const settings = response.settings; + initialState = { + userSettings: settings + ? { + workspaceId: settings.workspace_id ?? null, + boardId: settings.board_id ?? null, + repositoryIds: settings.repository_ids ?? [], + preferredShell: settings.preferred_shell ?? null, + shellOptions: response.shell_options ?? [], + defaultEditorId: settings.default_editor_id ?? null, + loaded: true, + } + : undefined, + }; + } catch { + initialState = {}; + } + + return ( + + + + ); } diff --git a/apps/web/app/settings/layout.tsx b/apps/web/app/settings/layout.tsx index 21dd14ae28..eb8d235126 100644 --- a/apps/web/app/settings/layout.tsx +++ b/apps/web/app/settings/layout.tsx @@ -6,10 +6,6 @@ import { listEnvironments, listExecutors, listWorkspaces, - fetchUserSettings, - listPrompts, - listNotificationProviders, - listEditors, } from '@/lib/http'; export default function SettingsLayout({ @@ -31,26 +27,13 @@ async function SettingsLayoutServer({ children }: { children: React.ReactNode }) environments, agents, discovery, - userSettings, - promptsResponse, - notificationProviders, - editorsResponse, ] = await Promise.all([ listWorkspaces({ cache: 'no-store' }), listExecutors({ cache: 'no-store' }), listEnvironments({ cache: 'no-store' }), listAgents({ cache: 'no-store' }), listAgentDiscovery({ cache: 'no-store' }), - fetchUserSettings({ cache: 'no-store' }).catch(() => null), - listPrompts({ cache: 'no-store' }).catch(() => ({ prompts: [] })), - listNotificationProviders({ cache: 'no-store' }).catch(() => ({ - providers: [], - apprise_available: false, - events: [], - })), - listEditors({ cache: 'no-store' }).catch(() => ({ editors: [] })), ]); - const settings = userSettings?.settings; initialState = { workspaces: { items: workspaces.workspaces.map((workspace) => ({ @@ -88,32 +71,6 @@ async function SettingsLayoutServer({ children }: { children: React.ReactNode }) environmentsLoaded: true, agentsLoaded: true, }, - prompts: { - items: promptsResponse.prompts ?? [], - loaded: true, - loading: false, - }, - editors: { - items: editorsResponse.editors ?? [], - loaded: true, - loading: false, - }, - notificationProviders: { - items: notificationProviders.providers ?? [], - events: notificationProviders.events ?? [], - appriseAvailable: notificationProviders.apprise_available ?? false, - loaded: true, - loading: false, - }, - userSettings: { - workspaceId: settings?.workspace_id ?? null, - boardId: settings?.board_id ?? null, - repositoryIds: settings?.repository_ids ?? [], - preferredShell: settings?.preferred_shell ?? null, - shellOptions: userSettings?.shell_options ?? [], - defaultEditorId: settings?.default_editor_id ?? null, - loaded: Boolean(settings), - }, }; } catch { initialState = {}; diff --git a/apps/web/app/settings/prompts/page.tsx b/apps/web/app/settings/prompts/page.tsx index 323415dce6..1e3010b5f3 100644 --- a/apps/web/app/settings/prompts/page.tsx +++ b/apps/web/app/settings/prompts/page.tsx @@ -1,5 +1,25 @@ import { PromptsSettings } from '@/components/settings/prompts-settings'; +import { StateProvider } from '@/components/state-provider'; +import { listPrompts } from '@/lib/http'; export default async function PromptsSettingsPage() { - return ; + let initialState = {}; + try { + const response = await listPrompts({ cache: 'no-store' }); + initialState = { + prompts: { + items: response.prompts ?? [], + loaded: true, + loading: false, + }, + }; + } catch { + initialState = {}; + } + + return ( + + + + ); } diff --git a/apps/web/components/settings/notifications-settings.tsx b/apps/web/components/settings/notifications-settings.tsx index bfc9258b41..c92bce919b 100644 --- a/apps/web/components/settings/notifications-settings.tsx +++ b/apps/web/components/settings/notifications-settings.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useMemo, useState, type FormEvent } from 'react'; +import { useMemo, useReducer, useState, useSyncExternalStore, type FormEvent } from 'react'; import { IconBell, IconRefresh } from '@tabler/icons-react'; import { Button } from '@kandev/ui/button'; import { Checkbox } from '@kandev/ui/checkbox'; @@ -105,14 +105,17 @@ export function NotificationsSettings() { const [appriseFormMode, setAppriseFormMode] = useState('create'); const [activeAppriseId, setActiveAppriseId] = useState(null); const [pendingDeletes, setPendingDeletes] = useState>(new Set()); - const [notificationPermission, setNotificationPermission] = useState< - NotificationPermission | 'unsupported' - >(() => { - if (typeof Notification === 'undefined') { - return 'unsupported'; - } - return Notification.permission; - }); + const mounted = useSyncExternalStore( + () => () => undefined, + () => true, + () => false + ); + const [, bumpPermission] = useReducer((value) => value + 1, 0); + const notificationPermission: NotificationPermission | 'unsupported' = !mounted + ? 'default' + : typeof Notification === 'undefined' + ? 'unsupported' + : Notification.permission; const saveRequest = useRequest(async () => { const updates: Array> = []; @@ -215,19 +218,17 @@ export function NotificationsSettings() { const handleRequestPermission = async () => { if (typeof Notification === 'undefined') { - setNotificationPermission('unsupported'); return; } - const result = await Notification.requestPermission(); - setNotificationPermission(result); + await Notification.requestPermission(); + bumpPermission(); }; const handleRefreshPermission = () => { if (typeof Notification === 'undefined') { - setNotificationPermission('unsupported'); return; } - setNotificationPermission(Notification.permission); + bumpPermission(); }; const handleTestNotification = async () => { @@ -237,7 +238,7 @@ export function NotificationsSettings() { let permission = Notification.permission; if (permission === 'default') { permission = await Notification.requestPermission(); - setNotificationPermission(permission); + bumpPermission(); } if (permission !== 'granted') { return; diff --git a/apps/web/components/settings/settings-app-sidebar.tsx b/apps/web/components/settings/settings-app-sidebar.tsx index 822cbc3dc0..494e2eefa3 100644 --- a/apps/web/components/settings/settings-app-sidebar.tsx +++ b/apps/web/components/settings/settings-app-sidebar.tsx @@ -28,6 +28,7 @@ import { SidebarMenuAction, SidebarHeader, } from '@kandev/ui/sidebar'; +import { ScrollArea } from '@kandev/ui/scroll-area'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@kandev/ui/collapsible'; import { useAppStore } from '@/components/state-provider'; import { useAvailableAgents } from '@/hooks/use-available-agents'; @@ -59,245 +60,187 @@ export function SettingsAppSidebar() { - - - Settings - - + + + + Settings + + {/* General */} - - - - - - General - - - - - - Toggle - - - - - - - - - Notifications - - - - - - - - Editors - - - - - - - + + + + + General + + + + + + + + Notifications + + + + + + + + Editors + + + + + {/* Workspaces */} - - - - - - Workspaces - - - {workspaces.length > 0 && ( - <> - - - - Toggle - - - - - {workspaces.map((workspace) => { - const workspacePath = `/settings/workspace/${workspace.id}`; - const boardsPath = `${workspacePath}/boards`; - const repositoriesPath = `${workspacePath}/repositories`; + + + + + Workspaces + + + {workspaces.length > 0 && ( + + {workspaces.map((workspace) => { + const workspacePath = `/settings/workspace/${workspace.id}`; + const boardsPath = `${workspacePath}/boards`; + const repositoriesPath = `${workspacePath}/repositories`; - return ( - - - - {workspace.name} - - - - - - - Repositories - - - - - - - Boards - - - - - - ); - })} - - - - )} - - + return ( + + + + {workspace.name} + + + + + + + Repositories + + + + + + + Boards + + + + + + ); + })} + + )} + {/* Environments */} - - - - - - Environments - - - {environments.length > 0 && ( - <> - - - - Toggle - - - - - {environments.map((env) => ( - - - - {env.name} - - - - ))} - - - - )} - - + + + + + Environments + + + {environments.length > 0 && ( + + {environments.map((env) => ( + + + + {env.name} + + + + ))} + + )} + {/* Executors */} - - - - - - Executors - - - {executors.length > 0 && ( - <> - - - - Toggle - - - - - {executors.map((executor) => ( - - - - {executor.name} - - - - ))} - - - - )} - - + + + + + Executors + + + {executors.length > 0 && ( + + {executors.map((executor) => ( + + + + {executor.name} + + + + ))} + + )} + {/* Agents */} - - - - - - Agents - - - {agents.length > 0 && ( - <> - - - - Toggle - - - - - {agents.flatMap((agent) => - agent.profiles.map((profile) => { - const encodedAgent = encodeURIComponent(agent.name); - const profilePath = `/settings/agents/${encodedAgent}/profiles/${profile.id}`; - const agentLabel = profile.agent_display_name || agent.name; - return ( - - - - {agentLabel} • {profile.name} - - - - ); - }) - )} - - - - )} - - + + + + + Agents + + + {agents.length > 0 && ( + + {agents.flatMap((agent) => + agent.profiles.map((profile) => { + const encodedAgent = encodeURIComponent(agent.name); + const profilePath = `/settings/agents/${encodedAgent}/profiles/${profile.id}`; + const agentLabel = profile.agent_display_name || agent.name; + return ( + + + + + {agentLabel} • {profile.name} + + + + + ); + }) + )} + + )} + {/* Prompts */} @@ -308,9 +251,10 @@ export function SettingsAppSidebar() { - - - + + + + );