mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-10-31 22:40:01 +00:00
[frontend] Basic user moderation actions (#1728)
* remove info banner * update swagger definition for AccountAction * basic user view, suspend action * clean up suspended user display * basic user searching * rename User -> Account for clarity * refactor error boundary component to give better info * appease the linter
This commit is contained in:
parent
b47661f033
commit
89dcbd5a20
16 changed files with 419 additions and 37 deletions
|
@ -53,7 +53,7 @@
|
||||||
// -
|
// -
|
||||||
// name: type
|
// name: type
|
||||||
// in: formData
|
// in: formData
|
||||||
// description: Type of action to be taken (`disable`, `silence`, or `suspend`).
|
// description: Type of action to be taken, currently only supports `suspend`.
|
||||||
// type: string
|
// type: string
|
||||||
// required: true
|
// required: true
|
||||||
// -
|
// -
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
"psl": "^1.9.0",
|
"psl": "^1.9.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-error-boundary": "^3.1.4",
|
|
||||||
"react-redux": "^8.0.4",
|
"react-redux": "^8.0.4",
|
||||||
"redux": "^4.2.0",
|
"redux": "^4.2.0",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
|
|
114
web/source/settings/admin/accounts/detail.jsx
Normal file
114
web/source/settings/admin/accounts/detail.jsx
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
const { useRoute, Redirect } = require("wouter");
|
||||||
|
|
||||||
|
const query = require("../../lib/query");
|
||||||
|
|
||||||
|
const FormWithData = require("../../lib/form/form-with-data");
|
||||||
|
|
||||||
|
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");
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
140
web/source/settings/admin/accounts/index.jsx
Normal file
140
web/source/settings/admin/accounts/index.jsx
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -165,7 +165,7 @@ function ReportedToot({ toot }) {
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
<aside className="info">
|
<aside className="info">
|
||||||
<time datetime={toot.created_at}>{new Date(toot.created_at).toLocaleString()}</time>
|
<time dateTime={toot.created_at}>{new Date(toot.created_at).toLocaleString()}</time>
|
||||||
</aside>
|
</aside>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
|
|
|
@ -48,13 +48,6 @@ function ReportOverview({ }) {
|
||||||
<>
|
<>
|
||||||
<h1>Reports</h1>
|
<h1>Reports</h1>
|
||||||
<div>
|
<div>
|
||||||
<div className="info">
|
|
||||||
<i className="fa fa-fw fa-exclamation-triangle" aria-hidden="true"></i>
|
|
||||||
<p>
|
|
||||||
<b>This interface is currently very limited</b>, only providing a basic overview. <br />
|
|
||||||
Work is in progress on a more full-fledged moderation experience.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p>
|
<p>
|
||||||
Here you can view and resolve reports made to your instance, originating from local and remote users.
|
Here you can view and resolve reports made to your instance, originating from local and remote users.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
|
const { Link } = require("wouter");
|
||||||
|
|
||||||
module.exports = function Username({ user, link = true }) {
|
module.exports = function Username({ user, link = true }) {
|
||||||
let className = "user";
|
let className = "user";
|
||||||
|
@ -41,12 +42,12 @@ module.exports = function Username({ user, link = true }) {
|
||||||
let href = null;
|
let href = null;
|
||||||
|
|
||||||
if (link) {
|
if (link) {
|
||||||
Element = "a";
|
Element = Link;
|
||||||
href = user.account.url;
|
href = `/settings/admin/accounts/${user.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Element className={className} href={href} target="_blank" rel="noreferrer" >
|
<Element className={className} to={href}>
|
||||||
<span className="acct">@{user.account.acct}</span>
|
<span className="acct">@{user.account.acct}</span>
|
||||||
<i className={`fa fa-fw ${icon.fa}`} aria-hidden="true" title={icon.info} />
|
<i className={`fa fa-fw ${icon.fa}`} aria-hidden="true" title={icon.info} />
|
||||||
<span className="sr-only">{icon.info}</span>
|
<span className="sr-only">{icon.info}</span>
|
||||||
|
|
|
@ -31,12 +31,14 @@ function ErrorFallback({ error, resetErrorBoundary }) {
|
||||||
<a href="https://matrix.to/#/#gotosocial-help:superseriousbusiness.org">Matrix support room</a>.
|
<a href="https://matrix.to/#/#gotosocial-help:superseriousbusiness.org">Matrix support room</a>.
|
||||||
<br />Include the details below:
|
<br />Include the details below:
|
||||||
</p>
|
</p>
|
||||||
<pre>
|
<div className="details">
|
||||||
{error.name}: {error.message}
|
<pre>
|
||||||
</pre>
|
{error.name}: {error.message}
|
||||||
<pre>
|
</pre>
|
||||||
{error.stack}
|
<pre>
|
||||||
</pre>
|
{error.stack}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<button onClick={resetErrorBoundary}>Try again</button> or <a href="">refresh the page</a>
|
<button onClick={resetErrorBoundary}>Try again</button> or <a href="">refresh the page</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -29,7 +29,7 @@ module.exports = function FakeProfile({ avatar, header, display_name, username,
|
||||||
<img src={header} alt={header ? `header image for ${username}` : "None set"} />
|
<img src={header} alt={header ? `header image for ${username}` : "None set"} />
|
||||||
</div>
|
</div>
|
||||||
<div className="basic-info" aria-hidden="true">
|
<div className="basic-info" aria-hidden="true">
|
||||||
<a className="avatar" href="{{.account.Avatar}}">
|
<a className="avatar" href={avatar}>
|
||||||
<img src={avatar} alt={avatar ? `avatar image for ${username}` : "None set"} />
|
<img src={avatar} alt={avatar ? `avatar image for ${username}` : "None set"} />
|
||||||
</a>
|
</a>
|
||||||
<span className="displayname text-cutoff">
|
<span className="displayname text-cutoff">
|
||||||
|
@ -37,9 +37,9 @@ module.exports = function FakeProfile({ avatar, header, display_name, username,
|
||||||
<span className="sr-only">.</span>
|
<span className="sr-only">.</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="username text-cutoff">@{username}</span>
|
<span className="username text-cutoff">@{username}</span>
|
||||||
{(role && role != "user") &&
|
{(role && role.name != "user") &&
|
||||||
<div className={`role ${role}`}>
|
<div className={`role ${role.name}`}>
|
||||||
<span className="sr-only">Role: </span>{role}
|
<span className="sr-only">Role: </span>{role.name}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,6 +44,7 @@ const { Sidebar, ViewRouter } = createNavigation("/settings", [
|
||||||
permissions: ["admin"]
|
permissions: ["admin"]
|
||||||
}, [
|
}, [
|
||||||
Item("Reports", { icon: "fa-flag", wildcard: true }, require("./admin/reports")),
|
Item("Reports", { icon: "fa-flag", wildcard: true }, require("./admin/reports")),
|
||||||
|
Item("Accounts", { icon: "fa-users", wildcard: true }, require("./admin/accounts")),
|
||||||
Menu("Federation", { icon: "fa-hubzilla" }, [
|
Menu("Federation", { icon: "fa-hubzilla" }, [
|
||||||
Item("Federation", { icon: "fa-hubzilla", url: "", wildcard: true }, require("./admin/federation")),
|
Item("Federation", { icon: "fa-hubzilla", url: "", wildcard: true }, require("./admin/federation")),
|
||||||
Item("Import/Export", { icon: "fa-floppy-o", wildcard: true }, require("./admin/federation/import-export")),
|
Item("Import/Export", { icon: "fa-floppy-o", wildcard: true }, require("./admin/federation/import-export")),
|
||||||
|
|
|
@ -21,11 +21,8 @@
|
||||||
|
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
const { Link, Route, Redirect, Switch, useLocation, useRouter } = require("wouter");
|
const { Link, Route, Redirect, Switch, useLocation, useRouter } = require("wouter");
|
||||||
const { ErrorBoundary } = require("react-error-boundary");
|
|
||||||
const syncpipe = require("syncpipe");
|
const syncpipe = require("syncpipe");
|
||||||
|
|
||||||
const { ErrorFallback } = require("../../components/error");
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
RoleContext,
|
RoleContext,
|
||||||
useHasPermission,
|
useHasPermission,
|
||||||
|
@ -72,8 +69,8 @@ function ViewRouter(routing, defaultRoute) {
|
||||||
(_) => _.map((route) => {
|
(_) => _.map((route) => {
|
||||||
return (
|
return (
|
||||||
<Route path={route.routingUrl} key={route.key}>
|
<Route path={route.routingUrl} key={route.key}>
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => { }}>
|
<ErrorBoundary>
|
||||||
{/* FIXME: implement onReset */}
|
{/* FIXME: implement reset */}
|
||||||
<BaseUrlContext.Provider value={route.url}>
|
<BaseUrlContext.Provider value={route.url}>
|
||||||
{route.view}
|
{route.view}
|
||||||
</BaseUrlContext.Provider>
|
</BaseUrlContext.Provider>
|
||||||
|
@ -134,6 +131,71 @@ function MenuComponent({ type, name, url, icon, permissions, links, level, child
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ErrorBoundary extends React.Component {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.state = {};
|
||||||
|
|
||||||
|
this.resetErrorBoundary = () => {
|
||||||
|
this.setState({});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error) {
|
||||||
|
return { hadError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(_e, info) {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
componentStack: info.componentStack
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hadError) {
|
||||||
|
return (
|
||||||
|
<ErrorFallback
|
||||||
|
error={this.state.error}
|
||||||
|
componentStack={this.state.componentStack}
|
||||||
|
resetErrorBoundary={this.resetErrorBoundary}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ErrorFallback({ error, componentStack, resetErrorBoundary }) {
|
||||||
|
return (
|
||||||
|
<div className="error">
|
||||||
|
<p>
|
||||||
|
{"An error occured, please report this on the "}
|
||||||
|
<a href="https://github.com/superseriousbusiness/gotosocial/issues">GoToSocial issue tracker</a>
|
||||||
|
{" or "}
|
||||||
|
<a href="https://matrix.to/#/#gotosocial-help:superseriousbusiness.org">Matrix support room</a>.
|
||||||
|
<br />Include the details below:
|
||||||
|
</p>
|
||||||
|
<div className="details">
|
||||||
|
<pre>
|
||||||
|
{error.name}: {error.message}
|
||||||
|
|
||||||
|
{componentStack && [
|
||||||
|
"\n\nComponent trace:",
|
||||||
|
componentStack
|
||||||
|
]}
|
||||||
|
{["\n\nError trace: ", error.stack]}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<button onClick={resetErrorBoundary}>Try again</button> or <a href="">refresh the page</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
ViewRouter,
|
ViewRouter,
|
||||||
|
|
|
@ -78,6 +78,32 @@ const endpoints = (build) => ({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
getAccount: build.query({
|
||||||
|
query: (id) => ({
|
||||||
|
url: `/api/v1/accounts/${id}`
|
||||||
|
}),
|
||||||
|
providesTags: (_, __, id) => [{ type: "Account", id }]
|
||||||
|
}),
|
||||||
|
actionAccount: build.mutation({
|
||||||
|
query: ({ id, action, reason }) => ({
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/v1/admin/accounts/${id}/action`,
|
||||||
|
asForm: true,
|
||||||
|
body: {
|
||||||
|
type: action,
|
||||||
|
text: reason
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
invalidatesTags: (_, __, { id }) => [{ type: "Account", id }]
|
||||||
|
}),
|
||||||
|
searchAccount: build.mutation({
|
||||||
|
query: (username) => ({
|
||||||
|
url: `/api/v2/search?q=${encodeURIComponent(username)}&resolve=true`
|
||||||
|
}),
|
||||||
|
transformResponse: (res) => {
|
||||||
|
return res.accounts ?? [];
|
||||||
|
}
|
||||||
|
}),
|
||||||
...require("./import-export")(build),
|
...require("./import-export")(build),
|
||||||
...require("./custom-emoji")(build),
|
...require("./custom-emoji")(build),
|
||||||
...require("./reports")(build)
|
...require("./reports")(build)
|
||||||
|
|
|
@ -73,7 +73,7 @@ function instanceBasedQuery(args, api, extraOptions) {
|
||||||
module.exports = createApi({
|
module.exports = createApi({
|
||||||
reducerPath: "api",
|
reducerPath: "api",
|
||||||
baseQuery: instanceBasedQuery,
|
baseQuery: instanceBasedQuery,
|
||||||
tagTypes: ["Auth", "Emoji", "Reports"],
|
tagTypes: ["Auth", "Emoji", "Reports", "Account"],
|
||||||
endpoints: (build) => ({
|
endpoints: (build) => ({
|
||||||
instance: build.query({
|
instance: build.query({
|
||||||
query: () => ({
|
query: () => ({
|
||||||
|
|
|
@ -61,6 +61,7 @@ header {
|
||||||
background: $bg-accent;
|
background: $bg-accent;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
border-radius: $br;
|
border-radius: $br;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
& > div, & > form {
|
& > div, & > form {
|
||||||
border-left: 0.2rem solid $border-accent;
|
border-left: 0.2rem solid $border-accent;
|
||||||
|
@ -92,6 +93,10 @@ header {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > .error {
|
||||||
|
display: grid; /* prevents error overflowing */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
|
@ -250,11 +255,20 @@ input, select, textarea {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $error-link;
|
color: $error-link;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
background: $bg;
|
background: $bg;
|
||||||
color: $fg;
|
color: $fg;
|
||||||
|
@ -395,6 +409,7 @@ section.with-sidebar > div, section.with-sidebar > form {
|
||||||
.user-profile {
|
.user-profile {
|
||||||
.overview {
|
.overview {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
max-width: 60rem;
|
||||||
grid-template-columns: 70% 30%;
|
grid-template-columns: 70% 30%;
|
||||||
grid-template-rows: 100%;
|
grid-template-rows: 100%;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
@ -1062,6 +1077,42 @@ button.with-padding {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-search {
|
||||||
|
form {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $fg;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
#username {
|
||||||
|
color: $link-fg;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.profile {
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 60rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (orientation: portrait) {
|
@media screen and (orientation: portrait) {
|
||||||
.reports .report .byline {
|
.reports .report .byline {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
|
@ -91,7 +91,7 @@ function UserProfileForm({ data: profile }) {
|
||||||
header={form.header.previewValue ?? profile.header}
|
header={form.header.previewValue ?? profile.header}
|
||||||
display_name={form.displayName.value ?? profile.username}
|
display_name={form.displayName.value ?? profile.username}
|
||||||
username={profile.username}
|
username={profile.username}
|
||||||
role={profile.role.name}
|
role={profile.role}
|
||||||
/>
|
/>
|
||||||
<div className="files">
|
<div className="files">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -4605,13 +4605,6 @@ react-dom@^18.2.0:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
scheduler "^0.23.0"
|
scheduler "^0.23.0"
|
||||||
|
|
||||||
react-error-boundary@^3.1.4:
|
|
||||||
version "3.1.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0"
|
|
||||||
integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.12.5"
|
|
||||||
|
|
||||||
react-is@^16.13.1, react-is@^16.7.0:
|
react-is@^16.13.1, react-is@^16.7.0:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
|
|
Loading…
Reference in a new issue