Remote path mappings, Client handling improvements (#481)

This commit is contained in:
Alex
2026-01-17 14:52:06 +00:00
committed by GitHub
parent fd74021594
commit 5a6db5f8a8
26 changed files with 1501 additions and 155 deletions
@@ -14,6 +14,7 @@ import {
ActionButtonConfig,
HeadingFieldConfig,
ShowWhenCondition,
TableFieldConfig,
} from '../../types/settings';
import { FieldWrapper } from './shared';
import {
@@ -26,6 +27,7 @@ import {
OrderableListField,
ActionButton,
HeadingField,
TableField,
} from './fields';
interface SettingsContentProps {
@@ -207,6 +209,15 @@ const renderField = (
);
case 'ActionButton':
return <ActionButton field={field as ActionButtonConfig} onAction={onAction} disabled={isDisabled} />;
case 'TableField':
return (
<TableField
field={field as TableFieldConfig}
value={(value as Record<string, unknown>[]) ?? []}
onChange={onChange}
disabled={isDisabled}
/>
);
case 'HeadingField':
return <HeadingField field={field as HeadingFieldConfig} />;
default:
@@ -0,0 +1,208 @@
import { useMemo } from 'react';
import { TableFieldConfig, TableFieldColumn } from '../../../types/settings';
import { DropdownList } from '../../DropdownList';
interface TableFieldProps {
field: TableFieldConfig;
value: Record<string, unknown>[];
onChange: (value: Record<string, unknown>[]) => void;
disabled?: boolean;
}
function defaultCellValue(column: TableFieldColumn): unknown {
if (column.defaultValue !== undefined) {
return column.defaultValue;
}
if (column.type === 'checkbox') {
return false;
}
return '';
}
function normalizeRows(rows: Record<string, unknown>[], columns: TableFieldColumn[]): Record<string, unknown>[] {
return (rows ?? []).map((row) => {
const normalized: Record<string, unknown> = { ...row };
for (const col of columns) {
if (!(col.key in normalized)) {
normalized[col.key] = defaultCellValue(col);
}
}
return normalized;
});
}
export const TableField = ({ field, value, onChange, disabled }: TableFieldProps) => {
const isDisabled = disabled ?? false;
const columns = useMemo(() => field.columns ?? [], [field.columns]);
const rows = useMemo(() => normalizeRows(value ?? [], columns), [value, columns]);
const updateCell = (rowIndex: number, key: string, cellValue: unknown) => {
const next = rows.map((row, idx) => (idx === rowIndex ? { ...row, [key]: cellValue } : row));
onChange(next);
};
const addRow = () => {
const newRow: Record<string, unknown> = {};
columns.forEach((col) => {
newRow[col.key] = defaultCellValue(col);
});
onChange([...(rows ?? []), newRow]);
};
const removeRow = (rowIndex: number) => {
const next = rows.filter((_, idx) => idx !== rowIndex);
onChange(next);
};
if (rows.length === 0) {
return (
<div className="space-y-3">
{field.emptyMessage && <p className="text-sm opacity-70">{field.emptyMessage}</p>}
<button
type="button"
onClick={addRow}
disabled={isDisabled}
className="px-3 py-2 rounded-lg text-sm font-medium
bg-[var(--bg-soft)] border border-[var(--border-muted)]
hover:bg-[var(--hover-surface)] transition-colors
disabled:opacity-60 disabled:cursor-not-allowed"
>
{field.addLabel || 'Add'}
</button>
</div>
);
}
// Use minmax(0, ...) so the grid can shrink inside the settings modal.
const gridTemplate = 'sm:grid-cols-[minmax(0,180px)_minmax(0,1fr)_minmax(0,1fr)_auto]';
return (
<div className="space-y-3 min-w-0">
<div className={`hidden sm:grid ${gridTemplate} gap-2 px-1 text-xs font-medium opacity-70`}>
{columns.map((col) => (
<div key={col.key} className="truncate">
{col.label}
</div>
))}
<div />
</div>
<div className="space-y-3 min-w-0">
{rows.map((row, rowIndex) => (
<div
key={rowIndex}
className={`grid grid-cols-1 ${gridTemplate} gap-3 items-start min-w-0`}
style={{ overflow: 'visible' }}
>
{columns.map((col) => {
const cellValue = row[col.key];
const mobileLabel = <div className="sm:hidden text-xs font-medium opacity-70">{col.label}</div>;
if (col.type === 'checkbox') {
return (
<div key={col.key} className="flex flex-col gap-1 min-w-0">
{mobileLabel}
<div className="pt-2">
<input
type="checkbox"
checked={Boolean(cellValue)}
onChange={(e) => updateCell(rowIndex, col.key, e.target.checked)}
disabled={isDisabled}
className="h-4 w-4 rounded border-gray-300 text-sky-600 focus:ring-sky-500
disabled:opacity-60 disabled:cursor-not-allowed"
/>
</div>
</div>
);
}
if (col.type === 'select') {
const options = (col.options ?? []).map((opt) => ({
value: String(opt.value),
label: opt.label,
description: opt.description,
}));
return (
<div key={col.key} className="flex flex-col gap-1 min-w-0">
{mobileLabel}
{isDisabled ? (
<div className="w-full px-3 py-2 rounded-lg border border-[var(--border-muted)] bg-[var(--bg-soft)] text-sm opacity-60 cursor-not-allowed">
{options.find((o) => o.value === String(cellValue ?? ''))?.label || 'Select...'}
</div>
) : (
<DropdownList
options={options}
value={String(cellValue ?? '')}
onChange={(val) => updateCell(rowIndex, col.key, Array.isArray(val) ? val[0] : val)}
placeholder={col.placeholder || 'Select...'}
widthClassName="w-full"
/>
)}
</div>
);
}
// text/path
return (
<div key={col.key} className="flex flex-col gap-1 min-w-0">
{mobileLabel}
<input
type="text"
value={String(cellValue ?? '')}
onChange={(e) => updateCell(rowIndex, col.key, e.target.value)}
placeholder={col.placeholder}
disabled={isDisabled}
className="w-full px-3 py-2 rounded-lg border border-[var(--border-muted)]
bg-[var(--bg-soft)] text-sm
focus:outline-none focus:ring-2 focus:ring-sky-500/50 focus:border-sky-500
disabled:opacity-60 disabled:cursor-not-allowed
transition-colors"
/>
</div>
);
})}
<div className="flex items-center justify-center">
<button
type="button"
onClick={() => removeRow(rowIndex)}
disabled={isDisabled}
className="p-2 rounded hover:bg-[var(--hover-surface)]
disabled:opacity-60 disabled:cursor-not-allowed"
aria-label="Remove row"
>
<svg
className="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="col-span-full border-t border-[var(--border-muted)] opacity-60" />
</div>
))}
</div>
<button
type="button"
onClick={addRow}
disabled={isDisabled}
className="px-3 py-2 rounded-lg text-sm font-medium
bg-[var(--bg-soft)] border border-[var(--border-muted)]
hover:bg-[var(--hover-surface)] transition-colors
disabled:opacity-60 disabled:cursor-not-allowed"
>
{field.addLabel || 'Add'}
</button>
</div>
);
};
@@ -6,4 +6,5 @@ export { SelectField } from './SelectField';
export { MultiSelectField } from './MultiSelectField';
export { OrderableListField } from './OrderableListField';
export { ActionButton } from './ActionButton';
export { TableField } from './TableField';
export { HeadingField } from './HeadingField';
+5
View File
@@ -37,6 +37,11 @@ function getFieldValue(field: SettingsField): unknown {
if (field.type === 'ActionButton' || field.type === 'HeadingField') {
return undefined;
}
if (field.type === 'TableField') {
return (field as unknown as { value?: unknown }).value ?? [];
}
// All other fields have a value property
return field.value ?? '';
}
+27
View File
@@ -8,6 +8,7 @@ export type FieldType =
| 'SelectField'
| 'MultiSelectField'
| 'OrderableListField'
| 'TableField'
| 'ActionButton'
| 'HeadingField';
@@ -118,6 +119,31 @@ export interface ActionButtonConfig extends BaseField {
style: 'default' | 'primary' | 'danger';
}
export interface TableFieldColumnOption {
value: string;
label: string;
description?: string;
}
export type TableFieldColumnType = 'text' | 'select' | 'checkbox' | 'path';
export interface TableFieldColumn {
key: string;
label: string;
type: TableFieldColumnType;
placeholder?: string;
options?: TableFieldColumnOption[];
defaultValue?: string | boolean;
}
export interface TableFieldConfig extends BaseField {
type: 'TableField';
value: Record<string, unknown>[];
columns: TableFieldColumn[];
addLabel?: string;
emptyMessage?: string;
}
export interface HeadingFieldConfig {
key: string;
type: 'HeadingField';
@@ -138,6 +164,7 @@ export type SettingsField =
| SelectFieldConfig
| MultiSelectFieldConfig
| OrderableListFieldConfig
| TableFieldConfig
| ActionButtonConfig
| HeadingFieldConfig;