Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/screens/ConfigEditorScreen/ConfigEditorScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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(<ConfigEditor {...defaultProps} onSave={onSave} />);

// 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(<ConfigEditor {...defaultProps} onSave={onSave} />);

// 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);
});
});
27 changes: 23 additions & 4 deletions src/screens/ConfigEditorScreen/ConfigEditorScreen.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -49,12 +49,25 @@ interface ConfigEditorProps {

export function ConfigEditor({ config, onSave, onClose, onPreview }: ConfigEditorProps) {
const [tab, setTab] = useState<Tab>('general');
const [draftConfig, setDraftConfig] = useState<AppConfig>(() => ({
...config,
modules: config.modules.map((m) => ({ ...m, links: m.links.map((l) => ({ ...l })) })),
}));
const [activePanel, setActivePanel] = useState<ImportExportPanel>('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');
Expand Down Expand Up @@ -166,15 +179,21 @@ export function ConfigEditor({ config, onSave, onClose, onPreview }: ConfigEdito

{tab === 'general' && (
<GeneralTab
config={config}
onSave={onSave}
config={draftConfig}
onSave={handleSave}
onClose={onClose}
onPreview={onPreview}
onConfigChange={handleConfigChange}
/>
)}

{tab === 'links' && (
<LinksTab config={config} onSave={onSave} onClose={onClose} />
<LinksTab
config={draftConfig}
onSave={handleSave}
onClose={onClose}
onConfigChange={handleConfigChange}
/>
)}

{activePanel !== 'none' && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('GeneralTab', () => {
onSave: vi.fn(),
onClose: vi.fn(),
onPreview: vi.fn(),
onConfigChange: vi.fn(),
};

beforeEach(() => {
Expand Down
30 changes: 27 additions & 3 deletions src/screens/ConfigEditorScreen/components/GeneralTab.tsx
Original file line number Diff line number Diff line change
@@ -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 }[] = [
Expand All @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -79,7 +104,6 @@ export function GeneralTab({ config, onSave, onClose, onPreview }: GeneralTabPro
placeholder: placeholder || undefined,
},
});
onClose();
};

return (
Expand Down
4 changes: 2 additions & 2 deletions src/screens/ConfigEditorScreen/components/LinksTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ describe('LinksTab', () => {
config: mockConfig,
onSave: vi.fn(),
onClose: vi.fn(),
onConfigChange: vi.fn(),
};

beforeEach(() => {
Expand Down Expand Up @@ -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(<LinksTab {...defaultProps} />);

Expand All @@ -133,6 +134,5 @@ describe('LinksTab', () => {
await user.click(saveButtons[0]);

expect(defaultProps.onSave).toHaveBeenCalledTimes(1);
expect(defaultProps.onClose).toHaveBeenCalledTimes(1);
});
});
11 changes: 8 additions & 3 deletions src/screens/ConfigEditorScreen/components/LinksTab.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<ModuleConfig[]>(
() => 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 = () => {
Expand Down Expand Up @@ -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);
Expand Down