mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
Add auto-redact to V2 (#4417)
# Description of Changes Adds auto-redact tool to V2, with manual-redact in the UI but explicitly disabled. Also creates a shared component for the large buttons we're using in a couple different tools and uses consistently.
This commit is contained in:
216
frontend/src/components/shared/ButtonSelector.test.tsx
Normal file
216
frontend/src/components/shared/ButtonSelector.test.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import ButtonSelector from './ButtonSelector';
|
||||
|
||||
// Wrapper component to provide Mantine context
|
||||
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<MantineProvider>{children}</MantineProvider>
|
||||
);
|
||||
|
||||
describe('ButtonSelector', () => {
|
||||
const mockOnChange = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render all options as buttons', () => {
|
||||
const options = [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ButtonSelector
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
options={options}
|
||||
label="Test Label"
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test Label')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should highlight selected button with filled variant', () => {
|
||||
const options = [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ButtonSelector
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
options={options}
|
||||
label="Selection Label"
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const selectedButton = screen.getByRole('button', { name: 'Option 1' });
|
||||
const unselectedButton = screen.getByRole('button', { name: 'Option 2' });
|
||||
|
||||
// Check data-variant attribute for filled/outline
|
||||
expect(selectedButton).toHaveAttribute('data-variant', 'filled');
|
||||
expect(unselectedButton).toHaveAttribute('data-variant', 'outline');
|
||||
expect(screen.getByText('Selection Label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call onChange when button is clicked', () => {
|
||||
const options = [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ButtonSelector
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
options={options}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Option 2' }));
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('option2');
|
||||
});
|
||||
|
||||
test('should handle undefined value (no selection)', () => {
|
||||
const options = [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ButtonSelector
|
||||
value={undefined}
|
||||
onChange={mockOnChange}
|
||||
options={options}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Both buttons should be outlined when no value is selected
|
||||
const button1 = screen.getByRole('button', { name: 'Option 1' });
|
||||
const button2 = screen.getByRole('button', { name: 'Option 2' });
|
||||
|
||||
expect(button1).toHaveAttribute('data-variant', 'outline');
|
||||
expect(button2).toHaveAttribute('data-variant', 'outline');
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
description: 'disable buttons when disabled prop is true',
|
||||
options: [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
],
|
||||
globalDisabled: true,
|
||||
expectedStates: [true, true],
|
||||
},
|
||||
{
|
||||
description: 'disable individual options when option.disabled is true',
|
||||
options: [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2', disabled: true },
|
||||
],
|
||||
globalDisabled: false,
|
||||
expectedStates: [false, true],
|
||||
},
|
||||
])('should $description', ({ options, globalDisabled, expectedStates }) => {
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ButtonSelector
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
options={options}
|
||||
disabled={globalDisabled}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
options.forEach((option, index) => {
|
||||
const button = screen.getByRole('button', { name: option.label });
|
||||
expect(button).toHaveProperty('disabled', expectedStates[index]);
|
||||
});
|
||||
});
|
||||
|
||||
test('should not call onChange when disabled button is clicked', () => {
|
||||
const options = [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2', disabled: true },
|
||||
];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ButtonSelector
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
options={options}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Option 2' }));
|
||||
|
||||
expect(mockOnChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not apply fullWidth styling when fullWidth is false', () => {
|
||||
const options = [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
];
|
||||
|
||||
render(
|
||||
<TestWrapper>
|
||||
<ButtonSelector
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
options={options}
|
||||
fullWidth={false}
|
||||
label="Layout Label"
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Option 1' });
|
||||
expect(button).not.toHaveStyle({ flex: '1' });
|
||||
expect(screen.getByText('Layout Label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not render label element when not provided', () => {
|
||||
const options = [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
];
|
||||
|
||||
const { container } = render(
|
||||
<TestWrapper>
|
||||
<ButtonSelector
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
options={options}
|
||||
/>
|
||||
</TestWrapper>
|
||||
);
|
||||
|
||||
// Should render buttons
|
||||
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
||||
|
||||
// Stack should only contain the Group (buttons), no Text element for label
|
||||
const stackElement = container.querySelector('[class*="mantine-Stack-root"]');
|
||||
expect(stackElement?.children).toHaveLength(1); // Only the Group, no label Text
|
||||
});
|
||||
});
|
||||
59
frontend/src/components/shared/ButtonSelector.tsx
Normal file
59
frontend/src/components/shared/ButtonSelector.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Button, Group, Stack, Text } from "@mantine/core";
|
||||
|
||||
export interface ButtonOption<T> {
|
||||
value: T;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface ButtonSelectorProps<T> {
|
||||
value: T | undefined;
|
||||
onChange: (value: T) => void;
|
||||
options: ButtonOption<T>[];
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const ButtonSelector = <T extends string>({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
label = undefined,
|
||||
disabled = false,
|
||||
fullWidth = true,
|
||||
}: ButtonSelectorProps<T>) => {
|
||||
return (
|
||||
<Stack gap='var(--mantine-spacing-sm)'>
|
||||
{/* Label (if it exists) */}
|
||||
{label && <Text style={{
|
||||
fontSize: "var(--mantine-font-size-sm)",
|
||||
lineHeight: "var(--mantine-line-height-sm)",
|
||||
fontWeight: "var(--font-weight-medium)",
|
||||
}}>{label}</Text>}
|
||||
|
||||
{/* Buttons */}
|
||||
<Group gap='4px'>
|
||||
{options.map((option) => (
|
||||
<Button
|
||||
key={option.value}
|
||||
variant={value === option.value ? 'filled' : 'outline'}
|
||||
color={value === option.value ? 'var(--color-primary-500)' : 'var(--text-muted)'}
|
||||
onClick={() => onChange(option.value)}
|
||||
disabled={disabled || option.disabled}
|
||||
style={{
|
||||
flex: fullWidth ? 1 : undefined,
|
||||
height: 'auto',
|
||||
minHeight: '2.5rem',
|
||||
fontSize: 'var(--mantine-font-size-sm)'
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ButtonSelector;
|
||||
Reference in New Issue
Block a user