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.
This commit is contained in:
Maurice
2026-10-09 17:58:08 +02:00
parent 24461ecf76
commit 1ad0ef6d78
4 changed files with 37 additions and 33 deletions
@@ -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')
@@ -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 });
});
});
@@ -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,
@@ -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 (
<MSetCard key={provider.id} title={provider.name || provider.id} icon={Camera} className="mt-3 first:mt-0">