mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
refactor(settings): share the integration and plugin tab gating between desktop and phone
The addon flags, the managed install flag and the addon reload of the integrations tab now come from one useIntegrationGates hook, and useSettings hands both shells the plugins tab flag next to the integrations one.
This commit is contained in:
@@ -1,26 +1,25 @@
|
||||
import Section from './Section'
|
||||
import React, { useEffect, useId } from 'react'
|
||||
import React, { useId } from 'react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { useToast } from '../shared/Toast'
|
||||
import { Trash2, Copy, Terminal, Plus, Check, Key, KeyRound, ChevronRight, RefreshCw, AlertTriangle, Link2 } from 'lucide-react'
|
||||
import { useAddonStore } from '../../store/addonStore'
|
||||
import PhotoProvidersSection from './PhotoProvidersSection'
|
||||
import AirTrailConnectionSection from './AirTrailConnectionSection'
|
||||
import DawarichConnectionSection from './DawarichConnectionSection'
|
||||
import LlmConnectionSection from './LlmConnectionSection'
|
||||
import ApiKeysSection from './ApiKeysSection'
|
||||
import ScopeGroupPicker from '../OAuth/ScopeGroupPicker'
|
||||
import { useAuthStore } from '../../store/authStore'
|
||||
import ConfirmDialog from '../shared/ConfirmDialog'
|
||||
import { Tooltip } from '../shared/Tooltip'
|
||||
import { DialogButton, DialogFooter, DialogHeader, DialogSection, DialogShell, DialogTile, FooterSpacer, NEUTRAL_TINT, fs } from '../shared/DialogShell'
|
||||
import { EditorField, INPUT, Segmented, TEXTAREA } from '../shared/dialogParts'
|
||||
import { SETTINGS_BUTTON, SETTINGS_BUTTON_PRIMARY, SETTINGS_ICON_BUTTON, SettingRows, SettingsHint, StatusPill } from './settingsKit'
|
||||
import { OAUTH_PRESETS, useMcpIntegration, type OAuthClient } from './useMcpIntegration'
|
||||
import { useIntegrationGates } from './useIntegrationGates'
|
||||
|
||||
export default function IntegrationsTab(): React.ReactElement {
|
||||
const S = useIntegrations()
|
||||
const managed = useAuthStore((s) => s.managed)
|
||||
const managed = S.managed
|
||||
return (
|
||||
<>
|
||||
{/* Immich, Synology Photos and AirTrail all connect to a server the reader
|
||||
@@ -46,19 +45,11 @@ export default function IntegrationsTab(): React.ReactElement {
|
||||
function useIntegrations() {
|
||||
const { t, locale } = useTranslation()
|
||||
const toast = useToast()
|
||||
const { isEnabled: addonEnabled, loadAddons } = useAddonStore()
|
||||
const mcpEnabled = addonEnabled('mcp')
|
||||
const airtrailEnabled = addonEnabled('airtrail')
|
||||
const llmEnabled = addonEnabled('llm_parsing')
|
||||
const dawarichEnabled = addonEnabled('dawarich')
|
||||
|
||||
useEffect(() => {
|
||||
loadAddons()
|
||||
}, [loadAddons])
|
||||
const { mcpEnabled, airtrailEnabled, llmEnabled, dawarichEnabled, managed } = useIntegrationGates({ reloadAddons: true })
|
||||
|
||||
const mcp = useMcpIntegration({ enabled: mcpEnabled, reloadClientsAfterWrite: false })
|
||||
|
||||
return { t, locale, toast, mcpEnabled, airtrailEnabled, llmEnabled, dawarichEnabled, ...mcp }
|
||||
return { t, locale, toast, mcpEnabled, airtrailEnabled, llmEnabled, dawarichEnabled, managed, ...mcp }
|
||||
}
|
||||
|
||||
type IntegrationsState = ReturnType<typeof useIntegrations>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// FE-COMP-INTEGRATION-GATES-001 to -004: which integrations the settings shells offer.
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
|
||||
import { useAddonStore } from '../../store/addonStore';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { useIntegrationGates } from './useIntegrationGates';
|
||||
|
||||
function seedAddons(enabled: string[]) {
|
||||
const loadAddons = vi.fn().mockResolvedValue(undefined);
|
||||
seedStore(useAddonStore, { isEnabled: (id: string) => enabled.includes(id), loadAddons });
|
||||
return loadAddons;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resetAllStores();
|
||||
});
|
||||
|
||||
describe('useIntegrationGates', () => {
|
||||
it('FE-COMP-INTEGRATION-GATES-001: maps each addon to its flag', () => {
|
||||
seedAddons(['mcp', 'llm_parsing']);
|
||||
const { result } = renderHook(() => useIntegrationGates({ reloadAddons: false }));
|
||||
expect(result.current).toMatchObject({
|
||||
memoriesEnabled: false,
|
||||
mcpEnabled: true,
|
||||
airtrailEnabled: false,
|
||||
llmEnabled: true,
|
||||
dawarichEnabled: false,
|
||||
hasIntegrations: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('FE-COMP-INTEGRATION-GATES-002: any one integration addon opens the tab, none keeps it closed', () => {
|
||||
for (const id of ['memories', 'mcp', 'airtrail', 'llm_parsing', 'dawarich']) {
|
||||
seedAddons([id]);
|
||||
expect(renderHook(() => useIntegrationGates({ reloadAddons: false })).result.current.hasIntegrations).toBe(true);
|
||||
}
|
||||
seedAddons(['vacay', 'atlas']);
|
||||
expect(renderHook(() => useIntegrationGates({ reloadAddons: false })).result.current.hasIntegrations).toBe(false);
|
||||
});
|
||||
|
||||
it('FE-COMP-INTEGRATION-GATES-003: reports a managed install', () => {
|
||||
seedAddons([]);
|
||||
seedStore(useAuthStore, { managed: true });
|
||||
expect(renderHook(() => useIntegrationGates({ reloadAddons: false })).result.current.managed).toBe(true);
|
||||
});
|
||||
|
||||
it('FE-COMP-INTEGRATION-GATES-004: reloads the addon list on mount only when asked to', () => {
|
||||
const quiet = seedAddons([]);
|
||||
renderHook(() => useIntegrationGates({ reloadAddons: false }));
|
||||
expect(quiet).not.toHaveBeenCalled();
|
||||
const loud = seedAddons([]);
|
||||
renderHook(() => useIntegrationGates({ reloadAddons: true }));
|
||||
expect(loud).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useAddonStore } from '../../store/addonStore';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
/**
|
||||
* Which integrations the settings shells offer: the addon flags behind each
|
||||
* integration section, whether any of them is on (the Integrations tab only shows
|
||||
* then), and whether this is a managed install. With `reloadAddons` the addon list
|
||||
* is fetched again on mount, as the integrations tab on both shells does.
|
||||
*/
|
||||
export function useIntegrationGates({ reloadAddons }: { reloadAddons: boolean }) {
|
||||
const { isEnabled: addonEnabled, loadAddons } = useAddonStore();
|
||||
const managed = useAuthStore((s) => s.managed);
|
||||
|
||||
const memoriesEnabled = addonEnabled('memories');
|
||||
const mcpEnabled = addonEnabled('mcp');
|
||||
const airtrailEnabled = addonEnabled('airtrail');
|
||||
const llmEnabled = addonEnabled('llm_parsing');
|
||||
const dawarichEnabled = addonEnabled('dawarich');
|
||||
const hasIntegrations = memoriesEnabled || mcpEnabled || airtrailEnabled || llmEnabled || dawarichEnabled;
|
||||
|
||||
useEffect(() => {
|
||||
if (reloadAddons) loadAddons();
|
||||
}, [loadAddons, reloadAddons]);
|
||||
|
||||
return {
|
||||
memoriesEnabled,
|
||||
mcpEnabled,
|
||||
airtrailEnabled,
|
||||
llmEnabled,
|
||||
dawarichEnabled,
|
||||
hasIntegrations,
|
||||
managed,
|
||||
loadAddons,
|
||||
};
|
||||
}
|
||||
@@ -17,8 +17,6 @@ import {
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import { useTranslation } from '../../../i18n'
|
||||
import { useSettings } from '../../../pages/settings/useSettings'
|
||||
import { useAuthStore } from '../../../store/authStore'
|
||||
import { usePluginStore } from '../../../store/pluginStore'
|
||||
import MSettingsPlugins from './MSettingsPlugins'
|
||||
import MSettingsOffline from './MSettingsOffline'
|
||||
import MSettingsGeneral from './MSettingsGeneral'
|
||||
@@ -44,9 +42,7 @@ interface SectionTab {
|
||||
export default function MSettings() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { hasIntegrations, appVersion, activeTab, setActiveTab } = useSettings()
|
||||
const managed = useAuthStore((s) => s.managed)
|
||||
const hasPlugins = usePluginStore((s) => s.plugins.length > 0)
|
||||
const { hasIntegrations, hasPlugins, appVersion, activeTab, setActiveTab, managed } = useSettings()
|
||||
const [dropOpen, setDropOpen] = useState(false)
|
||||
|
||||
const tabs: SectionTab[] = [
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useAddonStore } from '../../../store/addonStore'
|
||||
import MPhotoProvidersSection from './MPhotoProvidersSection'
|
||||
import MAirTrailConnectionSection from './MAirTrailConnectionSection'
|
||||
import MDawarichConnectionSection from './MDawarichConnectionSection'
|
||||
import MLlmConnectionSection from './MLlmConnectionSection'
|
||||
import MSettingsMcp from './MSettingsMcp'
|
||||
import { useAuthStore } from '../../../store/authStore'
|
||||
import { useIntegrationGates } from '../../../components/Settings/useIntegrationGates'
|
||||
|
||||
/**
|
||||
* "Integrations" section. The photo-provider / AirTrail / LLM connection forms
|
||||
@@ -14,16 +12,7 @@ import { useAuthStore } from '../../../store/authStore'
|
||||
* function audit is rebuilt natively in the mobile design language.
|
||||
*/
|
||||
export default function MSettingsIntegrations() {
|
||||
const { isEnabled: addonEnabled, loadAddons } = useAddonStore()
|
||||
const mcpEnabled = addonEnabled('mcp')
|
||||
const airtrailEnabled = addonEnabled('airtrail')
|
||||
const llmEnabled = addonEnabled('llm_parsing')
|
||||
const dawarichEnabled = addonEnabled('dawarich')
|
||||
const managed = useAuthStore((s) => s.managed)
|
||||
|
||||
useEffect(() => {
|
||||
loadAddons()
|
||||
}, [loadAddons])
|
||||
const { mcpEnabled, airtrailEnabled, llmEnabled, dawarichEnabled, managed } = useIntegrationGates({ reloadAddons: true })
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -15,7 +15,6 @@ import HelpAnchor from '../components/Help/HelpAnchor'
|
||||
import { getHelpContext } from '../help/registry'
|
||||
import OfflineTab from '../components/Settings/OfflineTab'
|
||||
import PluginSettingsTab from '../components/Settings/PluginSettingsTab'
|
||||
import { usePluginStore } from '../store/pluginStore'
|
||||
import { useSettings } from './settings/useSettings'
|
||||
|
||||
export default function SettingsPage(): React.ReactElement {
|
||||
@@ -27,8 +26,7 @@ export default function SettingsPage(): React.ReactElement {
|
||||
function SettingsPageDesktop(): React.ReactElement {
|
||||
const { t } = useTranslation()
|
||||
// Page = wiring container: addon/version loading + active-tab state in the hook.
|
||||
const { hasIntegrations, appVersion, activeTab, setActiveTab, managed } = useSettings()
|
||||
const hasPlugins = usePluginStore(s => s.plugins.length > 0)
|
||||
const { hasIntegrations, hasPlugins, appVersion, activeTab, setActiveTab, managed } = useSettings()
|
||||
// Every tab is its own help screen; a tab without one falls back to the settings overview.
|
||||
const helpId = getHelpContext(`settings-${activeTab}`) ? `settings-${activeTab}` : 'settings'
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSearchParams } from 'react-router'
|
||||
import { authApi } from '../../api/client'
|
||||
import { useAddonStore } from '../../store/addonStore'
|
||||
import { useAuthStore } from '../../store/authStore'
|
||||
import { useIntegrationGates } from '../../components/Settings/useIntegrationGates'
|
||||
import { usePluginStore } from '../../store/pluginStore'
|
||||
|
||||
/**
|
||||
* Settings page logic — loads addons + the app version, tracks the active tab
|
||||
@@ -13,15 +13,8 @@ import { useAuthStore } from '../../store/authStore'
|
||||
*/
|
||||
export function useSettings() {
|
||||
const [searchParams] = useSearchParams()
|
||||
const { isEnabled: addonEnabled, loadAddons } = useAddonStore()
|
||||
const managed = useAuthStore(s => s.managed)
|
||||
|
||||
const memoriesEnabled = addonEnabled('memories')
|
||||
const mcpEnabled = addonEnabled('mcp')
|
||||
const airtrailEnabled = addonEnabled('airtrail')
|
||||
const llmEnabled = addonEnabled('llm_parsing')
|
||||
const dawarichEnabled = addonEnabled('dawarich')
|
||||
const hasIntegrations = memoriesEnabled || mcpEnabled || airtrailEnabled || llmEnabled || dawarichEnabled
|
||||
const { hasIntegrations, managed, loadAddons } = useIntegrationGates({ reloadAddons: false })
|
||||
const hasPlugins = usePluginStore(s => s.plugins.length > 0)
|
||||
|
||||
const [appVersion, setAppVersion] = useState<string | null>(null)
|
||||
const [activeTab, setActiveTab] = useState('display')
|
||||
@@ -46,5 +39,5 @@ export function useSettings() {
|
||||
if (tab) setActiveTab(tab)
|
||||
}, [searchParams])
|
||||
|
||||
return { hasIntegrations, appVersion, activeTab, setActiveTab, managed }
|
||||
return { hasIntegrations, hasPlugins, appVersion, activeTab, setActiveTab, managed }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user