diff --git a/src/screens/ConfigEditorScreen/ConfigEditorScreen.test.tsx b/src/screens/ConfigEditorScreen/ConfigEditorScreen.test.tsx
index d0f2a31..6fd9193 100644
--- a/src/screens/ConfigEditorScreen/ConfigEditorScreen.test.tsx
+++ b/src/screens/ConfigEditorScreen/ConfigEditorScreen.test.tsx
@@ -2,6 +2,7 @@ import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ConfigEditor } from './ConfigEditorScreen.tsx';
import { mockConfig } from '../../test/fixtures.ts';
+import type { AppConfig } from '../../types/config.ts';
describe('ConfigEditorScreen', () => {
const defaultProps = {
@@ -83,4 +84,50 @@ describe('ConfigEditorScreen', () => {
expect(screen.getByText('Invalid base64 string.')).toBeInTheDocument();
});
+
+ it('preserves link changes when saving from the General tab', async () => {
+ const user = userEvent.setup();
+ const onSave = vi.fn();
+ render();
+
+ // Switch to Links tab and add a new section
+ await user.click(screen.getByRole('button', { name: 'Links' }));
+ await user.click(screen.getByRole('button', { name: 'Add Section' }));
+
+ // Fill in the new section name
+ const sectionInputs = screen.getAllByPlaceholderText('Section name');
+ await user.type(sectionInputs[0], 'New Section');
+
+ // Switch back to General tab and save
+ await user.click(screen.getByRole('button', { name: 'General' }));
+ await user.click(screen.getByRole('button', { name: 'Save' }));
+
+ expect(onSave).toHaveBeenCalledTimes(1);
+ const savedConfig = onSave.mock.calls[0][0] as AppConfig;
+ // Should have 3 modules: the new one + the 2 original ones
+ expect(savedConfig.modules).toHaveLength(3);
+ expect(savedConfig.modules[0].title).toBe('New Section');
+ });
+
+ it('preserves general changes when saving from the Links tab', async () => {
+ const user = userEvent.setup();
+ const onSave = vi.fn();
+ render();
+
+ // Edit placeholder on General tab
+ const placeholderInput = screen.getByPlaceholderText('Filter links...');
+ await user.clear(placeholderInput);
+ await user.type(placeholderInput, 'Search...');
+
+ // Switch to Links tab and save
+ await user.click(screen.getByRole('button', { name: 'Links' }));
+ const saveButtons = screen.getAllByRole('button', { name: 'Save' });
+ await user.click(saveButtons[0]);
+
+ expect(onSave).toHaveBeenCalledTimes(1);
+ const savedConfig = onSave.mock.calls[0][0] as AppConfig;
+ expect(savedConfig.search?.placeholder).toBe('Search...');
+ // Original modules should still be there
+ expect(savedConfig.modules).toHaveLength(2);
+ });
});
diff --git a/src/screens/ConfigEditorScreen/ConfigEditorScreen.tsx b/src/screens/ConfigEditorScreen/ConfigEditorScreen.tsx
index 3526d2b..f7fcaf1 100644
--- a/src/screens/ConfigEditorScreen/ConfigEditorScreen.tsx
+++ b/src/screens/ConfigEditorScreen/ConfigEditorScreen.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, useCallback } from 'react';
import type { AppConfig, BackgroundConfig } from '../../types/config.ts';
import { GeneralTab } from './components/GeneralTab.tsx';
import { LinksTab } from './components/LinksTab.tsx';
@@ -49,12 +49,25 @@ interface ConfigEditorProps {
export function ConfigEditor({ config, onSave, onClose, onPreview }: ConfigEditorProps) {
const [tab, setTab] = useState('general');
+ const [draftConfig, setDraftConfig] = useState(() => ({
+ ...config,
+ modules: config.modules.map((m) => ({ ...m, links: m.links.map((l) => ({ ...l })) })),
+ }));
const [activePanel, setActivePanel] = useState('none');
const [exportString, setExportString] = useState('');
const [copied, setCopied] = useState(false);
const [importString, setImportString] = useState('');
const [importError, setImportError] = useState('');
+ const handleConfigChange = useCallback((updatedConfig: AppConfig) => {
+ setDraftConfig(updatedConfig);
+ }, []);
+
+ const handleSave = useCallback((configToSave: AppConfig) => {
+ onSave(configToSave);
+ onClose();
+ }, [onSave, onClose]);
+
const handleExport = () => {
if (activePanel === 'export') {
setActivePanel('none');
@@ -166,15 +179,21 @@ export function ConfigEditor({ config, onSave, onClose, onPreview }: ConfigEdito
{tab === 'general' && (
)}
{tab === 'links' && (
-
+
)}
{activePanel !== 'none' && (
diff --git a/src/screens/ConfigEditorScreen/components/GeneralTab.test.tsx b/src/screens/ConfigEditorScreen/components/GeneralTab.test.tsx
index c800e42..30785ca 100644
--- a/src/screens/ConfigEditorScreen/components/GeneralTab.test.tsx
+++ b/src/screens/ConfigEditorScreen/components/GeneralTab.test.tsx
@@ -10,6 +10,7 @@ describe('GeneralTab', () => {
onSave: vi.fn(),
onClose: vi.fn(),
onPreview: vi.fn(),
+ onConfigChange: vi.fn(),
};
beforeEach(() => {
diff --git a/src/screens/ConfigEditorScreen/components/GeneralTab.tsx b/src/screens/ConfigEditorScreen/components/GeneralTab.tsx
index b3ce124..d86ffd9 100644
--- a/src/screens/ConfigEditorScreen/components/GeneralTab.tsx
+++ b/src/screens/ConfigEditorScreen/components/GeneralTab.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import type { AppConfig, BackgroundConfig, GradientDirection } from '../../../types/config.ts';
const DIRECTION_ARROWS: { value: GradientDirection; arrow: string }[] = [
@@ -13,9 +13,10 @@ interface GeneralTabProps {
onSave: (config: AppConfig) => void;
onClose: () => void;
onPreview: (background: BackgroundConfig) => void;
+ onConfigChange: (config: AppConfig) => void;
}
-export function GeneralTab({ config, onSave, onClose, onPreview }: GeneralTabProps) {
+export function GeneralTab({ config, onSave, onClose, onPreview, onConfigChange }: GeneralTabProps) {
const [imageUrl, setImageUrl] = useState(config.background?.imageUrl ?? '');
const [appliedImageUrl, setAppliedImageUrl] = useState(config.background?.imageUrl ?? '');
const [opacity, setOpacity] = useState(config.background?.opacity ?? 0.5);
@@ -27,6 +28,30 @@ export function GeneralTab({ config, onSave, onClose, onPreview }: GeneralTabPro
const hasImage = appliedImageUrl.trim() !== '';
+ // Push general-tab changes to the shared draft so they persist across tab switches
+ useEffect(() => {
+ const updated: AppConfig = {
+ ...config,
+ background: {
+ ...config.background,
+ imageUrl: appliedImageUrl || undefined,
+ opacity,
+ color,
+ gradient: {
+ enabled: gradientEnabled,
+ color2: gradientColor2,
+ direction: gradientDirection,
+ },
+ },
+ search: {
+ ...config.search,
+ enabled: config.search?.enabled ?? false,
+ placeholder: placeholder || undefined,
+ },
+ };
+ onConfigChange(updated);
+ }, [appliedImageUrl, opacity, color, gradientEnabled, gradientColor2, gradientDirection, placeholder]);// eslint-disable-line react-hooks/exhaustive-deps
+
const buildGradient = (updates: { enabled?: boolean; color2?: string; direction?: GradientDirection } = {}) => ({
enabled: updates.enabled ?? gradientEnabled,
color2: updates.color2 ?? gradientColor2,
@@ -79,7 +104,6 @@ export function GeneralTab({ config, onSave, onClose, onPreview }: GeneralTabPro
placeholder: placeholder || undefined,
},
});
- onClose();
};
return (
diff --git a/src/screens/ConfigEditorScreen/components/LinksTab.test.tsx b/src/screens/ConfigEditorScreen/components/LinksTab.test.tsx
index 7ad897b..a07d091 100644
--- a/src/screens/ConfigEditorScreen/components/LinksTab.test.tsx
+++ b/src/screens/ConfigEditorScreen/components/LinksTab.test.tsx
@@ -8,6 +8,7 @@ describe('LinksTab', () => {
config: mockConfig,
onSave: vi.fn(),
onClose: vi.fn(),
+ onConfigChange: vi.fn(),
};
beforeEach(() => {
@@ -122,7 +123,7 @@ describe('LinksTab', () => {
expect(screen.queryByText('Clear All Links', { selector: 'h2' })).not.toBeInTheDocument();
});
- it('renders a top Save button that saves and closes', async () => {
+ it('renders a top Save button that calls onSave', async () => {
const user = userEvent.setup();
render();
@@ -133,6 +134,5 @@ describe('LinksTab', () => {
await user.click(saveButtons[0]);
expect(defaultProps.onSave).toHaveBeenCalledTimes(1);
- expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
});
});
diff --git a/src/screens/ConfigEditorScreen/components/LinksTab.tsx b/src/screens/ConfigEditorScreen/components/LinksTab.tsx
index fe4e674..fe1adce 100644
--- a/src/screens/ConfigEditorScreen/components/LinksTab.tsx
+++ b/src/screens/ConfigEditorScreen/components/LinksTab.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import type { AppConfig, ModuleConfig } from '../../../types/config.ts';
const MAX_SECTION_NAME = 50;
@@ -15,15 +15,21 @@ interface LinksTabProps {
config: AppConfig;
onSave: (config: AppConfig) => void;
onClose: () => void;
+ onConfigChange: (config: AppConfig) => void;
}
-export function LinksTab({ config, onSave, onClose }: LinksTabProps) {
+export function LinksTab({ config, onSave, onClose, onConfigChange }: LinksTabProps) {
const [modules, setModules] = useState(
() => config.modules.map((m) => ({ ...m, links: m.links.map((l) => ({ ...l })) })),
);
const [submitted, setSubmitted] = useState(false);
const [showClearConfirm, setShowClearConfirm] = useState(false);
+ // Push link changes to the shared draft so they persist across tab switches
+ useEffect(() => {
+ onConfigChange({ ...config, modules });
+ }, [modules]); // eslint-disable-line react-hooks/exhaustive-deps
+
// --- Section handlers ---
const addSection = () => {
@@ -164,7 +170,6 @@ export function LinksTab({ config, onSave, onClose }: LinksTabProps) {
}));
onSave({ ...config, modules: cleaned });
- onClose();
};
const totalLinks = modules.reduce((sum, m) => sum + m.links.length, 0);