Files
bface/src/ui/pages/SettingsPage.jsx
Amer Agovic 94a9f32969 Initial commit: bface library, build fixes, and refreshed docs
- Externalize all @tamagui/* and tamagui subpaths so dist no longer vendors Tamagui.
- Emit TypeScript declarations with vite-plugin-dts; fix package exports types for ui/*.
- Align initEnv with profiles: displayName, brandLogo, api.baseURL, themeColor, uiShell.
- Stabilize tests with Node localStorage file; env tests pass.
- Update README and component docs for services, menus, API client, and development.
2026-04-18 10:43:52 -05:00

144 lines
5.1 KiB
JavaScript

/**
* SettingsPage - Settings page component
* Derived from Page component
* Displays all settings fragment routes as panels
*/
import React, { useMemo } from 'react';
import { Page } from '../components/Page.jsx';
import { YStack, Input, Text } from 'tamagui';
import { useRouter, useRoute } from '../components/Router.jsx';
import { getRootItem } from '../../platform/menu.js';
import { securityService, useSecurityState } from '../../security/runtime/security-service.js';
/**
* SettingsPage Component
* Settings page with icon and title
* Queries Router for all routes under /settings and renders them as panels
*/
export function SettingsPage() {
const router = useRouter();
const route = useRoute();
const securityState = useSecurityState();
// Initialize search query from fragment path if we navigated via a fragment route
const initialSearchQuery = useMemo(() => {
if (route.fragment_path) {
// Extract the last segment from fragment path (e.g., "general" from "/settings/general")
const segments = route.fragment_path.split('/').filter(s => s.length > 0);
if (segments.length > 1) {
return segments[segments.length - 1]; // Return last segment
}
}
return ''; // No fragment, show all
}, [route.fragment_path]);
const [searchQuery, setSearchQuery] = React.useState(initialSearchQuery);
// Update search query when fragment path changes (e.g., navigating between fragments)
React.useEffect(() => {
setSearchQuery(initialSearchQuery);
}, [initialSearchQuery]);
// Get the base path for this settings page (flexible - could be /settings, /config, etc.)
const basePath = useMemo(() => {
// Get current route path and extract the base (e.g., /settings from /settings/general)
if (route.path) {
// If it's a fragment route, use fragment_path, otherwise use path
const currentPath = route.fragment_path || route.path;
// Extract base path (first segment)
const segments = currentPath.split('/').filter(s => s.length > 0);
return segments.length > 0 ? `/${segments[0]}` : '/settings';
}
return '/settings'; // Default fallback
}, [route.path, route.fragment_path]);
// Query Router for all routes under the base path
// Depend on router.currentRoute to trigger recalculation when routes are registered
// (currentRoute depends on routesVersion which changes when routes are registered)
const childRoutes = useMemo(() => {
const allRoutes = router.getRoutes();
const settingsRoot = getRootItem('settings');
const settingsItems = settingsRoot ? Array.from(settingsRoot.items.values()) : [];
const security = {
...securityState,
isPermitted: (rights, resourcePath, options = {}) => securityService.isPermitted(rights, resourcePath, options)
};
const routes = [];
// Find all routes that start with basePath but are not the basePath itself
for (const [path, routeData] of allRoutes.entries()) {
if (path.startsWith(basePath + '/') && path !== basePath) {
// Only include fragment routes
if (routeData.is_fragment) {
const matchingMenuItem = settingsItems.find((item) => item.invoke_target === path);
if (matchingMenuItem && !matchingMenuItem.isRenderable(security)) {
continue;
}
routes.push({
path,
component: routeData.component,
options: routeData.options || {}
});
}
}
}
// Sort by path for consistent ordering
routes.sort((a, b) => a.path.localeCompare(b.path));
return routes;
}, [router, basePath, router.currentRoute, securityState]); // Include router.currentRoute to trigger when routes are registered
// Filter routes based on search query
const filteredRoutes = useMemo(() => {
if (!searchQuery.trim()) {
return childRoutes;
}
const query = searchQuery.toLowerCase();
return childRoutes.filter(route => {
// Extract the last segment of the path as the route name
const segments = route.path.split('/').filter(s => s.length > 0);
const routeName = segments[segments.length - 1] || '';
return routeName.toLowerCase().includes(query);
});
}, [childRoutes, searchQuery]);
return (
<Page
icon="settings"
title="Settings"
headerRight={[
// Add buttons or controls here later
]}
>
<YStack gap="$4" width="100%">
{/* Search bar */}
<Input
placeholder="Search settings..."
value={searchQuery}
onChangeText={setSearchQuery}
size="$4"
/>
{/* Render all child route components */}
{filteredRoutes.length > 0 ? (
filteredRoutes.map((routeItem) => {
const RouteComponent = routeItem.component;
return (
<RouteComponent key={routeItem.path} />
);
})
) : (
<Text fontSize="$4" color="$color" opacity={0.6}>
{searchQuery ? 'No settings found matching your search.' : 'No settings available.'}
</Text>
)}
</YStack>
</Page>
);
}
export default SettingsPage;