Files
BanGUI/frontend/src/components/config/RegexList.tsx

134 lines
3.6 KiB
TypeScript

/**
* RegexList — editable list of regex patterns.
*
* Renders a list of monospace inputs with add/delete controls.
* Used in jail config panels and the filter form.
*/
import { useCallback, useMemo, useRef, useState } from "react";
import { Button, Input, Text } from "@fluentui/react-components";
import { Dismiss24Regular } from "@fluentui/react-icons";
import { useConfigStyles } from "./configStyles";
import {
createStableStringEntry,
reconcileStableStringEntries,
StableStringEntry,
} from "./stableListEntries";
export interface RegexListProps {
/** Section label displayed above the list. */
label: string;
/** Current list of regex patterns. */
patterns: string[];
/** Called when the list changes (add, delete, or edit). */
onChange: (next: string[]) => void;
/** When true, patterns are displayed read-only with no add/delete controls. */
readOnly?: boolean;
}
/**
* Renders an editable list of regex patterns with add and delete controls.
*
* @param props - Component props.
* @returns JSX element.
*/
export function RegexList({
label,
patterns,
onChange,
readOnly = false,
}: RegexListProps): React.JSX.Element {
const styles = useConfigStyles();
const entriesRef = useRef<StableStringEntry[]>(
patterns.map(createStableStringEntry),
);
const entries = useMemo(() => {
if (
entriesRef.current.length === patterns.length &&
entriesRef.current.every((entry, index) => entry.value === patterns[index])
) {
return entriesRef.current;
}
const reconciled = reconcileStableStringEntries(entriesRef.current, patterns);
entriesRef.current = reconciled;
return reconciled;
}, [patterns]);
const [newPattern, setNewPattern] = useState("");
const handleAdd = useCallback(() => {
const p = newPattern.trim();
if (p) {
onChange([...patterns, p]);
setNewPattern("");
}
}, [newPattern, patterns, onChange]);
const handleDelete = useCallback(
(idx: number) => {
onChange(patterns.filter((_, i) => i !== idx));
},
[patterns, onChange],
);
return (
<div>
<Text size={200} weight="semibold">
{label}
</Text>
{patterns.length === 0 && (
<Text className={styles.infoText} size={200}>
{" "}
(none)
</Text>
)}
{entries.map(({ id, value }, i) => (
<div key={id} className={styles.regexItem}>
<Input
className={styles.regexInput}
value={value}
readOnly={readOnly}
aria-label={`${label} pattern ${String(i + 1)}`}
onChange={(_e, d) => {
const next = [...patterns];
next[i] = d.value;
onChange(next);
}}
/>
{!readOnly && (
<Button
appearance="subtle"
icon={<Dismiss24Regular />}
size="small"
aria-label={`Remove ${label} pattern ${String(i + 1)}`}
onClick={() => {
handleDelete(i);
}}
/>
)}
</div>
))}
{!readOnly && (
<div className={styles.regexItem}>
<Input
className={styles.regexInput}
placeholder="New pattern…"
value={newPattern}
onChange={(_e, d) => {
setNewPattern(d.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") handleAdd();
}}
/>
<Button size="small" onClick={handleAdd}>
Add
</Button>
</div>
)}
</div>
);
}