update settings panels, add pending overview + approve/deny functions

This commit is contained in:
tobi 2024-04-11 12:03:17 +02:00
parent c097745c38
commit 85a76cc26e
32 changed files with 1124 additions and 471 deletions

View file

@ -130,10 +130,11 @@ main {
}
}
&:disabled {
&:disabled,
&.disabled {
color: $white2;
background: $gray2;
cursor: auto;
cursor: not-allowed;
&:hover {
background: $gray3;

View file

@ -1,112 +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/>.
*/
const React = require("react");
const { useRoute, Redirect } = require("wouter");
const query = require("../../lib/query");
const FormWithData = require("../../lib/form/form-with-data").default;
const { useBaseUrl } = require("../../lib/navigation/util");
const FakeProfile = require("../../components/fake-profile");
const MutationButton = require("../../components/form/mutation-button");
const useFormSubmit = require("../../lib/form/submit").default;
const { useValue, useTextInput } = require("../../lib/form");
const { TextInput } = require("../../components/form/inputs");
module.exports = function AccountDetail({ }) {
const baseUrl = useBaseUrl();
let [_match, params] = useRoute(`${baseUrl}/:accountId`);
if (params?.accountId == undefined) {
return <Redirect to={baseUrl} />;
} else {
return (
<div className="account-detail">
<h1>
Account Details
</h1>
<FormWithData
dataQuery={query.useGetAccountQuery}
queryArg={params.accountId}
DataForm={AccountDetailForm}
/>
</div>
);
}
};
function AccountDetailForm({ data: account }) {
let content;
if (account.suspended) {
content = (
<h2 className="error">Account is suspended.</h2>
);
} else {
content = <ModifyAccount account={account} />;
}
return (
<>
<FakeProfile {...account} />
{content}
</>
);
}
function ModifyAccount({ account }) {
const form = {
id: useValue("id", account.id),
reason: useTextInput("text")
};
const [modifyAccount, result] = useFormSubmit(form, query.useActionAccountMutation());
return (
<form onSubmit={modifyAccount}>
<h2>Actions</h2>
<TextInput
field={form.reason}
placeholder="Reason for this action"
/>
<div className="action-buttons">
{/* <MutationButton
label="Disable"
name="disable"
result={result}
/>
<MutationButton
label="Silence"
name="silence"
result={result}
/> */}
<MutationButton
label="Suspend"
name="suspend"
result={result}
/>
</div>
</form>
);
}

View file

@ -0,0 +1,89 @@
/*
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 { useActionAccountMutation } from "../../../lib/query";
import MutationButton from "../../../components/form/mutation-button";
import useFormSubmit from "../../../lib/form/submit";
import {
useValue,
useTextInput,
useBoolInput,
} from "../../../lib/form";
import { Checkbox, TextInput } from "../../../components/form/inputs";
import { AdminAccount } from "../../../lib/types/account";
export interface AccountActionsProps {
account: AdminAccount,
}
export function AccountActions({ account }: AccountActionsProps) {
const form = {
id: useValue("id", account.id),
reason: useTextInput("text")
};
const reallySuspend = useBoolInput("reallySuspend");
const [accountAction, result] = useFormSubmit(form, useActionAccountMutation());
return (
<form
onSubmit={accountAction}
aria-labelledby="account-moderation-actions"
>
<h3 id="account-moderation-actions">Account Moderation Actions</h3>
<div>
Currently only the "suspend" action is implemented.<br/>
Suspending an account will delete it from your server, and remove all of its media, posts, relationships, etc.<br/>
If the suspended account is local, suspending will also send out a "delete" message to other servers, requesting them to remove its data from their instance as well.<br/>
<b>Account suspension cannot be reversed.</b>
</div>
<TextInput
field={form.reason}
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"
name="suspend"
result={result}
/>
<Checkbox
label="Really suspend"
field={reallySuspend}
></Checkbox>
</div>
</form>
);
}

View file

@ -0,0 +1,118 @@
/*
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";
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,
accountsBaseUrl: string,
}
export function HandleSignup({account, accountsBaseUrl}: 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(accountsBaseUrl);
}
}
});
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>
);
}

View file

@ -0,0 +1,179 @@
/*
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 { useRoute, Redirect } from "wouter";
import { useGetAccountQuery } from "../../../lib/query";
import FormWithData from "../../../lib/form/form-with-data";
import { useBaseUrl } from "../../../lib/navigation/util";
import FakeProfile from "../../../components/fake-profile";
import { AdminAccount } from "../../../lib/types/account";
import { HandleSignup } from "./handlesignup";
import { AccountActions } from "./actions";
import BackButton from "../../../components/back-button";
export default function AccountDetail({ }) {
// /settings/admin/accounts
const accountsBaseUrl = useBaseUrl();
let [_match, params] = useRoute(`${accountsBaseUrl}/:accountId`);
if (params?.accountId == undefined) {
return <Redirect to={accountsBaseUrl} />;
} else {
return (
<div className="account-detail">
<h1 className="text-cutoff">
<BackButton to={accountsBaseUrl} /> Account Details
</h1>
<FormWithData
dataQuery={useGetAccountQuery}
queryArg={params.accountId}
DataForm={AccountDetailForm}
{...{accountsBaseUrl}}
/>
</div>
);
}
};
interface AccountDetailFormProps {
accountsBaseUrl: string,
data: AdminAccount,
}
function AccountDetailForm({ data: adminAcct, accountsBaseUrl }: AccountDetailFormProps) {
let yesOrNo = (b: boolean) => {
return b ? "yes" : "no"
};
let 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>
}
<dl className="info-list">
{ !local &&
<div className="info-list-entry">
<dt>Domain</dt>
<dd>{adminAcct.domain}</dd>
</div>}
<div className="info-list-entry">
<dt>Created</dt>
<dd><time dateTime={adminAcct.created_at}>{created}</time></dd>
</div>
<div className="info-list-entry">
<dt>Last posted</dt>
<dd>{lastPosted}</dd>
</div>
<div className="info-list-entry">
<dt>Suspended</dt>
<dd>{yesOrNo(adminAcct.suspended)}</dd>
</div>
<div className="info-list-entry">
<dt>Silenced</dt>
<dd>{yesOrNo(adminAcct.silenced)}</dd>
</div>
<div className="info-list-entry">
<dt>Statuses</dt>
<dd>{adminAcct.account.statuses_count}</dd>
</div>
<div className="info-list-entry">
<dt>Followers</dt>
<dd>{adminAcct.account.followers_count}</dd>
</div>
<div className="info-list-entry">
<dt>Following</dt>
<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 &&
<div className="info">
<i className="fa fa-fw fa-info-circle" aria-hidden="true"></i>
<b>Account is pending.</b>
</div>
}
{ !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") &&
<div className="info-list-entry">
<dt>Sign-Up IP</dt>
<dd>{adminAcct.ip}</dd>
</div> }
{ adminAcct.locale &&
<div className="info-list-entry">
<dt>Locale</dt>
<dd>{adminAcct.locale}</dd>
</div> }
</dl>
</> }
{ local && !adminAcct.approved
?
<HandleSignup
account={adminAcct}
accountsBaseUrl={accountsBaseUrl}
/>
:
<AccountActions account={adminAcct} />
}
</>
);
}

View file

@ -1,138 +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/>.
*/
const React = require("react");
const { Switch, Route, Link } = require("wouter");
const query = require("../../lib/query");
const { useTextInput } = require("../../lib/form");
const AccountDetail = require("./detail");
const { useBaseUrl } = require("../../lib/navigation/util");
const { Error } = require("../../components/error");
module.exports = function Accounts({ baseUrl }) {
return (
<div className="accounts">
<Switch>
<Route path={`${baseUrl}/:accountId`}>
<AccountDetail />
</Route>
<AccountOverview />
</Switch>
</div>
);
};
function AccountOverview({ }) {
return (
<>
<h1>Accounts</h1>
<div>
Pending <a href="https://github.com/superseriousbusiness/gotosocial/issues/581">#581</a>,
there is currently no way to list accounts.<br />
You can perform actions on reported accounts by clicking their name in the report, or searching for a username below.
</div>
<AccountSearchForm />
</>
);
}
function AccountSearchForm() {
const [searchAccount, result] = query.useSearchAccountMutation();
const [onAccountChange, _resetAccount, { account }] = useTextInput("account");
function submitSearch(e) {
e.preventDefault();
if (account.trim().length != 0) {
searchAccount(account);
}
}
return (
<div className="account-search">
<form onSubmit={submitSearch}>
<div className="form-field text">
<label htmlFor="url">
Account:
</label>
<div className="row">
<input
type="text"
id="account"
name="account"
onChange={onAccountChange}
value={account}
/>
<button disabled={result.isLoading}>
<i className={[
"fa fa-fw",
(result.isLoading
? "fa-refresh fa-spin"
: "fa-search")
].join(" ")} aria-hidden="true" title="Search" />
<span className="sr-only">Search</span>
</button>
</div>
</div>
</form>
<AccountList
isSuccess={result.isSuccess}
data={result.data}
isError={result.isError}
error={result.error}
/>
</div>
);
}
function AccountList({ isSuccess, data, isError, error }) {
const baseUrl = useBaseUrl();
if (!(isSuccess || isError)) {
return null;
}
if (error) {
return <Error error={error} />;
}
if (data.length == 0) {
return <b>No accounts found that match your query</b>;
}
return (
<>
<h2>Results:</h2>
<div className="list">
{data.map((acc) => (
<Link key={acc.acct} className="account entry" to={`${baseUrl}/${acc.id}`}>
{acc.display_name?.length > 0
? acc.display_name
: acc.username
}
<span id="username">(@{acc.acct})</span>
</Link>
))}
</div>
</>
);
}

View file

@ -0,0 +1,49 @@
/*
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 { Switch, Route } from "wouter";
import AccountDetail from "./detail";
import { AccountSearchForm } from "./search";
export default function Accounts({ baseUrl }) {
return (
<Switch>
<Route path={`${baseUrl}/:accountId`}>
<AccountDetail />
</Route>
<AccountOverview />
</Switch>
);
};
function AccountOverview({ }) {
return (
<div className="accounts-view">
<h1>Accounts Overview</h1>
<span>
You can perform actions on an account by clicking
its name in a report, or by searching for the account
using the form below and clicking on its name.
</span>
<AccountSearchForm />
</div>
);
}

View file

@ -0,0 +1,40 @@
/*
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 { useSearchAccountsQuery } from "../../../lib/query";
import { AccountList } from "../../../components/account-list";
export default function AccountsPending({ baseUrl }) {
const searchRes = useSearchAccountsQuery({status: "pending"});
return (
<div className="accounts-view">
<h1>Pending Accounts</h1>
<AccountList
isLoading={searchRes.isLoading}
isSuccess={searchRes.isSuccess}
data={searchRes.data}
isError={searchRes.isError}
error={searchRes.error}
emptyMessage="No pending account sign-ups."
/>
</div>
);
};

View file

@ -0,0 +1,125 @@
/*
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 { useLazySearchAccountsQuery } from "../../../lib/query";
import { useTextInput } from "../../../lib/form";
import { AccountList } from "../../../components/account-list";
import { SearchAccountParams } from "../../../lib/types/account";
import { Select, TextInput } from "../../../components/form/inputs";
import MutationButton from "../../../components/form/mutation-button";
export function AccountSearchForm() {
const [searchAcct, searchRes] = useLazySearchAccountsQuery();
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"),
}
function submitSearch(e) {
e.preventDefault();
// Parse query parameters.
const entries = Object.entries(form).map(([k, v]) => {
// Take only defined form fields.
if (v.value === undefined || v.value.length === 0) {
return null;
}
return [[k, v.value]]
}).flatMap(kv => {
// Remove any nulls.
return kv || [];
})
const params: SearchAccountParams = Object.fromEntries(entries);
searchAcct(params)
}
return (
<>
<form onSubmit={submitSearch}>
<TextInput
field={form.username}
label={"(Optional) username (without leading '@' symbol)"}
placeholder="someone"
/>
<TextInput
field={form.by_domain}
label={"(Optional) domain"}
placeholder="example.org"
/>
<Select
field={form.origin}
label="Account origin"
options={
<>
<option value="">Local or remote</option>
<option value="local">Local only</option>
<option value="remote">Remote only</option>
</>
}
></Select>
<TextInput
field={form.email}
label={"(Optional) email address (local accounts only)"}
placeholder={"someone@example.org"}
/>
<TextInput
field={form.ip}
label={"(Optional) IP address (local accounts only)"}
placeholder={"198.51.100.0"}
/>
<Select
field={form.status}
label="Account status"
options={
<>
<option value="">Any</option>
<option value="pending">Pending only</option>
<option value="disabled">Disabled only</option>
<option value="suspended">Suspended only</option>
</>
}
></Select>
<MutationButton
disabled={false}
label={"Search"}
result={searchRes}
/>
</form>
<AccountList
isLoading={searchRes.isLoading}
isSuccess={searchRes.isSuccess}
data={searchRes.data}
isError={searchRes.isError}
error={searchRes.error}
emptyMessage="No accounts found that match your query"
/>
</>
);
}

View file

@ -17,19 +17,19 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import React from "react";
const query = require("../../../lib/query");
import { useInstanceKeysExpireMutation } from "../../../lib/query";
const { useTextInput } = require("../../../lib/form");
const { TextInput } = require("../../../components/form/inputs");
import { useTextInput } from "../../../lib/form";
import { TextInput } from "../../../components/form/inputs";
const MutationButton = require("../../../components/form/mutation-button");
import MutationButton from "../../../components/form/mutation-button";
module.exports = function ExpireRemote({}) {
export default function ExpireRemote({}) {
const domainField = useTextInput("domain");
const [expire, expireResult] = query.useInstanceKeysExpireMutation();
const [expire, expireResult] = useInstanceKeysExpireMutation();
function submitExpire(e) {
e.preventDefault();
@ -53,7 +53,11 @@ module.exports = function ExpireRemote({}) {
type="string"
placeholder="example.org"
/>
<MutationButton label="Expire keys" result={expireResult} />
<MutationButton
disabled={false}
label="Expire keys"
result={expireResult}
/>
</form>
);
};

View file

@ -17,10 +17,10 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const ExpireRemote = require("./expireremote");
import React from "react";
import ExpireRemote from "./expireremote";
module.exports = function Keys() {
export default function Keys() {
return (
<>
<h1>Key Actions</h1>

View file

@ -17,19 +17,19 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import React from "react";
const query = require("../../../lib/query");
import { useMediaCleanupMutation } from "../../../lib/query";
const { useTextInput } = require("../../../lib/form");
const { TextInput } = require("../../../components/form/inputs");
import { useTextInput } from "../../../lib/form";
import { TextInput } from "../../../components/form/inputs";
const MutationButton = require("../../../components/form/mutation-button");
import MutationButton from "../../../components/form/mutation-button";
module.exports = function Cleanup({}) {
const daysField = useTextInput("days", { defaultValue: 30 });
export default function Cleanup({}) {
const daysField = useTextInput("days", { defaultValue: "30" });
const [mediaCleanup, mediaCleanupResult] = query.useMediaCleanupMutation();
const [mediaCleanup, mediaCleanupResult] = useMediaCleanupMutation();
function submitCleanup(e) {
e.preventDefault();
@ -51,7 +51,11 @@ module.exports = function Cleanup({}) {
min="0"
placeholder="30"
/>
<MutationButton label="Remove old media" result={mediaCleanupResult} />
<MutationButton
disabled={false}
label="Remove old media"
result={mediaCleanupResult}
/>
</form>
);
};

View file

@ -17,10 +17,10 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const Cleanup = require("./cleanup");
import React from "react";
import Cleanup from "./cleanup";
module.exports = function Media() {
export default function Media() {
return (
<>
<h1>Media Actions</h1>

View file

@ -100,9 +100,9 @@ export default function ImportExportForm({ form, submitParse, parseResult }: Imp
onClick={() => submitParse()}
result={parseResult}
showError={false}
disabled={false}
disabled={form.permType.value === undefined || form.permType.value.length === 0}
/>
<label className="button with-icon">
<label className={`button with-icon${form.permType.value === undefined || form.permType.value.length === 0 ? " disabled" : ""}`}>
<i className="fa fa-fw " aria-hidden="true" />
Import file
<input
@ -110,6 +110,7 @@ export default function ImportExportForm({ form, submitParse, parseResult }: Imp
className="hidden"
onChange={fileChanged}
accept="application/json,text/plain,text/csv"
disabled={form.permType.value === undefined || form.permType.value.length === 0}
/>
</label>
<b /> {/* grid filler */}
@ -118,7 +119,7 @@ export default function ImportExportForm({ form, submitParse, parseResult }: Imp
type="button"
onClick={() => submitExport("export")}
result={exportResult} showError={false}
disabled={false}
disabled={form.permType.value === undefined || form.permType.value.length === 0}
/>
<MutationButton
label="Export to file"
@ -127,7 +128,7 @@ export default function ImportExportForm({ form, submitParse, parseResult }: Imp
onClick={() => submitExport("export-file")}
result={exportResult}
showError={false}
disabled={false}
disabled={form.permType.value === undefined || form.permType.value.length === 0}
/>
<div className="export-file">
<span>

View file

@ -17,29 +17,25 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const { useRoute, Link, Redirect } = require("wouter");
import React, { useEffect } from "react";
import { useRoute, Link, Redirect } from "wouter";
const { useComboBoxInput, useFileInput, useValue } = require("../../../lib/form");
const { CategorySelect } = require("../category-select");
import { useComboBoxInput, useFileInput, useValue } from "../../../lib/form";
import { CategorySelect } from "../category-select";
const useFormSubmit = require("../../../lib/form/submit").default;
const { useBaseUrl } = require("../../../lib/navigation/util");
import useFormSubmit from "../../../lib/form/submit";
import { useBaseUrl } from "../../../lib/navigation/util";
const FakeToot = require("../../../components/fake-toot");
const FormWithData = require("../../../lib/form/form-with-data").default;
const Loading = require("../../../components/loading");
const { FileInput } = require("../../../components/form/inputs");
const MutationButton = require("../../../components/form/mutation-button");
const { Error } = require("../../../components/error");
import FakeToot from "../../../components/fake-toot";
import FormWithData from "../../../lib/form/form-with-data";
import Loading from "../../../components/loading";
import { FileInput } from "../../../components/form/inputs";
import MutationButton from "../../../components/form/mutation-button";
import { Error } from "../../../components/error";
const {
useGetEmojiQuery,
useEditEmojiMutation,
useDeleteEmojiMutation,
} = require("../../../lib/query/admin/custom-emoji");
import { useGetEmojiQuery, useEditEmojiMutation, useDeleteEmojiMutation } from "../../../lib/query/admin/custom-emoji";
module.exports = function EmojiDetailRoute({ }) {
export default function EmojiDetailRoute({ }) {
const baseUrl = useBaseUrl();
let [_match, params] = useRoute(`${baseUrl}/:emojiId`);
if (params?.emojiId == undefined) {
@ -68,7 +64,7 @@ function EmojiDetailForm({ data: emoji }) {
const [modifyEmoji, result] = useFormSubmit(form, useEditEmojiMutation());
// Automatic submitting of category change
React.useEffect(() => {
useEffect(() => {
if (
form.category.hasChanged() &&
!form.category.state.open &&

View file

@ -17,13 +17,13 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const { Switch, Route } = require("wouter");
import React from "react";
import { Switch, Route } from "wouter";
const EmojiOverview = require("./overview");
const EmojiDetail = require("./detail");
import EmojiOverview from "./overview";
import EmojiDetail from "./detail";
module.exports = function CustomEmoji({ baseUrl }) {
export default function CustomEmoji({ baseUrl }) {
return (
<Switch>
<Route path={`${baseUrl}/:emojiId`}>

View file

@ -17,31 +17,26 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import React, { useMemo, useEffect } from "react";
const {
useFileInput,
useComboBoxInput
} = require("../../../lib/form");
const useShortcode = require("./use-shortcode");
import { useFileInput, useComboBoxInput } from "../../../lib/form";
import useShortcode from "./use-shortcode";
const useFormSubmit = require("../../../lib/form/submit").default;
import useFormSubmit from "../../../lib/form/submit";
const {
TextInput, FileInput
} = require("../../../components/form/inputs");
import { TextInput, FileInput } from "../../../components/form/inputs";
const { CategorySelect } = require('../category-select');
const FakeToot = require("../../../components/fake-toot");
const MutationButton = require("../../../components/form/mutation-button");
const { useAddEmojiMutation } = require("../../../lib/query/admin/custom-emoji");
const { useInstanceV1Query } = require("../../../lib/query");
import { CategorySelect } from '../category-select';
import FakeToot from "../../../components/fake-toot";
import MutationButton from "../../../components/form/mutation-button";
import { useAddEmojiMutation } from "../../../lib/query/admin/custom-emoji";
import { useInstanceV1Query } from "../../../lib/query";
module.exports = function NewEmojiForm() {
export default function NewEmojiForm() {
const shortcode = useShortcode();
const { data: instance } = useInstanceV1Query();
const emojiMaxSize = React.useMemo(() => {
const emojiMaxSize = useMemo(() => {
return instance?.configuration?.emojis?.emoji_size_limit ?? 50 * 1024;
}, [instance]);
@ -56,8 +51,8 @@ module.exports = function NewEmojiForm() {
shortcode, image, category
}, useAddEmojiMutation());
React.useEffect(() => {
if (shortcode.value.length == 0) {
useEffect(() => {
if (shortcode.value === undefined || shortcode.value.length == 0) {
if (image.value != undefined) {
let [name, _ext] = image.value.name.split(".");
shortcode.setter(name);
@ -71,7 +66,7 @@ module.exports = function NewEmojiForm() {
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [image.value]);
let emojiOrShortcode = `:${shortcode.value}:`;
let emojiOrShortcode;
if (image.previewValue != undefined) {
emojiOrShortcode = <img
@ -80,6 +75,10 @@ module.exports = function NewEmojiForm() {
title={`:${shortcode.value}:`}
alt={shortcode.value}
/>;
} else if (shortcode.value !== undefined && shortcode.value.length > 0) {
emojiOrShortcode = `:${shortcode.value}:`;
} else {
emojiOrShortcode = `:your_emoji_here:`;
}
return (
@ -103,9 +102,14 @@ module.exports = function NewEmojiForm() {
<CategorySelect
field={category}
children={[]}
/>
<MutationButton label="Upload emoji" result={result} />
<MutationButton
disabled={image.previewValue === undefined}
label="Upload emoji"
result={result}
/>
</form>
</div>
);

View file

@ -22,7 +22,7 @@ const { Link } = require("wouter");
const syncpipe = require("syncpipe");
const { matchSorter } = require("match-sorter");
const NewEmojiForm = require("./new-emoji");
const NewEmojiForm = require("./new-emoji").default;
const { useTextInput } = require("../../../lib/form");
const { useEmojiByCategory } = require("../category-select");

View file

@ -17,15 +17,15 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import React, { useMemo } from "react";
const ParseFromToot = require("./parse-from-toot");
import ParseFromToot from "./parse-from-toot";
const Loading = require("../../../components/loading");
const { Error } = require("../../../components/error");
const { useListEmojiQuery } = require("../../../lib/query/admin/custom-emoji");
import Loading from "../../../components/loading";
import { Error } from "../../../components/error";
import { useListEmojiQuery } from "../../../lib/query/admin/custom-emoji";
module.exports = function RemoteEmoji() {
export default function RemoteEmoji() {
// local emoji are queried for shortcode collision detection
const {
data: emoji = [],
@ -33,7 +33,7 @@ module.exports = function RemoteEmoji() {
error
} = useListEmojiQuery({ filter: "domain:local" });
const emojiCodes = React.useMemo(() => {
const emojiCodes = useMemo(() => {
return new Set(emoji.map((e) => e.shortcode));
}, [emoji]);
@ -46,7 +46,7 @@ module.exports = function RemoteEmoji() {
{isLoading
? <Loading />
: <>
<ParseFromToot emoji={emoji} emojiCodes={emojiCodes} />
<ParseFromToot emojiCodes={emojiCodes} />
</>
}
</>

View file

@ -17,26 +17,23 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const { useRoute, Redirect } = require("wouter");
import React, { useState } from "react";
import { useRoute, Redirect } from "wouter";
const FormWithData = require("../../lib/form/form-with-data").default;
const BackButton = require("../../components/back-button");
import FormWithData from "../../lib/form/form-with-data";
import BackButton from "../../components/back-button";
const { useValue, useTextInput } = require("../../lib/form");
const useFormSubmit = require("../../lib/form/submit").default;
import { useValue, useTextInput } from "../../lib/form";
import useFormSubmit from "../../lib/form/submit";
const { TextArea } = require("../../components/form/inputs");
import { TextArea } from "../../components/form/inputs";
const MutationButton = require("../../components/form/mutation-button");
const Username = require("./username");
const { useBaseUrl } = require("../../lib/navigation/util");
const {
useGetReportQuery,
useResolveReportMutation,
} = require("../../lib/query/admin/reports");
import MutationButton from "../../components/form/mutation-button";
import Username from "./username";
import { useBaseUrl } from "../../lib/navigation/util";
import { useGetReportQuery, useResolveReportMutation } from "../../lib/query/admin/reports";
module.exports = function ReportDetail({ }) {
export default function ReportDetail({ }) {
const baseUrl = useBaseUrl();
let [_match, params] = useRoute(`${baseUrl}/:reportId`);
if (params?.reportId == undefined) {
@ -131,7 +128,11 @@ function ReportActionForm({ report }) {
field={form.comment}
label="Comment"
/>
<MutationButton label="Resolve" result={result} />
<MutationButton
disabled={false}
label="Resolve"
result={result}
/>
</form>
);
}
@ -170,10 +171,10 @@ function ReportedToot({ toot }) {
}
</section>
<aside className="status-info">
<dl class="status-stats">
<div class="stats-grouping">
<div class="stats-item published-at text-cutoff">
<dt class="sr-only">Published</dt>
<dl className="status-stats">
<div className="stats-grouping">
<div className="stats-item published-at text-cutoff">
<dt className="sr-only">Published</dt>
<dd>
<time dateTime={toot.created_at}>{new Date(toot.created_at).toLocaleString()}</time>
</dd>
@ -186,7 +187,7 @@ function ReportedToot({ toot }) {
}
function TootCW({ note, content }) {
const [visible, setVisible] = React.useState(false);
const [visible, setVisible] = useState(false);
function toggleVisible() {
setVisible(!visible);
@ -217,12 +218,12 @@ function TootMedia({ media, sensitive }) {
<input id={`sensitiveMedia-${m.id}`} type="checkbox" className="sensitive-checkbox hidden" />
<div className="sensitive">
<div className="open">
<label htmlFor={`sensitiveMedia-${m.id}`} className="button" role="button" tabIndex="0">
<label htmlFor={`sensitiveMedia-${m.id}`} className="button" role="button" tabIndex={0}>
<i className="fa fa-eye-slash" title="Hide sensitive media"></i>
</label>
</div>
<div className="closed" title={m.description}>
<label htmlFor={`sensitiveMedia-${m.id}`} className="button" role="button" tabIndex="0">
<label htmlFor={`sensitiveMedia-${m.id}`} className="button" role="button" tabIndex={0}>
Show sensitive media
</label>
</div>
@ -241,8 +242,7 @@ function TootMedia({ media, sensitive }) {
alt={m.description}
src={m.url}
// thumb={m.preview_url}
size={m.meta?.original}
type={m.type}
sizes={m.meta?.original}
/>
</a>
</div>

View file

@ -17,17 +17,17 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const { Link, Switch, Route } = require("wouter");
import React from "react";
import { Link, Switch, Route } from "wouter";
const FormWithData = require("../../lib/form/form-with-data").default;
import FormWithData from "../../lib/form/form-with-data";
const ReportDetail = require("./detail");
const Username = require("./username");
const { useBaseUrl } = require("../../lib/navigation/util");
const { useListReportsQuery } = require("../../lib/query/admin/reports");
import ReportDetail from "./detail";
import Username from "./username";
import { useBaseUrl } from "../../lib/navigation/util";
import { useListReportsQuery } from "../../lib/query/admin/reports";
module.exports = function Reports({ baseUrl }) {
export default function Reports({ baseUrl }) {
return (
<div className="reports">
<Switch>

View file

@ -17,10 +17,10 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const { Link } = require("wouter");
import React from "react";
import { Link } from "wouter";
module.exports = function Username({ user, link = true }) {
export default function Username({ user, link = true }) {
let className = "user";
let isLocal = user.domain == null;
@ -36,8 +36,8 @@ module.exports = function Username({ user, link = true }) {
? { fa: "fa-home", info: "Local user" }
: { fa: "fa-external-link-square", info: "Remote user" };
let Element = "div";
let href = null;
let Element: any = "div";
let href: any = null;
if (link) {
Element = Link;

View file

@ -17,25 +17,26 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const { Switch, Route, Link, Redirect, useRoute } = require("wouter");
import React from "react";
import { Switch, Route, Link, Redirect, useRoute } from "wouter";
const query = require("../../lib/query");
const FormWithData = require("../../lib/form/form-with-data").default;
const { useBaseUrl } = require("../../lib/navigation/util");
import { useInstanceRulesQuery, useAddInstanceRuleMutation, useUpdateInstanceRuleMutation, useDeleteInstanceRuleMutation } from "../../lib/query";
import FormWithData from "../../lib/form/form-with-data";
import { useBaseUrl } from "../../lib/navigation/util";
const { useValue, useTextInput } = require("../../lib/form");
const useFormSubmit = require("../../lib/form/submit").default;
import { useValue, useTextInput } from "../../lib/form";
import useFormSubmit from "../../lib/form/submit";
const { TextArea } = require("../../components/form/inputs");
const MutationButton = require("../../components/form/mutation-button");
import { TextArea } from "../../components/form/inputs";
import MutationButton from "../../components/form/mutation-button";
import { Error } from "../../components/error";
module.exports = function InstanceRulesData({ baseUrl }) {
export default function InstanceRulesData({ baseUrl }) {
return (
<FormWithData
dataQuery={query.useInstanceRulesQuery}
dataQuery={useInstanceRulesQuery}
DataForm={InstanceRules}
baseUrl={baseUrl}
{...{baseUrl}}
/>
);
};
@ -64,7 +65,8 @@ function InstanceRules({ baseUrl, data: rules }) {
function InstanceRuleList({ rules }) {
const newRule = useTextInput("text", {});
const [submitForm, result] = useFormSubmit({ newRule }, query.useAddInstanceRuleMutation(), {
const [submitForm, result] = useFormSubmit({ newRule }, useAddInstanceRuleMutation(), {
changedOnly: true,
onFinish: () => newRule.reset()
});
@ -72,7 +74,7 @@ function InstanceRuleList({ rules }) {
<>
<form onSubmit={submitForm} className="new-rule">
<ol className="instance-rules">
{Object.values(rules).map((rule) => (
{Object.values(rules).map((rule: any) => (
<InstanceRule key={rule.id} rule={rule} />
))}
</ol>
@ -80,7 +82,11 @@ function InstanceRuleList({ rules }) {
field={newRule}
label="New instance rule"
/>
<MutationButton label="Add rule" result={result} />
<MutationButton
disabled={newRule.value === undefined || newRule.value.length === 0}
label="Add rule"
result={result}
/>
</form>
</>
);
@ -124,9 +130,9 @@ function InstanceRuleForm({ rule }) {
rule: useTextInput("text", { defaultValue: rule.text })
};
const [submitForm, result] = useFormSubmit(form, query.useUpdateInstanceRuleMutation());
const [submitForm, result] = useFormSubmit(form, useUpdateInstanceRuleMutation());
const [deleteRule, deleteResult] = query.useDeleteInstanceRuleMutation({ fixedCacheKey: rule.id });
const [deleteRule, deleteResult] = useDeleteInstanceRuleMutation({ fixedCacheKey: rule.id });
if (result.isSuccess || deleteResult.isSuccess) {
return (
@ -150,6 +156,7 @@ function InstanceRuleForm({ rule }) {
/>
<MutationButton
disabled={false}
type="button"
onClick={() => deleteRule(rule.id)}
label="Delete"

View file

@ -0,0 +1,85 @@
/*
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 { useBaseUrl } from "../lib/navigation/util";
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) {
const baseUrl = useBaseUrl();
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={`/settings/admin/accounts/${acc.id}`}
>
{acc.display_name?.length > 0
? acc.display_name
: acc.username
}
<span id="username">(@{acc.acct})</span>
</Link>
))}
</div>
);
}

View file

@ -1,48 +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/>.
*/
const React = require("react");
const { Error } = require("../error");
module.exports = function MutationButton({ label, result, disabled, showError = true, className = "", wrapperClassName = "", ...inputProps }) {
let iconClass = "";
const targetsThisButton = result.action == inputProps.name; // can also both be undefined, which is correct
if (targetsThisButton) {
if (result.isLoading) {
iconClass = "fa-spin fa-refresh";
} else if (result.isSuccess) {
iconClass = "fa-check fadeout";
}
}
return (<div className={wrapperClassName}>
{(showError && targetsThisButton && result.error) &&
<Error error={result.error} />
}
<button type="submit" className={"with-icon " + className} disabled={result.isLoading || disabled} {...inputProps}>
<i className={`fa fa-fw ${iconClass}`} aria-hidden="true"></i>
{(targetsThisButton && result.isLoading)
? "Processing..."
: label
}
</button>
</div>
);
};

View file

@ -0,0 +1,72 @@
/*
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 { Error } from "../error";
export interface MutationButtonProps extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
label: string,
result,
disabled: boolean,
showError?: boolean,
className?: string,
wrapperClassName?: string,
}
export default function MutationButton({
label,
result,
disabled,
showError = true,
className = "",
wrapperClassName = "",
...inputProps
}: MutationButtonProps) {
let iconClass = "";
// Can also both be undefined, which is correct.
const targetsThisButton = result.action == inputProps.name;
if (targetsThisButton) {
if (result.isLoading) {
iconClass = " fa-spin fa-refresh";
} else if (result.isSuccess) {
iconClass = " fa-check fadeout";
}
}
return (
<div className={wrapperClassName}>
{(showError && targetsThisButton && result.error) &&
<Error error={result.error} />
}
<button
type="submit"
className={"with-icon " + className}
disabled={result.isLoading || disabled}
{...inputProps}
>
<i className={`fa fa-fw${iconClass}`} aria-hidden="true"></i>
{(targetsThisButton && result.isLoading)
? "Processing..."
: label
}
</button>
</div>
);
};

View file

@ -34,10 +34,22 @@ const UserProfile = require("./user/profile").default;
const UserSettings = require("./user/settings").default;
const UserMigration = require("./user/migration").default;
const Reports = require("./admin/reports").default;
const Accounts = require("./admin/accounts").default;
const AccountsPending = require("./admin/accounts/pending").default;
const DomainPerms = require("./admin/domain-permissions").default;
const DomainPermsImportExport = require("./admin/domain-permissions/import-export").default;
const AdminMedia = require("./admin/actions/media").default;
const AdminKeys = require("./admin/actions/keys").default;
const LocalEmoji = require("./admin/emoji/local").default;
const RemoteEmoji = require("./admin/emoji/remote").default;
const InstanceSettings = require("./admin/settings").default;
const InstanceRules = require("./admin/settings/rules").default;
require("./style.css");
@ -51,8 +63,11 @@ const { Sidebar, ViewRouter } = createNavigation("/settings", [
url: "admin",
permissions: ["admin"]
}, [
Item("Reports", { icon: "fa-flag", wildcard: true }, require("./admin/reports")),
Item("Accounts", { icon: "fa-users", wildcard: true }, require("./admin/accounts")),
Item("Reports", { icon: "fa-flag", wildcard: true }, Reports),
Item("Accounts", { icon: "fa-users", wildcard: true }, [
Item("Overview", { icon: "fa-list", url: "", wildcard: true }, Accounts),
Item("Pending", { icon: "fa-question", url: "pending", wildcard: true }, AccountsPending),
]),
Menu("Domain Permissions", { icon: "fa-hubzilla" }, [
Item("Blocks", { icon: "fa-close", url: "block", wildcard: true }, DomainPerms),
Item("Allows", { icon: "fa-check", url: "allow", wildcard: true }, DomainPerms),
@ -65,16 +80,16 @@ const { Sidebar, ViewRouter } = createNavigation("/settings", [
permissions: ["admin"]
}, [
Menu("Actions", { icon: "fa-bolt" }, [
Item("Media", { icon: "fa-photo" }, require("./admin/actions/media")),
Item("Keys", { icon: "fa-key-modern" }, require("./admin/actions/keys")),
Item("Media", { icon: "fa-photo" }, AdminMedia),
Item("Keys", { icon: "fa-key-modern" }, AdminKeys),
]),
Menu("Custom Emoji", { icon: "fa-smile-o" }, [
Item("Local", { icon: "fa-home", wildcard: true }, require("./admin/emoji/local")),
Item("Remote", { icon: "fa-cloud" }, require("./admin/emoji/remote"))
Item("Local", { icon: "fa-home", wildcard: true }, LocalEmoji),
Item("Remote", { icon: "fa-cloud" }, RemoteEmoji),
]),
Menu("Settings", { icon: "fa-sliders" }, [
Item("Settings", { icon: "fa-sliders", url: "" }, InstanceSettings),
Item("Rules", { icon: "fa-dot-circle-o", wildcard: true }, require("./admin/settings/rules"))
Item("Rules", { icon: "fa-dot-circle-o", wildcard: true }, InstanceRules),
]),
])
]);

View file

@ -17,16 +17,16 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const RoleContext = React.createContext([]);
const BaseUrlContext = React.createContext(null);
import { createContext, useContext } from "react";
const RoleContext = createContext([]);
const BaseUrlContext = createContext<string>("");
function urlSafe(str) {
return str.toLowerCase().replace(/[\s/]+/g, "-");
}
function useHasPermission(permissions) {
const roles = React.useContext(RoleContext);
const roles = useContext(RoleContext);
return checkPermission(permissions, roles);
}
@ -41,9 +41,14 @@ function checkPermission(requiredPermissisons, user) {
}
function useBaseUrl() {
return React.useContext(BaseUrlContext);
return useContext(BaseUrlContext);
}
module.exports = {
urlSafe, RoleContext, useHasPermission, checkPermission, BaseUrlContext, useBaseUrl
};
export {
urlSafe,
RoleContext,
useHasPermission,
checkPermission,
BaseUrlContext,
useBaseUrl
};

View file

@ -20,6 +20,7 @@
import { replaceCacheOnMutation, removeFromCacheOnMutation } from "../query-modifiers";
import { gtsApi } from "../gts-api";
import { listToKeyedObject } from "../transforms";
import { AdminAccount, HandleSignupParams, SearchAccountParams } from "../../types/account";
const extended = gtsApi.injectEndpoints({
endpoints: (build) => ({
@ -54,14 +55,43 @@ const extended = gtsApi.injectEndpoints({
})
}),
getAccount: build.query({
getAccount: build.query<AdminAccount, string>({
query: (id) => ({
url: `/api/v1/accounts/${id}`
url: `/api/v1/admin/accounts/${id}`
}),
providesTags: (_, __, id) => [{ type: "Account", id }]
providesTags: (_result, _error, id) => [
{ type: 'Account', id }
],
}),
actionAccount: build.mutation({
searchAccounts: build.query<AdminAccount[], SearchAccountParams>({
query: (form) => {
const params = new(URLSearchParams)
Object.entries(form).forEach(([k, v]) => {
if (v !== undefined) {
params.append(k, v)
}
})
let query = "";
if (params.size !== 0) {
query = `?${params.toString()}`;
}
return {
url: `/api/v2/admin/accounts${query}`
}
},
providesTags: (res) =>
res
? [
...res.map(({ id }) => ({ type: 'Account' as const, id })),
{ type: 'Account', id: 'LIST' },
]
: [{ type: 'Account', id: 'LIST' }],
}),
actionAccount: build.mutation<string, { id: string, action: string, reason: string }>({
query: ({ id, action, reason }) => ({
method: "POST",
url: `/api/v1/admin/accounts/${id}/action`,
@ -71,16 +101,24 @@ const extended = gtsApi.injectEndpoints({
text: reason
}
}),
invalidatesTags: (_, __, { id }) => [{ type: "Account", id }]
invalidatesTags: (_result, _error, { id }) => [
{ type: 'Account', id },
],
}),
searchAccount: build.mutation({
query: (username) => ({
url: `/api/v2/search?q=${encodeURIComponent(username)}&resolve=true`
}),
transformResponse: (res) => {
return res.accounts ?? [];
}
handleSignup: build.mutation<AdminAccount, HandleSignupParams>({
query: ({id, approve_or_reject, ...formData}) => {
console.log(formData);
return {
method: "POST",
url: `/api/v1/admin/accounts/${id}/${approve_or_reject}`,
asForm: true,
body: approve_or_reject === "reject" ?? formData,
}
},
invalidatesTags: (_result, _error, { id }) => [
{ type: 'Account', id },
],
}),
instanceRules: build.query({
@ -140,7 +178,9 @@ export const {
useInstanceKeysExpireMutation,
useGetAccountQuery,
useActionAccountMutation,
useSearchAccountMutation,
useSearchAccountsQuery,
useLazySearchAccountsQuery,
useHandleSignupMutation,
useInstanceRulesQuery,
useAddInstanceRuleMutation,
useUpdateInstanceRuleMutation,

View file

@ -36,7 +36,7 @@ const extended = gtsApi.injectEndpoints({
...params
}
}),
providesTags: ["Reports"]
providesTags: [{ type: "Reports", id: "LIST" }]
}),
getReport: build.query<AdminReport, string>({

View file

@ -0,0 +1,88 @@
/*
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 { CustomEmoji } from "./custom-emoji";
export interface AdminAccount {
id: string,
username: string,
domain: string | null,
created_at: string,
email: string,
ip: string | null,
ips: [],
locale: string,
invite_request: string | null,
role: any,
confirmed: boolean,
approved: boolean,
disabled: boolean,
silenced: boolean,
suspended: boolean,
created_by_application_id: string,
account: Account,
}
export interface Account {
id: string,
username: string,
acct: string,
display_name: string,
locked: boolean,
discoverable: boolean,
bot: boolean,
created_at: string,
note: string,
url: string,
avatar: string,
avatar_static: string,
header: string,
header_static: string,
followers_count: number,
following_count: number,
statuses_count: number,
last_status_at: string,
emojis: CustomEmoji[],
fields: [],
enable_rss: boolean,
role: any,
}
export interface SearchAccountParams {
origin?: "local" | "remote",
status?: "active" | "pending" | "disabled" | "silenced" | "suspended",
permissions?: "staff",
username?: string,
display_name?: string,
by_domain?: string,
email?: string,
ip?: string,
max_id?: string,
since_id?: string,
min_id?: string,
limit?: number,
}
export interface HandleSignupParams {
id: string,
approve_or_reject: "approve" | "reject",
private_comment?: string,
message?: string,
send_email?: boolean,
}

View file

@ -804,16 +804,12 @@ span.form-info {
.info {
color: $info-fg;
background: $info-bg;
padding: 0.5rem;
padding: 0.25rem;
border-radius: $br;
display: flex;
gap: 0.5rem;
align-items: center;
i {
margin-top: 0.1em;
}
a {
color: $info-link;
@ -1145,7 +1141,7 @@ button.with-padding {
}
}
.account-search {
.accounts-view {
form {
margin-bottom: 1rem;
}
@ -1175,9 +1171,42 @@ button.with-padding {
max-width: 60rem;
}
h4, h3, h2 {
margin-top: 0;
margin-bottom: 0;
}
.info-list {
border: 0.1rem solid $gray1;
display: flex;
flex-direction: column;
.info-list-entry {
background: $list-entry-bg;
border: 0.1rem solid transparent;
padding: 0.25rem;
&:nth-child(even) {
background: $list-entry-alternate-bg;
}
display: grid;
grid-template-columns: max(20%, 10rem) 1fr;
dt {
font-weight: bold;
}
dd {
word-break: break-word;
}
}
}
.action-buttons {
display: flex;
gap: 0.5rem;
align-items: center;
}
}