feat(housekeeping): deliver people account workflows
This commit is contained in:
1 parent
7920d4f46c
commit
e1b31ff773
45 files changed
+4290
-1243
No files matched your search
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -3,6 +3,7 @@ import {
|
||||
anyCapability,
|
||||
type HousekeepingDomainManifest,
|
||||
} from "../../foundation/contracts";
|
||||
import { PEOPLE_PRIMARY_ROUTES } from "./routes";
|
||||
|
||||
export const peopleManifest = {
|
||||
id: "people",
|
||||
@@ -34,7 +35,7 @@ export const peopleManifest = {
|
||||
PERMS.MOD_CFH_EDIT,
|
||||
PERMS.MOD_TICKETS_EDIT,
|
||||
),
|
||||
routes: [],
|
||||
routes: PEOPLE_PRIMARY_ROUTES,
|
||||
searchProviders: [],
|
||||
inboxSources: [],
|
||||
widgets: [],
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,68 @@
|
||||
import type {
|
||||
HousekeepingCapabilityContext,
|
||||
HousekeepingResult,
|
||||
} from "../../../foundation/contracts";
|
||||
import type { HousekeepingPageInput } from "../../../route-handlers";
|
||||
import { type PeopleUsersQueryData, peopleUsersQuery } from "../queries/users";
|
||||
import { PeoplePageFrame } from "./page-state";
|
||||
|
||||
interface PeopleMultiAccountsPageProps {
|
||||
readonly context: HousekeepingCapabilityContext;
|
||||
readonly result?: HousekeepingResult<PeopleUsersQueryData>;
|
||||
readonly partialDependencies?: readonly string[];
|
||||
}
|
||||
|
||||
export function PeopleMultiAccountsPage({
|
||||
context: _context,
|
||||
result,
|
||||
partialDependencies,
|
||||
}: PeopleMultiAccountsPageProps) {
|
||||
return (
|
||||
<PeoplePageFrame
|
||||
title="Multi-account clusters"
|
||||
description="Review bounded account clusters grouped by current IP."
|
||||
result={result}
|
||||
partialDependencies={partialDependencies}
|
||||
isEmpty={(data) =>
|
||||
data.kind === "multi-accounts" && data.page.items.length === 0
|
||||
}
|
||||
>
|
||||
{(data) =>
|
||||
data.kind === "multi-accounts" ? (
|
||||
<ul className="space-y-3">
|
||||
{data.page.items.map((cluster) => (
|
||||
<li
|
||||
key={cluster.key}
|
||||
className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4"
|
||||
>
|
||||
<h2 className="font-medium text-[var(--admin-text)]">
|
||||
{cluster.key}
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--admin-text-muted)]">
|
||||
{cluster.accountCount} accounts
|
||||
</p>
|
||||
<ul className="mt-2 flex flex-wrap gap-3 text-sm">
|
||||
{cluster.accounts.map((account) => (
|
||||
<li key={account.id}>
|
||||
<a href={account.href}>{account.username}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null
|
||||
}
|
||||
</PeoplePageFrame>
|
||||
);
|
||||
}
|
||||
|
||||
export async function renderPeopleMultiAccountsPage(
|
||||
input: HousekeepingPageInput,
|
||||
) {
|
||||
const result = await peopleUsersQuery.run(input.context, {
|
||||
routeId: "people.users.multi-accounts",
|
||||
list: {},
|
||||
});
|
||||
return <PeopleMultiAccountsPage context={input.context} result={result} />;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,83 @@
|
||||
import { PERMS } from "@/lib/permission-slugs";
|
||||
import {
|
||||
createCorrelationId,
|
||||
fail,
|
||||
type HousekeepingCapabilityContext,
|
||||
type HousekeepingResult,
|
||||
} from "../../../foundation/contracts";
|
||||
import type { HousekeepingPageInput } from "../../../route-handlers";
|
||||
import { type PeopleStaffQueryData, peopleStaffQuery } from "../queries/staff";
|
||||
import { PeoplePageFrame } from "./page-state";
|
||||
|
||||
interface PeopleStaffPageProps {
|
||||
readonly context: HousekeepingCapabilityContext;
|
||||
readonly result?: HousekeepingResult<PeopleStaffQueryData>;
|
||||
readonly partialDependencies?: readonly string[];
|
||||
}
|
||||
|
||||
export function PeopleStaffPage({
|
||||
context,
|
||||
result,
|
||||
partialDependencies,
|
||||
}: PeopleStaffPageProps) {
|
||||
return (
|
||||
<PeoplePageFrame
|
||||
title="Staff"
|
||||
description="Review staff applications and team definitions."
|
||||
result={result}
|
||||
partialDependencies={partialDependencies}
|
||||
isEmpty={(data) => data.page.items.length === 0}
|
||||
>
|
||||
{(data) =>
|
||||
data.kind === "applications" ? (
|
||||
<ul className="space-y-2">
|
||||
{data.page.items.map((application) => (
|
||||
<li
|
||||
key={application.id}
|
||||
className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4"
|
||||
>
|
||||
<h2>{application.username ?? `User #${application.userId}`}</h2>
|
||||
<p>{application.content}</p>
|
||||
{context.has(PERMS.USERS_EDIT) ? (
|
||||
<span data-housekeeping-command="people.application.decide">
|
||||
Dismiss application
|
||||
</span>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : data.kind === "teams" ? (
|
||||
<ul className="space-y-2">
|
||||
{data.page.items.map((team) => (
|
||||
<li
|
||||
key={team.id}
|
||||
className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4"
|
||||
>
|
||||
<h2>{team.name}</h2>
|
||||
<p>{team.jobDescription ?? "No job description"}</p>
|
||||
{context.has(PERMS.USERS_EDIT) ? (
|
||||
<span data-housekeeping-command="people.team.change">
|
||||
Team controls
|
||||
</span>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null
|
||||
}
|
||||
</PeoplePageFrame>
|
||||
);
|
||||
}
|
||||
|
||||
export async function renderPeopleStaffPage(input: HousekeepingPageInput) {
|
||||
const routeId = input.match.routeId;
|
||||
const result =
|
||||
routeId === "people.staff.applications" || routeId === "people.staff.teams"
|
||||
? await peopleStaffQuery.run(input.context, { routeId, list: {} })
|
||||
: fail(
|
||||
"NOT_FOUND",
|
||||
"errors.housekeeping.notFound",
|
||||
createCorrelationId(),
|
||||
);
|
||||
return <PeopleStaffPage context={input.context} result={result} />;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,86 @@
|
||||
import { PERMS } from "@/lib/permission-slugs";
|
||||
import type {
|
||||
HousekeepingCapabilityContext,
|
||||
HousekeepingResult,
|
||||
} from "../../../foundation/contracts";
|
||||
import type { HousekeepingPageInput } from "../../../route-handlers";
|
||||
import { type PeopleUsersQueryData, peopleUsersQuery } from "../queries/users";
|
||||
import { PeoplePageFrame } from "./page-state";
|
||||
|
||||
interface PeopleUsersPageProps {
|
||||
readonly context: HousekeepingCapabilityContext;
|
||||
readonly result?: HousekeepingResult<PeopleUsersQueryData>;
|
||||
readonly partialDependencies?: readonly string[];
|
||||
}
|
||||
|
||||
export function PeopleUsersPage({
|
||||
context,
|
||||
result,
|
||||
partialDependencies,
|
||||
}: PeopleUsersPageProps) {
|
||||
return (
|
||||
<PeoplePageFrame
|
||||
title="Users"
|
||||
description="Search accounts and open the canonical People workflow."
|
||||
result={result}
|
||||
partialDependencies={partialDependencies}
|
||||
isEmpty={(data) => data.kind === "users" && data.page.items.length === 0}
|
||||
>
|
||||
{(data) =>
|
||||
data.kind === "users" ? (
|
||||
<section className="rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] p-4">
|
||||
<p className="text-sm text-[var(--admin-text-muted)]">
|
||||
{data.page.total} matching users
|
||||
</p>
|
||||
<ul className="mt-3 divide-y divide-[var(--admin-border)]">
|
||||
{data.page.items.map((user) => (
|
||||
<li
|
||||
key={user.id}
|
||||
className="flex flex-wrap items-center gap-3 py-3 text-sm"
|
||||
>
|
||||
<a
|
||||
className="font-medium text-[var(--admin-text)]"
|
||||
href={user.href}
|
||||
>
|
||||
{user.username}
|
||||
</a>
|
||||
<span className="text-[var(--admin-text-muted)]">
|
||||
Rank {user.rank}
|
||||
</span>
|
||||
<span className="text-[var(--admin-text-muted)]">
|
||||
{user.online ? "Online" : "Offline"}
|
||||
</span>
|
||||
{user.mail !== null ? <span>{user.mail}</span> : null}
|
||||
{user.ipCurrent !== null ? (
|
||||
<span>{user.ipCurrent}</span>
|
||||
) : null}
|
||||
{context.has(PERMS.USERS_EDIT) ? (
|
||||
<a
|
||||
href={`${user.href}/edit`}
|
||||
data-housekeeping-command="people.user.update"
|
||||
>
|
||||
Edit
|
||||
</a>
|
||||
) : null}
|
||||
{context.has(PERMS.USERS_BAN) ? (
|
||||
<span data-housekeeping-command="people.user.ban">
|
||||
Ban controls available
|
||||
</span>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null
|
||||
}
|
||||
</PeoplePageFrame>
|
||||
);
|
||||
}
|
||||
|
||||
export async function renderPeopleUsersPage(input: HousekeepingPageInput) {
|
||||
const result = await peopleUsersQuery.run(input.context, {
|
||||
routeId: "people.users.list",
|
||||
list: {},
|
||||
});
|
||||
return <PeopleUsersPage context={input.context} result={result} />;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { HousekeepingRouteHandler } from "../../route-handlers";
|
||||
import { renderPeopleCommunityPage } from "./pages/community";
|
||||
import { renderPeopleMultiAccountsPage } from "./pages/multi-accounts";
|
||||
import { renderPeopleStaffPage } from "./pages/staff";
|
||||
import { renderPeopleUserDetailPage } from "./pages/user-detail";
|
||||
import { renderPeopleUserEditPage } from "./pages/user-edit";
|
||||
import { renderPeopleUsersPage } from "./pages/users";
|
||||
|
||||
export const PEOPLE_PRIMARY_ROUTE_IDS = [
|
||||
"people.users.list",
|
||||
"people.users.edit",
|
||||
"people.users.multi-accounts",
|
||||
"people.users.detail",
|
||||
"people.community.online",
|
||||
"people.community.guilds",
|
||||
"people.community.guild-detail",
|
||||
"people.staff.applications",
|
||||
"people.staff.teams",
|
||||
] as const;
|
||||
|
||||
type PeoplePrimaryRouteId = (typeof PEOPLE_PRIMARY_ROUTE_IDS)[number];
|
||||
|
||||
function rendererFor(
|
||||
routeId: PeoplePrimaryRouteId,
|
||||
): HousekeepingRouteHandler["render"] {
|
||||
if (routeId === "people.users.list") return renderPeopleUsersPage;
|
||||
if (routeId === "people.users.edit") return renderPeopleUserEditPage;
|
||||
if (routeId === "people.users.multi-accounts")
|
||||
return renderPeopleMultiAccountsPage;
|
||||
if (routeId === "people.users.detail") return renderPeopleUserDetailPage;
|
||||
if (routeId.startsWith("people.community.")) return renderPeopleCommunityPage;
|
||||
return renderPeopleStaffPage;
|
||||
}
|
||||
|
||||
export const PEOPLE_PRIMARY_ROUTE_HANDLERS: readonly HousekeepingRouteHandler[] =
|
||||
Object.freeze(
|
||||
PEOPLE_PRIMARY_ROUTE_IDS.map((routeId) =>
|
||||
Object.freeze({ routeId, render: rendererFor(routeId) }),
|
||||
),
|
||||
);
|
||||
@@ -157,3 +157,5 @@ export const PEOPLE_ROUTES = [
|
||||
[PERMS.TICKETS_VIEW, PERMS.MOD_TICKETS_VIEW],
|
||||
),
|
||||
] as const satisfies readonly HousekeepingRouteDefinition[];
|
||||
|
||||
export const PEOPLE_PRIMARY_ROUTES = Object.freeze(PEOPLE_ROUTES.slice(0, 9));
|
||||
File diff suppressed because it is too large.
Load diff
+104
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user