From 1ad0ef6d781dbb8c61a63839fe68efec3f2da658 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 9 Oct 2026 17:58:08 +0200 Subject: [PATCH] refactor(settings): photo provider cards read their state from the shared hook With the badge and toast options gone from usePhotoProviderConnections, the desktop and phone cards called the hook identically and then derived the same seven per card values (fields, config, values, badge, testing, whether save and test have a route) line for line, which lint:dup counts as a copied block of 25 lines in MPhotoProvidersSection.tsx. The hook now hands each card those values through providerCard, and both views keep only their markup. Nothing the user sees changes. New test FE-COMP-PHOTOCONN-011 covers providerCard. No existing assertion changed. --- .../Settings/PhotoProvidersSection.tsx | 19 +++---------------- .../usePhotoProviderConnections.test.ts | 18 +++++++++++++++++- .../Settings/usePhotoProviderConnections.ts | 14 ++++++++++++++ .../settings/MPhotoProvidersSection.tsx | 19 +++---------------- 4 files changed, 37 insertions(+), 33 deletions(-) diff --git a/client/src/components/Settings/PhotoProvidersSection.tsx b/client/src/components/Settings/PhotoProvidersSection.tsx index ac1794a8a..a1012c97d 100644 --- a/client/src/components/Settings/PhotoProvidersSection.tsx +++ b/client/src/components/Settings/PhotoProvidersSection.tsx @@ -7,21 +7,14 @@ import { SETTINGS_BUTTON, SETTINGS_BUTTON_PRIMARY, SettingRow, SettingRows, Stat import { EditorField, INPUT } from '../shared/dialogParts' import { fs } from '../shared/DialogShell' import { Tooltip } from '../shared/Tooltip' -import { - getProviderConfig, - getProviderFields, - usePhotoProviderConnections, - type PhotoProviderAddon, -} from './usePhotoProviderConnections' +import { usePhotoProviderConnections, type PhotoProviderAddon } from './usePhotoProviderConnections' export default function PhotoProvidersSection(): React.ReactElement { const { t } = useTranslation() const { memoriesEnabled, activePhotoProviders, - providerValues, - providerConnected, - providerTesting, + providerCard, saving, handleProviderFieldChange, isProviderSaveDisabled, @@ -30,13 +23,7 @@ export default function PhotoProvidersSection(): React.ReactElement { } = usePhotoProviderConnections() const renderPhotoProviderSection = (provider: PhotoProviderAddon): React.ReactElement => { - const fields = getProviderFields(provider) - const cfg = getProviderConfig(provider) - const values = providerValues[provider.id] || {} - const connected = !!providerConnected[provider.id] - const testing = !!providerTesting[provider.id] - const canSave = !!cfg.settings_put - const canTest = !!(cfg.test_post || cfg.test_get || cfg.status_get) + const { fields, values, connected, testing, canSave, canTest } = providerCard(provider) const saveDisabledReason = !canSave ? t('memories.saveRouteNotConfigured') diff --git a/client/src/components/Settings/usePhotoProviderConnections.test.ts b/client/src/components/Settings/usePhotoProviderConnections.test.ts index 7d88bfd36..f7d259279 100644 --- a/client/src/components/Settings/usePhotoProviderConnections.test.ts +++ b/client/src/components/Settings/usePhotoProviderConnections.test.ts @@ -1,4 +1,4 @@ -// FE-COMP-PHOTOCONN-001 to -010: the photo provider connection logic both settings shells share. +// FE-COMP-PHOTOCONN-001 to -011: the photo provider connection logic both settings shells share. import { act, renderHook, waitFor } from '@testing-library/react'; import { resetAllStores, seedStore } from '../../../tests/helpers/store'; import apiClient from '../../api/client'; @@ -227,4 +227,20 @@ describe('usePhotoProviderConnections', () => { await act(() => desktop.result.current.handleTestProvider(PROVIDER)); expect(toast.error).toHaveBeenLastCalledWith('memories.connectionError(Immich)'); }); + + it('FE-COMP-PHOTOCONN-011: providerCard hands each card its fields, values, badge and working buttons', async () => { + seedProviders([PROVIDER]); + stubGets({ + '/addons/immich/settings': { data: { url: 'https://a' } }, + '/addons/immich/status': { data: { connected: true } }, + }); + const { result } = render(); + await waitFor(() => expect(result.current.providerCard(PROVIDER).connected).toBe(true)); + const card = result.current.providerCard(PROVIDER); + expect(card.fields.map((f) => f.key)).toEqual(['url', 'api_key', 'shared']); + expect(card.values).toEqual({ url: 'https://a', shared: 'false' }); + expect(card).toMatchObject({ testing: false, canSave: true, canTest: true }); + const bare = result.current.providerCard({ ...PROVIDER, id: 'other', config: {} }); + expect(bare).toMatchObject({ values: {}, connected: false, canSave: false, canTest: false }); + }); }); diff --git a/client/src/components/Settings/usePhotoProviderConnections.ts b/client/src/components/Settings/usePhotoProviderConnections.ts index 8fa325349..a0838bd97 100644 --- a/client/src/components/Settings/usePhotoProviderConnections.ts +++ b/client/src/components/Settings/usePhotoProviderConnections.ts @@ -221,9 +221,23 @@ export function usePhotoProviderConnections() { } }; + /** What one provider card draws from: its fields, values, badge and which buttons work. */ + const providerCard = (provider: PhotoProviderAddon) => { + const cfg = getProviderConfig(provider); + return { + fields: getProviderFields(provider), + values: providerValues[provider.id] || {}, + connected: !!providerConnected[provider.id], + testing: !!providerTesting[provider.id], + canSave: !!cfg.settings_put, + canTest: !!(cfg.test_post || cfg.test_get || cfg.status_get), + }; + }; + return { memoriesEnabled, activePhotoProviders, + providerCard, providerValues, providerConnected, providerTesting, diff --git a/client/src/mobile/screens/settings/MPhotoProvidersSection.tsx b/client/src/mobile/screens/settings/MPhotoProvidersSection.tsx index 09ae85e99..84ed21188 100644 --- a/client/src/mobile/screens/settings/MPhotoProvidersSection.tsx +++ b/client/src/mobile/screens/settings/MPhotoProvidersSection.tsx @@ -3,12 +3,7 @@ import { Camera, Save, RefreshCw } from 'lucide-react' import { useTranslation } from '../../../i18n' import { MSetCard, MSetEyebrow, MSetRow, MSetInput, MSetButton, MSetHint } from './MSettingsUi' import MToggle from '../../components/MToggle' -import { - getProviderConfig, - getProviderFields, - usePhotoProviderConnections, - type PhotoProviderAddon, -} from '../../../components/Settings/usePhotoProviderConnections' +import { usePhotoProviderConnections, type PhotoProviderAddon } from '../../../components/Settings/usePhotoProviderConnections' /** * Mobile-native twin of components/Settings/PhotoProvidersSection. Same logic @@ -22,9 +17,7 @@ export default function MPhotoProvidersSection(): React.ReactElement { const { memoriesEnabled, activePhotoProviders, - providerValues, - providerConnected, - providerTesting, + providerCard, saving, handleProviderFieldChange, isProviderSaveDisabled, @@ -33,13 +26,7 @@ export default function MPhotoProvidersSection(): React.ReactElement { } = usePhotoProviderConnections() const renderPhotoProviderSection = (provider: PhotoProviderAddon): React.ReactElement => { - const fields = getProviderFields(provider) - const cfg = getProviderConfig(provider) - const values = providerValues[provider.id] || {} - const connected = !!providerConnected[provider.id] - const testing = !!providerTesting[provider.id] - const canSave = !!cfg.settings_put - const canTest = !!(cfg.test_post || cfg.test_get || cfg.status_get) + const { fields, values, connected, testing, canSave, canTest } = providerCard(provider) return (