diff --git a/frontend/src/components/contacts/ContactDetail.tsx b/frontend/src/components/contacts/ContactDetail.tsx index 9fbdc75..f8e7700 100644 --- a/frontend/src/components/contacts/ContactDetail.tsx +++ b/frontend/src/components/contacts/ContactDetail.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/Button'; import { Badge } from '@/components/ui/Badge'; @@ -157,7 +157,14 @@ export function ContactDetail({ contact, loading, onEdit, onDeleted }: ContactDe const [personModalOpen, setPersonModalOpen] = useState(false); const [editingPerson, setEditingPerson] = useState(null); const [activeTab, setActiveTab] = useState('details'); - const pluginTabs = usePluginStore(s => s.getDetailTabsForEntity('contact')); + const manifests = usePluginStore(s => s.manifests); + const pluginTabs = useMemo( + () => manifests + .flatMap((m) => m.detail_tabs) + .filter((t) => t.entity_type === 'contact') + .sort((a, b) => a.order - b.order), + [manifests] + ); const aiActiveTab = useAIUIControlStore(s => s.activeTab); const aiActiveModal = useAIUIControlStore(s => s.activeModal); @@ -180,7 +187,12 @@ export function ContactDetail({ contact, loading, onEdit, onDeleted }: ContactDe const user = useAuthStore(s => s.user); // Custom fields from plugin definitions - const customFieldDefs = usePluginStore(s => s.getCustomFieldsForEntity('contact')); + const customFieldDefs = useMemo( + () => manifests + .flatMap((m) => m.custom_fields || []) + .filter((cf) => cf.entity === 'contact'), + [manifests] + ); const { data: customFieldsData } = useCustomFields(contact?.id); if (loading) { diff --git a/frontend/src/components/contacts/ContactEditModal.tsx b/frontend/src/components/contacts/ContactEditModal.tsx index ea3a588..ba9bd84 100644 --- a/frontend/src/components/contacts/ContactEditModal.tsx +++ b/frontend/src/components/contacts/ContactEditModal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -109,7 +109,13 @@ export function ContactEditModal({ open, onClose, contact, onSaved }: ContactEdi const currentType = watch('type'); // Custom fields - const customFieldDefs = usePluginStore(s => s.getCustomFieldsForEntity('contact')); + const manifests = usePluginStore(s => s.manifests); + const customFieldDefs = useMemo( + () => manifests + .flatMap((m) => m.custom_fields || []) + .filter((cf) => cf.entity === 'contact'), + [manifests] + ); const { data: customFieldsData } = useCustomFields(contact?.id); const updateCustomFields = useUpdateCustomFields(); const [customValues, setCustomValues] = React.useState>({}); diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index cb2b274..1c41ce3 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import clsx from 'clsx'; import { NavLink, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -38,7 +38,13 @@ export function Sidebar() { const { t } = useTranslation(); const { sidebarOpen, setSidebarOpen } = useUIStore(); const location = useLocation(); - const pluginMenuItems = usePluginStore(s => s.getAllMenuItems()); + const manifests = usePluginStore(s => s.manifests); + const pluginMenuItems = useMemo( + () => manifests + .flatMap((m) => m.menu_items) + .sort((a, b) => a.order - b.order), + [manifests] + ); const [expandedItems, setExpandedItems] = useState>(new Set()); diff --git a/frontend/src/components/plugins/PluginLoader.tsx b/frontend/src/components/plugins/PluginLoader.tsx index 1e0c54a..6088f22 100644 --- a/frontend/src/components/plugins/PluginLoader.tsx +++ b/frontend/src/components/plugins/PluginLoader.tsx @@ -40,8 +40,9 @@ function getLazyComponent(componentPath: string): React.LazyExoticComponent import(/* @vite-ignore */ importPath).then((m) => ({ diff --git a/frontend/src/components/plugins/PluginRouteRenderer.tsx b/frontend/src/components/plugins/PluginRouteRenderer.tsx index ce144f3..c4822fb 100644 --- a/frontend/src/components/plugins/PluginRouteRenderer.tsx +++ b/frontend/src/components/plugins/PluginRouteRenderer.tsx @@ -1,4 +1,6 @@ +import { useMemo } from 'react'; import { useLocation } from 'react-router-dom'; +import { Loader2 } from 'lucide-react'; import { usePluginStore } from '@/store/pluginStore'; import { PluginPage } from './PluginLoader'; @@ -15,10 +17,15 @@ import { PluginPage } from './PluginLoader'; */ export function PluginRouteRenderer() { const location = useLocation(); - const getAllPageRoutes = usePluginStore((s) => s.getAllPageRoutes); + const manifests = usePluginStore((s) => s.manifests); const loaded = usePluginStore((s) => s.loaded); - const routes = getAllPageRoutes(); + const routes = useMemo( + () => manifests + .flatMap((m) => m.page_routes) + .sort((a, b) => a.order - b.order), + [manifests] + ); // Find the first matching route (exact match or prefix match for nested routes) const matchedRoute = routes.find((r) => { @@ -31,7 +38,6 @@ export function PluginRouteRenderer() { if (matchedRoute) { // Find the plugin name for display - const manifests = usePluginStore.getState().manifests; const plugin = manifests.find((m) => m.page_routes.some((pr) => pr.path === matchedRoute.path) ); @@ -45,9 +51,13 @@ export function PluginRouteRenderer() { ); } - // If manifests haven't loaded yet, show nothing (the static router handles it) + // If manifests haven't loaded yet, show a spinner (not null/blank) if (!loaded) { - return null; + return ( +
+
+ ); } // No plugin route matched — show a simple not-found diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index af19e2a..b807b19 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { NavLink, Outlet } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { usePluginStore } from '@/store/pluginStore'; @@ -6,7 +6,13 @@ import * as LucideIcons from 'lucide-react'; export function SettingsPage() { const { t } = useTranslation(); - const pluginSettingsPages = usePluginStore(s => s.getAllSettingsPages()); + const manifests = usePluginStore(s => s.manifests); + const pluginSettingsPages = useMemo( + () => manifests + .flatMap((m) => m.settings_pages) + .sort((a, b) => a.order - b.order), + [manifests] + ); const hardcodedNavItems = [ { to: '/settings/system', label: t('systemSettings.title'), icon: '\u2699\ufe0f' },