feat(admin): comma-separated bulk add on the Blacklist page
The add-entry input took the whole field as one literal value, so typing "word1, word2" created a single entry matching that exact phrase instead of two separate words. Splits on comma client-side and calls the existing single-value createBlacklistEntry mutation once per value (dedup'd), with one consolidated toast instead of one per word. No backend change — reuses the existing retroactive-scan mutation as-is.
This commit is contained in:
1 parent
ee04bf5432
commit
2085336748
1 file changed
+53
-25
@@ -40,22 +40,8 @@ export default function AdminBlacklistPage() {
|
||||
)
|
||||
);
|
||||
|
||||
const createMutation = useMutation(
|
||||
trpc.admin.createBlacklistEntry.mutationOptions({
|
||||
onSuccess: ({ hiddenCount }) => {
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.listBlacklist.queryKey() });
|
||||
setNewValue("");
|
||||
if (hiddenCount > 0) {
|
||||
toast.success(`Added — ${hiddenCount} existing post${hiddenCount !== 1 ? "s" : ""} hidden retroactively.`);
|
||||
} else {
|
||||
toast.success("Blacklist entry added.");
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(`Failed to add entry: ${err.message}`);
|
||||
},
|
||||
})
|
||||
);
|
||||
const createMutation = useMutation(trpc.admin.createBlacklistEntry.mutationOptions());
|
||||
const [isBulkAdding, setIsBulkAdding] = useState(false);
|
||||
|
||||
const deleteMutation = useMutation(
|
||||
trpc.admin.deleteBlacklistEntry.mutationOptions({
|
||||
@@ -66,10 +52,45 @@ export default function AdminBlacklistPage() {
|
||||
})
|
||||
);
|
||||
|
||||
function handleCreate() {
|
||||
const value = newValue.trim();
|
||||
if (!value) return;
|
||||
createMutation.mutate({ type: newType, value });
|
||||
/**
|
||||
* Comma-separated bulk add — splits on "," so pasting "word1, word2, word3"
|
||||
* creates one entry per word instead of a single literal-phrase entry.
|
||||
* Reuses the existing single-value createBlacklistEntry mutation (with its
|
||||
* retroactive scan) once per word rather than a new batch endpoint.
|
||||
*/
|
||||
async function handleCreate() {
|
||||
const values = [...new Set(newValue.split(",").map((v) => v.trim()).filter(Boolean))];
|
||||
if (values.length === 0) return;
|
||||
|
||||
setIsBulkAdding(true);
|
||||
let addedCount = 0;
|
||||
let hiddenTotal = 0;
|
||||
let failedCount = 0;
|
||||
|
||||
for (const value of values) {
|
||||
try {
|
||||
const { hiddenCount } = await createMutation.mutateAsync({ type: newType, value });
|
||||
addedCount++;
|
||||
hiddenTotal += hiddenCount;
|
||||
} catch {
|
||||
failedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
setIsBulkAdding(false);
|
||||
void qc.invalidateQueries({ queryKey: trpc.admin.listBlacklist.queryKey() });
|
||||
setNewValue("");
|
||||
|
||||
if (addedCount > 0) {
|
||||
const suffix =
|
||||
hiddenTotal > 0
|
||||
? ` — ${hiddenTotal} existing post${hiddenTotal !== 1 ? "s" : ""} hidden retroactively.`
|
||||
: "";
|
||||
toast.success(`${addedCount} ${addedCount !== 1 ? "entries" : "entry"} added.${suffix}`);
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
toast.error(`${failedCount} ${failedCount !== 1 ? "entries" : "entry"} failed to add.`);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -79,6 +100,7 @@ export default function AdminBlacklistPage() {
|
||||
<p className="text-sm text-zinc-500 mt-1">
|
||||
Words, links, and hashtags that auto-hide matching posts — new posts are checked on
|
||||
creation, and adding an entry here retroactively scans + hides existing matches.
|
||||
Separate multiple values with a comma to add them all at once.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -96,19 +118,25 @@ export default function AdminBlacklistPage() {
|
||||
</Select>
|
||||
<Input
|
||||
placeholder={
|
||||
newType === "WORD" ? "word…" : newType === "LINK" ? "domain, e.g. spam.example" : "hashtag (without #)"
|
||||
newType === "WORD"
|
||||
? "word1, word2, …"
|
||||
: newType === "LINK"
|
||||
? "domain1.example, domain2.example, …"
|
||||
: "tag1, tag2 (without #)"
|
||||
}
|
||||
value={newValue}
|
||||
onChange={(e) => setNewValue(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleCreate()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") void handleCreate();
|
||||
}}
|
||||
className="flex-1 min-w-[200px] bg-zinc-800 border-zinc-700 text-zinc-100 placeholder:text-zinc-500"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
disabled={createMutation.isPending || !newValue.trim()}
|
||||
onClick={() => void handleCreate()}
|
||||
disabled={isBulkAdding || !newValue.trim()}
|
||||
className="bg-zinc-700 hover:bg-zinc-600"
|
||||
>
|
||||
{createMutation.isPending ? (
|
||||
{isBulkAdding ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-1.5" />
|
||||
) : (
|
||||
<Plus className="h-4 w-4 mr-1.5" />
|
||||
|
||||
Reference in new issue
Block a user