Skip to the content

Capsid menu

components/mentions-list/mentions-list.ts

73 lines and 7 functions in DrDustinEdwards/capsomer, at commit d3966cc on 2026-10-24. This file on GitHub.

Functions

The functions of mentions-list.ts
FunctionLinesComplexityCoveredCRAPMutation
mentionsHref15 to 171No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
withMentions21 to 294No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
currentView33 to 374No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
mentionActions54 to 678No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
countLine73 to 772No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
emptyWords79 to 812No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
sweepLabel84 to 871No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet

Imports

It imports components/content/content.ts, components/moderation-queue/moderation-queue.ts. Imported by components/mentions-list/examples.tsx, components/mentions-list/mentions-list.react.tsx.

Source

// The mentions list's pure helpers: the addresses of its tabs and pages, which view it opens on,
// which actions a person is offered here and why the others are withheld, the count and the empty
// state. The states, their words and the decisions are the moderation queue's (moderation-queue.ts),
// which the list is built from. No DOM; the React component and a server render use the same
// functions.
import { queryHref, type MentionsData, type MentionsQuery } from "../content/content.ts";
import { EMPTY_TEXT, VIEWS, bulkActionsFor, mentionsWord, type ModAction, type ModView } from "../moderation-queue/moderation-queue.ts";

// ---------------------------------------------------------------------------------------
// Addresses.

const QUERY_ORDER: ReadonlyArray<keyof MentionsQuery> = ["view", "cursor", "open"];

// The list's address with a query (content.ts, queryHref).
export function mentionsHref(base: string, q: MentionsQuery): string {
  return queryHref(base, q, QUERY_ORDER);
}

// A tab change starts again at the first page with nothing open; opening a mention keeps the
// page it is on.
export function withMentions(q: MentionsQuery, change: Partial<MentionsQuery>): MentionsQuery {
  const next: MentionsQuery = { ...q, ...change };
  if (!("open" in change)) {
    delete next.cursor;
    delete next.open;
  }
  for (const k of Object.keys(change) as Array<keyof MentionsQuery>) if (change[k] === undefined) delete next[k];
  return next;
}

// The tab shown: the one asked for, else Waiting, else All when nothing waits (Carrel and
// dustinedwards.info already agree on this).
export function currentView(data: Pick<MentionsData, "query" | "counts">): ModView {
  const v = data.query.view;
  if (v && (VIEWS as readonly string[]).includes(v)) return v;
  return data.counts.pending > 0 ? "pending" : "all";
}

// ---------------------------------------------------------------------------------------
// What is offered.

export type MentionIntent = ModAction | "delete";

export interface OfferedAction {
  intent: MentionIntent;
  label: string;
  destructive?: boolean;
}

const BULK_LABEL: Record<MentionIntent, string> = { approve: "Approve", reject: "Reject", reset: "Back to waiting", delete: "Delete" };

// The bulk actions this person may run on this tab of this site, in the bar's order, and a
// sentence for each one held back.
export function mentionActions(data: Pick<MentionsData, "offers" | "can" | "query" | "counts">): { bulk: OfferedAction[]; withheld: string[] } {
  const { offers, can } = data;
  const view = currentView(data);
  const bulk: OfferedAction[] = [];
  const withheld: string[] = [];
  if (can.decideMentions) for (const a of bulkActionsFor(view, offers.reset)) bulk.push({ intent: a, label: BULK_LABEL[a] });
  if (can.decideMentions && offers.delete) bulk.push({ intent: "delete", label: "Delete", destructive: true });
  if (!can.decideMentions) withheld.push("You can read these mentions. Deciding them needs the site owner's role.");
  else {
    if (!offers.delete) withheld.push("Delete is not offered: this site keeps every mention. Reject one to keep it off its post.");
    if (!offers.reset) withheld.push("Back to waiting is not offered by this site, so a decision on a waiting mention has no Undo.");
  }
  return { bulk, withheld };
}

// ---------------------------------------------------------------------------------------
// Words.

// "4 mentions", "Showing 50 of 212 mentions".
export function countLine(data: Pick<MentionsData, "rows" | "query" | "counts">): string {
  const n = data.rows.length;
  const total = data.counts[currentView(data)];
  return total > n ? `Showing ${n} of ${total} ${mentionsWord(total)}` : `${n} ${mentionsWord(n)}`;
}

export function emptyWords(view: ModView): { kind: "all-clear" | "nothing-yet"; title: string; text: string } {
  return { kind: view === "pending" ? "all-clear" : "nothing-yet", ...EMPTY_TEXT[view] };
}

// The sweep's button: "Remove 3 expired".
export function sweepLabel(expiring: MentionsData["expiring"]): { n: number; label: string } {
  const n = expiring.failed + expiring.rejected;
  return { n, label: `Remove ${n} expired` };
}