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
98 changes: 72 additions & 26 deletions src/options/components/RuleEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ interface RuleEditorProps {
onCancel: () => void;
}

const hasWhitespace = (value: string) => value !== value.trim();

export function RuleEditor({ rule, onSave, onCancel }: RuleEditorProps) {
const [headerValueWarnings, setHeaderValueWarnings] = useState<Record<string, boolean>>({});

const [formData, setFormData] = useState<ModificationRule>(() => {
if (rule) {
return rule;
Expand Down Expand Up @@ -72,6 +76,14 @@ export function RuleEditor({ rule, onSave, onCancel }: RuleEditorProps) {
h.id === id ? { ...h, [field]: value } : h
),
});

// Update whitespace warning for header values
if (field === 'value') {
setHeaderValueWarnings(prev => ({
...prev,
[id]: hasWhitespace(value)
}));
}
};

const removeHeader = (id: string) => {
Expand All @@ -83,19 +95,43 @@ export function RuleEditor({ rule, onSave, onCancel }: RuleEditorProps) {

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name) {

const trimmedName = formData.name.trim();
const trimmedTabUrl = formData.tabUrl?.trim() || '';

if (!trimmedName) {
showError('Please fill in the rule name');
return;
}
if (formData.targetDomains.length === 0) {
showError('Please add at least one target domain');
return;
}
if (formData.targetDomains.some(d => !d.url)) {

// Trim target domains URLs
const trimmedDomains = formData.targetDomains.map(d => ({
...d,
url: d.url.trim()
}));

if (trimmedDomains.some(d => !d.url)) {
showError('All target domains must have a URL');
return;
}
onSave(formData);

// Trim header names only, keep values as-is
const processedHeaders = formData.headers.map(h => ({
...h,
name: h.name.trim()
}));

onSave({
...formData,
name: trimmedName,
tabUrl: trimmedTabUrl,
targetDomains: trimmedDomains,
headers: processedHeaders
});
};

return (
Expand Down Expand Up @@ -178,29 +214,39 @@ export function RuleEditor({ rule, onSave, onCancel }: RuleEditorProps) {
<div className="headers-section">
<h4>HTTP Headers</h4>
{formData.headers.map((header) => (
<div key={header.id} className="header-item">
<input
data-testid="header-name-input"
type="text"
value={header.name}
onChange={(e) => updateHeader(header.id, 'name', e.target.value)}
placeholder="Header name"
/>
<input
data-testid="header-value-input"
type="text"
value={header.value}
onChange={(e) => updateHeader(header.id, 'value', e.target.value)}
placeholder="Value"
/>
<button
type="button"
className="btn-icon"
onClick={() => removeHeader(header.id)}
title="Remove header"
>
</button>
<div key={header.id}>
<div className="header-item">
<input
data-testid="header-name-input"
type="text"
value={header.name}
onChange={(e) => updateHeader(header.id, 'name', e.target.value)}
placeholder="Header name"
/>
<input
data-testid="header-value-input"
type="text"
value={header.value}
onChange={(e) => updateHeader(header.id, 'value', e.target.value)}
placeholder="Value"
/>
<button
type="button"
className="btn-icon"
onClick={() => removeHeader(header.id)}
title="Remove header"
>
</button>
</div>
{headerValueWarnings[header.id] && (
<div
data-testid="header-value-whitespace-warning"
className="text-xs text-amber-600 dark:text-amber-400 mt-1 ml-1"
>
Value contains leading or trailing spaces
</div>
)}
</div>
))}
<button
Expand Down
35 changes: 26 additions & 9 deletions src/options/components/VariableEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function VariableEditor({
}: VariableEditorProps) {
const [editName, setEditName] = useState('');
const [editValue, setEditValue] = useState('');
const [valueHasWhitespace, setValueHasWhitespace] = useState(false);
const [editIsSensitive, setEditIsSensitive] = useState(false);
const [editRefreshUrl, setEditRefreshUrl] = useState('');
const [editRefreshMethod, setEditRefreshMethod] = useState<string>('POST');
Expand All @@ -30,6 +31,7 @@ export function VariableEditor({
if (variable) {
setEditName(variable.name);
setEditValue(variable.value);
setValueHasWhitespace(variable.value !== variable.value.trim());
setEditIsSensitive(variable.isSensitive || false);

if (variable.refreshConfig) {
Expand Down Expand Up @@ -69,6 +71,7 @@ export function VariableEditor({
// Reset for adding new variable
setEditName('');
setEditValue('');
setValueHasWhitespace(false);
setEditIsSensitive(false);
setEditRefreshUrl('');
setEditRefreshMethod('POST');
Expand All @@ -79,23 +82,26 @@ export function VariableEditor({
}, [variable]);

const handleSave = () => {
if (!editName.trim()) {
const trimmedName = editName.trim();

if (!trimmedName) {
showError('Variable name cannot be empty');
return;
}

// Check for duplicate names (excluding current variable being edited)
// Check for duplicate names with trimmed values
const duplicate = existingVariables.find(
v => v.name === editName && v.id !== variable?.id
v => v.name.trim() === trimmedName && v.id !== variable?.id
);
if (duplicate) {
showError(`Variable "${editName}" already exists`);
showError(`Variable "${trimmedName}" already exists`);
return;
}

// Build refresh config from separate fields
const trimmedRefreshUrl = editRefreshUrl.trim();
let refreshConfig: RefreshConfig | undefined;
if (editRefreshUrl.trim()) {
if (trimmedRefreshUrl) {
// Build headers object
const headers: Record<string, string> = {};
editRefreshHeaders.forEach(header => {
Expand All @@ -117,7 +123,7 @@ export function VariableEditor({

// Build complete refresh config
refreshConfig = {
url: editRefreshUrl,
url: trimmedRefreshUrl,
method: editRefreshMethod as RefreshConfig['method'],
headers: Object.keys(headers).length > 0 ? headers : undefined,
body,
Expand All @@ -130,8 +136,8 @@ export function VariableEditor({

const savedVariable: Variable = {
id: variable?.id || generateId(),
name: editName,
value: editValue,
name: trimmedName,
value: editValue, // NOT trimmed - user choice
isSensitive: editIsSensitive,
refreshConfig,
};
Expand All @@ -154,11 +160,22 @@ export function VariableEditor({
type={editIsSensitive ? 'password' : 'text'}
placeholder="Variable value"
value={editValue}
onChange={e => setEditValue(e.target.value)}
onChange={e => {
setEditValue(e.target.value);
setValueHasWhitespace(e.target.value !== e.target.value.trim());
}}
className="flex-1 px-2.5 py-2 border border-[#bdc3c7] rounded text-sm
dark:border-[#404040] dark:bg-[#2d2d2d] dark:text-[#e4e4e4]"
/>
</div>
{valueHasWhitespace && (
<div
data-testid="variable-value-whitespace-warning"
className="text-xs text-amber-600 dark:text-amber-400 mb-2.5"
>
Value contains leading or trailing spaces
</div>
)}
<div className="mb-2.5">
<label className="flex items-center gap-2 cursor-pointer">
<input
Expand Down
Loading