mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-27 02:41:01 +00:00
[chore] Refactor settings panel routing (and other fixes) (#2864)
This commit is contained in:
parent
62788aa116
commit
7a1e639483
55 changed files with 1788 additions and 1445 deletions
13
.vscode/settings.json
vendored
13
.vscode/settings.json
vendored
|
@ -10,5 +10,14 @@
|
||||||
},
|
},
|
||||||
"eslint.workingDirectories": ["web/source"],
|
"eslint.workingDirectories": ["web/source"],
|
||||||
"eslint.lintTask.enable": true,
|
"eslint.lintTask.enable": true,
|
||||||
"eslint.lintTask.options": "${workspaceFolder}/web/source"
|
"eslint.lintTask.options": "${workspaceFolder}/web/source",
|
||||||
}
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact"
|
||||||
|
],
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ skulk({
|
||||||
// commonjs here, no need for the typescript preset.
|
// commonjs here, no need for the typescript preset.
|
||||||
["babelify", {
|
["babelify", {
|
||||||
global: true,
|
global: true,
|
||||||
ignore: [/node_modules\/(?!nanoid)/],
|
ignore: [/node_modules\/(?!(nanoid)|(wouter))/],
|
||||||
}]
|
}]
|
||||||
],
|
],
|
||||||
presets: [
|
presets: [
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^1.8.6",
|
"@reduxjs/toolkit": "^1.8.6",
|
||||||
"ariakit": "^2.0.0-next.41",
|
"ariakit": "^2.0.0-next.41",
|
||||||
"bluebird": "^3.7.2",
|
|
||||||
"get-by-dot": "^1.0.2",
|
"get-by-dot": "^1.0.2",
|
||||||
"is-valid-domain": "^0.1.6",
|
"is-valid-domain": "^0.1.6",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
|
@ -33,9 +32,7 @@
|
||||||
"redux": "^4.2.0",
|
"redux": "^4.2.0",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"skulk": "^0.0.8-fix",
|
"skulk": "^0.0.8-fix",
|
||||||
"split-filter-n": "^1.1.3",
|
"wouter": "^3.1.0"
|
||||||
"syncpipe": "^1.0.0",
|
|
||||||
"wouter": "^2.8.0-alpha.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.23.0",
|
"@babel/core": "^7.23.0",
|
||||||
|
@ -45,14 +42,13 @@
|
||||||
"@browserify/envify": "^6.0.0",
|
"@browserify/envify": "^6.0.0",
|
||||||
"@browserify/uglifyify": "^6.0.0",
|
"@browserify/uglifyify": "^6.0.0",
|
||||||
"@joepie91/eslint-config": "^1.1.1",
|
"@joepie91/eslint-config": "^1.1.1",
|
||||||
"@types/bluebird": "^3.5.39",
|
|
||||||
"@types/is-valid-domain": "^0.0.2",
|
"@types/is-valid-domain": "^0.0.2",
|
||||||
"@types/papaparse": "^5.3.9",
|
"@types/papaparse": "^5.3.9",
|
||||||
"@types/psl": "^1.1.1",
|
"@types/psl": "^1.1.1",
|
||||||
"@types/react-dom": "^18.2.8",
|
"@types/react-dom": "^18.2.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||||
"@typescript-eslint/parser": "^6.7.4",
|
"@typescript-eslint/parser": "^6.7.4",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.19",
|
||||||
"babelify": "^10.0.0",
|
"babelify": "^10.0.0",
|
||||||
"css-extract": "^2.0.0",
|
"css-extract": "^2.0.0",
|
||||||
"eslint": "^8.26.0",
|
"eslint": "^8.26.0",
|
||||||
|
|
|
@ -1,49 +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 { Switch, Route } from "wouter";
|
|
||||||
|
|
||||||
import DomainPermissionsOverview from "./overview";
|
|
||||||
import { PermType } from "../../lib/types/domain-permission";
|
|
||||||
import DomainPermDetail from "./detail";
|
|
||||||
|
|
||||||
export default function DomainPermissions({ baseUrl }: { baseUrl: string }) {
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route path="/settings/admin/domain-permissions/:permType/:domain">
|
|
||||||
{params => (
|
|
||||||
<DomainPermDetail
|
|
||||||
permType={params.permType as PermType}
|
|
||||||
baseUrl={baseUrl}
|
|
||||||
domain={params.domain}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Route>
|
|
||||||
<Route path="/settings/admin/domain-permissions/:permType">
|
|
||||||
{params => (
|
|
||||||
<DomainPermissionsOverview
|
|
||||||
permType={params.permType as PermType}
|
|
||||||
baseUrl={baseUrl}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,96 +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 splitFilterN = require("split-filter-n");
|
|
||||||
const syncpipe = require('syncpipe');
|
|
||||||
const { matchSorter } = require("match-sorter");
|
|
||||||
|
|
||||||
const ComboBox = require("../../components/combo-box");
|
|
||||||
const { useListEmojiQuery } = require("../../lib/query/admin/custom-emoji");
|
|
||||||
|
|
||||||
function useEmojiByCategory(emoji) {
|
|
||||||
// split all emoji over an object keyed by the category names (or Unsorted)
|
|
||||||
return React.useMemo(() => splitFilterN(
|
|
||||||
emoji,
|
|
||||||
[],
|
|
||||||
(entry) => entry.category ?? "Unsorted"
|
|
||||||
), [emoji]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CategorySelect({ field, children }) {
|
|
||||||
const { value, setIsNew } = field;
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: emoji = [],
|
|
||||||
isLoading,
|
|
||||||
isSuccess,
|
|
||||||
error
|
|
||||||
} = useListEmojiQuery({ filter: "domain:local" });
|
|
||||||
|
|
||||||
const emojiByCategory = useEmojiByCategory(emoji);
|
|
||||||
|
|
||||||
const categories = React.useMemo(() => new Set(Object.keys(emojiByCategory)), [emojiByCategory]);
|
|
||||||
|
|
||||||
// data used by the ComboBox element to select an emoji category
|
|
||||||
const categoryItems = React.useMemo(() => {
|
|
||||||
return syncpipe(emojiByCategory, [
|
|
||||||
(_) => Object.keys(_), // just emoji category names
|
|
||||||
(_) => matchSorter(_, value, { threshold: matchSorter.rankings.NO_MATCH }), // sorted by complex algorithm
|
|
||||||
(_) => _.map((categoryName) => [ // map to input value, and selectable element with icon
|
|
||||||
categoryName,
|
|
||||||
<>
|
|
||||||
<img src={emojiByCategory[categoryName][0].static_url} aria-hidden="true"></img>
|
|
||||||
{categoryName}
|
|
||||||
</>
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
}, [emojiByCategory, value]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (value != undefined && isSuccess && value.trim().length > 0) {
|
|
||||||
setIsNew(!categories.has(value.trim()));
|
|
||||||
}
|
|
||||||
}, [categories, value, isSuccess, setIsNew]);
|
|
||||||
|
|
||||||
if (error) { // fall back to plain text input, but this would almost certainly have caused a bigger error message elsewhere
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<input type="text" placeholder="e.g., reactions" onChange={(e) => { field.value = e.target.value; }} />;
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else if (isLoading) {
|
|
||||||
return <input type="text" value="Loading categories..." disabled={true} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ComboBox
|
|
||||||
field={field}
|
|
||||||
items={categoryItems}
|
|
||||||
label="Category"
|
|
||||||
placeholder="e.g., reactions"
|
|
||||||
children={children}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
useEmojiByCategory,
|
|
||||||
CategorySelect
|
|
||||||
};
|
|
|
@ -1,153 +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 { Link } = require("wouter");
|
|
||||||
const syncpipe = require("syncpipe");
|
|
||||||
const { matchSorter } = require("match-sorter");
|
|
||||||
|
|
||||||
const NewEmojiForm = require("./new-emoji").default;
|
|
||||||
const { useTextInput } = require("../../../lib/form");
|
|
||||||
|
|
||||||
const { useEmojiByCategory } = require("../category-select");
|
|
||||||
const { useBaseUrl } = require("../../../lib/navigation/util");
|
|
||||||
|
|
||||||
const Loading = require("../../../components/loading");
|
|
||||||
const { Error } = require("../../../components/error");
|
|
||||||
const { TextInput } = require("../../../components/form/inputs");
|
|
||||||
const { useListEmojiQuery } = require("../../../lib/query/admin/custom-emoji");
|
|
||||||
|
|
||||||
module.exports = function EmojiOverview({ }) {
|
|
||||||
const {
|
|
||||||
data: emoji = [],
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
error
|
|
||||||
} = useListEmojiQuery({ filter: "domain:local" });
|
|
||||||
|
|
||||||
let content = null;
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
content = <Loading />;
|
|
||||||
} else if (isError) {
|
|
||||||
content = <Error error={error} />;
|
|
||||||
} else {
|
|
||||||
content = (
|
|
||||||
<>
|
|
||||||
<EmojiList emoji={emoji} />
|
|
||||||
<NewEmojiForm emoji={emoji} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1>Local Custom Emoji</h1>
|
|
||||||
<p>
|
|
||||||
To use custom emoji in your toots they have to be 'local' to the instance.
|
|
||||||
You can either upload them here directly, or copy from those already
|
|
||||||
present on other (known) instances through the <Link to={`./remote`}>Remote Emoji</Link> page.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Be warned!</strong> If you upload more than about 300-400 custom emojis in
|
|
||||||
total on your instance, this may lead to rate-limiting issues for users and clients
|
|
||||||
if they try to load all the emoji images at once (which is what many clients do).
|
|
||||||
</p>
|
|
||||||
{content}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function EmojiList({ emoji }) {
|
|
||||||
const filterField = useTextInput("filter");
|
|
||||||
const filter = filterField.value;
|
|
||||||
|
|
||||||
const emojiByCategory = useEmojiByCategory(emoji);
|
|
||||||
|
|
||||||
/* Filter emoji based on shortcode match with user input, hiding empty categories */
|
|
||||||
const { filteredEmoji, hidden } = React.useMemo(() => {
|
|
||||||
let hidden = emoji.length;
|
|
||||||
const filteredEmoji = syncpipe(emojiByCategory, [
|
|
||||||
(_) => Object.entries(emojiByCategory),
|
|
||||||
(_) => _.map(([category, entries]) => {
|
|
||||||
let filteredEntries = matchSorter(entries, filter, { keys: ["shortcode"] });
|
|
||||||
if (filteredEntries.length == 0) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
hidden -= filteredEntries.length;
|
|
||||||
return [category, filteredEntries];
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
(_) => _.filter((value) => value !== null)
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { filteredEmoji, hidden };
|
|
||||||
}, [filter, emojiByCategory, emoji.length]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>Overview</h2>
|
|
||||||
{emoji.length > 0
|
|
||||||
? <span>{emoji.length} custom emoji {hidden > 0 && `(${hidden} filtered)`}</span>
|
|
||||||
: <span>No custom emoji yet, you can add one below.</span>
|
|
||||||
}
|
|
||||||
<div className="list emoji-list">
|
|
||||||
<div className="header">
|
|
||||||
<TextInput
|
|
||||||
field={filterField}
|
|
||||||
name="emoji-shortcode"
|
|
||||||
placeholder="Search"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="entries scrolling">
|
|
||||||
{filteredEmoji.length > 0
|
|
||||||
? (
|
|
||||||
<div className="entries scrolling">
|
|
||||||
{filteredEmoji.map(([category, entries]) => {
|
|
||||||
return <EmojiCategory key={category} category={category} entries={entries} />;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: <div className="entry">No local emoji matched your filter.</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmojiCategory({ category, entries }) {
|
|
||||||
const baseUrl = useBaseUrl();
|
|
||||||
return (
|
|
||||||
<div className="entry">
|
|
||||||
<b>{category}</b>
|
|
||||||
<div className="emoji-group">
|
|
||||||
{entries.map((e) => {
|
|
||||||
return (
|
|
||||||
<Link key={e.id} to={`${baseUrl}/${e.id}`}>
|
|
||||||
<a>
|
|
||||||
<img src={e.url} alt={e.shortcode} title={`:${e.shortcode}:`} />
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,174 +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 { Switch, Route, Link, Redirect, useRoute } from "wouter";
|
|
||||||
|
|
||||||
import { useInstanceRulesQuery, useAddInstanceRuleMutation, useUpdateInstanceRuleMutation, useDeleteInstanceRuleMutation } from "../../lib/query";
|
|
||||||
import FormWithData from "../../lib/form/form-with-data";
|
|
||||||
import { useBaseUrl } from "../../lib/navigation/util";
|
|
||||||
|
|
||||||
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 { Error } from "../../components/error";
|
|
||||||
|
|
||||||
export default function InstanceRulesData({ baseUrl }) {
|
|
||||||
return (
|
|
||||||
<FormWithData
|
|
||||||
dataQuery={useInstanceRulesQuery}
|
|
||||||
DataForm={InstanceRules}
|
|
||||||
{...{baseUrl}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function InstanceRules({ baseUrl, data: rules }) {
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route path={`${baseUrl}/:ruleId`}>
|
|
||||||
<InstanceRuleDetail rules={rules} />
|
|
||||||
</Route>
|
|
||||||
<Route>
|
|
||||||
<div>
|
|
||||||
<h1>Instance Rules</h1>
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
The rules for your instance are listed on the about page, and can be selected when submitting reports.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<InstanceRuleList rules={rules} />
|
|
||||||
</div>
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function InstanceRuleList({ rules }) {
|
|
||||||
const newRule = useTextInput("text", {});
|
|
||||||
|
|
||||||
const [submitForm, result] = useFormSubmit({ newRule }, useAddInstanceRuleMutation(), {
|
|
||||||
changedOnly: true,
|
|
||||||
onFinish: () => newRule.reset()
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<form onSubmit={submitForm} className="new-rule">
|
|
||||||
<ol className="instance-rules">
|
|
||||||
{Object.values(rules).map((rule: any) => (
|
|
||||||
<InstanceRule key={rule.id} rule={rule} />
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
<TextArea
|
|
||||||
field={newRule}
|
|
||||||
label="New instance rule"
|
|
||||||
/>
|
|
||||||
<MutationButton
|
|
||||||
disabled={newRule.value === undefined || newRule.value.length === 0}
|
|
||||||
label="Add rule"
|
|
||||||
result={result}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function InstanceRule({ rule }) {
|
|
||||||
const baseUrl = useBaseUrl();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link to={`${baseUrl}/${rule.id}`}>
|
|
||||||
<a className="rule">
|
|
||||||
<li>
|
|
||||||
<h2>{rule.text} <i className="fa fa-pencil edit-icon" /></h2>
|
|
||||||
</li>
|
|
||||||
<span>{new Date(rule.created_at).toLocaleString()}</span>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function InstanceRuleDetail({ rules }) {
|
|
||||||
const baseUrl = useBaseUrl();
|
|
||||||
let [_match, params] = useRoute(`${baseUrl}/:ruleId`);
|
|
||||||
|
|
||||||
if (params?.ruleId == undefined || rules[params.ruleId] == undefined) {
|
|
||||||
return <Redirect to={baseUrl} />;
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Link to={baseUrl}><a>< go back</a></Link>
|
|
||||||
<InstanceRuleForm rule={rules[params.ruleId]} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function InstanceRuleForm({ rule }) {
|
|
||||||
const baseUrl = useBaseUrl();
|
|
||||||
const form = {
|
|
||||||
id: useValue("id", rule.id),
|
|
||||||
rule: useTextInput("text", { defaultValue: rule.text })
|
|
||||||
};
|
|
||||||
|
|
||||||
const [submitForm, result] = useFormSubmit(form, useUpdateInstanceRuleMutation());
|
|
||||||
|
|
||||||
const [deleteRule, deleteResult] = useDeleteInstanceRuleMutation({ fixedCacheKey: rule.id });
|
|
||||||
|
|
||||||
if (result.isSuccess || deleteResult.isSuccess) {
|
|
||||||
return (
|
|
||||||
<Redirect to={baseUrl} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rule-detail">
|
|
||||||
<form onSubmit={submitForm}>
|
|
||||||
<TextArea
|
|
||||||
field={form.rule}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="action-buttons row">
|
|
||||||
<MutationButton
|
|
||||||
label="Save"
|
|
||||||
showError={false}
|
|
||||||
result={result}
|
|
||||||
disabled={!form.rule.hasChanged()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MutationButton
|
|
||||||
disabled={false}
|
|
||||||
type="button"
|
|
||||||
onClick={() => deleteRule(rule.id)}
|
|
||||||
label="Delete"
|
|
||||||
className="button danger"
|
|
||||||
showError={false}
|
|
||||||
result={deleteResult}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{result.error && <Error error={result.error} />}
|
|
||||||
{deleteResult.error && <Error error={deleteResult.error} />}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -64,11 +64,11 @@ export function AccountList({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="list">
|
<div className="list">
|
||||||
{data.map(({ account: acc }) => (
|
{data.map(({ account: acc }) => (
|
||||||
<Link
|
<Link
|
||||||
key={acc.acct}
|
key={acc.acct}
|
||||||
className="account entry"
|
className="account entry"
|
||||||
href={`/settings/admin/accounts/${acc.id}`}
|
href={`/${acc.id}`}
|
||||||
>
|
>
|
||||||
{acc.display_name?.length > 0
|
{acc.display_name?.length > 0
|
||||||
? acc.display_name
|
? acc.display_name
|
||||||
|
@ -79,4 +79,4 @@ export function AccountList({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,11 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
import React from "react";
|
||||||
const { Link } = require("wouter");
|
import { Link } from "wouter";
|
||||||
|
|
||||||
module.exports = function BackButton({ to }) {
|
export default function BackButton({ to }) {
|
||||||
return (
|
return (
|
||||||
<Link to={to}>
|
<Link className="button" to={to}>< back</Link>
|
||||||
<a className="button">< back</a>
|
|
||||||
</Link>
|
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
@ -1,124 +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 ReactDom = require("react-dom/client");
|
|
||||||
const { Provider } = require("react-redux");
|
|
||||||
const { PersistGate } = require("redux-persist/integration/react");
|
|
||||||
|
|
||||||
const { store, persistor } = require("./redux/store");
|
|
||||||
const { createNavigation, Menu, Item } = require("./lib/navigation");
|
|
||||||
|
|
||||||
const { Authorization } = require("./components/authorization");
|
|
||||||
const Loading = require("./components/loading");
|
|
||||||
const UserLogoutCard = require("./components/user-logout-card");
|
|
||||||
const { RoleContext } = require("./lib/navigation/util");
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
const { Sidebar, ViewRouter } = createNavigation("/settings", [
|
|
||||||
Menu("User", [
|
|
||||||
Item("Profile", { icon: "fa-user" }, UserProfile),
|
|
||||||
Item("Settings", { icon: "fa-cogs" }, UserSettings),
|
|
||||||
Item("Migration", { icon: "fa-exchange" }, UserMigration),
|
|
||||||
]),
|
|
||||||
Menu("Moderation", {
|
|
||||||
url: "admin",
|
|
||||||
permissions: ["admin"]
|
|
||||||
}, [
|
|
||||||
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),
|
|
||||||
Item("Import/Export", { icon: "fa-floppy-o", url: "import-export", wildcard: true }, DomainPermsImportExport),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
Menu("Administration", {
|
|
||||||
url: "admin",
|
|
||||||
defaultUrl: "/settings/admin/settings",
|
|
||||||
permissions: ["admin"]
|
|
||||||
}, [
|
|
||||||
Menu("Actions", { icon: "fa-bolt" }, [
|
|
||||||
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 }, 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 }, InstanceRules),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
|
|
||||||
function App({ account }) {
|
|
||||||
const permissions = [account.role.name];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RoleContext.Provider value={permissions}>
|
|
||||||
<div className="sidebar">
|
|
||||||
<UserLogoutCard />
|
|
||||||
<Sidebar />
|
|
||||||
</div>
|
|
||||||
<section className="with-sidebar">
|
|
||||||
<ViewRouter />
|
|
||||||
</section>
|
|
||||||
</RoleContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Main() {
|
|
||||||
return (
|
|
||||||
<Provider store={store}>
|
|
||||||
<PersistGate loading={<section><Loading /></section>} persistor={persistor}>
|
|
||||||
<Authorization App={App} />
|
|
||||||
</PersistGate>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = ReactDom.createRoot(document.getElementById("root"));
|
|
||||||
root.render(<React.StrictMode><Main /></React.StrictMode>);
|
|
84
web/source/settings/index.tsx
Normal file
84
web/source/settings/index.tsx
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
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, { StrictMode } from "react";
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import { PersistGate } from "redux-persist/integration/react";
|
||||||
|
import { store, persistor } from "./redux/store";
|
||||||
|
import { Authorization } from "./components/authorization";
|
||||||
|
import Loading from "./components/loading";
|
||||||
|
import { Account } from "./lib/types/account";
|
||||||
|
import { BaseUrlContext, RoleContext } from "./lib/navigation/util";
|
||||||
|
import { SidebarMenu } from "./lib/navigation/menu";
|
||||||
|
import { UserMenu, UserRouter } from "./views/user/routes";
|
||||||
|
import { ModerationMenu, ModerationRouter } from "./views/moderation/routes";
|
||||||
|
import { AdminMenu, AdminRouter } from "./views/admin/routes";
|
||||||
|
import { Redirect, Route, Router } from "wouter";
|
||||||
|
|
||||||
|
interface AppProps {
|
||||||
|
account: Account;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function App({ account }: AppProps) {
|
||||||
|
const roles: string[] = [ account.role.name ];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RoleContext.Provider value={roles}>
|
||||||
|
<BaseUrlContext.Provider value={"/settings"}>
|
||||||
|
<SidebarMenu>
|
||||||
|
<UserMenu />
|
||||||
|
<ModerationMenu />
|
||||||
|
<AdminMenu />
|
||||||
|
</SidebarMenu>
|
||||||
|
<section className="with-sidebar">
|
||||||
|
<Router base="/settings">
|
||||||
|
<UserRouter />
|
||||||
|
<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>
|
||||||
|
</Router>
|
||||||
|
</section>
|
||||||
|
</BaseUrlContext.Provider>
|
||||||
|
</RoleContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Main() {
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<PersistGate
|
||||||
|
loading={<section><Loading /></section>}
|
||||||
|
persistor={persistor}
|
||||||
|
>
|
||||||
|
<Authorization App={App} />
|
||||||
|
</PersistGate>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = createRoot(document.getElementById("root") as HTMLElement);
|
||||||
|
root.render(<StrictMode><Main /></StrictMode>);
|
|
@ -1,201 +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 { Link, Route, Redirect, Switch, useLocation, useRouter } = require("wouter");
|
|
||||||
const syncpipe = require("syncpipe");
|
|
||||||
|
|
||||||
const {
|
|
||||||
RoleContext,
|
|
||||||
useHasPermission,
|
|
||||||
checkPermission,
|
|
||||||
BaseUrlContext
|
|
||||||
} = require("./util");
|
|
||||||
|
|
||||||
const ActiveRouteCtx = React.createContext();
|
|
||||||
function useActiveRoute() {
|
|
||||||
return React.useContext(ActiveRouteCtx);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Sidebar(menuTree, routing) {
|
|
||||||
const components = menuTree.map((m) => m.MenuEntry);
|
|
||||||
|
|
||||||
return function SidebarComponent() {
|
|
||||||
const router = useRouter();
|
|
||||||
const [location] = useLocation();
|
|
||||||
|
|
||||||
let activeRoute = routing.find((l) => {
|
|
||||||
let [match] = router.matcher(l.routingUrl, location);
|
|
||||||
return match;
|
|
||||||
})?.routingUrl;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className="menu-tree">
|
|
||||||
<ul className="top-level">
|
|
||||||
<ActiveRouteCtx.Provider value={activeRoute}>
|
|
||||||
{components}
|
|
||||||
</ActiveRouteCtx.Provider>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function ViewRouter(routing, defaultRoute) {
|
|
||||||
return function ViewRouterComponent() {
|
|
||||||
const permissions = React.useContext(RoleContext);
|
|
||||||
|
|
||||||
const filteredRoutes = React.useMemo(() => {
|
|
||||||
return syncpipe(routing, [
|
|
||||||
(_) => _.filter((route) => checkPermission(route.permissions, permissions)),
|
|
||||||
(_) => _.map((route) => {
|
|
||||||
return (
|
|
||||||
<Route path={route.routingUrl} key={route.key}>
|
|
||||||
<ErrorBoundary>
|
|
||||||
{/* FIXME: implement reset */}
|
|
||||||
<BaseUrlContext.Provider value={route.url}>
|
|
||||||
{route.view}
|
|
||||||
</BaseUrlContext.Provider>
|
|
||||||
</ErrorBoundary>
|
|
||||||
</Route>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
}, [permissions]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
{filteredRoutes}
|
|
||||||
<Redirect to={defaultRoute} />
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenuComponent({ type, name, url, icon, permissions, links, level, children }) {
|
|
||||||
const activeRoute = useActiveRoute();
|
|
||||||
|
|
||||||
if (!useHasPermission(permissions)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const classes = [type];
|
|
||||||
|
|
||||||
if (level == 0) {
|
|
||||||
classes.push("top-level");
|
|
||||||
} else if (level == 1) {
|
|
||||||
classes.push("expanding");
|
|
||||||
} else {
|
|
||||||
classes.push("nested");
|
|
||||||
}
|
|
||||||
|
|
||||||
const isActive = links.includes(activeRoute);
|
|
||||||
if (isActive) {
|
|
||||||
classes.push("active");
|
|
||||||
}
|
|
||||||
|
|
||||||
const className = classes.join(" ");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li className={className}>
|
|
||||||
<Link href={url}>
|
|
||||||
<a tabIndex={level == 0 ? "-1" : null} className="title">
|
|
||||||
{icon && <i className={`icon fa fa-fw ${icon}`} aria-hidden="true" />}
|
|
||||||
{name}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
{(type == "category" && (level == 0 || isActive) && children?.length > 0) &&
|
|
||||||
<ul>
|
|
||||||
{children}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = {
|
|
||||||
Sidebar,
|
|
||||||
ViewRouter,
|
|
||||||
MenuComponent
|
|
||||||
};
|
|
98
web/source/settings/lib/navigation/error.tsx
Normal file
98
web/source/settings/lib/navigation/error.tsx
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
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, { Component, ReactNode } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
interface ErrorBoundaryProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorBoundaryState {
|
||||||
|
hadError?: boolean;
|
||||||
|
componentStack?;
|
||||||
|
error?;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||||
|
resetErrorBoundary: () => void;
|
||||||
|
|
||||||
|
constructor(props: ErrorBoundaryProps) {
|
||||||
|
super(props);
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ErrorBoundary };
|
|
@ -1,136 +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 { nanoid } = require("nanoid");
|
|
||||||
const { Redirect } = require("wouter");
|
|
||||||
|
|
||||||
const { urlSafe } = require("./util");
|
|
||||||
|
|
||||||
const {
|
|
||||||
Sidebar,
|
|
||||||
ViewRouter,
|
|
||||||
MenuComponent
|
|
||||||
} = require("./components");
|
|
||||||
|
|
||||||
function createNavigation(rootUrl, menus) {
|
|
||||||
const root = {
|
|
||||||
url: rootUrl,
|
|
||||||
links: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const routing = [];
|
|
||||||
|
|
||||||
const menuTree = menus.map((creatorFunc) =>
|
|
||||||
creatorFunc(root, routing)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
Sidebar: Sidebar(menuTree, routing),
|
|
||||||
ViewRouter: ViewRouter(routing, root.redirectUrl)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenuEntry(name, opts, contents) {
|
|
||||||
if (contents == undefined) { // opts argument is optional
|
|
||||||
contents = opts;
|
|
||||||
opts = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return function createMenuEntry(root, routing) {
|
|
||||||
const type = Array.isArray(contents) ? "category" : "view";
|
|
||||||
|
|
||||||
let urlParts = [root.url];
|
|
||||||
if (opts.url != "") {
|
|
||||||
urlParts.push(opts.url ?? urlSafe(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = urlParts.join("/");
|
|
||||||
let routingUrl = url;
|
|
||||||
|
|
||||||
if (opts.wildcard) {
|
|
||||||
routingUrl += "/:wildcard*";
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = {
|
|
||||||
name, type,
|
|
||||||
url, routingUrl,
|
|
||||||
key: nanoid(),
|
|
||||||
permissions: opts.permissions ?? false,
|
|
||||||
icon: opts.icon,
|
|
||||||
links: [routingUrl],
|
|
||||||
level: (root.level ?? -1) + 1,
|
|
||||||
redirectUrl: opts.defaultUrl
|
|
||||||
};
|
|
||||||
|
|
||||||
if (type == "category") {
|
|
||||||
let entries = contents.map((creatorFunc) => creatorFunc(entry, routing));
|
|
||||||
let routes = [];
|
|
||||||
|
|
||||||
entries.forEach((e) => {
|
|
||||||
// move empty wildcard routes to end of category, to prevent overlap
|
|
||||||
if (e.url == entry.url) {
|
|
||||||
routes.unshift(e);
|
|
||||||
} else {
|
|
||||||
routes.push(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
routes.reverse();
|
|
||||||
|
|
||||||
routing.push(...routes);
|
|
||||||
|
|
||||||
if (opts.redirectUrl != entry.url) {
|
|
||||||
routing.push({
|
|
||||||
key: entry.key,
|
|
||||||
url: entry.url,
|
|
||||||
permissions: entry.permissions,
|
|
||||||
routingUrl: entry.redirectUrl + "/:fallback*",
|
|
||||||
view: React.createElement(Redirect, { to: entry.redirectUrl })
|
|
||||||
});
|
|
||||||
entry.url = entry.redirectUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
root.links.push(...entry.links);
|
|
||||||
|
|
||||||
entry.MenuEntry = React.createElement(
|
|
||||||
MenuComponent,
|
|
||||||
entry,
|
|
||||||
entries.map((e) => e.MenuEntry)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
entry.links.push(routingUrl);
|
|
||||||
root.links.push(routingUrl);
|
|
||||||
|
|
||||||
entry.view = React.createElement(contents, { baseUrl: url });
|
|
||||||
entry.MenuEntry = React.createElement(MenuComponent, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (root.redirectUrl == undefined) {
|
|
||||||
root.redirectUrl = entry.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
createNavigation,
|
|
||||||
Menu: MenuEntry,
|
|
||||||
Item: MenuEntry
|
|
||||||
};
|
|
175
web/source/settings/lib/navigation/menu.tsx
Normal file
175
web/source/settings/lib/navigation/menu.tsx
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
/*
|
||||||
|
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, { PropsWithChildren } from "react";
|
||||||
|
import { Link, useRoute } from "wouter";
|
||||||
|
import {
|
||||||
|
BaseUrlContext,
|
||||||
|
MenuLevelContext,
|
||||||
|
useBaseUrl,
|
||||||
|
useHasPermission,
|
||||||
|
useMenuLevel,
|
||||||
|
} from "./util";
|
||||||
|
import UserLogoutCard from "../../components/user-logout-card";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
export interface MenuItemProps {
|
||||||
|
/**
|
||||||
|
* Name / title of this menu item.
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Url path component for this menu item.
|
||||||
|
*/
|
||||||
|
itemUrl: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this menu item is a category containing
|
||||||
|
* children, which child should be selected by
|
||||||
|
* default when category title is clicked.
|
||||||
|
*
|
||||||
|
* Optional, use for categories only.
|
||||||
|
*/
|
||||||
|
defaultChild?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permissions required to access this
|
||||||
|
* menu item (none, "moderator", "admin").
|
||||||
|
*/
|
||||||
|
permissions?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fork-awesome string to render
|
||||||
|
* icon for this menu item.
|
||||||
|
*/
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MenuItem(props: PropsWithChildren<MenuItemProps>) {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
itemUrl,
|
||||||
|
defaultChild,
|
||||||
|
permissions,
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
// Derive where this item is
|
||||||
|
// in terms of URL routing.
|
||||||
|
const baseUrl = useBaseUrl();
|
||||||
|
const thisUrl = [ baseUrl, itemUrl ].join('/');
|
||||||
|
|
||||||
|
// Derive where this item is in
|
||||||
|
// terms of nesting within the menu.
|
||||||
|
const thisLevel = useMenuLevel();
|
||||||
|
const nextLevel = thisLevel+1;
|
||||||
|
const topLevel = thisLevel === 0;
|
||||||
|
|
||||||
|
// Check whether this item is currently active
|
||||||
|
// (ie., user has selected it in the menu).
|
||||||
|
//
|
||||||
|
// This uses a wildcard to mark both parent
|
||||||
|
// and relevant child as active.
|
||||||
|
//
|
||||||
|
// See:
|
||||||
|
// https://github.com/molefrog/wouter?tab=readme-ov-file#useroute-route-matching-and-parameters
|
||||||
|
const [isActive] = useRoute([ thisUrl, "*?" ].join("/"));
|
||||||
|
|
||||||
|
// Don't render item if logged-in user
|
||||||
|
// doesn't have permissions to use it.
|
||||||
|
if (!useHasPermission(permissions)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether this item has children.
|
||||||
|
const hasChildren = children !== undefined;
|
||||||
|
const childrenArray = hasChildren && Array.isArray(children);
|
||||||
|
|
||||||
|
// Class name of the item varies depending
|
||||||
|
// on where it is in the menu, and whether
|
||||||
|
// it has children beneath it or not.
|
||||||
|
const classNames: string[] = [];
|
||||||
|
if (topLevel) {
|
||||||
|
classNames.push("category", "top-level");
|
||||||
|
} else {
|
||||||
|
if (thisLevel === 1 && hasChildren) {
|
||||||
|
classNames.push("category", "expanding");
|
||||||
|
} else if (thisLevel === 1 && !hasChildren) {
|
||||||
|
classNames.push("view", "expanding");
|
||||||
|
} else if (thisLevel === 2) {
|
||||||
|
classNames.push("view", "nested");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
classNames.push("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
let content: React.JSX.Element | null;
|
||||||
|
if ((isActive || topLevel) && childrenArray) {
|
||||||
|
// Render children as a nested list.
|
||||||
|
content = <ul>{children}</ul>;
|
||||||
|
} else if (isActive && hasChildren) {
|
||||||
|
// Render child as solo element.
|
||||||
|
content = <>{children}</>;
|
||||||
|
} else {
|
||||||
|
// Not active: hide children.
|
||||||
|
content = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a default child is defined, this item should point to that.
|
||||||
|
const href = defaultChild ? [ thisUrl, defaultChild ].join("/") : thisUrl;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={nanoid()} className={classNames.join(" ")}>
|
||||||
|
<Link href={href} className="title">
|
||||||
|
<span>
|
||||||
|
{icon && <i className={`icon fa fa-fw ${icon}`} aria-hidden="true" />}
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
{ content &&
|
||||||
|
<BaseUrlContext.Provider value={thisUrl}>
|
||||||
|
<MenuLevelContext.Provider value={nextLevel}>
|
||||||
|
{content}
|
||||||
|
</MenuLevelContext.Provider>
|
||||||
|
</BaseUrlContext.Provider>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SidebarMenuProps{}
|
||||||
|
|
||||||
|
export function SidebarMenu({ children }: PropsWithChildren<SidebarMenuProps>) {
|
||||||
|
return (
|
||||||
|
<div className="sidebar">
|
||||||
|
<UserLogoutCard />
|
||||||
|
<nav className="menu-tree">
|
||||||
|
<MenuLevelContext.Provider value={0}>
|
||||||
|
<ul className="top-level">
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
</MenuLevelContext.Provider>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -18,37 +18,62 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createContext, useContext } from "react";
|
import { createContext, useContext } from "react";
|
||||||
const RoleContext = createContext([]);
|
const RoleContext = createContext<string[]>([]);
|
||||||
const BaseUrlContext = createContext<string>("");
|
const BaseUrlContext = createContext<string>("");
|
||||||
|
const MenuLevelContext = createContext<number>(0);
|
||||||
|
|
||||||
function urlSafe(str) {
|
function urlSafe(str: string) {
|
||||||
return str.toLowerCase().replace(/[\s/]+/g, "-");
|
return str.toLowerCase().replace(/[\s/]+/g, "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
function useHasPermission(permissions) {
|
function useHasPermission(permissions: string[] | undefined) {
|
||||||
const roles = useContext(RoleContext);
|
const roles = useContext<string[]>(RoleContext);
|
||||||
return checkPermission(permissions, roles);
|
return checkPermission(permissions, roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkPermission(requiredPermissisons, user) {
|
// checkPermission returns true if the user's roles
|
||||||
// requiredPermissions can be 'false', in which case there are no restrictions
|
// include requiredPermissions, or false otherwise.
|
||||||
if (requiredPermissisons === false) {
|
function checkPermission(requiredPermissions: string[] | undefined, userRoles: string[]): boolean {
|
||||||
|
if (requiredPermissions === undefined) {
|
||||||
|
// No perms defined, so user
|
||||||
|
// implicitly has permission.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// or an array of roles, check if one of the user's roles is sufficient
|
if (requiredPermissions.length === 0) {
|
||||||
return user.some((role) => requiredPermissisons.includes(role));
|
// No perms defined, so user
|
||||||
|
// implicitly has permission.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if one of the user's
|
||||||
|
// roles is sufficient.
|
||||||
|
return userRoles.some((role) => {
|
||||||
|
if (role === "admin") {
|
||||||
|
// Admins can
|
||||||
|
// see everything.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return requiredPermissions.includes(role);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function useBaseUrl() {
|
function useBaseUrl() {
|
||||||
return useContext(BaseUrlContext);
|
return useContext(BaseUrlContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useMenuLevel() {
|
||||||
|
return useContext(MenuLevelContext);
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
urlSafe,
|
urlSafe,
|
||||||
RoleContext,
|
RoleContext,
|
||||||
useHasPermission,
|
useHasPermission,
|
||||||
checkPermission,
|
checkPermission,
|
||||||
BaseUrlContext,
|
BaseUrlContext,
|
||||||
useBaseUrl
|
useBaseUrl,
|
||||||
|
MenuLevelContext,
|
||||||
|
useMenuLevel,
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { replaceCacheOnMutation, removeFromCacheOnMutation } from "../query-modi
|
||||||
import { gtsApi } from "../gts-api";
|
import { gtsApi } from "../gts-api";
|
||||||
import { listToKeyedObject } from "../transforms";
|
import { listToKeyedObject } from "../transforms";
|
||||||
import { AdminAccount, HandleSignupParams, SearchAccountParams } from "../../types/account";
|
import { AdminAccount, HandleSignupParams, SearchAccountParams } from "../../types/account";
|
||||||
|
import { InstanceRule, MappedRules } from "../../types/rules";
|
||||||
|
|
||||||
const extended = gtsApi.injectEndpoints({
|
const extended = gtsApi.injectEndpoints({
|
||||||
endpoints: (build) => ({
|
endpoints: (build) => ({
|
||||||
|
@ -120,14 +121,14 @@ const extended = gtsApi.injectEndpoints({
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
instanceRules: build.query({
|
instanceRules: build.query<MappedRules, void>({
|
||||||
query: () => ({
|
query: () => ({
|
||||||
url: `/api/v1/admin/instance/rules`
|
url: `/api/v1/admin/instance/rules`
|
||||||
}),
|
}),
|
||||||
transformResponse: listToKeyedObject<any>("id")
|
transformResponse: listToKeyedObject<InstanceRule>("id")
|
||||||
}),
|
}),
|
||||||
|
|
||||||
addInstanceRule: build.mutation({
|
addInstanceRule: build.mutation<MappedRules, any>({
|
||||||
query: (formData) => ({
|
query: (formData) => ({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `/api/v1/admin/instance/rules`,
|
url: `/api/v1/admin/instance/rules`,
|
||||||
|
@ -135,11 +136,7 @@ const extended = gtsApi.injectEndpoints({
|
||||||
body: formData,
|
body: formData,
|
||||||
discardEmpty: true
|
discardEmpty: true
|
||||||
}),
|
}),
|
||||||
transformResponse: (data) => {
|
transformResponse: listToKeyedObject<InstanceRule>("id"),
|
||||||
return {
|
|
||||||
[data.id]: data
|
|
||||||
};
|
|
||||||
},
|
|
||||||
...replaceCacheOnMutation("instanceRules"),
|
...replaceCacheOnMutation("instanceRules"),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,15 @@
|
||||||
export interface CustomEmoji {
|
export interface CustomEmoji {
|
||||||
id?: string;
|
id?: string;
|
||||||
shortcode: string;
|
shortcode: string;
|
||||||
|
url: string;
|
||||||
|
static_url: string;
|
||||||
|
visible_in_picker: boolean;
|
||||||
category?: string;
|
category?: string;
|
||||||
|
disabled: boolean;
|
||||||
|
updated_at: string;
|
||||||
|
total_file_size: number;
|
||||||
|
content_type: string;
|
||||||
|
uri: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -17,19 +17,13 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
export interface InstanceRule {
|
||||||
import { Switch, Route } from "wouter";
|
id: string;
|
||||||
|
created_at: string;
|
||||||
import EmojiOverview from "./overview";
|
updated_at: string;
|
||||||
import EmojiDetail from "./detail";
|
text: string;
|
||||||
|
}
|
||||||
export default function CustomEmoji({ baseUrl }) {
|
|
||||||
return (
|
export interface MappedRules {
|
||||||
<Switch>
|
[key: string]: InstanceRule;
|
||||||
<Route path={`${baseUrl}/:emojiId`}>
|
|
||||||
<EmojiDetail />
|
|
||||||
</Route>
|
|
||||||
<EmojiOverview />
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
}
|
}
|
|
@ -53,21 +53,13 @@ ul li::before {
|
||||||
|
|
||||||
& > div,
|
& > div,
|
||||||
& > form {
|
& > form {
|
||||||
border-left: 0.2rem solid $border-accent;
|
|
||||||
padding-left: 0.4rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
|
||||||
h1, h2 {
|
h1, h2, h3, h4, h5 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-top: 0.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:only-child {
|
|
||||||
border-left: none;
|
|
||||||
padding-left: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
|
@ -77,12 +69,6 @@ ul li::before {
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.without-border,
|
|
||||||
.without-border {
|
|
||||||
border-left: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .error {
|
& > .error {
|
||||||
|
@ -305,7 +291,8 @@ input, select, textarea {
|
||||||
) !important;
|
) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
section.with-sidebar > div, section.with-sidebar > form {
|
section.with-sidebar > div,
|
||||||
|
section.with-sidebar > form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
@ -348,10 +335,6 @@ section.with-sidebar > div, section.with-sidebar > form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.labelinput .border {
|
.labelinput .border {
|
||||||
|
|
|
@ -18,13 +18,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useInstanceKeysExpireMutation } from "../../../../lib/query";
|
||||||
import { useInstanceKeysExpireMutation } from "../../../lib/query";
|
import { TextInput } from "../../../../components/form/inputs";
|
||||||
|
import MutationButton from "../../../../components/form/mutation-button";
|
||||||
import { useTextInput } from "../../../lib/form";
|
import { useTextInput } from "../../../../lib/form";
|
||||||
import { TextInput } from "../../../components/form/inputs";
|
|
||||||
|
|
||||||
import MutationButton from "../../../components/form/mutation-button";
|
|
||||||
|
|
||||||
export default function ExpireRemote({}) {
|
export default function ExpireRemote({}) {
|
||||||
const domainField = useTextInput("domain");
|
const domainField = useTextInput("domain");
|
||||||
|
@ -54,7 +51,7 @@ export default function ExpireRemote({}) {
|
||||||
placeholder="example.org"
|
placeholder="example.org"
|
||||||
/>
|
/>
|
||||||
<MutationButton
|
<MutationButton
|
||||||
disabled={false}
|
disabled={!domainField.value}
|
||||||
label="Expire keys"
|
label="Expire keys"
|
||||||
result={expireResult}
|
result={expireResult}
|
||||||
/>
|
/>
|
|
@ -19,12 +19,10 @@
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useMediaCleanupMutation } from "../../../lib/query";
|
import { useMediaCleanupMutation } from "../../../../lib/query";
|
||||||
|
import { useTextInput } from "../../../../lib/form";
|
||||||
import { useTextInput } from "../../../lib/form";
|
import { TextInput } from "../../../../components/form/inputs";
|
||||||
import { TextInput } from "../../../components/form/inputs";
|
import MutationButton from "../../../../components/form/mutation-button";
|
||||||
|
|
||||||
import MutationButton from "../../../components/form/mutation-button";
|
|
||||||
|
|
||||||
export default function Cleanup({}) {
|
export default function Cleanup({}) {
|
||||||
const daysField = useTextInput("days", { defaultValue: "30" });
|
const daysField = useTextInput("days", { defaultValue: "30" });
|
||||||
|
@ -52,7 +50,7 @@ export default function Cleanup({}) {
|
||||||
placeholder="30"
|
placeholder="30"
|
||||||
/>
|
/>
|
||||||
<MutationButton
|
<MutationButton
|
||||||
disabled={false}
|
disabled={!daysField.value}
|
||||||
label="Remove old media"
|
label="Remove old media"
|
||||||
result={mediaCleanupResult}
|
result={mediaCleanupResult}
|
||||||
/>
|
/>
|
134
web/source/settings/views/admin/emoji/category-select.tsx
Normal file
134
web/source/settings/views/admin/emoji/category-select.tsx
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
/*
|
||||||
|
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, { useMemo, useEffect, PropsWithChildren, ReactElement } from "react";
|
||||||
|
import { matchSorter } from "match-sorter";
|
||||||
|
import ComboBox from "../../../components/combo-box";
|
||||||
|
import { useListEmojiQuery } from "../../../lib/query/admin/custom-emoji";
|
||||||
|
import { CustomEmoji } from "../../../lib/types/custom-emoji";
|
||||||
|
import { ComboboxFormInputHook } from "../../../lib/form/types";
|
||||||
|
import Loading from "../../../components/loading";
|
||||||
|
import { Error } from "../../../components/error";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort all emoji into a map keyed by
|
||||||
|
* the category names (or "Unsorted").
|
||||||
|
*/
|
||||||
|
export function useEmojiByCategory(emojis: CustomEmoji[]) {
|
||||||
|
return useMemo(() => {
|
||||||
|
const byCategory = new Map<string, CustomEmoji[]>();
|
||||||
|
|
||||||
|
emojis.forEach((emoji) => {
|
||||||
|
const key = emoji.category ?? "Unsorted";
|
||||||
|
const value = byCategory.get(key) ?? [];
|
||||||
|
value.push(emoji);
|
||||||
|
byCategory.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return byCategory;
|
||||||
|
}, [emojis]);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategorySelectProps {
|
||||||
|
field: ComboboxFormInputHook;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Renders a cute lil searchable "category select" dropdown.
|
||||||
|
*/
|
||||||
|
export function CategorySelect({ field, children }: PropsWithChildren<CategorySelectProps>) {
|
||||||
|
// Get all local emojis.
|
||||||
|
const {
|
||||||
|
data: emoji = [],
|
||||||
|
isLoading,
|
||||||
|
isSuccess,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = useListEmojiQuery({ filter: "domain:local" });
|
||||||
|
|
||||||
|
const emojiByCategory = useEmojiByCategory(emoji);
|
||||||
|
const categories = useMemo(() => new Set(emojiByCategory.keys()), [emojiByCategory]);
|
||||||
|
const { value, setIsNew } = field;
|
||||||
|
|
||||||
|
// Data used by the ComboBox element
|
||||||
|
// to select an emoji category.
|
||||||
|
const categoryItems = useMemo(() => {
|
||||||
|
const categoriesArr = Array.from(categories);
|
||||||
|
|
||||||
|
// Sorted by complex algorithm.
|
||||||
|
const categoryNames = matchSorter(
|
||||||
|
categoriesArr,
|
||||||
|
value ?? "",
|
||||||
|
{ threshold: matchSorter.rankings.NO_MATCH },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Map each category to the static image
|
||||||
|
// of the first emoji it contains.
|
||||||
|
const categoryItems: [string, ReactElement][] = [];
|
||||||
|
categoryNames.forEach((categoryName) => {
|
||||||
|
let src: string | undefined;
|
||||||
|
const items = emojiByCategory.get(categoryName);
|
||||||
|
if (items && items.length > 0) {
|
||||||
|
src = items[0].static_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryItems.push([
|
||||||
|
categoryName,
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{categoryName}
|
||||||
|
</>
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return categoryItems;
|
||||||
|
}, [emojiByCategory, categories, value]);
|
||||||
|
|
||||||
|
// New category if something has been entered
|
||||||
|
// and we don't have it in categories yet.
|
||||||
|
useEffect(() => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (trimmed.length > 0) {
|
||||||
|
setIsNew(!categories.has(trimmed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [categories, value, isSuccess, setIsNew]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
} else if (isError) {
|
||||||
|
return <Error error={error} />;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<ComboBox
|
||||||
|
field={field}
|
||||||
|
items={categoryItems}
|
||||||
|
label="Category"
|
||||||
|
placeholder="e.g., reactions"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ComboBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,36 +18,29 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useRoute, Link, Redirect } from "wouter";
|
import { Redirect, useParams } from "wouter";
|
||||||
|
import { useComboBoxInput, useFileInput, useValue } from "../../../../lib/form";
|
||||||
import { useComboBoxInput, useFileInput, useValue } from "../../../lib/form";
|
import useFormSubmit from "../../../../lib/form/submit";
|
||||||
|
import { useBaseUrl } from "../../../../lib/navigation/util";
|
||||||
|
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";
|
||||||
|
import { useGetEmojiQuery, useEditEmojiMutation, useDeleteEmojiMutation } from "../../../../lib/query/admin/custom-emoji";
|
||||||
import { CategorySelect } from "../category-select";
|
import { CategorySelect } from "../category-select";
|
||||||
|
import BackButton from "../../../../components/back-button";
|
||||||
|
|
||||||
import useFormSubmit from "../../../lib/form/submit";
|
export default function EmojiDetail() {
|
||||||
import { useBaseUrl } from "../../../lib/navigation/util";
|
|
||||||
|
|
||||||
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";
|
|
||||||
|
|
||||||
import { useGetEmojiQuery, useEditEmojiMutation, useDeleteEmojiMutation } from "../../../lib/query/admin/custom-emoji";
|
|
||||||
|
|
||||||
export default function EmojiDetailRoute({ }) {
|
|
||||||
const baseUrl = useBaseUrl();
|
const baseUrl = useBaseUrl();
|
||||||
let [_match, params] = useRoute(`${baseUrl}/:emojiId`);
|
const params = useParams();
|
||||||
if (params?.emojiId == undefined) {
|
return (
|
||||||
return <Redirect to={baseUrl} />;
|
<div className="emoji-detail">
|
||||||
} else {
|
<BackButton to={`~${baseUrl}/local`} />
|
||||||
return (
|
<FormWithData dataQuery={useGetEmojiQuery} queryArg={params.emojiId} DataForm={EmojiDetailForm} />
|
||||||
<div className="emoji-detail">
|
</div>
|
||||||
<Link to={baseUrl}><a>< go back</a></Link>
|
);
|
||||||
<FormWithData dataQuery={useGetEmojiQuery} queryArg={params.emojiId} DataForm={EmojiDetailForm} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmojiDetailForm({ data: emoji }) {
|
function EmojiDetailForm({ data: emoji }) {
|
||||||
|
@ -77,7 +70,7 @@ function EmojiDetailForm({ data: emoji }) {
|
||||||
const [deleteEmoji, deleteResult] = useDeleteEmojiMutation();
|
const [deleteEmoji, deleteResult] = useDeleteEmojiMutation();
|
||||||
|
|
||||||
if (deleteResult.isSuccess) {
|
if (deleteResult.isSuccess) {
|
||||||
return <Redirect to={baseUrl} />;
|
return <Redirect to={`~${baseUrl}/local`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -93,6 +86,7 @@ function EmojiDetailForm({ data: emoji }) {
|
||||||
className="danger"
|
className="danger"
|
||||||
showError={false}
|
showError={false}
|
||||||
result={deleteResult}
|
result={deleteResult}
|
||||||
|
disabled={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -110,6 +104,7 @@ function EmojiDetailForm({ data: emoji }) {
|
||||||
result={result}
|
result={result}
|
||||||
showError={false}
|
showError={false}
|
||||||
style={{ visibility: (form.category.isNew ? "initial" : "hidden") }}
|
style={{ visibility: (form.category.isNew ? "initial" : "hidden") }}
|
||||||
|
disabled={!form.category.value}
|
||||||
/>
|
/>
|
||||||
</CategorySelect>
|
</CategorySelect>
|
||||||
</div>
|
</div>
|
||||||
|
@ -126,12 +121,13 @@ function EmojiDetailForm({ data: emoji }) {
|
||||||
label="Replace image"
|
label="Replace image"
|
||||||
showError={false}
|
showError={false}
|
||||||
result={result}
|
result={result}
|
||||||
|
disabled={!form.image.value}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FakeToot>
|
<FakeToot>
|
||||||
Look at this new custom emoji <img
|
Look at this new custom emoji <img
|
||||||
className="emoji"
|
className="emoji"
|
||||||
src={form.image.previewURL ?? emoji.url}
|
src={form.image.previewValue ?? emoji.url}
|
||||||
title={`:${emoji.shortcode}:`}
|
title={`:${emoji.shortcode}:`}
|
||||||
alt={emoji.shortcode}
|
alt={emoji.shortcode}
|
||||||
/> isn't it cool?
|
/> isn't it cool?
|
|
@ -18,19 +18,15 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo, useEffect } from "react";
|
import React, { useMemo, useEffect } from "react";
|
||||||
|
import { useFileInput, useComboBoxInput } from "../../../../lib/form";
|
||||||
import { useFileInput, useComboBoxInput } from "../../../lib/form";
|
|
||||||
import useShortcode from "./use-shortcode";
|
import useShortcode from "./use-shortcode";
|
||||||
|
import useFormSubmit from "../../../../lib/form/submit";
|
||||||
import useFormSubmit from "../../../lib/form/submit";
|
import { TextInput, FileInput } from "../../../../components/form/inputs";
|
||||||
|
|
||||||
import { TextInput, FileInput } from "../../../components/form/inputs";
|
|
||||||
|
|
||||||
import { CategorySelect } from '../category-select';
|
import { CategorySelect } from '../category-select';
|
||||||
import FakeToot from "../../../components/fake-toot";
|
import FakeToot from "../../../../components/fake-toot";
|
||||||
import MutationButton from "../../../components/form/mutation-button";
|
import MutationButton from "../../../../components/form/mutation-button";
|
||||||
import { useAddEmojiMutation } from "../../../lib/query/admin/custom-emoji";
|
import { useAddEmojiMutation } from "../../../../lib/query/admin/custom-emoji";
|
||||||
import { useInstanceV1Query } from "../../../lib/query";
|
import { useInstanceV1Query } from "../../../../lib/query";
|
||||||
|
|
||||||
export default function NewEmojiForm() {
|
export default function NewEmojiForm() {
|
||||||
const shortcode = useShortcode();
|
const shortcode = useShortcode();
|
||||||
|
@ -113,4 +109,4 @@ export default function NewEmojiForm() {
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
173
web/source/settings/views/admin/emoji/local/overview.tsx
Normal file
173
web/source/settings/views/admin/emoji/local/overview.tsx
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
/*
|
||||||
|
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, { useMemo, useState } from "react";
|
||||||
|
import { Link } from "wouter";
|
||||||
|
import { matchSorter } from "match-sorter";
|
||||||
|
import NewEmojiForm from "./new-emoji";
|
||||||
|
import { useTextInput } from "../../../../lib/form";
|
||||||
|
import { useEmojiByCategory } from "../category-select";
|
||||||
|
import Loading from "../../../../components/loading";
|
||||||
|
import { Error } from "../../../../components/error";
|
||||||
|
import { TextInput } from "../../../../components/form/inputs";
|
||||||
|
import { useListEmojiQuery } from "../../../../lib/query/admin/custom-emoji";
|
||||||
|
import { CustomEmoji } from "../../../../lib/types/custom-emoji";
|
||||||
|
|
||||||
|
export function EmojiOverview() {
|
||||||
|
const { data: emoji = [], isLoading, isError, error } = useListEmojiQuery({ filter: "domain:local" });
|
||||||
|
|
||||||
|
let content: React.JSX.Element;
|
||||||
|
if (isLoading) {
|
||||||
|
content = <Loading />;
|
||||||
|
} else if (isError) {
|
||||||
|
content = <Error error={error} />;
|
||||||
|
} else {
|
||||||
|
content = (
|
||||||
|
<>
|
||||||
|
<EmojiList emoji={emoji} />
|
||||||
|
<NewEmojiForm />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Local Custom Emoji</h1>
|
||||||
|
<p>
|
||||||
|
To use custom emoji in your toots they have to be 'local' to the instance.
|
||||||
|
You can either upload them here directly, or copy from those already
|
||||||
|
present on other (known) instances through the <Link to={`/remote`}>Remote Emoji</Link> page.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Be warned!</strong> If you upload more than about 300-400 custom emojis in
|
||||||
|
total on your instance, this may lead to rate-limiting issues for users and clients
|
||||||
|
if they try to load all the emoji images at once (which is what many clients do).
|
||||||
|
</p>
|
||||||
|
{content}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EmojiListParams {
|
||||||
|
emoji: CustomEmoji[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmojiList({ emoji }: EmojiListParams) {
|
||||||
|
const filterField = useTextInput("filter");
|
||||||
|
const filter = filterField.value ?? "";
|
||||||
|
const emojiByCategory = useEmojiByCategory(emoji);
|
||||||
|
|
||||||
|
// Filter emoji based on shortcode match
|
||||||
|
// with user input, hiding empty categories.
|
||||||
|
const { filteredEmojis, filteredCount } = useMemo(() => {
|
||||||
|
// Amount of emojis removed by the filter.
|
||||||
|
// Start with the length of the array since
|
||||||
|
// that's the max that can be filtered out.
|
||||||
|
let filteredCount = emoji.length;
|
||||||
|
|
||||||
|
// Results of the filtering.
|
||||||
|
const filteredEmojis: [string, CustomEmoji[]][] = [];
|
||||||
|
|
||||||
|
// Filter from emojis in this category.
|
||||||
|
emojiByCategory.forEach((entries, category) => {
|
||||||
|
const filteredEntries = matchSorter(entries, filter, {
|
||||||
|
keys: ["shortcode"]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filteredEntries.length == 0) {
|
||||||
|
// Nothing left in this category, don't
|
||||||
|
// bother adding it to filteredEmojis.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredCount -= filteredEntries.length;
|
||||||
|
filteredEmojis.push([category, filteredEntries]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { filteredEmojis, filteredCount };
|
||||||
|
}, [filter, emojiByCategory, emoji.length]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2>Overview</h2>
|
||||||
|
{emoji.length > 0
|
||||||
|
? <span>{emoji.length} custom emoji {filteredCount > 0 && `(${filteredCount} filtered)`}</span>
|
||||||
|
: <span>No custom emoji yet, you can add one below.</span>
|
||||||
|
}
|
||||||
|
<div className="list emoji-list">
|
||||||
|
<div className="header">
|
||||||
|
<TextInput
|
||||||
|
field={filterField}
|
||||||
|
name="emoji-shortcode"
|
||||||
|
placeholder="Search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="entries scrolling">
|
||||||
|
{filteredEmojis.length > 0
|
||||||
|
? (
|
||||||
|
<div className="entries scrolling">
|
||||||
|
{filteredEmojis.map(([category, emojis]) => {
|
||||||
|
return <EmojiCategory key={category} category={category} emojis={emojis} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: <div className="entry">No local emoji matched your filter.</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EmojiCategoryProps {
|
||||||
|
category: string;
|
||||||
|
emojis: CustomEmoji[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmojiCategory({ category, emojis }: EmojiCategoryProps) {
|
||||||
|
return (
|
||||||
|
<div className="entry">
|
||||||
|
<b>{category}</b>
|
||||||
|
<div className="emoji-group">
|
||||||
|
{emojis.map((emoji) => {
|
||||||
|
return (
|
||||||
|
<Link key={emoji.id} to={`/local/${emoji.id}`} >
|
||||||
|
<EmojiPreview emoji={emoji} />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmojiPreview({ emoji }) {
|
||||||
|
const [ animate, setAnimate ] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
onMouseEnter={() => { setAnimate(true); }}
|
||||||
|
onMouseLeave={() => { setAnimate(false); }}
|
||||||
|
src={animate ? emoji.url : emoji.static_url}
|
||||||
|
alt={emoji.shortcode}
|
||||||
|
title={emoji.shortcode}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -17,19 +17,19 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
import { useMemo } from "react";
|
||||||
|
|
||||||
const { useTextInput } = require("../../../lib/form");
|
import { useTextInput } from "../../../../lib/form";
|
||||||
const { useListEmojiQuery } = require("../../../lib/query/admin/custom-emoji");
|
import { useListEmojiQuery } from "../../../../lib/query/admin/custom-emoji";
|
||||||
|
|
||||||
const shortcodeRegex = /^\w{2,30}$/;
|
const shortcodeRegex = /^\w{2,30}$/;
|
||||||
|
|
||||||
module.exports = function useShortcode() {
|
export default function useShortcode() {
|
||||||
const { data: emoji = [] } = useListEmojiQuery({
|
const { data: emoji = [] } = useListEmojiQuery({
|
||||||
filter: "domain:local"
|
filter: "domain:local"
|
||||||
});
|
});
|
||||||
|
|
||||||
const emojiCodes = React.useMemo(() => {
|
const emojiCodes = useMemo(() => {
|
||||||
return new Set(emoji.map((e) => e.shortcode));
|
return new Set(emoji.map((e) => e.shortcode));
|
||||||
}, [emoji]);
|
}, [emoji]);
|
||||||
|
|
||||||
|
@ -53,4 +53,4 @@ module.exports = function useShortcode() {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
}
|
|
@ -19,36 +19,28 @@
|
||||||
|
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
|
|
||||||
import ParseFromToot from "./parse-from-toot";
|
import StealThisLook from "./steal-this-look";
|
||||||
|
|
||||||
import Loading from "../../../components/loading";
|
import Loading from "../../../../components/loading";
|
||||||
import { Error } from "../../../components/error";
|
import { Error } from "../../../../components/error";
|
||||||
import { useListEmojiQuery } from "../../../lib/query/admin/custom-emoji";
|
import { useListEmojiQuery } from "../../../../lib/query/admin/custom-emoji";
|
||||||
|
|
||||||
export default function RemoteEmoji() {
|
export default function RemoteEmoji() {
|
||||||
// local emoji are queried for shortcode collision detection
|
// Local emoji are queried for
|
||||||
|
// shortcode collision detection
|
||||||
const {
|
const {
|
||||||
data: emoji = [],
|
data: emoji = [],
|
||||||
isLoading,
|
isLoading,
|
||||||
error
|
error
|
||||||
} = useListEmojiQuery({ filter: "domain:local" });
|
} = useListEmojiQuery({ filter: "domain:local" });
|
||||||
|
|
||||||
const emojiCodes = useMemo(() => {
|
const emojiCodes = useMemo(() => new Set(emoji.map((e) => e.shortcode)), [emoji]);
|
||||||
return new Set(emoji.map((e) => e.shortcode));
|
|
||||||
}, [emoji]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>Custom Emoji (remote)</h1>
|
<h1>Custom Emoji (remote)</h1>
|
||||||
{error &&
|
{error && <Error error={error} />}
|
||||||
<Error error={error} />
|
{isLoading ? <Loading /> : <StealThisLook emojiCodes={emojiCodes} />}
|
||||||
}
|
|
||||||
{isLoading
|
|
||||||
? <Loading />
|
|
||||||
: <>
|
|
||||||
<ParseFromToot emojiCodes={emojiCodes} />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -19,19 +19,19 @@
|
||||||
|
|
||||||
import React, { useCallback, useEffect } from "react";
|
import React, { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
import { useTextInput, useComboBoxInput, useCheckListInput } from "../../../lib/form";
|
import { useTextInput, useComboBoxInput, useCheckListInput } from "../../../../lib/form";
|
||||||
|
|
||||||
import useFormSubmit from "../../../lib/form/submit";
|
import useFormSubmit from "../../../../lib/form/submit";
|
||||||
|
|
||||||
import CheckList from "../../../components/check-list";
|
import CheckList from "../../../../components/check-list";
|
||||||
import { CategorySelect } from '../category-select';
|
import { CategorySelect } from '../category-select';
|
||||||
|
|
||||||
import { TextInput } from "../../../components/form/inputs";
|
import { TextInput } from "../../../../components/form/inputs";
|
||||||
import MutationButton from "../../../components/form/mutation-button";
|
import MutationButton from "../../../../components/form/mutation-button";
|
||||||
import { Error } from "../../../components/error";
|
import { Error } from "../../../../components/error";
|
||||||
import { useSearchItemForEmojiMutation, usePatchRemoteEmojisMutation } from "../../../lib/query/admin/custom-emoji";
|
import { useSearchItemForEmojiMutation, usePatchRemoteEmojisMutation } from "../../../../lib/query/admin/custom-emoji";
|
||||||
|
|
||||||
export default function ParseFromToot({ emojiCodes }) {
|
export default function StealThisLook({ emojiCodes }) {
|
||||||
const [searchStatus, result] = useSearchItemForEmojiMutation();
|
const [searchStatus, result] = useSearchItemForEmojiMutation();
|
||||||
const urlField = useTextInput("url");
|
const urlField = useTextInput("url");
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ export default function ParseFromToot({ emojiCodes }) {
|
||||||
<form onSubmit={submitSearch}>
|
<form onSubmit={submitSearch}>
|
||||||
<div className="form-field text">
|
<div className="form-field text">
|
||||||
<label htmlFor="url">
|
<label htmlFor="url">
|
||||||
Link to a toot:
|
Link to a status:
|
||||||
</label>
|
</label>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<input
|
<input
|
||||||
|
@ -85,13 +85,13 @@ function SearchResult({ result, localEmojiCodes }) {
|
||||||
if (error == "NONE_FOUND") {
|
if (error == "NONE_FOUND") {
|
||||||
return "No results found";
|
return "No results found";
|
||||||
} else if (error == "LOCAL_INSTANCE") {
|
} else if (error == "LOCAL_INSTANCE") {
|
||||||
return <b>This is a local user/toot, all referenced emoji are already on your instance</b>;
|
return <b>This is a local user/status, all referenced emoji are already on your instance</b>;
|
||||||
} else if (error != undefined) {
|
} else if (error != undefined) {
|
||||||
return <Error error={result.error} />;
|
return <Error error={result.error} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.list.length == 0) {
|
if (data.list.length == 0) {
|
||||||
return <b>This {data.type == "statuses" ? "toot" : "account"} doesn't use any custom emoji</b>;
|
return <b>This {data.type == "statuses" ? "status" : "account"} doesn't use any custom emoji</b>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -143,7 +143,7 @@ function CopyEmojiForm({ localEmojiCodes, type, emojiList }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="parsed">
|
<div className="parsed">
|
||||||
<span>This {type == "statuses" ? "toot" : "account"} uses the following custom emoji, select the ones you want to copy/disable:</span>
|
<span>This {type == "statuses" ? "status" : "account"} uses the following custom emoji, select the ones you want to copy/disable:</span>
|
||||||
<form onSubmit={formSubmit}>
|
<form onSubmit={formSubmit}>
|
||||||
<CheckList
|
<CheckList
|
||||||
field={form.selectedEmoji}
|
field={form.selectedEmoji}
|
||||||
|
@ -232,4 +232,4 @@ function EmojiEntry({ entry: emoji, onChange, extraProps: { localEmojiCodes } })
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
177
web/source/settings/views/admin/routes.tsx
Normal file
177
web/source/settings/views/admin/routes.tsx
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
/*
|
||||||
|
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 { MenuItem } from "../../lib/navigation/menu";
|
||||||
|
import React from "react";
|
||||||
|
import { BaseUrlContext, useBaseUrl } from "../../lib/navigation/util";
|
||||||
|
import { Route, Router, Switch } from "wouter";
|
||||||
|
import EmojiDetail from "./emoji/local/detail";
|
||||||
|
import { EmojiOverview } from "./emoji/local/overview";
|
||||||
|
import RemoteEmoji from "./emoji/remote";
|
||||||
|
import InstanceSettings from "./settings";
|
||||||
|
import { InstanceRuleDetail, InstanceRules } from "./settings/rules";
|
||||||
|
import Media from "./actions/media";
|
||||||
|
import Keys from "./actions/keys";
|
||||||
|
|
||||||
|
/*
|
||||||
|
EXPORTED COMPONENTS
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admininistration menu. Admin actions,
|
||||||
|
* emoji import, instance settings.
|
||||||
|
*/
|
||||||
|
export function AdminMenu() {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
name="Administration"
|
||||||
|
itemUrl="admin"
|
||||||
|
defaultChild="actions"
|
||||||
|
permissions={["admin"]}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
name="Instance Settings"
|
||||||
|
itemUrl="instance-settings"
|
||||||
|
icon="fa-sliders"
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name="Instance Rules"
|
||||||
|
itemUrl="instance-rules"
|
||||||
|
icon="fa-dot-circle-o"
|
||||||
|
/>
|
||||||
|
<AdminEmojisMenu />
|
||||||
|
<AdminActionsMenu />
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admininistration router. Admin actions,
|
||||||
|
* emoji import, instance settings.
|
||||||
|
*/
|
||||||
|
export function AdminRouter() {
|
||||||
|
const parentUrl = useBaseUrl();
|
||||||
|
const thisBase = "/admin";
|
||||||
|
const absBase = parentUrl + thisBase;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseUrlContext.Provider value={absBase}>
|
||||||
|
<Router base={thisBase}>
|
||||||
|
<Route path="/instance-settings" component={InstanceSettings}/>
|
||||||
|
<Route path="/instance-rules" component={InstanceRules} />
|
||||||
|
<Route path="/instance-rules/:ruleId" component={InstanceRuleDetail} />
|
||||||
|
<AdminEmojisRouter />
|
||||||
|
<AdminActionsRouter />
|
||||||
|
</Router>
|
||||||
|
</BaseUrlContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
INTERNAL COMPONENTS
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
MENUS
|
||||||
|
*/
|
||||||
|
|
||||||
|
function AdminActionsMenu() {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
name="Actions"
|
||||||
|
itemUrl="actions"
|
||||||
|
defaultChild="media"
|
||||||
|
icon="fa-bolt"
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
name="Media"
|
||||||
|
itemUrl="media"
|
||||||
|
icon="fa-photo"
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name="Keys"
|
||||||
|
itemUrl="keys"
|
||||||
|
icon="fa-key-modern"
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AdminEmojisMenu() {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
name="Custom Emoji"
|
||||||
|
itemUrl="emojis"
|
||||||
|
defaultChild="local"
|
||||||
|
icon="fa-smile-o"
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
name="Local"
|
||||||
|
itemUrl="local"
|
||||||
|
icon="fa-home"
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name="Remote"
|
||||||
|
itemUrl="remote"
|
||||||
|
icon="fa-cloud"
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ROUTERS
|
||||||
|
*/
|
||||||
|
|
||||||
|
function AdminEmojisRouter() {
|
||||||
|
const parentUrl = useBaseUrl();
|
||||||
|
const thisBase = "/emojis";
|
||||||
|
const absBase = parentUrl + thisBase;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseUrlContext.Provider value={absBase}>
|
||||||
|
<Router base={thisBase}>
|
||||||
|
<Switch>
|
||||||
|
<Route path="/local/:emojiId" component={EmojiDetail} />
|
||||||
|
<Route path="/local" component={EmojiOverview} />
|
||||||
|
<Route path="/remote" component={RemoteEmoji} />
|
||||||
|
<Route component={EmojiOverview}/>
|
||||||
|
</Switch>
|
||||||
|
</Router>
|
||||||
|
</BaseUrlContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AdminActionsRouter() {
|
||||||
|
const parentUrl = useBaseUrl();
|
||||||
|
const thisBase = "/actions";
|
||||||
|
const absBase = parentUrl + thisBase;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseUrlContext.Provider value={absBase}>
|
||||||
|
<Router base={thisBase}>
|
||||||
|
<Switch>
|
||||||
|
<Route path="/media" component={Media} />
|
||||||
|
<Route path="/keys" component={Keys} />
|
||||||
|
<Route component={Media}/>
|
||||||
|
</Switch>
|
||||||
|
</Router>
|
||||||
|
</BaseUrlContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
|
@ -19,33 +19,33 @@
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useTextInput, useFileInput } from "../../lib/form";
|
import { useTextInput, useFileInput } from "../../../lib/form";
|
||||||
|
|
||||||
const useFormSubmit = require("../../lib/form/submit").default;
|
const useFormSubmit = require("../../../lib/form/submit").default;
|
||||||
|
|
||||||
import { TextInput, TextArea, FileInput } from "../../components/form/inputs";
|
import { TextInput, TextArea, FileInput } from "../../../components/form/inputs";
|
||||||
|
|
||||||
const FormWithData = require("../../lib/form/form-with-data").default;
|
const FormWithData = require("../../../lib/form/form-with-data").default;
|
||||||
import MutationButton from "../../components/form/mutation-button";
|
import MutationButton from "../../../components/form/mutation-button";
|
||||||
|
|
||||||
import { useInstanceV1Query } from "../../lib/query";
|
import { useInstanceV1Query } from "../../../lib/query";
|
||||||
import { useUpdateInstanceMutation } from "../../lib/query/admin";
|
import { useUpdateInstanceMutation } from "../../../lib/query/admin";
|
||||||
import { InstanceV1 } from "../../lib/types/instance";
|
import { InstanceV1 } from "../../../lib/types/instance";
|
||||||
|
|
||||||
export default function AdminSettings() {
|
export default function InstanceSettings() {
|
||||||
return (
|
return (
|
||||||
<FormWithData
|
<FormWithData
|
||||||
dataQuery={useInstanceV1Query}
|
dataQuery={useInstanceV1Query}
|
||||||
DataForm={AdminSettingsForm}
|
DataForm={InstanceSettingsForm}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AdminSettingsFormProps{
|
interface InstanceSettingsFormProps{
|
||||||
data: InstanceV1;
|
data: InstanceV1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AdminSettingsForm({ data: instance }: AdminSettingsFormProps) {
|
function InstanceSettingsForm({ data: instance }: InstanceSettingsFormProps) {
|
||||||
const titleLimit = 40;
|
const titleLimit = 40;
|
||||||
const shortDescLimit = 500;
|
const shortDescLimit = 500;
|
||||||
const descLimit = 5000;
|
const descLimit = 5000;
|
151
web/source/settings/views/admin/settings/rules.tsx
Normal file
151
web/source/settings/views/admin/settings/rules.tsx
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
/*
|
||||||
|
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, Redirect, useParams } from "wouter";
|
||||||
|
import { useInstanceRulesQuery, useAddInstanceRuleMutation, useUpdateInstanceRuleMutation, useDeleteInstanceRuleMutation } from "../../../lib/query";
|
||||||
|
import { useBaseUrl } from "../../../lib/navigation/util";
|
||||||
|
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 { Error } from "../../../components/error";
|
||||||
|
import BackButton from "../../../components/back-button";
|
||||||
|
import { InstanceRule, MappedRules } from "../../../lib/types/rules";
|
||||||
|
import Loading from "../../../components/loading";
|
||||||
|
import FormWithData from "../../../lib/form/form-with-data";
|
||||||
|
|
||||||
|
export function InstanceRules() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Instance Rules</h1>
|
||||||
|
<FormWithData
|
||||||
|
dataQuery={useInstanceRulesQuery}
|
||||||
|
DataForm={InstanceRulesForm}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InstanceRulesForm({ data: rules }: { data: MappedRules }) {
|
||||||
|
const baseUrl = useBaseUrl();
|
||||||
|
const newRule = useTextInput("text");
|
||||||
|
|
||||||
|
const [submitForm, result] = useFormSubmit({ newRule }, useAddInstanceRuleMutation(), {
|
||||||
|
changedOnly: true,
|
||||||
|
onFinish: () => newRule.reset()
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={submitForm} className="new-rule">
|
||||||
|
<ol className="instance-rules">
|
||||||
|
{Object.values(rules).map((rule: InstanceRule) => (
|
||||||
|
<Link className="rule" to={`~${baseUrl}/instance-rules/${rule.id}`}>
|
||||||
|
<li>
|
||||||
|
<h2>{rule.text} <i className="fa fa-pencil edit-icon" /></h2>
|
||||||
|
</li>
|
||||||
|
<span>{new Date(rule.created_at).toLocaleString()}</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
<TextArea
|
||||||
|
field={newRule}
|
||||||
|
label="New instance rule"
|
||||||
|
/>
|
||||||
|
<MutationButton
|
||||||
|
disabled={newRule.value === undefined || newRule.value.length === 0}
|
||||||
|
label="Add rule"
|
||||||
|
result={result}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InstanceRuleDetail() {
|
||||||
|
const baseUrl = useBaseUrl();
|
||||||
|
const params: { ruleId: string } = useParams();
|
||||||
|
|
||||||
|
const { data: rules, isLoading, isError, error } = useInstanceRulesQuery();
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
} else if (isError) {
|
||||||
|
return <Error error={error} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rules === undefined) {
|
||||||
|
throw "undefined rules";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BackButton to={`~${baseUrl}/instance-rules`} />
|
||||||
|
<EditInstanceRuleForm rule={rules[params.ruleId]} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditInstanceRuleForm({ rule }) {
|
||||||
|
const baseUrl = useBaseUrl();
|
||||||
|
const form = {
|
||||||
|
id: useValue("id", rule.id),
|
||||||
|
rule: useTextInput("text", { defaultValue: rule.text })
|
||||||
|
};
|
||||||
|
|
||||||
|
const [submitForm, result] = useFormSubmit(form, useUpdateInstanceRuleMutation());
|
||||||
|
|
||||||
|
const [deleteRule, deleteResult] = useDeleteInstanceRuleMutation({ fixedCacheKey: rule.id });
|
||||||
|
|
||||||
|
if (result.isSuccess || deleteResult.isSuccess) {
|
||||||
|
return (
|
||||||
|
<Redirect to={`~${baseUrl}/instance-rules`} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rule-detail">
|
||||||
|
<form onSubmit={submitForm}>
|
||||||
|
<TextArea
|
||||||
|
field={form.rule}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="action-buttons row">
|
||||||
|
<MutationButton
|
||||||
|
label="Save"
|
||||||
|
showError={false}
|
||||||
|
result={result}
|
||||||
|
disabled={!form.rule.hasChanged()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MutationButton
|
||||||
|
disabled={false}
|
||||||
|
type="button"
|
||||||
|
onClick={() => deleteRule(rule.id)}
|
||||||
|
label="Delete"
|
||||||
|
className="button danger"
|
||||||
|
showError={false}
|
||||||
|
result={deleteResult}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{result.error && <Error error={result.error} />}
|
||||||
|
{deleteResult.error && <Error error={deleteResult.error} />}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -19,19 +19,19 @@
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useActionAccountMutation } from "../../../lib/query";
|
import { useActionAccountMutation } from "../../../../lib/query";
|
||||||
|
|
||||||
import MutationButton from "../../../components/form/mutation-button";
|
import MutationButton from "../../../../components/form/mutation-button";
|
||||||
|
|
||||||
import useFormSubmit from "../../../lib/form/submit";
|
import useFormSubmit from "../../../../lib/form/submit";
|
||||||
import {
|
import {
|
||||||
useValue,
|
useValue,
|
||||||
useTextInput,
|
useTextInput,
|
||||||
useBoolInput,
|
useBoolInput,
|
||||||
} from "../../../lib/form";
|
} from "../../../../lib/form";
|
||||||
|
|
||||||
import { Checkbox, TextInput } from "../../../components/form/inputs";
|
import { Checkbox, TextInput } from "../../../../components/form/inputs";
|
||||||
import { AdminAccount } from "../../../lib/types/account";
|
import { AdminAccount } from "../../../../lib/types/account";
|
||||||
|
|
||||||
export interface AccountActionsProps {
|
export interface AccountActionsProps {
|
||||||
account: AdminAccount,
|
account: AdminAccount,
|
|
@ -20,26 +20,26 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
|
|
||||||
import { useHandleSignupMutation } from "../../../lib/query";
|
import { useHandleSignupMutation } from "../../../../lib/query";
|
||||||
|
|
||||||
import MutationButton from "../../../components/form/mutation-button";
|
import MutationButton from "../../../../components/form/mutation-button";
|
||||||
|
|
||||||
import useFormSubmit from "../../../lib/form/submit";
|
import useFormSubmit from "../../../../lib/form/submit";
|
||||||
import {
|
import {
|
||||||
useValue,
|
useValue,
|
||||||
useTextInput,
|
useTextInput,
|
||||||
useBoolInput,
|
useBoolInput,
|
||||||
} from "../../../lib/form";
|
} from "../../../../lib/form";
|
||||||
|
|
||||||
import { Checkbox, Select, TextInput } from "../../../components/form/inputs";
|
import { Checkbox, Select, TextInput } from "../../../../components/form/inputs";
|
||||||
import { AdminAccount } from "../../../lib/types/account";
|
import { AdminAccount } from "../../../../lib/types/account";
|
||||||
|
|
||||||
export interface HandleSignupProps {
|
export interface HandleSignupProps {
|
||||||
account: AdminAccount,
|
account: AdminAccount,
|
||||||
accountsBaseUrl: string,
|
backLocation: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HandleSignup({account, accountsBaseUrl}: HandleSignupProps) {
|
export function HandleSignup({account, backLocation}: HandleSignupProps) {
|
||||||
const form = {
|
const form = {
|
||||||
id: useValue("id", account.id),
|
id: useValue("id", account.id),
|
||||||
approveOrReject: useTextInput("approve_or_reject", { defaultValue: "approve" }),
|
approveOrReject: useTextInput("approve_or_reject", { defaultValue: "approve" }),
|
||||||
|
@ -67,7 +67,7 @@ export function HandleSignup({account, accountsBaseUrl}: HandleSignupProps) {
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
// "reject" successful,
|
// "reject" successful,
|
||||||
// redirect to accounts page.
|
// redirect to accounts page.
|
||||||
setLocation(accountsBaseUrl);
|
setLocation(backLocation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
|
@ -18,51 +18,39 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useRoute, Redirect } from "wouter";
|
|
||||||
|
|
||||||
import { useGetAccountQuery } from "../../../lib/query";
|
import { useGetAccountQuery } from "../../../../lib/query";
|
||||||
|
|
||||||
import FormWithData from "../../../lib/form/form-with-data";
|
import FormWithData from "../../../../lib/form/form-with-data";
|
||||||
|
|
||||||
import { useBaseUrl } from "../../../lib/navigation/util";
|
import FakeProfile from "../../../../components/fake-profile";
|
||||||
import FakeProfile from "../../../components/fake-profile";
|
|
||||||
|
|
||||||
import { AdminAccount } from "../../../lib/types/account";
|
import { AdminAccount } from "../../../../lib/types/account";
|
||||||
import { HandleSignup } from "./handlesignup";
|
import { HandleSignup } from "./handlesignup";
|
||||||
import { AccountActions } from "./actions";
|
import { AccountActions } from "./actions";
|
||||||
import BackButton from "../../../components/back-button";
|
import { useParams } from "wouter";
|
||||||
|
|
||||||
export default function AccountDetail() {
|
export default function AccountDetail() {
|
||||||
// /settings/admin/accounts
|
const params: { accountID: string } = useParams();
|
||||||
const accountsBaseUrl = useBaseUrl();
|
|
||||||
|
return (
|
||||||
let [_match, params] = useRoute(`${accountsBaseUrl}/:accountId`);
|
<div className="account-detail">
|
||||||
|
<h1>Account Details</h1>
|
||||||
if (params?.accountId == undefined) {
|
<FormWithData
|
||||||
return <Redirect to={accountsBaseUrl} />;
|
dataQuery={useGetAccountQuery}
|
||||||
} else {
|
queryArg={params.accountID}
|
||||||
return (
|
DataForm={AccountDetailForm}
|
||||||
<div className="account-detail">
|
/>
|
||||||
<h1 className="text-cutoff">
|
</div>
|
||||||
<BackButton to={accountsBaseUrl} /> Account Details
|
);
|
||||||
</h1>
|
|
||||||
<FormWithData
|
|
||||||
dataQuery={useGetAccountQuery}
|
|
||||||
queryArg={params.accountId}
|
|
||||||
DataForm={AccountDetailForm}
|
|
||||||
{...{accountsBaseUrl}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AccountDetailFormProps {
|
interface AccountDetailFormProps {
|
||||||
accountsBaseUrl: string,
|
backLocation: string,
|
||||||
data: AdminAccount,
|
data: AdminAccount,
|
||||||
}
|
}
|
||||||
|
|
||||||
function AccountDetailForm({ data: adminAcct, accountsBaseUrl }: AccountDetailFormProps) {
|
function AccountDetailForm({ data: adminAcct, backLocation }: AccountDetailFormProps) {
|
||||||
let yesOrNo = (b: boolean) => {
|
let yesOrNo = (b: boolean) => {
|
||||||
return b ? "yes" : "no";
|
return b ? "yes" : "no";
|
||||||
};
|
};
|
||||||
|
@ -169,7 +157,7 @@ function AccountDetailForm({ data: adminAcct, accountsBaseUrl }: AccountDetailFo
|
||||||
?
|
?
|
||||||
<HandleSignup
|
<HandleSignup
|
||||||
account={adminAcct}
|
account={adminAcct}
|
||||||
accountsBaseUrl={accountsBaseUrl}
|
backLocation={backLocation}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
<AccountActions account={adminAcct} />
|
<AccountActions account={adminAcct} />
|
|
@ -18,23 +18,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Switch, Route } from "wouter";
|
|
||||||
|
|
||||||
import AccountDetail from "./detail";
|
|
||||||
import { AccountSearchForm } from "./search";
|
import { AccountSearchForm } from "./search";
|
||||||
|
|
||||||
export default function Accounts({ baseUrl }) {
|
export default function AccountsOverview({ }) {
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route path={`${baseUrl}/:accountId`}>
|
|
||||||
<AccountDetail />
|
|
||||||
</Route>
|
|
||||||
<AccountOverview />
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AccountOverview({ }) {
|
|
||||||
return (
|
return (
|
||||||
<div className="accounts-view">
|
<div className="accounts-view">
|
||||||
<h1>Accounts Overview</h1>
|
<h1>Accounts Overview</h1>
|
|
@ -18,8 +18,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useSearchAccountsQuery } from "../../../lib/query";
|
import { useSearchAccountsQuery } from "../../../../lib/query";
|
||||||
import { AccountList } from "../../../components/account-list";
|
import { AccountList } from "../../../../components/account-list";
|
||||||
|
|
||||||
export default function AccountsPending() {
|
export default function AccountsPending() {
|
||||||
const searchRes = useSearchAccountsQuery({status: "pending"});
|
const searchRes = useSearchAccountsQuery({status: "pending"});
|
|
@ -19,17 +19,15 @@
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useLazySearchAccountsQuery } from "../../../lib/query";
|
import { useLazySearchAccountsQuery } from "../../../../lib/query";
|
||||||
import { useTextInput } from "../../../lib/form";
|
import { useTextInput } from "../../../../lib/form";
|
||||||
|
|
||||||
import { AccountList } from "../../../components/account-list";
|
import { AccountList } from "../../../../components/account-list";
|
||||||
import { SearchAccountParams } from "../../../lib/types/account";
|
import { SearchAccountParams } from "../../../../lib/types/account";
|
||||||
import { Select, TextInput } from "../../../components/form/inputs";
|
import { Select, TextInput } from "../../../../components/form/inputs";
|
||||||
import MutationButton from "../../../components/form/mutation-button";
|
import MutationButton from "../../../../components/form/mutation-button";
|
||||||
|
|
||||||
export function AccountSearchForm() {
|
export function AccountSearchForm() {
|
||||||
const [searchAcct, searchRes] = useLazySearchAccountsQuery();
|
|
||||||
|
|
||||||
const form = {
|
const form = {
|
||||||
origin: useTextInput("origin"),
|
origin: useTextInput("origin"),
|
||||||
status: useTextInput("status"),
|
status: useTextInput("status"),
|
||||||
|
@ -55,14 +53,20 @@ export function AccountSearchForm() {
|
||||||
// Remove any nulls.
|
// Remove any nulls.
|
||||||
return kv || [];
|
return kv || [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const params: SearchAccountParams = Object.fromEntries(entries);
|
const params: SearchAccountParams = Object.fromEntries(entries);
|
||||||
searchAcct(params);
|
searchAcct(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [ searchAcct, searchRes ] = useLazySearchAccountsQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form onSubmit={submitSearch}>
|
<form
|
||||||
|
onSubmit={submitSearch}
|
||||||
|
// Prevent password managers trying
|
||||||
|
// to fill in username/email fields.
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
field={form.username}
|
field={form.username}
|
||||||
label={"(Optional) username (without leading '@' symbol)"}
|
label={"(Optional) username (without leading '@' symbol)"}
|
||||||
|
@ -88,6 +92,8 @@ export function AccountSearchForm() {
|
||||||
field={form.email}
|
field={form.email}
|
||||||
label={"(Optional) email address (local accounts only)"}
|
label={"(Optional) email address (local accounts only)"}
|
||||||
placeholder={"someone@example.org"}
|
placeholder={"someone@example.org"}
|
||||||
|
// Get email validation for free.
|
||||||
|
{...{type: "email"}}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
field={form.ip}
|
field={form.ip}
|
|
@ -20,31 +20,35 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation, useParams, useSearch } from "wouter";
|
||||||
|
|
||||||
import { useTextInput, useBoolInput } from "../../lib/form";
|
import { useTextInput, useBoolInput } from "../../../lib/form";
|
||||||
|
|
||||||
import useFormSubmit from "../../lib/form/submit";
|
import useFormSubmit from "../../../lib/form/submit";
|
||||||
|
|
||||||
import { TextInput, Checkbox, TextArea } from "../../components/form/inputs";
|
import { TextInput, Checkbox, TextArea } from "../../../components/form/inputs";
|
||||||
|
|
||||||
import Loading from "../../components/loading";
|
import Loading from "../../../components/loading";
|
||||||
import BackButton from "../../components/back-button";
|
import BackButton from "../../../components/back-button";
|
||||||
import MutationButton from "../../components/form/mutation-button";
|
import MutationButton from "../../../components/form/mutation-button";
|
||||||
|
|
||||||
import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../lib/query/admin/domain-permissions/get";
|
import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../../lib/query/admin/domain-permissions/get";
|
||||||
import { useAddDomainAllowMutation, useAddDomainBlockMutation, useRemoveDomainAllowMutation, useRemoveDomainBlockMutation } from "../../lib/query/admin/domain-permissions/update";
|
import { useAddDomainAllowMutation, useAddDomainBlockMutation, useRemoveDomainAllowMutation, useRemoveDomainBlockMutation } from "../../../lib/query/admin/domain-permissions/update";
|
||||||
import { DomainPerm, PermType } from "../../lib/types/domain-permission";
|
import { DomainPerm, PermType } from "../../../lib/types/domain-permission";
|
||||||
import { NoArg } from "../../lib/types/query";
|
import { NoArg } from "../../../lib/types/query";
|
||||||
import { Error } from "../../components/error";
|
import { Error } from "../../../components/error";
|
||||||
|
import { useBaseUrl } from "../../../lib/navigation/util";
|
||||||
|
|
||||||
export interface DomainPermDetailProps {
|
export default function DomainPermDetail() {
|
||||||
baseUrl: string;
|
const baseUrl = useBaseUrl();
|
||||||
permType: PermType;
|
|
||||||
domain: string;
|
// Parse perm type from routing params.
|
||||||
}
|
let params = useParams();
|
||||||
|
if (params.permType !== "blocks" && params.permType !== "allows") {
|
||||||
export default function DomainPermDetail({ baseUrl, permType, domain }: DomainPermDetailProps) {
|
throw "unrecognized perm type " + params.permType;
|
||||||
|
}
|
||||||
|
const permType = params.permType.slice(0, -1) as PermType;
|
||||||
|
|
||||||
const { data: domainBlocks = {}, isLoading: isLoadingDomainBlocks } = useDomainBlocksQuery(NoArg, { skip: permType !== "block" });
|
const { data: domainBlocks = {}, isLoading: isLoadingDomainBlocks } = useDomainBlocksQuery(NoArg, { skip: permType !== "block" });
|
||||||
const { data: domainAllows = {}, isLoading: isLoadingDomainAllows } = useDomainAllowsQuery(NoArg, { skip: permType !== "allow" });
|
const { data: domainAllows = {}, isLoading: isLoadingDomainAllows } = useDomainAllowsQuery(NoArg, { skip: permType !== "allow" });
|
||||||
|
|
||||||
|
@ -60,13 +64,19 @@ export default function DomainPermDetail({ baseUrl, permType, domain }: DomainPe
|
||||||
throw "perm type unknown";
|
throw "perm type unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (domain == "view") {
|
// Parse domain from routing params.
|
||||||
// Retrieve domain from form field submission.
|
let domain = params.domain ?? "unknown";
|
||||||
domain = (new URL(document.location.toString())).searchParams.get("domain")?? "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (domain == "unknown") {
|
const search = useSearch();
|
||||||
throw "unknown domain";
|
if (domain === "view") {
|
||||||
|
// Retrieve domain from form field submission.
|
||||||
|
const searchParams = new URLSearchParams(search);
|
||||||
|
const searchDomain = searchParams.get("domain");
|
||||||
|
if (!searchDomain) {
|
||||||
|
throw "empty view domain";
|
||||||
|
}
|
||||||
|
|
||||||
|
domain = searchDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize / decode domain (it may be URL-encoded).
|
// Normalize / decode domain (it may be URL-encoded).
|
||||||
|
@ -98,13 +108,12 @@ export default function DomainPermDetail({ baseUrl, permType, domain }: DomainPe
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-cutoff"><BackButton to={baseUrl} /> Domain {permType} for: <span title={domain}>{domain}</span></h1>
|
<h1 className="text-cutoff"><BackButton to={`~${baseUrl}/${permType}s`}/> Domain {permType} for: <span title={domain}>{domain}</span></h1>
|
||||||
{infoContent}
|
{infoContent}
|
||||||
<DomainPermForm
|
<DomainPermForm
|
||||||
defaultDomain={domain}
|
defaultDomain={domain}
|
||||||
perm={existingPerm}
|
perm={existingPerm}
|
||||||
permType={permType}
|
permType={permType}
|
||||||
baseUrl={baseUrl}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -114,10 +123,9 @@ interface DomainPermFormProps {
|
||||||
defaultDomain: string;
|
defaultDomain: string;
|
||||||
perm?: DomainPerm;
|
perm?: DomainPerm;
|
||||||
permType: PermType;
|
permType: PermType;
|
||||||
baseUrl: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DomainPermForm({ defaultDomain, perm, permType, baseUrl }: DomainPermFormProps) {
|
function DomainPermForm({ defaultDomain, perm, permType }: DomainPermFormProps) {
|
||||||
const isExistingPerm = perm !== undefined;
|
const isExistingPerm = perm !== undefined;
|
||||||
const disabledForm = isExistingPerm
|
const disabledForm = isExistingPerm
|
||||||
? {
|
? {
|
||||||
|
@ -186,7 +194,7 @@ function DomainPermForm({ defaultDomain, perm, permType, baseUrl }: DomainPermFo
|
||||||
// but if domain input changes, that doesn't match anymore
|
// but if domain input changes, that doesn't match anymore
|
||||||
// and causes issues later on so, before submitting the form,
|
// and causes issues later on so, before submitting the form,
|
||||||
// silently change url, and THEN submit.
|
// silently change url, and THEN submit.
|
||||||
let correctUrl = `${baseUrl}/${form.domain.value}`;
|
let correctUrl = `/${permType}s/${form.domain.value}`;
|
||||||
if (location != correctUrl) {
|
if (location != correctUrl) {
|
||||||
setLocation(correctUrl);
|
setLocation(correctUrl);
|
||||||
}
|
}
|
|
@ -17,11 +17,11 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const React = require("react");
|
import React from "react";
|
||||||
|
|
||||||
module.exports = function ExportFormatTable() {
|
export default function ExportFormatTable() {
|
||||||
return (
|
return (
|
||||||
<div className="export-format-table-wrapper without-border">
|
<div className="export-format-table-wrapper">
|
||||||
<table className="export-format-table">
|
<table className="export-format-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -44,7 +44,7 @@ module.exports = function ExportFormatTable() {
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
function Format({ name, info }) {
|
function Format({ name, info }) {
|
||||||
return (
|
return (
|
|
@ -21,18 +21,18 @@ import React from "react";
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { useExportDomainListMutation } from "../../lib/query/admin/domain-permissions/export";
|
import { useExportDomainListMutation } from "../../../lib/query/admin/domain-permissions/export";
|
||||||
import useFormSubmit from "../../lib/form/submit";
|
import useFormSubmit from "../../../lib/form/submit";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
TextArea,
|
TextArea,
|
||||||
Select,
|
Select,
|
||||||
} from "../../components/form/inputs";
|
} from "../../../components/form/inputs";
|
||||||
|
|
||||||
import MutationButton from "../../components/form/mutation-button";
|
import MutationButton from "../../../components/form/mutation-button";
|
||||||
|
|
||||||
import { Error } from "../../components/error";
|
import { Error } from "../../../components/error";
|
||||||
import ExportFormatTable from "./export-format-table";
|
import ExportFormatTable from "./export-format-table";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -40,7 +40,7 @@ import type {
|
||||||
FormSubmitResult,
|
FormSubmitResult,
|
||||||
RadioFormInputHook,
|
RadioFormInputHook,
|
||||||
TextFormInputHook,
|
TextFormInputHook,
|
||||||
} from "../../lib/form/types";
|
} from "../../../lib/form/types";
|
||||||
|
|
||||||
export interface ImportExportFormProps {
|
export interface ImportExportFormProps {
|
||||||
form: {
|
form: {
|
|
@ -20,20 +20,19 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Switch, Route, Redirect, useLocation } from "wouter";
|
import { Switch, Route, Redirect, useLocation } from "wouter";
|
||||||
|
import { useProcessDomainPermissionsMutation } from "../../../lib/query/admin/domain-permissions/process";
|
||||||
import { useProcessDomainPermissionsMutation } from "../../lib/query/admin/domain-permissions/process";
|
import { useTextInput, useRadioInput } from "../../../lib/form";
|
||||||
|
import useFormSubmit from "../../../lib/form/submit";
|
||||||
import { useTextInput, useRadioInput } from "../../lib/form";
|
|
||||||
|
|
||||||
import useFormSubmit from "../../lib/form/submit";
|
|
||||||
|
|
||||||
import { ProcessImport } from "./process";
|
import { ProcessImport } from "./process";
|
||||||
import ImportExportForm from "./form";
|
import ImportExportForm from "./form";
|
||||||
|
|
||||||
export default function ImportExport({ baseUrl }) {
|
export default function ImportExport() {
|
||||||
const form = {
|
const form = {
|
||||||
domains: useTextInput("domains"),
|
domains: useTextInput("domains"),
|
||||||
exportType: useTextInput("exportType", { defaultValue: "plain", dontReset: true }),
|
exportType: useTextInput("exportType", {
|
||||||
|
defaultValue: "plain",
|
||||||
|
dontReset: true,
|
||||||
|
}),
|
||||||
permType: useRadioInput("permType", {
|
permType: useRadioInput("permType", {
|
||||||
options: {
|
options: {
|
||||||
block: "Domain blocks",
|
block: "Domain blocks",
|
||||||
|
@ -43,12 +42,11 @@ export default function ImportExport({ baseUrl }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const [submitParse, parseResult] = useFormSubmit(form, useProcessDomainPermissionsMutation(), { changedOnly: false });
|
const [submitParse, parseResult] = useFormSubmit(form, useProcessDomainPermissionsMutation(), { changedOnly: false });
|
||||||
|
|
||||||
const [_location, setLocation] = useLocation();
|
const [_location, setLocation] = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={`${baseUrl}/process`}>
|
<Route path={"/process"}>
|
||||||
{
|
{
|
||||||
parseResult.isSuccess
|
parseResult.isSuccess
|
||||||
? (
|
? (
|
||||||
|
@ -58,7 +56,7 @@ export default function ImportExport({ baseUrl }) {
|
||||||
className="button"
|
className="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
parseResult.reset();
|
parseResult.reset();
|
||||||
setLocation(baseUrl);
|
setLocation("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
< back
|
< back
|
||||||
|
@ -71,13 +69,13 @@ export default function ImportExport({ baseUrl }) {
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
: <Redirect to={baseUrl} />
|
: <Redirect to={""} />
|
||||||
}
|
}
|
||||||
</Route>
|
</Route>
|
||||||
<Route>
|
<Route>
|
||||||
{
|
{
|
||||||
parseResult.isSuccess
|
parseResult.isSuccess
|
||||||
? <Redirect to={`${baseUrl}/process`} />
|
? <Redirect to={"/process"} />
|
||||||
: <ImportExportForm
|
: <ImportExportForm
|
||||||
form={form}
|
form={form}
|
||||||
submitParse={submitParse}
|
submitParse={submitParse}
|
|
@ -20,29 +20,25 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Link, useLocation } from "wouter";
|
import { Link, useLocation, useParams } from "wouter";
|
||||||
import { matchSorter } from "match-sorter";
|
import { matchSorter } from "match-sorter";
|
||||||
|
|
||||||
import { useTextInput } from "../../lib/form";
|
import { useTextInput } from "../../../lib/form";
|
||||||
|
|
||||||
import { TextInput } from "../../components/form/inputs";
|
import { TextInput } from "../../../components/form/inputs";
|
||||||
|
|
||||||
import Loading from "../../components/loading";
|
import Loading from "../../../components/loading";
|
||||||
import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../lib/query/admin/domain-permissions/get";
|
import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../../lib/query/admin/domain-permissions/get";
|
||||||
import type { MappedDomainPerms, PermType } from "../../lib/types/domain-permission";
|
import type { MappedDomainPerms, PermType } from "../../../lib/types/domain-permission";
|
||||||
import { NoArg } from "../../lib/types/query";
|
import { NoArg } from "../../../lib/types/query";
|
||||||
|
|
||||||
export interface DomainPermissionsOverviewProps {
|
export default function DomainPermissionsOverview() {
|
||||||
// Params injected by
|
// Parse perm type from routing params.
|
||||||
// the wouter router.
|
let params = useParams();
|
||||||
permType: PermType;
|
if (params.permType !== "blocks" && params.permType !== "allows") {
|
||||||
baseUrl: string,
|
throw "unrecognized perm type " + params.permType;
|
||||||
}
|
|
||||||
|
|
||||||
export default function DomainPermissionsOverview({ permType, baseUrl }: DomainPermissionsOverviewProps) {
|
|
||||||
if (permType !== "block" && permType !== "allow") {
|
|
||||||
throw "unrecognized perm type " + permType;
|
|
||||||
}
|
}
|
||||||
|
const permType = params.permType.slice(0, -1) as PermType;
|
||||||
|
|
||||||
// Uppercase first letter of given permType.
|
// Uppercase first letter of given permType.
|
||||||
const permTypeUpper = useMemo(() => {
|
const permTypeUpper = useMemo(() => {
|
||||||
|
@ -69,30 +65,28 @@ export default function DomainPermissionsOverview({ permType, baseUrl }: DomainP
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<h1>Domain {permTypeUpper}s</h1>
|
<h1>Domain {permTypeUpper}s</h1>
|
||||||
{ permType == "block" ? <BlockHelperText/> : <AllowHelperText/> }
|
{ permType == "block" ? <BlockHelperText/> : <AllowHelperText/> }
|
||||||
<DomainPermsList
|
<DomainPermsList
|
||||||
data={data}
|
data={data}
|
||||||
baseUrl={baseUrl}
|
|
||||||
permType={permType}
|
permType={permType}
|
||||||
permTypeUpper={permTypeUpper}
|
permTypeUpper={permTypeUpper}
|
||||||
/>
|
/>
|
||||||
<Link to="/settings/admin/domain-permissions/import-export">
|
<Link to="/settings/admin/domain-permissions/import-export">
|
||||||
<a>Or use the bulk import/export interface</a>
|
Or use the bulk import/export interface
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DomainPermsListProps {
|
interface DomainPermsListProps {
|
||||||
data: MappedDomainPerms;
|
data: MappedDomainPerms;
|
||||||
baseUrl: string;
|
|
||||||
permType: PermType;
|
permType: PermType;
|
||||||
permTypeUpper: string;
|
permTypeUpper: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DomainPermsList({ data, baseUrl, permType, permTypeUpper }: DomainPermsListProps) {
|
function DomainPermsList({ data, permType, permTypeUpper }: DomainPermsListProps) {
|
||||||
// Format perms into a list.
|
// Format perms into a list.
|
||||||
const perms = useMemo(() => {
|
const perms = useMemo(() => {
|
||||||
return Object.values(data);
|
return Object.values(data);
|
||||||
|
@ -103,7 +97,7 @@ function DomainPermsList({ data, baseUrl, permType, permTypeUpper }: DomainPerms
|
||||||
|
|
||||||
function filterFormSubmit(e) {
|
function filterFormSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLocation(`${baseUrl}/${filter}`);
|
setLocation(`/${filter}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filter = filterField.value ?? "";
|
const filter = filterField.value ?? "";
|
||||||
|
@ -120,11 +114,13 @@ function DomainPermsList({ data, baseUrl, permType, permTypeUpper }: DomainPerms
|
||||||
|
|
||||||
const entries = filteredPerms.map((entry) => {
|
const entries = filteredPerms.map((entry) => {
|
||||||
return (
|
return (
|
||||||
<Link key={entry.domain} to={`${baseUrl}/${entry.domain}`}>
|
<Link
|
||||||
<a className="entry nounderline">
|
className="entry nounderline"
|
||||||
<span id="domain">{entry.domain}</span>
|
key={entry.domain}
|
||||||
<span id="date">{new Date(entry.created_at ?? "").toLocaleString()}</span>
|
to={`/${permType}s/${entry.domain}`}
|
||||||
</a>
|
>
|
||||||
|
<span id="domain">{entry.domain}</span>
|
||||||
|
<span id="date">{new Date(entry.created_at ?? "").toLocaleString()}</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -137,8 +133,11 @@ function DomainPermsList({ data, baseUrl, permType, permTypeUpper }: DomainPerms
|
||||||
placeholder="example.org"
|
placeholder="example.org"
|
||||||
label={`Search or add domain ${permType}`}
|
label={`Search or add domain ${permType}`}
|
||||||
/>
|
/>
|
||||||
<Link to={`${baseUrl}/${filter}`}>
|
<Link
|
||||||
<a className="button">{permTypeUpper} {filter}</a>
|
className="button"
|
||||||
|
to={`/${permType}s/${filter}`}
|
||||||
|
>
|
||||||
|
{permTypeUpper} {filter}
|
||||||
</Link>
|
</Link>
|
||||||
</form>
|
</form>
|
||||||
<div>
|
<div>
|
|
@ -21,14 +21,14 @@ import React from "react";
|
||||||
|
|
||||||
import { memo, useMemo, useCallback, useEffect } from "react";
|
import { memo, useMemo, useCallback, useEffect } from "react";
|
||||||
|
|
||||||
import { isValidDomainPermission, hasBetterScope } from "../../lib/util/domain-permission";
|
import { isValidDomainPermission, hasBetterScope } from "../../../lib/util/domain-permission";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useTextInput,
|
useTextInput,
|
||||||
useBoolInput,
|
useBoolInput,
|
||||||
useRadioInput,
|
useRadioInput,
|
||||||
useCheckListInput,
|
useCheckListInput,
|
||||||
} from "../../lib/form";
|
} from "../../../lib/form";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
@ -36,22 +36,22 @@ import {
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
TextInput,
|
TextInput,
|
||||||
} from "../../components/form/inputs";
|
} from "../../../components/form/inputs";
|
||||||
|
|
||||||
import useFormSubmit from "../../lib/form/submit";
|
import useFormSubmit from "../../../lib/form/submit";
|
||||||
|
|
||||||
import CheckList from "../../components/check-list";
|
import CheckList from "../../../components/check-list";
|
||||||
import MutationButton from "../../components/form/mutation-button";
|
import MutationButton from "../../../components/form/mutation-button";
|
||||||
import FormWithData from "../../lib/form/form-with-data";
|
import FormWithData from "../../../lib/form/form-with-data";
|
||||||
|
|
||||||
import { useImportDomainPermsMutation } from "../../lib/query/admin/domain-permissions/import";
|
import { useImportDomainPermsMutation } from "../../../lib/query/admin/domain-permissions/import";
|
||||||
import {
|
import {
|
||||||
useDomainAllowsQuery,
|
useDomainAllowsQuery,
|
||||||
useDomainBlocksQuery
|
useDomainBlocksQuery
|
||||||
} from "../../lib/query/admin/domain-permissions/get";
|
} from "../../../lib/query/admin/domain-permissions/get";
|
||||||
|
|
||||||
import type { DomainPerm, MappedDomainPerms } from "../../lib/types/domain-permission";
|
import type { DomainPerm, MappedDomainPerms } from "../../../lib/types/domain-permission";
|
||||||
import type { ChecklistInputHook, RadioFormInputHook } from "../../lib/form/types";
|
import type { ChecklistInputHook, RadioFormInputHook } from "../../../lib/form/types";
|
||||||
|
|
||||||
export interface ProcessImportProps {
|
export interface ProcessImportProps {
|
||||||
list: DomainPerm[],
|
list: DomainPerm[],
|
||||||
|
@ -61,16 +61,14 @@ export interface ProcessImportProps {
|
||||||
export const ProcessImport = memo(
|
export const ProcessImport = memo(
|
||||||
function ProcessImport({ list, permType }: ProcessImportProps) {
|
function ProcessImport({ list, permType }: ProcessImportProps) {
|
||||||
return (
|
return (
|
||||||
<div className="without-border">
|
<FormWithData
|
||||||
<FormWithData
|
dataQuery={permType.value == "allow"
|
||||||
dataQuery={permType.value == "allow"
|
? useDomainAllowsQuery
|
||||||
? useDomainAllowsQuery
|
: useDomainBlocksQuery
|
||||||
: useDomainBlocksQuery
|
}
|
||||||
}
|
DataForm={ImportList}
|
||||||
DataForm={ImportList}
|
{...{ list, permType }}
|
||||||
{...{ list, permType }}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -399,4 +397,4 @@ function DomainEntryIcon({ alreadyExists, suggestion, permTypeString }: DomainEn
|
||||||
<span className="sr-only">{text}</span>
|
<span className="sr-only">{text}</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -18,40 +18,31 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useRoute, Redirect } from "wouter";
|
import { useParams } from "wouter";
|
||||||
|
import FormWithData from "../../../lib/form/form-with-data";
|
||||||
import FormWithData from "../../lib/form/form-with-data";
|
import BackButton from "../../../components/back-button";
|
||||||
import BackButton from "../../components/back-button";
|
import { useValue, useTextInput } from "../../../lib/form";
|
||||||
|
import useFormSubmit from "../../../lib/form/submit";
|
||||||
import { useValue, useTextInput } from "../../lib/form";
|
import { TextArea } from "../../../components/form/inputs";
|
||||||
import useFormSubmit from "../../lib/form/submit";
|
import MutationButton from "../../../components/form/mutation-button";
|
||||||
|
|
||||||
import { TextArea } from "../../components/form/inputs";
|
|
||||||
|
|
||||||
import MutationButton from "../../components/form/mutation-button";
|
|
||||||
import Username from "./username";
|
import Username from "./username";
|
||||||
import { useBaseUrl } from "../../lib/navigation/util";
|
import { useGetReportQuery, useResolveReportMutation } from "../../../lib/query/admin/reports";
|
||||||
import { useGetReportQuery, useResolveReportMutation } from "../../lib/query/admin/reports";
|
import { useBaseUrl } from "../../../lib/navigation/util";
|
||||||
|
|
||||||
export default function ReportDetail({ }) {
|
export default function ReportDetail({ }) {
|
||||||
const baseUrl = useBaseUrl();
|
const baseUrl = useBaseUrl();
|
||||||
let [_match, params] = useRoute(`${baseUrl}/:reportId`);
|
const params = useParams();
|
||||||
if (params?.reportId == undefined) {
|
|
||||||
return <Redirect to={baseUrl} />;
|
return (
|
||||||
} else {
|
<div className="reports">
|
||||||
return (
|
<h1><BackButton to={`~${baseUrl}`}/> Report Details</h1>
|
||||||
<div className="report-detail">
|
<FormWithData
|
||||||
<h1>
|
dataQuery={useGetReportQuery}
|
||||||
<BackButton to={baseUrl} /> Report Details
|
queryArg={params.reportId}
|
||||||
</h1>
|
DataForm={ReportDetailForm}
|
||||||
<FormWithData
|
/>
|
||||||
dataQuery={useGetReportQuery}
|
</div>
|
||||||
queryArg={params.reportId}
|
);
|
||||||
DataForm={ReportDetailForm}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReportDetailForm({ data: report }) {
|
function ReportDetailForm({ data: report }) {
|
|
@ -18,57 +18,50 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link, Switch, Route } from "wouter";
|
import { Link } from "wouter";
|
||||||
|
|
||||||
import FormWithData from "../../lib/form/form-with-data";
|
import FormWithData from "../../../lib/form/form-with-data";
|
||||||
|
|
||||||
import ReportDetail from "./detail";
|
|
||||||
import Username from "./username";
|
import Username from "./username";
|
||||||
import { useBaseUrl } from "../../lib/navigation/util";
|
import { useListReportsQuery } from "../../../lib/query/admin/reports";
|
||||||
import { useListReportsQuery } from "../../lib/query/admin/reports";
|
|
||||||
|
|
||||||
export default function Reports({ baseUrl }) {
|
export function ReportOverview({ }) {
|
||||||
return (
|
return (
|
||||||
<div className="reports">
|
<FormWithData
|
||||||
<Switch>
|
dataQuery={useListReportsQuery}
|
||||||
<Route path={`${baseUrl}/:reportId`}>
|
DataForm={ReportsList}
|
||||||
<ReportDetail />
|
/>
|
||||||
</Route>
|
|
||||||
<ReportOverview />
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ReportOverview({ }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1>Reports</h1>
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
Here you can view and resolve reports made to your instance, originating from local and remote users.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<FormWithData
|
|
||||||
dataQuery={useListReportsQuery}
|
|
||||||
DataForm={ReportsList}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReportsList({ data: reports }) {
|
function ReportsList({ data: reports }) {
|
||||||
return (
|
return (
|
||||||
<div className="list">
|
<div className="reports">
|
||||||
{reports.map((report) => (
|
<div className="form-section-docs">
|
||||||
<ReportEntry key={report.id} report={report} />
|
<h1>Reports</h1>
|
||||||
))}
|
<p>
|
||||||
|
Here you can view and resolve reports made to your
|
||||||
|
instance, originating from local and remote users.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="https://docs.gotosocial.org/en/latest/admin/settings/#reports"
|
||||||
|
target="_blank"
|
||||||
|
className="docslink"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Learn more about this (opens in a new tab)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="list">
|
||||||
|
{reports.map((report) => (
|
||||||
|
<ReportEntry key={report.id} report={report} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReportEntry({ report }) {
|
function ReportEntry({ report }) {
|
||||||
const baseUrl = useBaseUrl();
|
|
||||||
const from = report.account;
|
const from = report.account;
|
||||||
const target = report.target_account;
|
const target = report.target_account;
|
||||||
|
|
||||||
|
@ -77,8 +70,11 @@ function ReportEntry({ report }) {
|
||||||
: report.comment;
|
: report.comment;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`${baseUrl}/${report.id}`}>
|
<Link
|
||||||
<a className={`report entry${report.action_taken ? " resolved" : ""}`}>
|
to={`/${report.id}`}
|
||||||
|
className="nounderline"
|
||||||
|
>
|
||||||
|
<div className={`report entry${report.action_taken ? " resolved" : ""}`}>
|
||||||
<div className="byline">
|
<div className="byline">
|
||||||
<div className="usernames">
|
<div className="usernames">
|
||||||
<Username user={from} link={false} /> reported <Username user={target} link={false} />
|
<Username user={from} link={false} /> reported <Username user={target} link={false} />
|
||||||
|
@ -97,7 +93,7 @@ function ReportEntry({ report }) {
|
||||||
: <i className="no-comment">none provided</i>
|
: <i className="no-comment">none provided</i>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
201
web/source/settings/views/moderation/routes.tsx
Normal file
201
web/source/settings/views/moderation/routes.tsx
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
/*
|
||||||
|
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 { MenuItem } from "../../lib/navigation/menu";
|
||||||
|
import React from "react";
|
||||||
|
import { BaseUrlContext, useBaseUrl } from "../../lib/navigation/util";
|
||||||
|
import { Redirect, Route, Router, Switch } from "wouter";
|
||||||
|
import AccountsOverview from "./accounts";
|
||||||
|
import AccountsPending from "./accounts/pending";
|
||||||
|
import AccountDetail from "./accounts/detail";
|
||||||
|
import { ReportOverview } from "./reports/overview";
|
||||||
|
import DomainPermissionsOverview from "./domain-permissions/overview";
|
||||||
|
import DomainPermDetail from "./domain-permissions/detail";
|
||||||
|
import ImportExport from "./domain-permissions/import-export";
|
||||||
|
import ReportDetail from "./reports/detail";
|
||||||
|
|
||||||
|
/*
|
||||||
|
EXPORTED COMPONENTS
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moderation menu. Reports, accounts,
|
||||||
|
* domain permissions import + export.
|
||||||
|
*/
|
||||||
|
export function ModerationMenu() {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
name="Moderation"
|
||||||
|
itemUrl="moderation"
|
||||||
|
defaultChild="reports"
|
||||||
|
permissions={["moderator"]}
|
||||||
|
>
|
||||||
|
<ModerationReportsMenu />
|
||||||
|
<ModerationAccountsMenu />
|
||||||
|
<ModerationDomainPermsMenu />
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moderation router. Reports, accounts,
|
||||||
|
* domain permissions import + export.
|
||||||
|
*/
|
||||||
|
export function ModerationRouter() {
|
||||||
|
const parentUrl = useBaseUrl();
|
||||||
|
const thisBase = "/moderation";
|
||||||
|
const absBase = parentUrl + thisBase;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseUrlContext.Provider value={absBase}>
|
||||||
|
<Router base={thisBase}>
|
||||||
|
<ModerationReportsRouter />
|
||||||
|
<ModerationAccountsRouter />
|
||||||
|
<ModerationDomainPermsRouter />
|
||||||
|
</Router>
|
||||||
|
</BaseUrlContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
INTERNAL COMPONENTS
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
MENUS
|
||||||
|
*/
|
||||||
|
|
||||||
|
function ModerationReportsMenu() {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
name="Reports"
|
||||||
|
itemUrl="reports"
|
||||||
|
icon="fa-flag"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModerationAccountsMenu() {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
name="Accounts"
|
||||||
|
itemUrl="accounts"
|
||||||
|
defaultChild="overview"
|
||||||
|
icon="fa-users"
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
name="Overview"
|
||||||
|
itemUrl="overview"
|
||||||
|
icon="fa-list"
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name="Pending"
|
||||||
|
itemUrl="pending"
|
||||||
|
icon="fa-question"
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModerationDomainPermsMenu() {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
name="Domain Permissions"
|
||||||
|
itemUrl="domain-permissions"
|
||||||
|
defaultChild="blocks"
|
||||||
|
icon="fa-hubzilla"
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
name="Blocks"
|
||||||
|
itemUrl="blocks"
|
||||||
|
icon="fa-close"
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name="Allows"
|
||||||
|
itemUrl="allows"
|
||||||
|
icon="fa-check"
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name="Import/Export"
|
||||||
|
itemUrl="import-export"
|
||||||
|
icon="fa-floppy-o"
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ROUTERS
|
||||||
|
*/
|
||||||
|
|
||||||
|
function ModerationReportsRouter() {
|
||||||
|
const parentUrl = useBaseUrl();
|
||||||
|
const thisBase = "/reports";
|
||||||
|
const absBase = parentUrl + thisBase;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseUrlContext.Provider value={absBase}>
|
||||||
|
<Router base={thisBase}>
|
||||||
|
<Switch>
|
||||||
|
<Route path={"/:reportId"} component={ReportDetail} />
|
||||||
|
<Route component={ReportOverview}/>
|
||||||
|
</Switch>
|
||||||
|
</Router>
|
||||||
|
</BaseUrlContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModerationAccountsRouter() {
|
||||||
|
const parentUrl = useBaseUrl();
|
||||||
|
const thisBase = "/accounts";
|
||||||
|
const absBase = parentUrl + thisBase;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseUrlContext.Provider value={absBase}>
|
||||||
|
<Router base={thisBase}>
|
||||||
|
<Switch>
|
||||||
|
<Route path="/overview" component={AccountsOverview}/>
|
||||||
|
<Route path="/pending" component={AccountsPending}/>
|
||||||
|
<Route path="/:accountID" component={AccountDetail}/>
|
||||||
|
<Route><Redirect to="/overview"/></Route>
|
||||||
|
</Switch>
|
||||||
|
</Router>
|
||||||
|
</BaseUrlContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModerationDomainPermsRouter() {
|
||||||
|
const parentUrl = useBaseUrl();
|
||||||
|
const thisBase = "/domain-permissions";
|
||||||
|
const absBase = parentUrl + thisBase;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseUrlContext.Provider value={absBase}>
|
||||||
|
<Router base={thisBase}>
|
||||||
|
<Switch>
|
||||||
|
<Route path="/import-export" component={ImportExport} />
|
||||||
|
<Route path="/process" component={ImportExport} />
|
||||||
|
<Route path="/:permType/:domain" component={DomainPermDetail} />
|
||||||
|
<Route path="/:permType" component={DomainPermissionsOverview} />
|
||||||
|
<Route><Redirect to="/blocks"/></Route>
|
||||||
|
</Switch>
|
||||||
|
</Router>
|
||||||
|
</BaseUrlContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
|
@ -19,16 +19,16 @@
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import FormWithData from "../lib/form/form-with-data";
|
import FormWithData from "../../lib/form/form-with-data";
|
||||||
|
|
||||||
import { useVerifyCredentialsQuery } from "../lib/query/oauth";
|
import { useVerifyCredentialsQuery } from "../../lib/query/oauth";
|
||||||
import { useArrayInput, useTextInput } from "../lib/form";
|
import { useArrayInput, useTextInput } from "../../lib/form";
|
||||||
import { TextInput } from "../components/form/inputs";
|
import { TextInput } from "../../components/form/inputs";
|
||||||
import useFormSubmit from "../lib/form/submit";
|
import useFormSubmit from "../../lib/form/submit";
|
||||||
import MutationButton from "../components/form/mutation-button";
|
import MutationButton from "../../components/form/mutation-button";
|
||||||
import { useAliasAccountMutation, useMoveAccountMutation } from "../lib/query/user";
|
import { useAliasAccountMutation, useMoveAccountMutation } from "../../lib/query/user";
|
||||||
import { FormContext, useWithFormContext } from "../lib/form/context";
|
import { FormContext, useWithFormContext } from "../../lib/form/context";
|
||||||
import { store } from "../redux/store";
|
import { store } from "../../redux/store";
|
||||||
|
|
||||||
export default function UserMigration() {
|
export default function UserMigration() {
|
||||||
return (
|
return (
|
||||||
|
@ -81,7 +81,7 @@ function AliasForm({ data: profile }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="user-migration-alias" onSubmit={submitForm}>
|
<form className="user-migration-alias" onSubmit={submitForm}>
|
||||||
<div className="form-section-docs without-border">
|
<div className="form-section-docs">
|
||||||
<h3>Alias Account</h3>
|
<h3>Alias Account</h3>
|
||||||
<a
|
<a
|
||||||
href="https://docs.gotosocial.org/en/latest/user_guide/settings/#alias-account"
|
href="https://docs.gotosocial.org/en/latest/user_guide/settings/#alias-account"
|
||||||
|
@ -157,37 +157,33 @@ function MoveForm({ data: profile }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="user-migration-move" onSubmit={submitForm}>
|
<form className="user-migration-move" onSubmit={submitForm}>
|
||||||
<div className="form-section-docs without-border">
|
<div className="form-section-docs">
|
||||||
<h3>Move Account</h3>
|
<h3>Move Account</h3>
|
||||||
<p>
|
<p>
|
||||||
<p>
|
|
||||||
For a move to be successful, you must have already set an alias from the
|
For a move to be successful, you must have already set an alias from the
|
||||||
target account back to the account you're moving from (ie., this account),
|
target account back to the account you're moving from (ie., this account),
|
||||||
using the settings panel of the instance on which the target account resides.
|
using the settings panel of the instance on which the target account resides.
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
To do this, provide the following details to the other instance:
|
To do this, provide the following details to the other instance:
|
||||||
</p>
|
|
||||||
<dl className="migration-details">
|
|
||||||
<div>
|
|
||||||
<dt>Account handle/username:</dt>
|
|
||||||
<dd>@{profile.acct}@{url.host}</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<dt>Account URI:</dt>
|
|
||||||
<dd>{urlStr}/users/{profile.username}</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
<br/>
|
|
||||||
<a
|
|
||||||
href="https://docs.gotosocial.org/en/latest/user_guide/settings/#move-account"
|
|
||||||
target="_blank"
|
|
||||||
className="docslink"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Learn more about moving your account (opens in a new tab)
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
<dl className="migration-details">
|
||||||
|
<div>
|
||||||
|
<dt>Account handle/username:</dt>
|
||||||
|
<dd>@{profile.acct}@{url.host}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Account URI:</dt>
|
||||||
|
<dd>{urlStr}/users/{profile.username}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
<br/>
|
||||||
|
<a
|
||||||
|
href="https://docs.gotosocial.org/en/latest/user_guide/settings/#move-account"
|
||||||
|
target="_blank"
|
||||||
|
className="docslink"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Learn more about moving your account (opens in a new tab)
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<TextInput
|
<TextInput
|
||||||
disabled={false}
|
disabled={false}
|
|
@ -25,10 +25,10 @@ import {
|
||||||
useBoolInput,
|
useBoolInput,
|
||||||
useFieldArrayInput,
|
useFieldArrayInput,
|
||||||
useRadioInput
|
useRadioInput
|
||||||
} from "../lib/form";
|
} from "../../lib/form";
|
||||||
|
|
||||||
import useFormSubmit from "../lib/form/submit";
|
import useFormSubmit from "../../lib/form/submit";
|
||||||
import { useWithFormContext, FormContext } from "../lib/form/context";
|
import { useWithFormContext, FormContext } from "../../lib/form/context";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TextInput,
|
TextInput,
|
||||||
|
@ -36,15 +36,15 @@ import {
|
||||||
FileInput,
|
FileInput,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
RadioGroup
|
RadioGroup
|
||||||
} from "../components/form/inputs";
|
} from "../../components/form/inputs";
|
||||||
|
|
||||||
import FormWithData from "../lib/form/form-with-data";
|
import FormWithData from "../../lib/form/form-with-data";
|
||||||
import FakeProfile from "../components/fake-profile";
|
import FakeProfile from "../../components/fake-profile";
|
||||||
import MutationButton from "../components/form/mutation-button";
|
import MutationButton from "../../components/form/mutation-button";
|
||||||
|
|
||||||
import { useAccountThemesQuery, useInstanceV1Query } from "../lib/query";
|
import { useAccountThemesQuery, useInstanceV1Query } from "../../lib/query";
|
||||||
import { useUpdateCredentialsMutation } from "../lib/query/user";
|
import { useUpdateCredentialsMutation } from "../../lib/query/user";
|
||||||
import { useVerifyCredentialsQuery } from "../lib/query/oauth";
|
import { useVerifyCredentialsQuery } from "../../lib/query/oauth";
|
||||||
|
|
||||||
export default function UserProfile() {
|
export default function UserProfile() {
|
||||||
return (
|
return (
|
||||||
|
@ -276,4 +276,4 @@ function Field({ index, data }) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
80
web/source/settings/views/user/routes.tsx
Normal file
80
web/source/settings/views/user/routes.tsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
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 { MenuItem } from "../../lib/navigation/menu";
|
||||||
|
import React from "react";
|
||||||
|
import { BaseUrlContext, useBaseUrl } from "../../lib/navigation/util";
|
||||||
|
import UserProfile from "./profile";
|
||||||
|
import UserSettings from "./settings";
|
||||||
|
import UserMigration from "./migration";
|
||||||
|
import { Redirect, Route, Router, Switch } from "wouter";
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Basic user menu. Profile + accounts
|
||||||
|
* settings, post settings, migration.
|
||||||
|
*/
|
||||||
|
export function UserMenu() {
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
name="User"
|
||||||
|
itemUrl="user"
|
||||||
|
defaultChild="profile"
|
||||||
|
>
|
||||||
|
{/* Profile */}
|
||||||
|
<MenuItem
|
||||||
|
name="Profile"
|
||||||
|
itemUrl="profile"
|
||||||
|
icon="fa-user"
|
||||||
|
/>
|
||||||
|
{/* Settings */}
|
||||||
|
<MenuItem
|
||||||
|
name="Settings"
|
||||||
|
itemUrl="settings"
|
||||||
|
icon="fa-cogs"
|
||||||
|
/>
|
||||||
|
{/* Migration */}
|
||||||
|
<MenuItem
|
||||||
|
name="Migration"
|
||||||
|
itemUrl="migration"
|
||||||
|
icon="fa-exchange"
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UserRouter() {
|
||||||
|
const baseUrl = useBaseUrl();
|
||||||
|
const thisBase = "/user";
|
||||||
|
const absBase = baseUrl + thisBase;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseUrlContext.Provider value={absBase}>
|
||||||
|
<Router base={thisBase}>
|
||||||
|
<Switch>
|
||||||
|
<Route path="/profile" component={UserProfile} />
|
||||||
|
<Route path="/settings" component={UserSettings} />
|
||||||
|
<Route path="/migration" component={UserMigration} />
|
||||||
|
{/* Fallback component */}
|
||||||
|
<Route><Redirect to="/profile" /></Route>
|
||||||
|
</Switch>
|
||||||
|
</Router>
|
||||||
|
</BaseUrlContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
|
@ -18,18 +18,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import query from "../../lib/query";
|
||||||
import query from "../lib/query";
|
import { useTextInput, useBoolInput } from "../../lib/form";
|
||||||
|
import useFormSubmit from "../../lib/form/submit";
|
||||||
import { useTextInput, useBoolInput } from "../lib/form";
|
import { Select, TextInput, Checkbox } from "../../components/form/inputs";
|
||||||
|
import FormWithData from "../../lib/form/form-with-data";
|
||||||
import useFormSubmit from "../lib/form/submit";
|
import Languages from "../../components/languages";
|
||||||
|
import MutationButton from "../../components/form/mutation-button";
|
||||||
import { Select, TextInput, Checkbox } from "../components/form/inputs";
|
|
||||||
|
|
||||||
import FormWithData from "../lib/form/form-with-data";
|
|
||||||
import Languages from "../components/languages";
|
|
||||||
import MutationButton from "../components/form/mutation-button";
|
|
||||||
|
|
||||||
export default function UserSettings() {
|
export default function UserSettings() {
|
||||||
return (
|
return (
|
||||||
|
@ -59,8 +54,19 @@ function UserSettingsForm({ data }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<h1>Account Settings</h1>
|
||||||
<form className="user-settings" onSubmit={submitForm}>
|
<form className="user-settings" onSubmit={submitForm}>
|
||||||
<h1>Post settings</h1>
|
<div className="form-section-docs">
|
||||||
|
<h3>Post Settings</h3>
|
||||||
|
<a
|
||||||
|
href="https://docs.gotosocial.org/en/latest/user_guide/posts"
|
||||||
|
target="_blank"
|
||||||
|
className="docslink"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Learn more about these settings (opens in a new tab)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<Select field={form.language} label="Default post language" options={
|
<Select field={form.language} label="Default post language" options={
|
||||||
<Languages />
|
<Languages />
|
||||||
}>
|
}>
|
||||||
|
@ -72,7 +78,6 @@ function UserSettingsForm({ data }) {
|
||||||
<option value="public">Public</option>
|
<option value="public">Public</option>
|
||||||
</>
|
</>
|
||||||
}>
|
}>
|
||||||
<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#privacy-settings" target="_blank" className="docslink" rel="noreferrer">Learn more about post privacy settings (opens in a new tab)</a>
|
|
||||||
</Select>
|
</Select>
|
||||||
<Select field={form.statusContentType} label="Default post (and bio) format" options={
|
<Select field={form.statusContentType} label="Default post (and bio) format" options={
|
||||||
<>
|
<>
|
||||||
|
@ -80,13 +85,11 @@ function UserSettingsForm({ data }) {
|
||||||
<option value="text/markdown">Markdown</option>
|
<option value="text/markdown">Markdown</option>
|
||||||
</>
|
</>
|
||||||
}>
|
}>
|
||||||
<a href="https://docs.gotosocial.org/en/latest/user_guide/posts/#input-types" target="_blank" className="docslink" rel="noreferrer">Learn more about post format settings (opens in a new tab)</a>
|
|
||||||
</Select>
|
</Select>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
field={form.isSensitive}
|
field={form.isSensitive}
|
||||||
label="Mark my posts as sensitive by default"
|
label="Mark my posts as sensitive by default"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MutationButton
|
<MutationButton
|
||||||
disabled={false}
|
disabled={false}
|
||||||
label="Save settings"
|
label="Save settings"
|
||||||
|
@ -124,24 +127,37 @@ function PasswordChange() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="change-password" onSubmit={submitForm}>
|
<form className="change-password" onSubmit={submitForm}>
|
||||||
<h1>Change password</h1>
|
<div className="form-section-docs">
|
||||||
|
<h3>Change Password</h3>
|
||||||
|
<a
|
||||||
|
href="https://docs.gotosocial.org/en/latest/user_guide/settings/#password-change"
|
||||||
|
target="_blank"
|
||||||
|
className="docslink"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Learn more about this (opens in a new tab)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
field={form.oldPassword}
|
field={form.oldPassword}
|
||||||
label="Current password"
|
label="Current password"
|
||||||
|
autoComplete="current-password"
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="password"
|
type="password"
|
||||||
name="newPassword"
|
name="newPassword"
|
||||||
field={form.newPassword}
|
field={form.newPassword}
|
||||||
label="New password"
|
label="New password"
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="password"
|
type="password"
|
||||||
name="confirmNewPassword"
|
name="confirmNewPassword"
|
||||||
field={verifyNewPassword}
|
field={verifyNewPassword}
|
||||||
label="Confirm new password"
|
label="Confirm new password"
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
<MutationButton
|
<MutationButton
|
||||||
disabled={false}
|
disabled={false}
|
||||||
|
@ -150,4 +166,4 @@ function PasswordChange() {
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -1229,11 +1229,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
|
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
|
||||||
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
|
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
|
||||||
|
|
||||||
"@types/bluebird@^3.5.39":
|
|
||||||
version "3.5.39"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.39.tgz#6aaf8bcbf005bb091d06ddaa0f620be078bf6a73"
|
|
||||||
integrity sha512-0h2lKudcFwHih8NHAgt/uyAIUQDO0AdfJYlWBXD8r+gFDulUi2CMZoQSh2Q5ol1FMaHV9k7/4HtcbA8ABtexmA==
|
|
||||||
|
|
||||||
"@types/hoist-non-react-statics@^3.3.1":
|
"@types/hoist-non-react-statics@^3.3.1":
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#dc1e9ded53375d37603c479cc12c693b0878aa2a"
|
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#dc1e9ded53375d37603c479cc12c693b0878aa2a"
|
||||||
|
@ -2056,14 +2051,14 @@ asynciterator.prototype@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-symbols "^1.0.3"
|
has-symbols "^1.0.3"
|
||||||
|
|
||||||
autoprefixer@^10.4.13:
|
autoprefixer@^10.4.19:
|
||||||
version "10.4.16"
|
version "10.4.19"
|
||||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.16.tgz#fad1411024d8670880bdece3970aa72e3572feb8"
|
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f"
|
||||||
integrity sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==
|
integrity sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist "^4.21.10"
|
browserslist "^4.23.0"
|
||||||
caniuse-lite "^1.0.30001538"
|
caniuse-lite "^1.0.30001599"
|
||||||
fraction.js "^4.3.6"
|
fraction.js "^4.3.7"
|
||||||
normalize-range "^0.1.2"
|
normalize-range "^0.1.2"
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
postcss-value-parser "^4.2.0"
|
postcss-value-parser "^4.2.0"
|
||||||
|
@ -2339,7 +2334,7 @@ browserify@^17.0.0:
|
||||||
vm-browserify "^1.0.0"
|
vm-browserify "^1.0.0"
|
||||||
xtend "^4.0.0"
|
xtend "^4.0.0"
|
||||||
|
|
||||||
browserslist@^4.21.10, browserslist@^4.21.9, browserslist@^4.22.1:
|
browserslist@^4.21.9, browserslist@^4.22.1:
|
||||||
version "4.22.1"
|
version "4.22.1"
|
||||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619"
|
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619"
|
||||||
integrity sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==
|
integrity sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==
|
||||||
|
@ -2349,6 +2344,16 @@ browserslist@^4.21.10, browserslist@^4.21.9, browserslist@^4.22.1:
|
||||||
node-releases "^2.0.13"
|
node-releases "^2.0.13"
|
||||||
update-browserslist-db "^1.0.13"
|
update-browserslist-db "^1.0.13"
|
||||||
|
|
||||||
|
browserslist@^4.23.0:
|
||||||
|
version "4.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab"
|
||||||
|
integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==
|
||||||
|
dependencies:
|
||||||
|
caniuse-lite "^1.0.30001587"
|
||||||
|
electron-to-chromium "^1.4.668"
|
||||||
|
node-releases "^2.0.14"
|
||||||
|
update-browserslist-db "^1.0.13"
|
||||||
|
|
||||||
buffer-from@^1.0.0:
|
buffer-from@^1.0.0:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
||||||
|
@ -2408,11 +2413,16 @@ callsites@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
|
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
|
||||||
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
|
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001541:
|
caniuse-lite@^1.0.30001541:
|
||||||
version "1.0.30001543"
|
version "1.0.30001543"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001543.tgz#478a3e9dddbb353c5ab214b0ecb0dbed529ed1d8"
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001543.tgz#478a3e9dddbb353c5ab214b0ecb0dbed529ed1d8"
|
||||||
integrity sha512-qxdO8KPWPQ+Zk6bvNpPeQIOH47qZSYdFZd6dXQzb2KzhnSXju4Kd7H1PkSJx6NICSMgo/IhRZRhhfPTHYpJUCA==
|
integrity sha512-qxdO8KPWPQ+Zk6bvNpPeQIOH47qZSYdFZd6dXQzb2KzhnSXju4Kd7H1PkSJx6NICSMgo/IhRZRhhfPTHYpJUCA==
|
||||||
|
|
||||||
|
caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599:
|
||||||
|
version "1.0.30001612"
|
||||||
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz#d34248b4ec1f117b70b24ad9ee04c90e0b8a14ae"
|
||||||
|
integrity sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==
|
||||||
|
|
||||||
chalk@^2.4.2:
|
chalk@^2.4.2:
|
||||||
version "2.4.2"
|
version "2.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
|
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
|
||||||
|
@ -2933,6 +2943,11 @@ electron-to-chromium@^1.4.535:
|
||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.540.tgz#c685f2f035e93eb21dd6a9cfe2c735bad8f77401"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.540.tgz#c685f2f035e93eb21dd6a9cfe2c735bad8f77401"
|
||||||
integrity sha512-aoCqgU6r9+o9/S7wkcSbmPRFi7OWZWiXS9rtjEd+Ouyu/Xyw5RSq2XN8s5Qp8IaFOLiRrhQCphCIjAxgG3eCAg==
|
integrity sha512-aoCqgU6r9+o9/S7wkcSbmPRFi7OWZWiXS9rtjEd+Ouyu/Xyw5RSq2XN8s5Qp8IaFOLiRrhQCphCIjAxgG3eCAg==
|
||||||
|
|
||||||
|
electron-to-chromium@^1.4.668:
|
||||||
|
version "1.4.746"
|
||||||
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.746.tgz#787213e75f6c7bccb55dfe8b68170555c548d093"
|
||||||
|
integrity sha512-jeWaIta2rIG2FzHaYIhSuVWqC6KJYo7oSBX4Jv7g+aVujKztfvdpf+n6MGwZdC5hQXbax4nntykLH2juIQrfPg==
|
||||||
|
|
||||||
elliptic@^6.5.3, elliptic@^6.5.4:
|
elliptic@^6.5.3, elliptic@^6.5.4:
|
||||||
version "6.5.4"
|
version "6.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
|
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
|
||||||
|
@ -3537,10 +3552,10 @@ forwarded@0.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
||||||
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
|
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
|
||||||
|
|
||||||
fraction.js@^4.3.6:
|
fraction.js@^4.3.7:
|
||||||
version "4.3.6"
|
version "4.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.6.tgz#e9e3acec6c9a28cf7bc36cbe35eea4ceb2c5c92d"
|
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
||||||
integrity sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==
|
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
|
||||||
|
|
||||||
fresh@0.5.2:
|
fresh@0.5.2:
|
||||||
version "0.5.2"
|
version "0.5.2"
|
||||||
|
@ -4649,6 +4664,11 @@ minimist@~0.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.2.4.tgz#0085d5501e29033748a2f2a4da0180142697a475"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.2.4.tgz#0085d5501e29033748a2f2a4da0180142697a475"
|
||||||
integrity sha512-Pkrrm8NjyQ8yVt8Am9M+yUt74zE3iokhzbG1bFVNjLB92vwM71hf40RkEsryg98BujhVOncKm/C1xROxZ030LQ==
|
integrity sha512-Pkrrm8NjyQ8yVt8Am9M+yUt74zE3iokhzbG1bFVNjLB92vwM71hf40RkEsryg98BujhVOncKm/C1xROxZ030LQ==
|
||||||
|
|
||||||
|
mitt@^3.0.1:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
|
||||||
|
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
|
||||||
|
|
||||||
mkdirp-classic@^0.5.2:
|
mkdirp-classic@^0.5.2:
|
||||||
version "0.5.3"
|
version "0.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
|
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
|
||||||
|
@ -4735,6 +4755,11 @@ node-releases@^2.0.13:
|
||||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
|
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
|
||||||
integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
|
integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
|
||||||
|
|
||||||
|
node-releases@^2.0.14:
|
||||||
|
version "2.0.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
|
||||||
|
integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==
|
||||||
|
|
||||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||||
|
@ -5436,6 +5461,11 @@ regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1:
|
||||||
define-properties "^1.2.0"
|
define-properties "^1.2.0"
|
||||||
set-function-name "^2.0.0"
|
set-function-name "^2.0.0"
|
||||||
|
|
||||||
|
regexparam@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-3.0.0.tgz#1673e09d41cb7fd41eaafd4040a6aa90daa0a21a"
|
||||||
|
integrity sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==
|
||||||
|
|
||||||
regexpu-core@^5.3.1:
|
regexpu-core@^5.3.1:
|
||||||
version "5.3.2"
|
version "5.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b"
|
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b"
|
||||||
|
@ -5841,7 +5871,7 @@ sourcemap-codec@^1.4.1:
|
||||||
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
|
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
|
||||||
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
|
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
|
||||||
|
|
||||||
split-filter-n@^1.1.2, split-filter-n@^1.1.3:
|
split-filter-n@^1.1.2:
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/split-filter-n/-/split-filter-n-1.1.3.tgz#c983ae1e52402e70071f711a7af767a91f09f740"
|
resolved "https://registry.yarnpkg.com/split-filter-n/-/split-filter-n-1.1.3.tgz#c983ae1e52402e70071f711a7af767a91f09f740"
|
||||||
integrity sha512-EU0EjvBI/mYBQMSAHq+ua/YNCuThuDjbU5h036k01+xieFW1aNvLNKb90xLihXIz5xJQX4VkEKan4LjSIyv7lg==
|
integrity sha512-EU0EjvBI/mYBQMSAHq+ua/YNCuThuDjbU5h036k01+xieFW1aNvLNKb90xLihXIz5xJQX4VkEKan4LjSIyv7lg==
|
||||||
|
@ -6592,11 +6622,13 @@ word-wrap@~1.2.3:
|
||||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||||
|
|
||||||
wouter@^2.8.0-alpha.2:
|
wouter@^3.1.0:
|
||||||
version "2.11.0"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/wouter/-/wouter-2.11.0.tgz#3db485dec158115b67330821e7673bf3e2f78678"
|
resolved "https://registry.yarnpkg.com/wouter/-/wouter-3.1.2.tgz#8fe1d1c08a415b64d7d2583090bb66f2166636ef"
|
||||||
integrity sha512-Y2CzNCwIN8kHjR2Q10D+UAgQND6TvBNmwXxgYw5ltXjjTlL7cLDUDpCip3a927Svxrmxr6vJMcPUysFxSvriCw==
|
integrity sha512-oyYrbwnIbal7Hz6LzeqRoyWFEkNA64SCmF9r48f6hkUcLnT0y0o+hthuT1X1OIbj80YBT9zE+mH4GYUWH98nIg==
|
||||||
dependencies:
|
dependencies:
|
||||||
|
mitt "^3.0.1"
|
||||||
|
regexparam "^3.0.0"
|
||||||
use-sync-external-store "^1.0.0"
|
use-sync-external-store "^1.0.0"
|
||||||
|
|
||||||
wrap-ansi@^6.0.1:
|
wrap-ansi@^6.0.1:
|
||||||
|
|
Loading…
Reference in a new issue