- );
-}
diff --git a/web/source/settings/components/pageable-list.tsx b/web/source/settings/components/pageable-list.tsx
new file mode 100644
index 000000000..918103ead
--- /dev/null
+++ b/web/source/settings/components/pageable-list.tsx
@@ -0,0 +1,113 @@
+/*
+ GoToSocial
+ Copyright (C) GoToSocial Authors admin@gotosocial.org
+ SPDX-License-Identifier: AGPL-3.0-or-later
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+import React, { ReactNode } from "react";
+import { useLocation } from "wouter";
+import { Error } from "./error";
+import { SerializedError } from "@reduxjs/toolkit";
+import { FetchBaseQueryError } from "@reduxjs/toolkit/query";
+import { Links } from "parse-link-header";
+import Loading from "./loading";
+
+export interface PageableListProps {
+ isSuccess: boolean;
+ items?: T[];
+ itemToEntry: (_item: T) => ReactNode;
+ isLoading: boolean;
+ isFetching: boolean;
+ isError: boolean;
+ error: FetchBaseQueryError | SerializedError | undefined;
+ emptyMessage: string;
+ prevNextLinks?: Links | null | undefined;
+}
+
+export function PageableList({
+ isLoading,
+ isFetching,
+ isSuccess,
+ items,
+ itemToEntry,
+ isError,
+ error,
+ emptyMessage,
+ prevNextLinks,
+}: PageableListProps) {
+ const [ location, setLocation ] = useLocation();
+
+ if (!(isSuccess || isError)) {
+ // Hasn't been called yet.
+ return null;
+ }
+
+ if (isLoading || isFetching) {
+ return ;
+ }
+
+ if (error) {
+ return ;
+ }
+
+ // Map response to items if possible.
+ let content: ReactNode;
+ if (items == undefined || items.length == 0) {
+ content = {emptyMessage};
+ } else {
+ content = (
+
+ {items.map(item => itemToEntry(item))}
+
+ );
+ }
+
+ // If it's possible to page to next and previous
+ // pages, instantiate button handlers for this.
+ let prevClick: (() => void) | undefined;
+ let nextClick: (() => void) | undefined;
+ if (prevNextLinks) {
+ const prev = prevNextLinks["prev"];
+ if (prev) {
+ const prevUrl = new URL(prev.url);
+ const prevParams = prevUrl.search;
+ prevClick = () => {
+ setLocation(location + prevParams.toString());
+ };
+ }
+
+ const next = prevNextLinks["next"];
+ if (next) {
+ const nextUrl = new URL(next.url);
+ const nextParams = nextUrl.search;
+ nextClick = () => {
+ setLocation(location + nextParams.toString());
+ };
+ }
+ }
+
+ return (
+
+ { content }
+ { prevNextLinks &&
+
+ { prevClick && }
+ { nextClick && }
+
+ }
+
+ );
+}
diff --git a/web/source/settings/components/username.tsx b/web/source/settings/components/username.tsx
new file mode 100644
index 000000000..f7be1cd4a
--- /dev/null
+++ b/web/source/settings/components/username.tsx
@@ -0,0 +1,90 @@
+/*
+ GoToSocial
+ Copyright (C) GoToSocial Authors admin@gotosocial.org
+ SPDX-License-Identifier: AGPL-3.0-or-later
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+import React from "react";
+import { useLocation } from "wouter";
+import { AdminAccount } from "../lib/types/account";
+
+interface UsernameProps {
+ account: AdminAccount;
+ linkTo?: string;
+ backLocation?: string;
+ classNames?: string[];
+}
+
+export default function Username({ account, linkTo, backLocation, classNames }: UsernameProps) {
+ const [ _location, setLocation ] = useLocation();
+
+ let className = "username-lozenge";
+ let isLocal = account.domain == null;
+
+ if (account.suspended) {
+ className += " suspended";
+ }
+
+ if (isLocal) {
+ className += " local";
+ }
+
+ if (classNames) {
+ className = [ className, classNames ].flat().join(" ");
+ }
+
+ let icon = isLocal
+ ? { fa: "fa-home", info: "Local user" }
+ : { fa: "fa-external-link-square", info: "Remote user" };
+
+ const content = (
+ <>
+
+ {icon.info}
+
+ @{account.account.acct}
+ >
+ );
+
+ if (linkTo) {
+ className += " spanlink";
+ return (
+ {
+ // When clicking on an account, direct
+ // to the detail view for that account.
+ setLocation(linkTo, {
+ // Store the back location in history so
+ // the detail view can use it to return to
+ // this page (including query parameters).
+ state: { backLocation: backLocation }
+ });
+ }}
+ role="link"
+ tabIndex={0}
+ >
+ {content}
+
+ );
+ } else {
+ return (
+
+ {content}
+
+ );
+ }
+}
diff --git a/web/source/settings/index.tsx b/web/source/settings/index.tsx
index 977a94150..25e3d1f3c 100644
--- a/web/source/settings/index.tsx
+++ b/web/source/settings/index.tsx
@@ -59,11 +59,10 @@ export function App({ account }: AppProps) {
{/*
- Redirect to first part of UserRouter if
- just the bare settings page is open, so
- user isn't greeted with a blank page.
- */}
-
+ Ensure user ends up somewhere
+ if they just open /settings.
+ */}
+
diff --git a/web/source/settings/lib/query/admin/index.ts b/web/source/settings/lib/query/admin/index.ts
index cbe66705b..3e7b1a0a0 100644
--- a/web/source/settings/lib/query/admin/index.ts
+++ b/web/source/settings/lib/query/admin/index.ts
@@ -20,8 +20,9 @@
import { replaceCacheOnMutation, removeFromCacheOnMutation } from "../query-modifiers";
import { gtsApi } from "../gts-api";
import { listToKeyedObject } from "../transforms";
-import { AdminAccount, HandleSignupParams, SearchAccountParams } from "../../types/account";
+import { AdminAccount, HandleSignupParams, SearchAccountParams, SearchAccountResp } from "../../types/account";
import { InstanceRule, MappedRules } from "../../types/rules";
+import parse from "parse-link-header";
const extended = gtsApi.injectEndpoints({
endpoints: (build) => ({
@@ -65,7 +66,7 @@ const extended = gtsApi.injectEndpoints({
],
}),
- searchAccounts: build.query({
+ searchAccounts: build.query({
query: (form) => {
const params = new(URLSearchParams);
Object.entries(form).forEach(([k, v]) => {
@@ -83,10 +84,16 @@ const extended = gtsApi.injectEndpoints({
url: `/api/v2/admin/accounts${query}`
};
},
+ transformResponse: (apiResp: AdminAccount[], meta) => {
+ const accounts = apiResp;
+ const linksStr = meta?.response?.headers.get("Link");
+ const links = parse(linksStr);
+ return { accounts, links };
+ },
providesTags: (res) =>
res
? [
- ...res.map(({ id }) => ({ type: 'Account' as const, id })),
+ ...res.accounts.map(({ id }) => ({ type: 'Account' as const, id })),
{ type: 'Account', id: 'LIST' },
]
: [{ type: 'Account', id: 'LIST' }],
diff --git a/web/source/settings/lib/query/gts-api.ts b/web/source/settings/lib/query/gts-api.ts
index a07f5ff1e..6e5eafeab 100644
--- a/web/source/settings/lib/query/gts-api.ts
+++ b/web/source/settings/lib/query/gts-api.ts
@@ -24,7 +24,7 @@ import type {
FetchBaseQueryError,
} from '@reduxjs/toolkit/query/react';
import { serialize as serializeForm } from "object-to-formdata";
-
+import type { FetchBaseQueryMeta } from "@reduxjs/toolkit/dist/query/fetchBaseQuery";
import type { RootState } from '../../redux/store';
import { InstanceV1 } from '../types/instance';
@@ -65,7 +65,9 @@ export interface GTSFetchArgs extends FetchArgs {
const gtsBaseQuery: BaseQueryFn<
string | GTSFetchArgs,
any,
- FetchBaseQueryError
+ FetchBaseQueryError,
+ {},
+ FetchBaseQueryMeta
> = async (args, api, extraOptions) => {
// Retrieve state at the moment
// this function was called.
diff --git a/web/source/settings/lib/types/account.ts b/web/source/settings/lib/types/account.ts
index 3e7e9640d..db97001ac 100644
--- a/web/source/settings/lib/types/account.ts
+++ b/web/source/settings/lib/types/account.ts
@@ -17,6 +17,7 @@
along with this program. If not, see .
*/
+import { Links } from "parse-link-header";
import { CustomEmoji } from "./custom-emoji";
export interface AdminAccount {
@@ -79,6 +80,11 @@ export interface SearchAccountParams {
limit?: number,
}
+export interface SearchAccountResp {
+ accounts: AdminAccount[];
+ links: Links | null;
+}
+
export interface HandleSignupParams {
id: string,
approve_or_reject: "approve" | "reject",
diff --git a/web/source/settings/style.css b/web/source/settings/style.css
index 5af9dbc67..01263c224 100644
--- a/web/source/settings/style.css
+++ b/web/source/settings/style.css
@@ -16,6 +16,11 @@
along with this program. If not, see .
*/
+/*
+ This source file uses PostCSS syntax.
+ See: https://postcss.org/
+*/
+
body {
grid-template-rows: auto 1fr;
}
@@ -521,6 +526,22 @@ span.form-info {
}
}
+.pageable-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+
+ .entries {
+ color: $fg;
+ border: 0.1rem solid var(--gray1);
+ }
+
+ .prev-next {
+ display: flex;
+ justify-content: space-between;
+ }
+}
+
.domain-permissions-list {
p {
margin-top: 0;
@@ -1098,49 +1119,58 @@ button.with-padding {
}
}
}
+}
- .user {
- line-height: 1.3rem;
- display: inline-block;
- background: $fg-accent;
- color: $bg;
- border-radius: $br;
- padding: 0.15rem 0.15rem;
- margin: 0 0.1rem;
- font-weight: bold;
- text-decoration: none;
-
- .acct {
- word-break: break-all;
- }
-
- &.suspended {
- background: $bg-accent;
- color: $fg;
- text-decoration: line-through;
- }
+.username-lozenge {
+ line-height: 1.3rem;
+ display: inline-block;
+ background: $fg-accent;
+ color: $bg;
+ border-radius: $br;
+ padding: 0.15rem;
+ font-weight: bold;
+ text-decoration: none;
+
+ .acct {
+ word-break: break-all;
+ }
- &.local {
- background: $green1;
- }
+ &.suspended {
+ background: $bg-accent;
+ color: $fg;
+ text-decoration: line-through;
}
-}
-.accounts-view {
- form {
- margin-bottom: 1rem;
+ &.local {
+ background: $green1;
}
+}
- .list {
- margin: 0.5rem 0;
+.spanlink {
+ cursor: pointer;
+ text-decoration: none;
+}
- a {
+.accounts-view {
+ .pageable-list {
+ .username-lozenge {
+ line-height: inherit;
color: $fg;
- text-decoration: none;
+ font-weight: initial;
+ width: 100%;
+ border-radius: 0;
+ background: $list-entry-bg;
+
+ .fa {
+ align-self: center;
+ }
+
+ &:nth-child(even) {
+ background: $list-entry-alternate-bg;
+ }
- #username {
- color: $link-fg;
- margin-left: 0.5em;
+ .acct {
+ color: var(--link-fg);
}
}
}
@@ -1154,6 +1184,7 @@ button.with-padding {
.profile {
overflow: hidden;
max-width: 60rem;
+ margin-top: 1rem;
}
h4, h3, h2 {
@@ -1185,6 +1216,16 @@ button.with-padding {
dd {
word-break: break-word;
}
+
+ dt, dd {
+ /*
+ Make sure any fa icons used in keys
+ or values are properly aligned.
+ */
+ .fa {
+ vertical-align: middle;
+ }
+ }
}
}
diff --git a/web/source/settings/views/moderation/accounts/detail/actions.tsx b/web/source/settings/views/moderation/accounts/detail/actions.tsx
index 212bb4089..4132b778a 100644
--- a/web/source/settings/views/moderation/accounts/detail/actions.tsx
+++ b/web/source/settings/views/moderation/accounts/detail/actions.tsx
@@ -19,7 +19,7 @@
import React from "react";
-import { useActionAccountMutation } from "../../../../lib/query/admin";
+import { useActionAccountMutation, useHandleSignupMutation } from "../../../../lib/query/admin";
import MutationButton from "../../../../components/form/mutation-button";
import useFormSubmit from "../../../../lib/form/submit";
import {
@@ -27,22 +27,50 @@ import {
useTextInput,
useBoolInput,
} from "../../../../lib/form";
-import { Checkbox, TextInput } from "../../../../components/form/inputs";
+import { Checkbox, Select, TextInput } from "../../../../components/form/inputs";
import { AdminAccount } from "../../../../lib/types/account";
+import { useLocation } from "wouter";
export interface AccountActionsProps {
account: AdminAccount,
+ backLocation: string,
}
-export function AccountActions({ account }: AccountActionsProps) {
+export function AccountActions({ account, backLocation }: AccountActionsProps) {
+ const local = !account.domain;
+
+ // Available actions differ depending
+ // on the account's current status.
+ switch (true) {
+ case account.suspended:
+ // Can't do anything with
+ // suspended accounts currently.
+ return null;
+ case local && !account.approved:
+ // Unapproved local account sign-up,
+ // only show HandleSignup form.
+ return (
+
+ );
+ default:
+ // Normal local or remote account, show
+ // full range of moderation options.
+ return ;
+ }
+}
+
+function ModerateAccount({ account }: { account: AdminAccount }) {
const form = {
id: useValue("id", account.id),
reason: useTextInput("text")
};
-
+
const reallySuspend = useBoolInput("reallySuspend");
const [accountAction, result] = useFormSubmit(form, useActionAccountMutation());
-
+
return (
- {/*
- */}
);
}
+
+function HandleSignup({ account, backLocation }: { account: AdminAccount, backLocation: string }) {
+ const form = {
+ id: useValue("id", account.id),
+ approveOrReject: useTextInput("approve_or_reject", { defaultValue: "approve" }),
+ privateComment: useTextInput("private_comment"),
+ message: useTextInput("message"),
+ sendEmail: useBoolInput("send_email"),
+ };
+
+ const [_location, setLocation] = useLocation();
+
+ const [handleSignup, result] = useFormSubmit(form, useHandleSignupMutation(), {
+ changedOnly: false,
+ // After submitting the form, redirect back to
+ // /settings/admin/accounts if rejecting, since
+ // account will no longer be available at
+ // /settings/admin/accounts/:accountID endpoint.
+ onFinish: (res) => {
+ if (form.approveOrReject.value === "approve") {
+ // An approve request:
+ // stay on this page and
+ // serve updated details.
+ return;
+ }
+
+ if (res.data) {
+ // "reject" successful,
+ // redirect to accounts page.
+ setLocation(backLocation);
+ }
+ }
+ });
+
+ return (
+
+ );
+}
diff --git a/web/source/settings/views/moderation/accounts/detail/handlesignup.tsx b/web/source/settings/views/moderation/accounts/detail/handlesignup.tsx
deleted file mode 100644
index 59fa8bc65..000000000
--- a/web/source/settings/views/moderation/accounts/detail/handlesignup.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- GoToSocial
- Copyright (C) GoToSocial Authors admin@gotosocial.org
- SPDX-License-Identifier: AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-import React from "react";
-import { useLocation } from "wouter";
-import { useHandleSignupMutation } from "../../../../lib/query/admin";
-import MutationButton from "../../../../components/form/mutation-button";
-import useFormSubmit from "../../../../lib/form/submit";
-import {
- useValue,
- useTextInput,
- useBoolInput,
-} from "../../../../lib/form";
-import { Checkbox, Select, TextInput } from "../../../../components/form/inputs";
-import { AdminAccount } from "../../../../lib/types/account";
-
-export interface HandleSignupProps {
- account: AdminAccount,
- backLocation: string,
-}
-
-export function HandleSignup({account, backLocation}: HandleSignupProps) {
- const form = {
- id: useValue("id", account.id),
- approveOrReject: useTextInput("approve_or_reject", { defaultValue: "approve" }),
- privateComment: useTextInput("private_comment"),
- message: useTextInput("message"),
- sendEmail: useBoolInput("send_email"),
- };
-
- const [_location, setLocation] = useLocation();
-
- const [handleSignup, result] = useFormSubmit(form, useHandleSignupMutation(), {
- changedOnly: false,
- // After submitting the form, redirect back to
- // /settings/admin/accounts if rejecting, since
- // account will no longer be available at
- // /settings/admin/accounts/:accountID endpoint.
- onFinish: (res) => {
- if (form.approveOrReject.value === "approve") {
- // An approve request:
- // stay on this page and
- // serve updated details.
- return;
- }
-
- if (res.data) {
- // "reject" successful,
- // redirect to accounts page.
- setLocation(backLocation);
- }
- }
- });
-
- return (
-
- );
-}
diff --git a/web/source/settings/views/moderation/accounts/detail/index.tsx b/web/source/settings/views/moderation/accounts/detail/index.tsx
index f34bc7481..830a894cb 100644
--- a/web/source/settings/views/moderation/accounts/detail/index.tsx
+++ b/web/source/settings/views/moderation/accounts/detail/index.tsx
@@ -23,51 +23,89 @@ import { useGetAccountQuery } from "../../../../lib/query/admin";
import FormWithData from "../../../../lib/form/form-with-data";
import FakeProfile from "../../../../components/fake-profile";
import { AdminAccount } from "../../../../lib/types/account";
-import { HandleSignup } from "./handlesignup";
import { AccountActions } from "./actions";
import { useParams } from "wouter";
+import { useBaseUrl } from "../../../../lib/navigation/util";
+import BackButton from "../../../../components/back-button";
+import { UseOurInstanceAccount, yesOrNo } from "./util";
export default function AccountDetail() {
const params: { accountID: string } = useParams();
-
+ const baseUrl = useBaseUrl();
+ const backLocation: String = history.state?.backLocation ?? `~${baseUrl}`;
+
return (
+
+
+ This is the service account for your instance; you
+ cannot perform moderation actions on this account.
+
+
+ >
+ );
+ }
+
+ const local = !adminAcct.domain;
+ return (
+ <>
+
+
+ {
+ // Only show local account details
+ // if this is a local account!
+ local &&
+ }
+
+ >
+ );
+}
- let created = new Date(adminAcct.created_at).toDateString();
+function GeneralAccountDetails({ adminAcct } : { adminAcct: AdminAccount }) {
+ const local = !adminAcct.domain;
+ const created = new Date(adminAcct.created_at).toDateString();
+
let lastPosted = "never";
if (adminAcct.account.last_status_at) {
lastPosted = new Date(adminAcct.account.last_status_at).toDateString();
}
- const local = !adminAcct.domain;
return (
<>
-
+ >
);
}
diff --git a/web/source/settings/views/moderation/accounts/detail/util.tsx b/web/source/settings/views/moderation/accounts/detail/util.tsx
new file mode 100644
index 000000000..b82d44a6e
--- /dev/null
+++ b/web/source/settings/views/moderation/accounts/detail/util.tsx
@@ -0,0 +1,43 @@
+/*
+ GoToSocial
+ Copyright (C) GoToSocial Authors admin@gotosocial.org
+ SPDX-License-Identifier: AGPL-3.0-or-later
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+import { useMemo } from "react";
+
+import { AdminAccount } from "../../../../lib/types/account";
+import { store } from "../../../../redux/store";
+
+export function yesOrNo(b: boolean): string {
+ return b ? "yes" : "no";
+}
+
+export function UseOurInstanceAccount(account: AdminAccount): boolean {
+ // Pull our own URL out of storage so we can
+ // tell if account is our instance account.
+ const ourDomain = useMemo(() => {
+ const instanceUrlStr = store.getState().oauth.instanceUrl;
+ if (!instanceUrlStr) {
+ return "";
+ }
+
+ const instanceUrl = new URL(instanceUrlStr);
+ return instanceUrl.host;
+ }, []);
+
+ return !account.domain && account.username == ourDomain;
+}
diff --git a/web/source/settings/views/moderation/accounts/index.tsx b/web/source/settings/views/moderation/accounts/index.tsx
index 79ba2c674..946ed323d 100644
--- a/web/source/settings/views/moderation/accounts/index.tsx
+++ b/web/source/settings/views/moderation/accounts/index.tsx
@@ -20,10 +20,10 @@
import React from "react";
import { AccountSearchForm } from "./search";
-export default function AccountsOverview({ }) {
+export default function AccountsSearch({ }) {
return (
-
Accounts Overview
+
Accounts Search
You can perform actions on an account by clicking
its name in a report, or by searching for the account
diff --git a/web/source/settings/views/moderation/accounts/pending/index.tsx b/web/source/settings/views/moderation/accounts/pending/index.tsx
index d5a32f09b..b72de52bf 100644
--- a/web/source/settings/views/moderation/accounts/pending/index.tsx
+++ b/web/source/settings/views/moderation/accounts/pending/index.tsx
@@ -17,20 +17,40 @@
along with this program. If not, see .
*/
-import React from "react";
+import React, { ReactNode } from "react";
import { useSearchAccountsQuery } from "../../../../lib/query/admin";
-import { AccountList } from "../../../../components/account-list";
+import { PageableList } from "../../../../components/pageable-list";
+import { useLocation } from "wouter";
+import Username from "../../../../components/username";
+import { AdminAccount } from "../../../../lib/types/account";
export default function AccountsPending() {
+ const [ location, _setLocation ] = useLocation();
const searchRes = useSearchAccountsQuery({status: "pending"});
+ // Function to map an item to a list entry.
+ function itemToEntry(account: AdminAccount): ReactNode {
+ const acc = account.account;
+ return (
+
+ );
+ }
+
return (
Pending Accounts
- .
*/
-import React from "react";
+import React, { ReactNode, useEffect, useMemo } from "react";
import { useLazySearchAccountsQuery } from "../../../../lib/query/admin";
import { useTextInput } from "../../../../lib/form";
-import { AccountList } from "../../../../components/account-list";
-import { SearchAccountParams } from "../../../../lib/types/account";
+import { PageableList } from "../../../../components/pageable-list";
import { Select, TextInput } from "../../../../components/form/inputs";
import MutationButton from "../../../../components/form/mutation-button";
+import { useLocation, useSearch } from "wouter";
+import { AdminAccount } from "../../../../lib/types/account";
+import Username from "../../../../components/username";
export function AccountSearchForm() {
+ const [ location, setLocation ] = useLocation();
+ const search = useSearch();
+ const urlQueryParams = useMemo(() => new URLSearchParams(search), [search]);
+ const [ searchAcct, searchRes ] = useLazySearchAccountsQuery();
+
+ // Populate search form using values from
+ // urlQueryParams, to allow paging.
const form = {
- origin: useTextInput("origin"),
- status: useTextInput("status"),
- permissions: useTextInput("permissions"),
- username: useTextInput("username"),
- display_name: useTextInput("display_name"),
- by_domain: useTextInput("by_domain"),
- email: useTextInput("email"),
- ip: useTextInput("ip"),
+ origin: useTextInput("origin", { defaultValue: urlQueryParams.get("origin") ?? ""}),
+ status: useTextInput("status", { defaultValue: urlQueryParams.get("status") ?? ""}),
+ permissions: useTextInput("permissions", { defaultValue: urlQueryParams.get("permissions") ?? ""}),
+ username: useTextInput("username", { defaultValue: urlQueryParams.get("username") ?? ""}),
+ display_name: useTextInput("display_name", { defaultValue: urlQueryParams.get("display_name") ?? ""}),
+ by_domain: useTextInput("by_domain", { defaultValue: urlQueryParams.get("by_domain") ?? ""}),
+ email: useTextInput("email", { defaultValue: urlQueryParams.get("email") ?? ""}),
+ ip: useTextInput("ip", { defaultValue: urlQueryParams.get("ip") ?? ""}),
+ limit: useTextInput("limit", { defaultValue: urlQueryParams.get("limit") ?? "50"})
};
- function submitSearch(e) {
+ // On mount, if urlQueryParams were provided,
+ // trigger the search. For example, if page
+ // was accessed at /search?origin=local&limit=20,
+ // then run a search with origin=local and
+ // limit=20 and immediately render the results.
+ useEffect(() => {
+ if (urlQueryParams.size > 0) {
+ searchAcct(Object.fromEntries(urlQueryParams), true);
+ }
+ }, [urlQueryParams, searchAcct]);
+
+ // Rather than triggering the search directly,
+ // the "submit" button changes the location
+ // based on form field params, and lets the
+ // useEffect hook above actually do the search.
+ function submitQuery(e) {
e.preventDefault();
// Parse query parameters.
@@ -52,16 +77,32 @@ export function AccountSearchForm() {
// Remove any nulls.
return kv || [];
});
- const params: SearchAccountParams = Object.fromEntries(entries);
- searchAcct(params);
+
+ const searchParams = new URLSearchParams(entries);
+ setLocation(location + "?" + searchParams.toString());
}
- const [ searchAcct, searchRes ] = useLazySearchAccountsQuery();
+ // Location to return to when user clicks "back" on the account detail view.
+ const backLocation = location + (urlQueryParams ? `?${urlQueryParams}` : "");
+
+ // Function to map an item to a list entry.
+ function itemToEntry(account: AdminAccount): ReactNode {
+ const acc = account.account;
+ return (
+
+ );
+ }
return (
<>
-
>
);
diff --git a/web/source/settings/views/moderation/menu.tsx b/web/source/settings/views/moderation/menu.tsx
index 4f01e0798..9488b8c30 100644
--- a/web/source/settings/views/moderation/menu.tsx
+++ b/web/source/settings/views/moderation/menu.tsx
@@ -28,7 +28,7 @@ import { useHasPermission } from "../../lib/navigation/util";
/**
* - /settings/moderation/reports/overview
* - /settings/moderation/reports/:reportId
- * - /settings/moderation/accounts/overview
+ * - /settings/moderation/accounts/search
* - /settings/moderation/accounts/pending
* - /settings/moderation/accounts/:accountID
* - /settings/moderation/domain-permissions/:permType
@@ -76,12 +76,12 @@ function ModerationAccountsMenu() {