feat(login): custom HTTP headers on initial Add Server dialog (#1187) (#1188)

This commit is contained in:
cucadmuh
2026-06-25 13:44:50 +03:00
committed by GitHub
parent a88d5f3181
commit 4aa6427727
6 changed files with 278 additions and 131 deletions
+13 -115
View File
@@ -4,8 +4,10 @@ import type { CustomHeaderEntry, CustomHeadersApplyTo, ServerProfile } from '../
import { showToast } from '../../utils/ui/toast';
import {
DEFAULT_CUSTOM_HEADERS_APPLY_TO,
serverCustomHeadersFromForm,
validateCustomHeaders,
} from '../../utils/server/serverHttpHeaders';
import { CustomHttpHeadersEditor } from './CustomHttpHeadersEditor';
import {
decodeServerMagicString,
encodeServerMagicString,
@@ -193,19 +195,8 @@ export function AddServerForm({
return true;
};
const customHeadersPayload = (): Pick<
ServerProfile,
'customHeaders' | 'customHeadersApplyTo'
> => {
const rows = form.customHeaders
.map(h => ({ name: h.name.trim(), value: h.value }))
.filter(h => h.name || h.value);
if (!rows.length) return {};
return {
customHeaders: rows,
customHeadersApplyTo: form.customHeadersApplyTo,
};
};
const customHeadersPayload = () =>
serverCustomHeadersFromForm(form.customHeaders, form.customHeadersApplyTo);
const submit = async () => {
const ms = magicString.trim();
@@ -381,108 +372,15 @@ export function AddServerForm({
)}
</div>
</div>
<div className="form-group" style={{ marginBottom: '0.75rem' }}>
<button
type="button"
className="btn btn-ghost"
style={{ fontSize: 13, padding: '4px 0' }}
onClick={() => setForm(f => ({ ...f, customHeadersOpen: !f.customHeadersOpen }))}
>
{form.customHeadersOpen ? '▾' : '▸'} {t('settings.customHeadersTitle')}
</button>
{form.customHeadersOpen && (
<div style={{ marginTop: 8 }}>
<p style={{ fontSize: 11, opacity: 0.75, margin: '0 0 8px' }}>
{t('settings.customHeadersHelp')}
</p>
{form.customHeaders.map((row, index) => (
<div key={index} className="form-row" style={{ marginBottom: 6, gap: 8 }}>
<input
className="input"
type="text"
value={row.name}
onChange={e => {
const name = e.target.value;
setForm(f => {
const customHeaders = f.customHeaders.map((h, i) =>
i === index ? { ...h, name } : h,
);
return { ...f, customHeaders };
});
}}
placeholder={t('settings.customHeadersNamePlaceholder')}
autoComplete="off"
/>
<input
className="input"
type="password"
value={row.value}
onChange={e => {
const value = e.target.value;
setForm(f => ({
...f,
customHeaders: f.customHeaders.map((h, i) =>
i === index ? { ...h, value } : h,
),
}));
}}
placeholder={t('settings.customHeadersValuePlaceholder')}
autoComplete="off"
/>
<button
type="button"
className="btn btn-ghost"
aria-label={t('settings.customHeadersRemoveRow')}
onClick={() =>
setForm(f => ({
...f,
customHeaders:
f.customHeaders.length <= 1
? [{ name: '', value: '' }]
: f.customHeaders.filter((_, i) => i !== index),
}))
}
>
×
</button>
</div>
))}
<button
type="button"
className="btn btn-ghost"
style={{ fontSize: 12, marginBottom: 8 }}
onClick={() =>
setForm(f => ({
...f,
customHeaders: [...f.customHeaders, { name: '', value: '' }],
}))
}
>
{t('settings.customHeadersAddRow')}
</button>
<fieldset
disabled={!form.customHeaders.some(h => h.name.trim() || h.value)}
style={{ border: 'none', padding: 0, margin: 0 }}
>
<legend style={{ fontSize: 12, marginBottom: 4 }}>{t('settings.customHeadersApplyTo')}</legend>
{(['public', 'local', 'both'] as const).map(kind => (
<label key={kind} style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>
<input
type="radio"
name="customHeadersApplyTo"
checked={form.customHeadersApplyTo === kind}
onChange={() => setForm(f => ({ ...f, customHeadersApplyTo: kind }))}
/>{' '}
{t(`settings.customHeadersApplyTo_${kind}`)}
</label>
))}
</fieldset>
<p style={{ fontSize: 11, opacity: 0.65, marginTop: 6 }}>
{t('settings.customHeadersNotInShare')}
</p>
</div>
)}
</div>
<CustomHttpHeadersEditor
headers={form.customHeaders}
applyTo={form.customHeadersApplyTo}
open={form.customHeadersOpen}
onOpenChange={open => setForm(f => ({ ...f, customHeadersOpen: open }))}
onHeadersChange={customHeaders => setForm(f => ({ ...f, customHeaders }))}
onApplyToChange={customHeadersApplyTo => setForm(f => ({ ...f, customHeadersApplyTo }))}
radioGroupName="addServerCustomHeadersApplyTo"
/>
{!isEdit && (
<div className="form-group" style={{ marginBottom: '0.75rem' }}>
<label style={{ fontSize: 13 }}>{t('login.orMagicString')}</label>
@@ -0,0 +1,114 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import type { CustomHeaderEntry, CustomHeadersApplyTo } from '../../store/authStoreTypes';
export type CustomHttpHeadersEditorProps = {
headers: CustomHeaderEntry[];
applyTo: CustomHeadersApplyTo;
open: boolean;
onOpenChange: (open: boolean) => void;
onHeadersChange: (headers: CustomHeaderEntry[]) => void;
onApplyToChange: (applyTo: CustomHeadersApplyTo) => void;
/** Optional id prefix for radio group name (avoid collisions when multiple forms mount). */
radioGroupName?: string;
};
export function CustomHttpHeadersEditor({
headers,
applyTo,
open,
onOpenChange,
onHeadersChange,
onApplyToChange,
radioGroupName = 'customHeadersApplyTo',
}: CustomHttpHeadersEditorProps) {
const { t } = useTranslation();
return (
<div className="form-group" style={{ marginBottom: '0.75rem' }}>
<button
type="button"
className="btn btn-ghost"
style={{ fontSize: 13, padding: '4px 0' }}
onClick={() => onOpenChange(!open)}
>
{open ? '▾' : '▸'} {t('settings.customHeadersTitle')}
</button>
{open && (
<div style={{ marginTop: 8 }}>
<p style={{ fontSize: 11, opacity: 0.75, margin: '0 0 8px' }}>
{t('settings.customHeadersHelp')}
</p>
{headers.map((row, index) => (
<div key={index} className="form-row" style={{ marginBottom: 6, gap: 8 }}>
<input
className="input"
type="text"
value={row.name}
onChange={e => {
const name = e.target.value;
onHeadersChange(headers.map((h, i) => (i === index ? { ...h, name } : h)));
}}
placeholder={t('settings.customHeadersNamePlaceholder')}
autoComplete="off"
/>
<input
className="input"
type="password"
value={row.value}
onChange={e => {
const value = e.target.value;
onHeadersChange(headers.map((h, i) => (i === index ? { ...h, value } : h)));
}}
placeholder={t('settings.customHeadersValuePlaceholder')}
autoComplete="off"
/>
<button
type="button"
className="btn btn-ghost"
aria-label={t('settings.customHeadersRemoveRow')}
onClick={() =>
onHeadersChange(
headers.length <= 1
? [{ name: '', value: '' }]
: headers.filter((_, i) => i !== index),
)
}
>
×
</button>
</div>
))}
<button
type="button"
className="btn btn-ghost"
style={{ fontSize: 12, marginBottom: 8 }}
onClick={() => onHeadersChange([...headers, { name: '', value: '' }])}
>
{t('settings.customHeadersAddRow')}
</button>
<fieldset
disabled={!headers.some(h => h.name.trim() || h.value)}
style={{ border: 'none', padding: 0, margin: 0 }}
>
<legend style={{ fontSize: 12, marginBottom: 4 }}>{t('settings.customHeadersApplyTo')}</legend>
{(['public', 'local', 'both'] as const).map(kind => (
<label key={kind} style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>
<input
type="radio"
name={radioGroupName}
checked={applyTo === kind}
onChange={() => onApplyToChange(kind)}
/>{' '}
{t(`settings.customHeadersApplyTo_${kind}`)}
</label>
))}
</fieldset>
<p style={{ fontSize: 11, opacity: 0.65, marginTop: 6 }}>
{t('settings.customHeadersNotInShare')}
</p>
</div>
)}
</div>
);
}