mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-10-29 09:42:26 -05:00
[feature] Page through accounts as moderator (#2881)
* [feature] Page through accounts as moderator * aaaaa * use COLLATE "C" for Postgres to ensure same ordering as SQLite * fix typo, test paging up * don't show moderation / info for our instance acct
This commit is contained in:
parent
1edcb06afe
commit
725a21b027
30 changed files with 1473 additions and 432 deletions
|
|
@ -22,6 +22,7 @@
|
|||
"nanoid": "^4.0.0",
|
||||
"object-to-formdata": "^4.4.2",
|
||||
"papaparse": "^5.3.2",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"photoswipe": "^5.3.3",
|
||||
"photoswipe-dynamic-caption-plugin": "^1.2.7",
|
||||
"plyr": "^3.7.8",
|
||||
|
|
@ -44,6 +45,7 @@
|
|||
"@joepie91/eslint-config": "^1.1.1",
|
||||
"@types/is-valid-domain": "^0.0.2",
|
||||
"@types/papaparse": "^5.3.9",
|
||||
"@types/parse-link-header": "^2.0.3",
|
||||
"@types/psl": "^1.1.1",
|
||||
"@types/react-dom": "^18.2.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
|
|
|
|||
|
|
@ -1,82 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Link } from "wouter";
|
||||
import { Error } from "./error";
|
||||
import { AdminAccount } from "../lib/types/account";
|
||||
import { SerializedError } from "@reduxjs/toolkit";
|
||||
import { FetchBaseQueryError } from "@reduxjs/toolkit/query";
|
||||
|
||||
export interface AccountListProps {
|
||||
isSuccess: boolean,
|
||||
data: AdminAccount[] | undefined,
|
||||
isLoading: boolean,
|
||||
isError: boolean,
|
||||
error: FetchBaseQueryError | SerializedError | undefined,
|
||||
emptyMessage: string,
|
||||
}
|
||||
|
||||
export function AccountList({
|
||||
isLoading,
|
||||
isSuccess,
|
||||
data,
|
||||
isError,
|
||||
error,
|
||||
emptyMessage,
|
||||
}: AccountListProps) {
|
||||
if (!(isSuccess || isError)) {
|
||||
// Hasn't been called yet.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <i
|
||||
className="fa fa-fw fa-refresh fa-spin"
|
||||
aria-hidden="true"
|
||||
title="Loading..."
|
||||
/>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <Error error={error} />;
|
||||
}
|
||||
|
||||
if (data == undefined || data.length == 0) {
|
||||
return <b>{emptyMessage}</b>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="list">
|
||||
{data.map(({ account: acc }) => (
|
||||
<Link
|
||||
key={acc.acct}
|
||||
className="account entry"
|
||||
href={`/${acc.id}`}
|
||||
>
|
||||
{acc.display_name?.length > 0
|
||||
? acc.display_name
|
||||
: acc.username
|
||||
}
|
||||
<span id="username">(@{acc.acct})</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
113
web/source/settings/components/pageable-list.tsx
Normal file
113
web/source/settings/components/pageable-list.tsx
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<T> {
|
||||
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<T>({
|
||||
isLoading,
|
||||
isFetching,
|
||||
isSuccess,
|
||||
items,
|
||||
itemToEntry,
|
||||
isError,
|
||||
error,
|
||||
emptyMessage,
|
||||
prevNextLinks,
|
||||
}: PageableListProps<T>) {
|
||||
const [ location, setLocation ] = useLocation();
|
||||
|
||||
if (!(isSuccess || isError)) {
|
||||
// Hasn't been called yet.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLoading || isFetching) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <Error error={error} />;
|
||||
}
|
||||
|
||||
// Map response to items if possible.
|
||||
let content: ReactNode;
|
||||
if (items == undefined || items.length == 0) {
|
||||
content = <b>{emptyMessage}</b>;
|
||||
} else {
|
||||
content = (
|
||||
<div className="entries">
|
||||
{items.map(item => itemToEntry(item))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="list pageable-list">
|
||||
{ content }
|
||||
{ prevNextLinks &&
|
||||
<div className="prev-next">
|
||||
{ prevClick && <button onClick={prevClick}>Previous page</button> }
|
||||
{ nextClick && <button onClick={nextClick}>Next page</button> }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -18,19 +18,23 @@
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Link } from "wouter";
|
||||
import { AdminAccount } from "../../../lib/types/account";
|
||||
import { useLocation } from "wouter";
|
||||
import { AdminAccount } from "../lib/types/account";
|
||||
|
||||
interface UsernameProps {
|
||||
user: AdminAccount;
|
||||
link?: string;
|
||||
account: AdminAccount;
|
||||
linkTo?: string;
|
||||
backLocation?: string;
|
||||
classNames?: string[];
|
||||
}
|
||||
|
||||
export default function Username({ user, link }: UsernameProps) {
|
||||
let className = "user";
|
||||
let isLocal = user.domain == null;
|
||||
export default function Username({ account, linkTo, backLocation, classNames }: UsernameProps) {
|
||||
const [ _location, setLocation ] = useLocation();
|
||||
|
||||
let className = "username-lozenge";
|
||||
let isLocal = account.domain == null;
|
||||
|
||||
if (user.suspended) {
|
||||
if (account.suspended) {
|
||||
className += " suspended";
|
||||
}
|
||||
|
||||
|
|
@ -38,23 +42,43 @@ export default function Username({ user, link }: UsernameProps) {
|
|||
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 = (
|
||||
<>
|
||||
<span className="acct">@{user.account.acct}</span>
|
||||
<i className={`fa fa-fw ${icon.fa}`} aria-hidden="true" title={icon.info} />
|
||||
<span className="sr-only">{icon.info}</span>
|
||||
|
||||
<span className="acct">@{account.account.acct}</span>
|
||||
</>
|
||||
);
|
||||
|
||||
if (link) {
|
||||
if (linkTo) {
|
||||
className += " spanlink";
|
||||
return (
|
||||
<Link className={className} to={link}>
|
||||
<span
|
||||
className={className}
|
||||
onClick={() => {
|
||||
// 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}
|
||||
</Link>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
|
|
@ -59,11 +59,10 @@ export function App({ account }: AppProps) {
|
|||
<ModerationRouter />
|
||||
<AdminRouter />
|
||||
{/*
|
||||
Redirect to first part of UserRouter if
|
||||
just the bare settings page is open, so
|
||||
user isn't greeted with a blank page.
|
||||
*/}
|
||||
<Route><Redirect to="/user/profile" /></Route>
|
||||
Ensure user ends up somewhere
|
||||
if they just open /settings.
|
||||
*/}
|
||||
<Route path="/"><Redirect to="/user" /></Route>
|
||||
</ErrorBoundary>
|
||||
</Router>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -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<AdminAccount[], SearchAccountParams>({
|
||||
searchAccounts: build.query<SearchAccountResp, SearchAccountParams>({
|
||||
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' }],
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
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;
|
||||
.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;
|
||||
}
|
||||
|
||||
.acct {
|
||||
word-break: break-all;
|
||||
}
|
||||
&.suspended {
|
||||
background: $bg-accent;
|
||||
color: $fg;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
&.suspended {
|
||||
background: $bg-accent;
|
||||
color: $fg;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
&.local {
|
||||
background: $green1;
|
||||
}
|
||||
&.local {
|
||||
background: $green1;
|
||||
}
|
||||
}
|
||||
|
||||
.spanlink {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.accounts-view {
|
||||
form {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0.5rem 0;
|
||||
|
||||
a {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<HandleSignup
|
||||
account={account}
|
||||
backLocation={backLocation}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
// Normal local or remote account, show
|
||||
// full range of moderation options.
|
||||
return <ModerateAccount account={account} />;
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<form
|
||||
onSubmit={accountAction}
|
||||
|
|
@ -60,16 +88,6 @@ export function AccountActions({ account }: AccountActionsProps) {
|
|||
placeholder="Reason for this action"
|
||||
/>
|
||||
<div className="action-buttons">
|
||||
{/* <MutationButton
|
||||
label="Disable"
|
||||
name="disable"
|
||||
result={result}
|
||||
/>
|
||||
<MutationButton
|
||||
label="Silence"
|
||||
name="silence"
|
||||
result={result}
|
||||
/> */}
|
||||
<MutationButton
|
||||
disabled={account.suspended || reallySuspend.value === undefined || reallySuspend.value === false}
|
||||
label="Suspend"
|
||||
|
|
@ -84,3 +102,81 @@ export function AccountActions({ account }: AccountActionsProps) {
|
|||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<form
|
||||
onSubmit={handleSignup}
|
||||
aria-labelledby="account-handle-signup"
|
||||
>
|
||||
<h3 id="account-handle-signup">Handle Account Sign-Up</h3>
|
||||
<Select
|
||||
field={form.approveOrReject}
|
||||
label="Approve or Reject"
|
||||
options={
|
||||
<>
|
||||
<option value="approve">Approve</option>
|
||||
<option value="reject">Reject</option>
|
||||
</>
|
||||
}
|
||||
>
|
||||
</Select>
|
||||
{ form.approveOrReject.value === "reject" &&
|
||||
// Only show form fields relevant
|
||||
// to "reject" if rejecting.
|
||||
// On "approve" these fields will
|
||||
// be ignored anyway.
|
||||
<>
|
||||
<TextInput
|
||||
field={form.privateComment}
|
||||
label="(Optional) private comment on why sign-up was rejected (shown to other admins only)"
|
||||
/>
|
||||
<Checkbox
|
||||
field={form.sendEmail}
|
||||
label="Send email to applicant"
|
||||
/>
|
||||
<TextInput
|
||||
field={form.message}
|
||||
label={"(Optional) message to include in email to applicant, if send email is checked"}
|
||||
/>
|
||||
</> }
|
||||
<MutationButton
|
||||
disabled={false}
|
||||
label={form.approveOrReject.value === "approve" ? "Approve" : "Reject"}
|
||||
result={result}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 (
|
||||
<form
|
||||
onSubmit={handleSignup}
|
||||
aria-labelledby="account-handle-signup"
|
||||
>
|
||||
<h3 id="account-handle-signup">Handle Account Sign-Up</h3>
|
||||
<Select
|
||||
field={form.approveOrReject}
|
||||
label="Approve or Reject"
|
||||
options={
|
||||
<>
|
||||
<option value="approve">Approve</option>
|
||||
<option value="reject">Reject</option>
|
||||
</>
|
||||
}
|
||||
>
|
||||
</Select>
|
||||
{ form.approveOrReject.value === "reject" &&
|
||||
// Only show form fields relevant
|
||||
// to "reject" if rejecting.
|
||||
// On "approve" these fields will
|
||||
// be ignored anyway.
|
||||
<>
|
||||
<TextInput
|
||||
field={form.privateComment}
|
||||
label="(Optional) private comment on why sign-up was rejected (shown to other admins only)"
|
||||
/>
|
||||
<Checkbox
|
||||
field={form.sendEmail}
|
||||
label="Send email to applicant"
|
||||
/>
|
||||
<TextInput
|
||||
field={form.message}
|
||||
label={"(Optional) message to include in email to applicant, if send email is checked"}
|
||||
/>
|
||||
</> }
|
||||
<MutationButton
|
||||
disabled={false}
|
||||
label={form.approveOrReject.value === "approve" ? "Approve" : "Reject"}
|
||||
result={result}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className="account-detail">
|
||||
<h1>Account Details</h1>
|
||||
<h1><BackButton to={backLocation} /> Account Details</h1>
|
||||
<FormWithData
|
||||
dataQuery={useGetAccountQuery}
|
||||
queryArg={params.accountID}
|
||||
DataForm={AccountDetailForm}
|
||||
{...{ backLocation: backLocation }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface AccountDetailFormProps {
|
||||
backLocation: string,
|
||||
data: AdminAccount,
|
||||
data: AdminAccount;
|
||||
backLocation: string;
|
||||
}
|
||||
|
||||
function AccountDetailForm({ data: adminAcct, backLocation }: AccountDetailFormProps) {
|
||||
let yesOrNo = (b: boolean) => {
|
||||
return b ? "yes" : "no";
|
||||
};
|
||||
function AccountDetailForm({ data: adminAcct, backLocation }: AccountDetailFormProps) {
|
||||
// If this is our instance account, don't
|
||||
// bother returning detailed account information.
|
||||
const ourInstanceAccount = UseOurInstanceAccount(adminAcct);
|
||||
if (ourInstanceAccount) {
|
||||
return (
|
||||
<>
|
||||
<FakeProfile {...adminAcct.account} />
|
||||
<div className="info">
|
||||
<i className="fa fa-fw fa-info-circle" aria-hidden="true"></i>
|
||||
<b>
|
||||
This is the service account for your instance; you
|
||||
cannot perform moderation actions on this account.
|
||||
</b>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
let created = new Date(adminAcct.created_at).toDateString();
|
||||
const local = !adminAcct.domain;
|
||||
return (
|
||||
<>
|
||||
<FakeProfile {...adminAcct.account} />
|
||||
<GeneralAccountDetails adminAcct={adminAcct} />
|
||||
{
|
||||
// Only show local account details
|
||||
// if this is a local account!
|
||||
local && <LocalAccountDetails adminAcct={adminAcct} />
|
||||
}
|
||||
<AccountActions
|
||||
account={adminAcct}
|
||||
backLocation={backLocation}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<FakeProfile {...adminAcct.account} />
|
||||
<h3>General Account Details</h3>
|
||||
{ adminAcct.suspended &&
|
||||
<div className="info">
|
||||
<i className="fa fa-fw fa-info-circle" aria-hidden="true"></i>
|
||||
<b>Account is suspended.</b>
|
||||
</div>
|
||||
{ adminAcct.suspended &&
|
||||
<div className="info">
|
||||
<i className="fa fa-fw fa-info-circle" aria-hidden="true"></i>
|
||||
<b>Account is suspended.</b>
|
||||
</div>
|
||||
}
|
||||
<dl className="info-list">
|
||||
{ !local &&
|
||||
|
|
@ -75,6 +113,18 @@ function AccountDetailForm({ data: adminAcct, backLocation }: AccountDetailFormP
|
|||
<dt>Domain</dt>
|
||||
<dd>{adminAcct.domain}</dd>
|
||||
</div>}
|
||||
<div className="info-list-entry">
|
||||
<dt>Profile URL</dt>
|
||||
<dd>
|
||||
<a
|
||||
href={adminAcct.account.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<i className="fa fa-fw fa-external-link" aria-hidden="true"></i> {adminAcct.account.url} (opens in a new tab)
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Created</dt>
|
||||
<dd><time dateTime={adminAcct.created_at}>{created}</time></dd>
|
||||
|
|
@ -104,61 +154,54 @@ function AccountDetailForm({ data: adminAcct, backLocation }: AccountDetailFormP
|
|||
<dd>{adminAcct.account.following_count}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{ local &&
|
||||
// Only show local account details
|
||||
// if this is a local account!
|
||||
<>
|
||||
<h3>Local Account Details</h3>
|
||||
{ !adminAcct.approved &&
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function LocalAccountDetails({ adminAcct }: { adminAcct: AdminAccount }) {
|
||||
return (
|
||||
<>
|
||||
<h3>Local Account Details</h3>
|
||||
{ !adminAcct.approved &&
|
||||
<div className="info">
|
||||
<i className="fa fa-fw fa-info-circle" aria-hidden="true"></i>
|
||||
<b>Account is pending.</b>
|
||||
</div>
|
||||
}
|
||||
{ !adminAcct.confirmed &&
|
||||
}
|
||||
{ !adminAcct.confirmed &&
|
||||
<div className="info">
|
||||
<i className="fa fa-fw fa-info-circle" aria-hidden="true"></i>
|
||||
<b>Account email not yet confirmed.</b>
|
||||
</div>
|
||||
}
|
||||
<dl className="info-list">
|
||||
<div className="info-list-entry">
|
||||
<dt>Email</dt>
|
||||
<dd>{adminAcct.email} {<b>{adminAcct.confirmed ? "(confirmed)" : "(not confirmed)"}</b> }</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Disabled</dt>
|
||||
<dd>{yesOrNo(adminAcct.disabled)}</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Approved</dt>
|
||||
<dd>{yesOrNo(adminAcct.approved)}</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Sign-Up Reason</dt>
|
||||
<dd>{adminAcct.invite_request ?? <i>none provided</i>}</dd>
|
||||
</div>
|
||||
{ (adminAcct.ip && adminAcct.ip !== "0.0.0.0") &&
|
||||
}
|
||||
<dl className="info-list">
|
||||
<div className="info-list-entry">
|
||||
<dt>Email</dt>
|
||||
<dd>{adminAcct.email} {<b>{adminAcct.confirmed ? "(confirmed)" : "(not confirmed)"}</b> }</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Disabled</dt>
|
||||
<dd>{yesOrNo(adminAcct.disabled)}</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Approved</dt>
|
||||
<dd>{yesOrNo(adminAcct.approved)}</dd>
|
||||
</div>
|
||||
<div className="info-list-entry">
|
||||
<dt>Sign-Up Reason</dt>
|
||||
<dd>{adminAcct.invite_request ?? <i>none provided</i>}</dd>
|
||||
</div>
|
||||
{ (adminAcct.ip && adminAcct.ip !== "0.0.0.0") &&
|
||||
<div className="info-list-entry">
|
||||
<dt>Sign-Up IP</dt>
|
||||
<dd>{adminAcct.ip}</dd>
|
||||
</div> }
|
||||
{ adminAcct.locale &&
|
||||
{ adminAcct.locale &&
|
||||
<div className="info-list-entry">
|
||||
<dt>Locale</dt>
|
||||
<dd>{adminAcct.locale}</dd>
|
||||
</div> }
|
||||
</dl>
|
||||
</> }
|
||||
{ local && !adminAcct.approved
|
||||
?
|
||||
<HandleSignup
|
||||
account={adminAcct}
|
||||
backLocation={backLocation}
|
||||
/>
|
||||
:
|
||||
<AccountActions account={adminAcct} />
|
||||
}
|
||||
</>
|
||||
</dl>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -20,10 +20,10 @@
|
|||
import React from "react";
|
||||
import { AccountSearchForm } from "./search";
|
||||
|
||||
export default function AccountsOverview({ }) {
|
||||
export default function AccountsSearch({ }) {
|
||||
return (
|
||||
<div className="accounts-view">
|
||||
<h1>Accounts Overview</h1>
|
||||
<h1>Accounts Search</h1>
|
||||
<span>
|
||||
You can perform actions on an account by clicking
|
||||
its name in a report, or by searching for the account
|
||||
|
|
|
|||
|
|
@ -17,20 +17,40 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 (
|
||||
<Username
|
||||
key={acc.acct}
|
||||
account={account}
|
||||
linkTo={`/${account.id}`}
|
||||
backLocation={location}
|
||||
classNames={["entry"]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="accounts-view">
|
||||
<h1>Pending Accounts</h1>
|
||||
<AccountList
|
||||
<PageableList
|
||||
isLoading={searchRes.isLoading}
|
||||
isFetching={searchRes.isFetching}
|
||||
isSuccess={searchRes.isSuccess}
|
||||
data={searchRes.data}
|
||||
items={searchRes.data?.accounts}
|
||||
itemToEntry={itemToEntry}
|
||||
isError={searchRes.isError}
|
||||
error={searchRes.error}
|
||||
emptyMessage="No pending account sign-ups."
|
||||
|
|
|
|||
|
|
@ -17,28 +17,53 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 (
|
||||
<Username
|
||||
key={acc.acct}
|
||||
account={account}
|
||||
linkTo={`/${account.id}`}
|
||||
backLocation={backLocation}
|
||||
classNames={["entry"]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
onSubmit={submitSearch}
|
||||
onSubmit={submitQuery}
|
||||
// Prevent password managers trying
|
||||
// to fill in username/email fields.
|
||||
autoComplete="off"
|
||||
|
|
@ -117,13 +158,16 @@ export function AccountSearchForm() {
|
|||
result={searchRes}
|
||||
/>
|
||||
</form>
|
||||
<AccountList
|
||||
<PageableList
|
||||
isLoading={searchRes.isLoading}
|
||||
isFetching={searchRes.isFetching}
|
||||
isSuccess={searchRes.isSuccess}
|
||||
data={searchRes.data}
|
||||
items={searchRes.data?.accounts}
|
||||
itemToEntry={itemToEntry}
|
||||
isError={searchRes.isError}
|
||||
error={searchRes.error}
|
||||
emptyMessage="No accounts found that match your query"
|
||||
prevNextLinks={searchRes.data?.links}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<MenuItem
|
||||
name="Accounts"
|
||||
itemUrl="accounts"
|
||||
defaultChild="overview"
|
||||
defaultChild="search"
|
||||
icon="fa-users"
|
||||
>
|
||||
<MenuItem
|
||||
name="Overview"
|
||||
itemUrl="overview"
|
||||
name="Search"
|
||||
itemUrl="search"
|
||||
icon="fa-list"
|
||||
/>
|
||||
<MenuItem
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { useValue, useTextInput } from "../../../lib/form";
|
|||
import useFormSubmit from "../../../lib/form/submit";
|
||||
import { TextArea } from "../../../components/form/inputs";
|
||||
import MutationButton from "../../../components/form/mutation-button";
|
||||
import Username from "./username";
|
||||
import Username from "../../../components/username";
|
||||
import { useGetReportQuery, useResolveReportMutation } from "../../../lib/query/admin/reports";
|
||||
import { useBaseUrl } from "../../../lib/navigation/util";
|
||||
|
||||
|
|
@ -53,13 +53,15 @@ function ReportDetailForm({ data: report }) {
|
|||
<div className="report detail">
|
||||
<div className="usernames">
|
||||
<Username
|
||||
user={from}
|
||||
link={`~/settings/moderation/accounts/${from.id}`}
|
||||
account={from}
|
||||
linkTo={`~/settings/moderation/accounts/${from.id}`}
|
||||
backLocation={`~/settings/moderation/reports/${report.id}`}
|
||||
/>
|
||||
<> reported </>
|
||||
<Username
|
||||
user={target}
|
||||
link={`~/settings/moderation/accounts/${target.id}`}
|
||||
account={target}
|
||||
linkTo={`~/settings/moderation/accounts/${target.id}`}
|
||||
backLocation={`~/settings/moderation/reports/${report.id}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
import React from "react";
|
||||
import { Link } from "wouter";
|
||||
import FormWithData from "../../../lib/form/form-with-data";
|
||||
import Username from "./username";
|
||||
import Username from "../../../components/username";
|
||||
import { useListReportsQuery } from "../../../lib/query/admin/reports";
|
||||
|
||||
export function ReportOverview({ }) {
|
||||
|
|
@ -75,7 +75,7 @@ function ReportEntry({ report }) {
|
|||
<div className={`report entry${report.action_taken ? " resolved" : ""}`}>
|
||||
<div className="byline">
|
||||
<div className="usernames">
|
||||
<Username user={from} /> reported <Username user={target} />
|
||||
<Username account={from} /> reported <Username account={target} />
|
||||
</div>
|
||||
<h3 className="report-status">
|
||||
{report.action_taken ? "Resolved" : "Open"}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import { ErrorBoundary } from "../../lib/navigation/error";
|
|||
import ImportExport from "./domain-permissions/import-export";
|
||||
import DomainPermissionsOverview from "./domain-permissions/overview";
|
||||
import DomainPermDetail from "./domain-permissions/detail";
|
||||
import AccountsOverview from "./accounts";
|
||||
import AccountsSearch from "./accounts";
|
||||
import AccountsPending from "./accounts/pending";
|
||||
import AccountDetail from "./accounts/detail";
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ import AccountDetail from "./accounts/detail";
|
|||
/**
|
||||
* - /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
|
||||
|
|
@ -95,7 +95,7 @@ function ModerationReportsRouter() {
|
|||
}
|
||||
|
||||
/**
|
||||
* - /settings/moderation/accounts/overview
|
||||
* - /settings/moderation/accounts/search
|
||||
* - /settings/moderation/accounts/pending
|
||||
* - /settings/moderation/accounts/:accountID
|
||||
*/
|
||||
|
|
@ -109,10 +109,10 @@ function ModerationAccountsRouter() {
|
|||
<Router base={thisBase}>
|
||||
<ErrorBoundary>
|
||||
<Switch>
|
||||
<Route path="/overview" component={AccountsOverview}/>
|
||||
<Route path="/search" component={AccountsSearch}/>
|
||||
<Route path="/pending" component={AccountsPending}/>
|
||||
<Route path="/:accountID" component={AccountDetail}/>
|
||||
<Route><Redirect to="/overview"/></Route>
|
||||
<Route><Redirect to="/search"/></Route>
|
||||
</Switch>
|
||||
</ErrorBoundary>
|
||||
</Router>
|
||||
|
|
|
|||
|
|
@ -1468,6 +1468,11 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/parse-link-header@^2.0.3":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-link-header/-/parse-link-header-2.0.3.tgz#37ad650d12aecb055b64c2d43ddb1534e356ad33"
|
||||
integrity sha512-ffLAxD6Xqcf2gSbtEJehj8yJ5R/2OZqD4liodQvQQ+hhO4kg1mk9ToEZQPMtNTm/zIQj2GNleQbsjPp9+UQm4Q==
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.8.tgz#805eae6e8f41bd19e88917d2ea200dc992f405d3"
|
||||
|
|
@ -5182,6 +5187,13 @@ parse-json@^2.2.0:
|
|||
dependencies:
|
||||
error-ex "^1.2.0"
|
||||
|
||||
parse-link-header@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-link-header/-/parse-link-header-2.0.0.tgz#949353e284f8aa01f2ac857a98f692b57733f6b7"
|
||||
integrity sha512-xjU87V0VyHZybn2RrCX5TIFGxTVZE6zqqZWMPlIKiSKuWh/X5WZdt+w1Ki1nXB+8L/KtL+nZ4iq+sfI6MrhhMw==
|
||||
dependencies:
|
||||
xtend "~4.0.1"
|
||||
|
||||
parse-ms@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue