feat: Add nesting of tags in a tree
- levels are joined by forward slashes in the keywords - behaviour is opt-in for now - long paths are shortened if there is not enough display room Closes #687.
This commit is contained in:
@@ -3,14 +3,15 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { KeywordSettings } from '../keyword-settings';
|
||||
import { useSettingsStore, DEFAULT_KEYWORDS } from '@/stores/settings-store';
|
||||
|
||||
// Mock SettingsSection to just render children
|
||||
vi.mock('../settings-section', () => ({
|
||||
// Mock SettingsSection to just render children, keeping the real controls
|
||||
vi.mock('../settings-section', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('../settings-section')>()),
|
||||
SettingsSection: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
describe('KeywordSettings', () => {
|
||||
beforeEach(() => {
|
||||
useSettingsStore.setState({ emailKeywords: [...DEFAULT_KEYWORDS] });
|
||||
useSettingsStore.setState({ emailKeywords: [...DEFAULT_KEYWORDS], nestedTags: false });
|
||||
});
|
||||
|
||||
it('renders all default keywords', () => {
|
||||
@@ -139,4 +140,74 @@ describe('KeywordSettings', () => {
|
||||
expect(added.id).toBe('my-custom-tag');
|
||||
expect(added.label).toBe('My Custom Tag!');
|
||||
});
|
||||
|
||||
it('offers no parent picker while nesting is off', () => {
|
||||
render(<KeywordSettings />);
|
||||
fireEvent.click(screen.getByText('add_keyword'));
|
||||
|
||||
expect(screen.queryByLabelText('parent_field')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('nests a new tag under the selected parent', () => {
|
||||
useSettingsStore.setState({
|
||||
emailKeywords: [{ id: 'work', label: 'Work', color: 'blue' }],
|
||||
nestedTags: true,
|
||||
});
|
||||
render(<KeywordSettings />);
|
||||
fireEvent.click(screen.getByText('add_keyword'));
|
||||
|
||||
fireEvent.change(screen.getByLabelText('parent_field'), { target: { value: 'work' } });
|
||||
fireEvent.change(screen.getByPlaceholderText('label_placeholder'), { target: { value: 'Clients' } });
|
||||
fireEvent.click(screen.getByText('add'));
|
||||
|
||||
const keywords = useSettingsStore.getState().emailKeywords;
|
||||
expect(keywords[keywords.length - 1]).toMatchObject({ id: 'work/clients', label: 'Clients' });
|
||||
});
|
||||
|
||||
it('shows nested tags by their full path', () => {
|
||||
useSettingsStore.setState({
|
||||
emailKeywords: [
|
||||
{ id: 'work', label: 'Work', color: 'blue' },
|
||||
{ id: 'work/clients', label: 'Clients', color: 'green' },
|
||||
],
|
||||
nestedTags: true,
|
||||
});
|
||||
render(<KeywordSettings />);
|
||||
|
||||
expect(screen.getByText('Work/Clients')).toBeInTheDocument();
|
||||
expect(screen.getByText('$label:work/clients')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('rejects a path that would exceed the keyword length limit', () => {
|
||||
const deepId = 'a'.repeat(240);
|
||||
useSettingsStore.setState({
|
||||
emailKeywords: [{ id: deepId, label: 'Deep', color: 'blue' }],
|
||||
nestedTags: true,
|
||||
});
|
||||
render(<KeywordSettings />);
|
||||
fireEvent.click(screen.getByText('add_keyword'));
|
||||
|
||||
fireEvent.change(screen.getByLabelText('parent_field'), { target: { value: deepId } });
|
||||
fireEvent.change(screen.getByPlaceholderText('label_placeholder'), { target: { value: 'Overflowing name' } });
|
||||
|
||||
expect(screen.getByText('too_long')).toBeInTheDocument();
|
||||
expect(screen.getByText('add').closest('button')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('locks the name and the delete action of a tag that has nested tags', () => {
|
||||
useSettingsStore.setState({
|
||||
emailKeywords: [
|
||||
{ id: 'work', label: 'Work', color: 'blue' },
|
||||
{ id: 'work/clients', label: 'Clients', color: 'green' },
|
||||
],
|
||||
nestedTags: true,
|
||||
});
|
||||
render(<KeywordSettings />);
|
||||
|
||||
expect(screen.getByTitle('has_children_delete')).toBeDisabled();
|
||||
|
||||
fireEvent.click(screen.getAllByTitle('edit')[0]);
|
||||
expect(screen.getByDisplayValue('Work')).toBeDisabled();
|
||||
expect(screen.getByText('has_children_locked')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user