Refine shared shell, router, and service worker behavior
This commit is contained in:
@@ -2,13 +2,12 @@
|
||||
* Service Worker Registration
|
||||
*/
|
||||
|
||||
import { isElectronHost } from './host.js';
|
||||
import { isElectronHost, isTauriHost } from './host.js';
|
||||
import { getConfig, isDevelopment, CONFIG_KEYS } from './env.js';
|
||||
|
||||
const SW_PATH = '/sw.js';
|
||||
const SW_SCOPE = '/';
|
||||
const DEV_SW_RESET_KEY = '__bface_dev_sw_reset__';
|
||||
|
||||
/**
|
||||
* Clear all caches
|
||||
*/
|
||||
@@ -127,8 +126,9 @@ export async function clearPWACache() {
|
||||
* Register service worker
|
||||
*/
|
||||
export async function registerServiceWorker() {
|
||||
if (isElectronHost()) {
|
||||
console.log('[SW] Skipping service worker registration in Electron host');
|
||||
if (isElectronHost() || isTauriHost()) {
|
||||
await unregisterAllServiceWorkers();
|
||||
console.log('[SW] Skipping service worker registration in desktop host');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -185,8 +185,9 @@ export async function registerServiceWorker() {
|
||||
* In development, stale service workers can break Vite module loading by
|
||||
* intercepting /@fs and related requests. Clear them once and reload cleanly.
|
||||
*/
|
||||
export async function ensureDevelopmentServiceWorkerState() {
|
||||
if (isElectronHost()) {
|
||||
export async function resetServiceWorkers() {
|
||||
if (isElectronHost() || isTauriHost()) {
|
||||
await unregisterAllServiceWorkers();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -214,11 +215,39 @@ export async function ensureDevelopmentServiceWorkerState() {
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function getServiceWorkerStatus() {
|
||||
if (isElectronHost() || isTauriHost()) {
|
||||
return 'Desktop Disabled';
|
||||
}
|
||||
|
||||
if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {
|
||||
return 'Not Supported';
|
||||
}
|
||||
|
||||
const devHost = await getConfig(CONFIG_KEYS.DEV_HOST, isDevelopment());
|
||||
try {
|
||||
if (devHost) {
|
||||
const registration = await navigator.serviceWorker.getRegistration();
|
||||
if (registration) {
|
||||
registration.update();
|
||||
return 'Active';
|
||||
}
|
||||
return 'Development Disabled';
|
||||
}
|
||||
|
||||
await navigator.serviceWorker.ready;
|
||||
return 'Active';
|
||||
} catch (error) {
|
||||
console.warn('[SW] Failed to resolve service worker status:', error);
|
||||
return 'Not Supported';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister service worker
|
||||
*/
|
||||
export async function unregisterServiceWorker() {
|
||||
if (isElectronHost()) {
|
||||
if (isElectronHost() || isTauriHost()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ export function SecurityAdminPage() {
|
||||
</Paragraph>
|
||||
</YStack>
|
||||
), 'No permits registered.')
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
+19
-26
@@ -6,7 +6,7 @@
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import { TamaguiProvider, Theme, createTamagui, YStack } from 'tamagui';
|
||||
import { registerServiceWorker, clearPWACache } from '../platform/sw-register.js';
|
||||
import { clearPWACache, getServiceWorkerStatus } from '../platform/sw-register.js';
|
||||
import { getProvider } from '../platform/storage.js';
|
||||
import * as apiClient from '../platform/api.js';
|
||||
import * as storageModuleRef from '../platform/storage.js';
|
||||
@@ -68,7 +68,15 @@ function getCachedTamaguiConfig(styleThemeName) {
|
||||
// App Component
|
||||
// ============================================================================
|
||||
|
||||
function App({ onInit, renderBootScreen = null, showInitialBootSplash = false, initialProfile = null }) {
|
||||
function App({
|
||||
onInit,
|
||||
renderBootScreen = null,
|
||||
showInitialBootSplash = false,
|
||||
initialProfile = null,
|
||||
initialThemeMode = THEME_MODES.SYSTEM,
|
||||
initialStyleThemeName = DEFAULT_STYLE_THEME,
|
||||
initialThemePreferencesLoaded = false
|
||||
}) {
|
||||
// App state
|
||||
const [swStatus, setSwStatus] = useState('Checking...');
|
||||
const [storageBackend, setStorageBackend] = useState('localStorage');
|
||||
@@ -80,10 +88,10 @@ function App({ onInit, renderBootScreen = null, showInitialBootSplash = false, i
|
||||
const [bootModeOverride, setBootModeOverride] = useState(null);
|
||||
|
||||
// Theme state
|
||||
const [themeMode, setThemeModeState] = useState(THEME_MODES.SYSTEM);
|
||||
const [themeMode, setThemeModeState] = useState(initialThemeMode);
|
||||
const [systemScheme, setSystemScheme] = useState(getSystemThemeMode());
|
||||
const [styleThemeName, setStyleThemeName] = useState(DEFAULT_STYLE_THEME);
|
||||
const [themePreferencesLoaded, setThemePreferencesLoaded] = useState(false);
|
||||
const [styleThemeName, setStyleThemeName] = useState(() => normalizeStyleThemeName(initialStyleThemeName));
|
||||
const [themePreferencesLoaded, setThemePreferencesLoaded] = useState(initialThemePreferencesLoaded);
|
||||
const securityState = useSecurityState();
|
||||
|
||||
// Initialize theme manager
|
||||
@@ -111,6 +119,10 @@ function App({ onInit, renderBootScreen = null, showInitialBootSplash = false, i
|
||||
|
||||
// Load theme preferences from storage on mount
|
||||
useEffect(() => {
|
||||
if (initialThemePreferencesLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
async function loadThemePreferences() {
|
||||
try {
|
||||
// Load theme mode (light/dark/system)
|
||||
@@ -132,7 +144,7 @@ function App({ onInit, renderBootScreen = null, showInitialBootSplash = false, i
|
||||
}
|
||||
|
||||
loadThemePreferences();
|
||||
}, []);
|
||||
}, [initialThemePreferencesLoaded]);
|
||||
|
||||
// Listen for system theme changes using platform-agnostic compat layer
|
||||
useEffect(() => {
|
||||
@@ -285,26 +297,7 @@ function App({ onInit, renderBootScreen = null, showInitialBootSplash = false, i
|
||||
appLogger.warn('Menu service not available');
|
||||
}
|
||||
|
||||
// Update service worker status if available
|
||||
const devHost = await getConfig(CONFIG_KEYS.DEV_HOST, isDevelopment());
|
||||
if ('serviceWorker' in navigator) {
|
||||
try {
|
||||
if (devHost) {
|
||||
const registration = await navigator.serviceWorker.getRegistration();
|
||||
setSwStatus(registration ? 'Active' : 'Development Disabled');
|
||||
if (registration) {
|
||||
registration.update();
|
||||
}
|
||||
} else {
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
setSwStatus('Active');
|
||||
}
|
||||
} catch (error) {
|
||||
setSwStatus('Not Supported');
|
||||
}
|
||||
} else {
|
||||
setSwStatus('Not Supported');
|
||||
}
|
||||
setSwStatus(await getServiceWorkerStatus());
|
||||
|
||||
setInitialized(true);
|
||||
initTrace.end({
|
||||
|
||||
@@ -15,12 +15,14 @@ import { SideBar } from './SideBar.jsx';
|
||||
* @param {React.ReactNode} props.children - Content to render inside shell
|
||||
*/
|
||||
export function DashboardShell(props) {
|
||||
const { secondaryStyle = 'inline', ...shellProps } = props;
|
||||
|
||||
return (
|
||||
<EmptyShell {...props} initialLeftWidth={250}>
|
||||
<SideBar placement="leftSide">
|
||||
<EmptyShell {...shellProps} initialLeftWidth={250}>
|
||||
<SideBar placement="leftSide" secondaryStyle={secondaryStyle}>
|
||||
{/* Menu items will be placed here via placement */}
|
||||
</SideBar>
|
||||
{props.children}
|
||||
{shellProps.children}
|
||||
</EmptyShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,16 +28,26 @@ function getComponentLabel(component) {
|
||||
* Route registry for module-declared routes
|
||||
* Modules can publish routes here during initialization, and Router will collect them on mount
|
||||
*/
|
||||
const routeRegistry = [];
|
||||
let registryModified = false; // Flag to indicate routes have been published
|
||||
const routeRegistryListeners = new Set();
|
||||
const ROUTE_REGISTRY_KEY = '__bface_route_registry__';
|
||||
|
||||
function getRouteRegistryState() {
|
||||
const scope = typeof globalThis !== 'undefined' ? globalThis : window;
|
||||
if (!scope[ROUTE_REGISTRY_KEY]) {
|
||||
scope[ROUTE_REGISTRY_KEY] = {
|
||||
routes: [],
|
||||
registryModified: false,
|
||||
listeners: new Set()
|
||||
};
|
||||
}
|
||||
return scope[ROUTE_REGISTRY_KEY];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all published routes from registry (called by Router on mount)
|
||||
* @returns {Array} Array of route definitions
|
||||
*/
|
||||
function getPublishedRoutes() {
|
||||
return routeRegistry; // Return registry directly
|
||||
return getRouteRegistryState().routes;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,13 +55,14 @@ function getPublishedRoutes() {
|
||||
* @returns {boolean} True if registry was modified since last check
|
||||
*/
|
||||
function getAndClearRegistryModified() {
|
||||
const wasModified = registryModified;
|
||||
registryModified = false; // Clear the flag
|
||||
const state = getRouteRegistryState();
|
||||
const wasModified = state.registryModified;
|
||||
state.registryModified = false;
|
||||
return wasModified;
|
||||
}
|
||||
|
||||
function notifyRouteRegistryListeners() {
|
||||
for (const listener of routeRegistryListeners) {
|
||||
for (const listener of getRouteRegistryState().listeners) {
|
||||
try {
|
||||
listener();
|
||||
} catch (error) {
|
||||
@@ -61,9 +72,10 @@ function notifyRouteRegistryListeners() {
|
||||
}
|
||||
|
||||
function subscribeToRouteRegistry(listener) {
|
||||
routeRegistryListeners.add(listener);
|
||||
const state = getRouteRegistryState();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
routeRegistryListeners.delete(listener);
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -71,8 +83,9 @@ function subscribeToRouteRegistry(listener) {
|
||||
* Clear the route registry (for testing/cleanup)
|
||||
*/
|
||||
export function clearRouteRegistry() {
|
||||
routeRegistry.length = 0;
|
||||
registryModified = false;
|
||||
const state = getRouteRegistryState();
|
||||
state.routes.length = 0;
|
||||
state.registryModified = false;
|
||||
notifyRouteRegistryListeners();
|
||||
}
|
||||
|
||||
@@ -261,10 +274,11 @@ export function publishRoutes(input, options = {}) {
|
||||
}
|
||||
|
||||
// Add to registry (Router will pick up on mount)
|
||||
routeRegistry.push(...normalizedRoutes);
|
||||
registryModified = true; // Set flag to indicate routes have been published
|
||||
const state = getRouteRegistryState();
|
||||
state.routes.push(...normalizedRoutes);
|
||||
state.registryModified = true;
|
||||
notifyRouteRegistryListeners();
|
||||
console.log(`[Router] Published ${normalizedRoutes.length} route(s) to registry. Total: ${routeRegistry.length}`);
|
||||
console.log(`[Router] Published ${normalizedRoutes.length} route(s) to registry. Total: ${state.routes.length}`);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -275,6 +289,45 @@ const RouterContext = createContext(null);
|
||||
const EMPTY_ROUTE = { path: '', component: null, params: {}, state: null, is_fragment: false, fragment_path: null, options: {} };
|
||||
const RouteContext = createContext(EMPTY_ROUTE);
|
||||
|
||||
class RouteErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
return { error };
|
||||
}
|
||||
|
||||
componentDidCatch(error) {
|
||||
console.error('[Router.SelectedComponent] Route component failed to render:', error);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.resetKey !== this.props.resetKey && this.state.error) {
|
||||
this.setState({ error: null });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { error } = this.state;
|
||||
const { children, routePath, debug } = this.props;
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorPage
|
||||
title="Page Failed To Load"
|
||||
icon="error"
|
||||
message={`The route "${routePath || '/'}" could not be rendered.`}
|
||||
error={error}
|
||||
debug={debug}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* useRouter Hook
|
||||
* Access router instance and navigation methods
|
||||
@@ -434,9 +487,15 @@ function SelectedComponent({ placement, fallback }) {
|
||||
console.log('[Router.SelectedComponent] Rendering component:', getComponentLabel(RouteComponent), route.fragment_path ? `(fragment path: ${route.fragment_path})` : '');
|
||||
}
|
||||
return (
|
||||
<RouteErrorBoundary
|
||||
resetKey={routeSignature}
|
||||
routePath={route.path}
|
||||
debug={true}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<RouteComponent />
|
||||
</Suspense>
|
||||
</RouteErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -112,41 +112,6 @@ function useSideBarContent(children) {
|
||||
}
|
||||
});
|
||||
|
||||
// Add secondary menu items to bottomSide
|
||||
secondaryMenuItems.forEach((item) => {
|
||||
// Validate that item is a MenuItem instance
|
||||
if (!(item instanceof MenuItem)) {
|
||||
console.error('[SideBar] Expected MenuItem instance but got:', {
|
||||
type: typeof item,
|
||||
constructor: item?.constructor?.name,
|
||||
item: item,
|
||||
stack: new Error().stack
|
||||
});
|
||||
return; // Skip invalid items
|
||||
}
|
||||
sections.bottomSide.push(
|
||||
<MenuItemButton
|
||||
key={item.id || item.path}
|
||||
menuItem={item}
|
||||
orientation="vertical"
|
||||
stateVersion={menuVersion}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
// Add personal menu item as last element in bottomSide
|
||||
if (personalRoot && personalRoot instanceof MenuItem) {
|
||||
sections.bottomSide.push(
|
||||
<PersonalMenuItem
|
||||
key="personal-menu"
|
||||
personalRoot={personalRoot}
|
||||
orientation="vertical"
|
||||
expand_mode="popup"
|
||||
stateVersion={menuVersion}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
sections,
|
||||
primaryMenuItems,
|
||||
@@ -167,7 +132,8 @@ function SideBarWide({
|
||||
topSideHeight = 0,
|
||||
bottomSideHeight = 0,
|
||||
expandedWidth = 250,
|
||||
collapsedWidth = 80
|
||||
collapsedWidth = 80,
|
||||
secondaryStyle = 'inline'
|
||||
}) {
|
||||
const [brandLogo, setBrandLogo] = useState(null);
|
||||
const [appName, setAppName] = useState(null);
|
||||
@@ -222,12 +188,30 @@ function SideBarWide({
|
||||
};
|
||||
|
||||
const currentWidth = isCollapsed ? collapsedWidth : expandedWidth;
|
||||
const canFitInlineSecondary = (() => {
|
||||
const itemCount = organizedChildren.secondaryMenuItems.length;
|
||||
if (itemCount <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Account for the sidebar's horizontal padding and a compact icon-only row.
|
||||
const availableWidth = Math.max(currentWidth - 16, 0);
|
||||
const estimatedButtonWidth = 24;
|
||||
const estimatedGapWidth = 4;
|
||||
const requiredWidth = (itemCount * estimatedButtonWidth) + ((itemCount - 1) * estimatedGapWidth);
|
||||
|
||||
return requiredWidth <= availableWidth;
|
||||
})();
|
||||
const resolvedSecondaryStyle = secondaryStyle === 'inline' && canFitInlineSecondary ? 'inline' : 'stacked';
|
||||
const hasBottomContent = organizedChildren.sections.bottomSide.length > 0
|
||||
|| organizedChildren.secondaryMenuItems.length > 0
|
||||
|| (organizedChildren.personalRoot && organizedChildren.personalRoot instanceof MenuItem);
|
||||
|
||||
return (
|
||||
<YStack
|
||||
width={currentWidth}
|
||||
height="100%"
|
||||
gap="$2"
|
||||
gap="$1"
|
||||
padding="$2"
|
||||
backgroundColor="$bgPanel"
|
||||
borderRightWidth={1}
|
||||
@@ -252,7 +236,7 @@ function SideBarWide({
|
||||
<XStack
|
||||
width="100%"
|
||||
alignItems="center"
|
||||
gap="$2"
|
||||
gap="$1"
|
||||
paddingVertical="$2"
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
@@ -316,21 +300,54 @@ function SideBarWide({
|
||||
</YStack>
|
||||
|
||||
{/* Bottom Side - Secondary and Personal menu items (always shown if has content) */}
|
||||
{organizedChildren.sections.bottomSide.length > 0 && (
|
||||
{hasBottomContent && (
|
||||
<YStack
|
||||
width="100%"
|
||||
gap="$2"
|
||||
gap="$1"
|
||||
alignItems="flex-start"
|
||||
justifyContent="flex-end"
|
||||
paddingTop="$2"
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{React.Children.map(organizedChildren.sections.bottomSide, (child) => {
|
||||
if (React.isValidElement(child) && (child.type === MenuItemButton || child.type === PersonalMenuItem)) {
|
||||
return React.cloneElement(child, { collapsed: isCollapsed });
|
||||
}
|
||||
return child;
|
||||
})}
|
||||
{organizedChildren.sections.bottomSide}
|
||||
|
||||
{organizedChildren.secondaryMenuItems.length > 0 && resolvedSecondaryStyle === 'inline' ? (
|
||||
<XStack width="100%" alignItems="center" justifyContent="space-between" gap="$1" flexWrap="nowrap">
|
||||
{organizedChildren.secondaryMenuItems.map((item) => (
|
||||
<XStack key={item.id || item.path} flex={1} justifyContent="center" minWidth={0}>
|
||||
<MenuItemButton
|
||||
menuItem={item}
|
||||
orientation="horizontal"
|
||||
displayStyle="icon_only"
|
||||
width="100%"
|
||||
padding="$1"
|
||||
stateVersion={organizedChildren.menuVersion}
|
||||
/>
|
||||
</XStack>
|
||||
))}
|
||||
</XStack>
|
||||
) : (
|
||||
organizedChildren.secondaryMenuItems.map((item) => (
|
||||
<MenuItemButton
|
||||
key={item.id || item.path}
|
||||
menuItem={item}
|
||||
orientation="vertical"
|
||||
collapsed={isCollapsed}
|
||||
stateVersion={organizedChildren.menuVersion}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
{organizedChildren.personalRoot && organizedChildren.personalRoot instanceof MenuItem && (
|
||||
<PersonalMenuItem
|
||||
key="personal-menu"
|
||||
personalRoot={organizedChildren.personalRoot}
|
||||
orientation="vertical"
|
||||
expand_mode="popup"
|
||||
collapsed={isCollapsed}
|
||||
stateVersion={organizedChildren.menuVersion}
|
||||
/>
|
||||
)}
|
||||
</YStack>
|
||||
)}
|
||||
</YStack>
|
||||
@@ -474,6 +491,7 @@ function SideBarNarrow({ children }) {
|
||||
* @param {React.ReactNode} props.children - Content to render (defaults to topSide)
|
||||
* @param {number} [props.topSideHeight=0] - Top side height (default: 0, wide only)
|
||||
* @param {number} [props.bottomSideHeight=0] - Bottom side height (default: 0, wide only)
|
||||
* @param {string} [props.secondaryStyle='inline'] - Secondary menu rendering: 'stacked' | 'inline' (wide only)
|
||||
*/
|
||||
export function SideBar(props) {
|
||||
const media = useMedia();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* SettingsPage - Settings page component
|
||||
* Derived from Page component
|
||||
* Displays all settings fragment routes as panels
|
||||
* Platform default: lists registered fragment routes under the settings base path
|
||||
* and mounts each with {@code settingsFragmentPath} so fragments can resolve their own meta.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
@@ -11,51 +11,21 @@ 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();
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
|
||||
// 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);
|
||||
const segments = currentPath.split('/').filter((s) => s.length > 0);
|
||||
return segments.length > 0 ? `/${segments[0]}` : '/settings';
|
||||
}
|
||||
return '/settings'; // Default fallback
|
||||
return '/settings';
|
||||
}, [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');
|
||||
@@ -66,10 +36,8 @@ export function SettingsPage() {
|
||||
};
|
||||
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 (path.startsWith(`${basePath}/`) && path !== basePath) {
|
||||
if (routeData.is_fragment) {
|
||||
const matchingMenuItem = settingsItems.find((item) => item.invoke_target === path);
|
||||
if (matchingMenuItem && !matchingMenuItem.isRenderable(security)) {
|
||||
@@ -84,55 +52,41 @@ export function SettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}, [router, basePath, router.currentRoute, securityState]);
|
||||
|
||||
// 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);
|
||||
return childRoutes.filter((r) => {
|
||||
const segments = r.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
|
||||
]}
|
||||
>
|
||||
<Page icon="settings" title="Settings" headerRight={[]}>
|
||||
<YStack gap="$4" width="100%">
|
||||
{/* Search bar */}
|
||||
<Input
|
||||
placeholder="Search settings..."
|
||||
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} />
|
||||
<RouteComponent key={routeItem.path} settingsFragmentPath={routeItem.path} />
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Text fontSize="$4" color="$color" opacity={0.6}>
|
||||
{searchQuery ? 'No settings found matching your search.' : 'No settings available.'}
|
||||
<Text fontSize="$4" color="$textMuted">
|
||||
{searchQuery.trim() ? 'No settings found matching your search.' : 'No settings available.'}
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
|
||||
Reference in New Issue
Block a user