refactor federation/suspend (overview, detail)
This commit is contained in:
parent
774cb78732
commit
4cbfa77907
|
@ -47,7 +47,11 @@ $blue3: #89caff; /* hover/selected accent to $blue2, can be used with $gray1 (7.
|
||||||
$error1: #860000; /* Error border/foreground text, can be used with $error2 (5.0), $white1 (10), $white2 (5.1) */
|
$error1: #860000; /* Error border/foreground text, can be used with $error2 (5.0), $white1 (10), $white2 (5.1) */
|
||||||
$error2: #ff9796; /* Error background text, can be used with $error1 (5.0), $gray1 (6.6), $gray2 (5.3), $gray3 (4.8) */
|
$error2: #ff9796; /* Error background text, can be used with $error1 (5.0), $gray1 (6.6), $gray2 (5.3), $gray3 (4.8) */
|
||||||
$error3: #dd2c2c; /* Error button background text, can be used with $white1 (4.51) */
|
$error3: #dd2c2c; /* Error button background text, can be used with $white1 (4.51) */
|
||||||
$error-link: #185F8C; /* Error link text, can be used with $error2 (5.54) */
|
$error-link: #01318C; /* Error link text, can be used with $error2 (5.56) */
|
||||||
|
|
||||||
|
$info-fg: $gray1;
|
||||||
|
$info-bg: #b3ddff;
|
||||||
|
$info-link: $error-link;
|
||||||
|
|
||||||
$fg: $white1;
|
$fg: $white1;
|
||||||
$bg: $gray1;
|
$bg: $gray1;
|
||||||
|
@ -92,6 +96,7 @@ $avatar-border: $orange2;
|
||||||
$input-bg: $gray4;
|
$input-bg: $gray4;
|
||||||
$input-disabled-bg: $gray2;
|
$input-disabled-bg: $gray2;
|
||||||
$input-border: $blue1;
|
$input-border: $blue1;
|
||||||
|
$input-error-border: $error3;
|
||||||
$input-focus-border: $blue3;
|
$input-focus-border: $blue3;
|
||||||
|
|
||||||
$settings-nav-bg: $bg-accent;
|
$settings-nav-bg: $bg-accent;
|
||||||
|
@ -107,5 +112,6 @@ $settings-nav-bg-active: $gray2;
|
||||||
$error-fg: $error1;
|
$error-fg: $error1;
|
||||||
$error-bg: $error2;
|
$error-bg: $error2;
|
||||||
|
|
||||||
$settings-entry-bg: $gray3;
|
$settings-entry-bg: $gray2;
|
||||||
|
$settings-entry-alternate-bg: $gray3;
|
||||||
$settings-entry-hover-bg: $gray4;
|
$settings-entry-hover-bg: $gray4;
|
|
@ -311,12 +311,16 @@ input, select, textarea, .input {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
padding: 0.3rem;
|
padding: 0.3rem;
|
||||||
|
|
||||||
&:focus {
|
&:focus, &:active {
|
||||||
border-color: $input-focus-border;
|
border-color: $input-focus-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:invalid {
|
||||||
|
border-color: $input-error-border;
|
||||||
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
background: $input-disabled-bg;
|
background: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ const { TextInput } = require("../components/form/inputs");
|
||||||
const MutationButton = require("../components/form/mutation-button");
|
const MutationButton = require("../components/form/mutation-button");
|
||||||
|
|
||||||
module.exports = function AdminActionPanel() {
|
module.exports = function AdminActionPanel() {
|
||||||
const daysField = useTextInput("days", {defaultValue: 30});
|
const daysField = useTextInput("days", { defaultValue: 30 });
|
||||||
|
|
||||||
const [mediaCleanup, mediaCleanupResult] = query.useMediaCleanupMutation();
|
const [mediaCleanup, mediaCleanupResult] = query.useMediaCleanupMutation();
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ module.exports = function AdminActionPanel() {
|
||||||
min="0"
|
min="0"
|
||||||
placeholder="30"
|
placeholder="30"
|
||||||
/>
|
/>
|
||||||
<MutationButton text="Remove old media" result={mediaCleanupResult} />
|
<MutationButton label="Remove old media" result={mediaCleanupResult} />
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -34,19 +34,19 @@ const base = "/settings/custom-emoji/local";
|
||||||
module.exports = function EmojiDetailRoute() {
|
module.exports = function EmojiDetailRoute() {
|
||||||
let [_match, params] = useRoute(`${base}/:emojiId`);
|
let [_match, params] = useRoute(`${base}/:emojiId`);
|
||||||
if (params?.emojiId == undefined) {
|
if (params?.emojiId == undefined) {
|
||||||
return <Redirect to={base}/>;
|
return <Redirect to={base} />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className="emoji-detail">
|
<div className="emoji-detail">
|
||||||
<Link to={base}><a>< go back</a></Link>
|
<Link to={base}><a>< go back</a></Link>
|
||||||
<EmojiDetailData emojiId={params.emojiId}/>
|
<EmojiDetailData emojiId={params.emojiId} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function EmojiDetailData({emojiId}) {
|
function EmojiDetailData({ emojiId }) {
|
||||||
const {currentData: emoji, isLoading, error} = query.useGetEmojiQuery(emojiId);
|
const { currentData: emoji, isLoading, error } = query.useGetEmojiQuery(emojiId);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
@ -57,20 +57,20 @@ function EmojiDetailData({emojiId}) {
|
||||||
} else if (isLoading) {
|
} else if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Loading/>
|
<Loading />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <EmojiDetail emoji={emoji}/>;
|
return <EmojiDetail emoji={emoji} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function EmojiDetail({emoji}) {
|
function EmojiDetail({ emoji }) {
|
||||||
const [modifyEmoji, modifyResult] = query.useEditEmojiMutation();
|
const [modifyEmoji, modifyResult] = query.useEditEmojiMutation();
|
||||||
|
|
||||||
const [isNewCategory, setIsNewCategory] = React.useState(false);
|
const [isNewCategory, setIsNewCategory] = React.useState(false);
|
||||||
|
|
||||||
const [categoryState, _resetCategory, { category }] = useComboBoxInput("category", {defaultValue: emoji.category});
|
const [categoryState, _resetCategory, { category }] = useComboBoxInput("category", { defaultValue: emoji.category });
|
||||||
|
|
||||||
const [onFileChange, _resetFile, { image, imageURL, imageInfo }] = useFileInput("image", {
|
const [onFileChange, _resetFile, { image, imageURL, imageInfo }] = useFileInput("image", {
|
||||||
withPreview: true,
|
withPreview: true,
|
||||||
|
@ -78,26 +78,26 @@ function EmojiDetail({emoji}) {
|
||||||
});
|
});
|
||||||
|
|
||||||
function modifyCategory() {
|
function modifyCategory() {
|
||||||
modifyEmoji({id: emoji.id, category: category.trim()});
|
modifyEmoji({ id: emoji.id, category: category.trim() });
|
||||||
}
|
}
|
||||||
|
|
||||||
function modifyImage() {
|
function modifyImage() {
|
||||||
modifyEmoji({id: emoji.id, image: image});
|
modifyEmoji({ id: emoji.id, image: image });
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (category != emoji.category && !categoryState.open && !isNewCategory && category.trim().length > 0) {
|
if (category != emoji.category && !categoryState.open && !isNewCategory && category?.trim().length > 0) {
|
||||||
modifyEmoji({id: emoji.id, category: category.trim()});
|
modifyEmoji({ id: emoji.id, category: category.trim() });
|
||||||
}
|
}
|
||||||
}, [isNewCategory, category, categoryState.open, emoji.category, emoji.id, modifyEmoji]);
|
}, [isNewCategory, category, categoryState.open, emoji.category, emoji.id, modifyEmoji]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="emoji-header">
|
<div className="emoji-header">
|
||||||
<img src={emoji.url} alt={emoji.shortcode} title={emoji.shortcode}/>
|
<img src={emoji.url} alt={emoji.shortcode} title={emoji.shortcode} />
|
||||||
<div>
|
<div>
|
||||||
<h2>{emoji.shortcode}</h2>
|
<h2>{emoji.shortcode}</h2>
|
||||||
<DeleteButton id={emoji.id}/>
|
<DeleteButton id={emoji.id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ function EmojiDetail({emoji}) {
|
||||||
categoryState={categoryState}
|
categoryState={categoryState}
|
||||||
setIsNew={setIsNewCategory}
|
setIsNew={setIsNewCategory}
|
||||||
>
|
>
|
||||||
<button style={{visibility: (isNewCategory ? "initial" : "hidden")}} onClick={modifyCategory}>
|
<button style={{ visibility: (isNewCategory ? "initial" : "hidden") }} onClick={modifyCategory}>
|
||||||
Create
|
Create
|
||||||
</button>
|
</button>
|
||||||
</CategorySelect>
|
</CategorySelect>
|
||||||
|
@ -153,7 +153,7 @@ function EmojiDetail({emoji}) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeleteButton({id}) {
|
function DeleteButton({ id }) {
|
||||||
// TODO: confirmation dialog?
|
// TODO: confirmation dialog?
|
||||||
const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation();
|
const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation();
|
||||||
|
|
||||||
|
@ -163,7 +163,7 @@ function DeleteButton({id}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deleteResult.isSuccess) {
|
if (deleteResult.isSuccess) {
|
||||||
return <Redirect to={base}/>;
|
return <Redirect to={base} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -50,7 +50,7 @@ module.exports = function NewEmojiForm({ emoji }) {
|
||||||
const [onShortcodeChange, resetShortcode, { shortcode, setShortcode, shortcodeRef }] = useTextInput("shortcode", {
|
const [onShortcodeChange, resetShortcode, { shortcode, setShortcode, shortcodeRef }] = useTextInput("shortcode", {
|
||||||
validator: function validateShortcode(code) {
|
validator: function validateShortcode(code) {
|
||||||
// technically invalid, but hacky fix to prevent validation error on page load
|
// technically invalid, but hacky fix to prevent validation error on page load
|
||||||
if (shortcode == "") {return "";}
|
if (shortcode == "") { return ""; }
|
||||||
|
|
||||||
if (emojiCodes.has(code)) {
|
if (emojiCodes.has(code)) {
|
||||||
return "Shortcode already in use";
|
return "Shortcode already in use";
|
||||||
|
@ -161,7 +161,7 @@ module.exports = function NewEmojiForm({ emoji }) {
|
||||||
categoryState={categoryState}
|
categoryState={categoryState}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MutationButton text="Upload emoji" result={result} />
|
<MutationButton label="Upload emoji" result={result} />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,386 +0,0 @@
|
||||||
/*
|
|
||||||
GoToSocial
|
|
||||||
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
const Promise = require("bluebird");
|
|
||||||
const React = require("react");
|
|
||||||
const Redux = require("react-redux");
|
|
||||||
const {Switch, Route, Link, Redirect, useRoute, useLocation} = require("wouter");
|
|
||||||
const fileDownload = require("js-file-download");
|
|
||||||
|
|
||||||
const { formFields } = require("../components/form-fields");
|
|
||||||
|
|
||||||
const api = require("../lib/api");
|
|
||||||
const adminActions = require("../redux/reducers/admin").actions;
|
|
||||||
const submit = require("../lib/submit");
|
|
||||||
const BackButton = require("../components/back-button");
|
|
||||||
const Loading = require("../components/loading");
|
|
||||||
const { matchSorter } = require("match-sorter");
|
|
||||||
|
|
||||||
const base = "/settings/admin/federation";
|
|
||||||
|
|
||||||
module.exports = function AdminSettings() {
|
|
||||||
const dispatch = Redux.useDispatch();
|
|
||||||
const loadedBlockedInstances = Redux.useSelector(state => state.admin.loadedBlockedInstances);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!loadedBlockedInstances ) {
|
|
||||||
Promise.try(() => {
|
|
||||||
return dispatch(api.admin.fetchDomainBlocks());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [dispatch, loadedBlockedInstances]);
|
|
||||||
|
|
||||||
if (!loadedBlockedInstances) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Federation</h1>
|
|
||||||
<div>
|
|
||||||
<Loading/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route path={`${base}/:domain`}>
|
|
||||||
<InstancePageWrapped />
|
|
||||||
</Route>
|
|
||||||
<InstanceOverview />
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function InstanceOverview() {
|
|
||||||
const [filter, setFilter] = React.useState("");
|
|
||||||
const blockedInstances = Redux.useSelector(state => state.admin.blockedInstances);
|
|
||||||
const [_location, setLocation] = useLocation();
|
|
||||||
|
|
||||||
const filteredInstances = React.useMemo(() => {
|
|
||||||
return matchSorter(Object.values(blockedInstances), filter, {keys: ["domain"]});
|
|
||||||
}, [blockedInstances, filter]);
|
|
||||||
|
|
||||||
function filterFormSubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
setLocation(`${base}/${filter}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1>Federation</h1>
|
|
||||||
Here you can see an overview of blocked instances.
|
|
||||||
|
|
||||||
<div className="instance-list">
|
|
||||||
<h2>Blocked instances</h2>
|
|
||||||
<form action={`${base}/view`} className="filter" role="search" onSubmit={filterFormSubmit}>
|
|
||||||
<input name="domain" value={filter} onChange={(e) => setFilter(e.target.value)}/>
|
|
||||||
<Link to={`${base}/${filter}`}><a className="button">Add block</a></Link>
|
|
||||||
</form>
|
|
||||||
<div className="list">
|
|
||||||
{filteredInstances.map((entry) => {
|
|
||||||
return (
|
|
||||||
<Link key={entry.domain} to={`${base}/${entry.domain}`}>
|
|
||||||
<a className="entry nounderline">
|
|
||||||
<span id="domain">
|
|
||||||
{entry.domain}
|
|
||||||
</span>
|
|
||||||
<span id="date">
|
|
||||||
{new Date(entry.created_at).toLocaleString()}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<BulkBlocking/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Bulk = formFields(adminActions.updateBulkBlockVal, (state) => state.admin.bulkBlock);
|
|
||||||
function BulkBlocking() {
|
|
||||||
const dispatch = Redux.useDispatch();
|
|
||||||
const {bulkBlock, blockedInstances} = Redux.useSelector(state => state.admin);
|
|
||||||
|
|
||||||
const [errorMsg, setError] = React.useState("");
|
|
||||||
const [statusMsg, setStatus] = React.useState("");
|
|
||||||
|
|
||||||
function importBlocks() {
|
|
||||||
setStatus("Processing");
|
|
||||||
setError("");
|
|
||||||
return Promise.try(() => {
|
|
||||||
return dispatch(api.admin.bulkDomainBlock());
|
|
||||||
}).then(({success, invalidDomains}) => {
|
|
||||||
return Promise.try(() => {
|
|
||||||
return resetBulk();
|
|
||||||
}).then(() => {
|
|
||||||
dispatch(adminActions.updateBulkBlockVal(["list", invalidDomains.join("\n")]));
|
|
||||||
|
|
||||||
let stat = "";
|
|
||||||
if (success == 0) {
|
|
||||||
return setError("No valid domains in import");
|
|
||||||
} else if (success == 1) {
|
|
||||||
stat = "Imported 1 domain";
|
|
||||||
} else {
|
|
||||||
stat = `Imported ${success} domains`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invalidDomains.length > 0) {
|
|
||||||
if (invalidDomains.length == 1) {
|
|
||||||
stat += ", input contained 1 invalid domain.";
|
|
||||||
} else {
|
|
||||||
stat += `, input contained ${invalidDomains.length} invalid domains.`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
stat += "!";
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus(stat);
|
|
||||||
});
|
|
||||||
}).catch((e) => {
|
|
||||||
setError(e.message);
|
|
||||||
setStatus("");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportBlocks() {
|
|
||||||
return Promise.try(() => {
|
|
||||||
setStatus("Exporting");
|
|
||||||
setError("");
|
|
||||||
let asJSON = bulkBlock.exportType.startsWith("json");
|
|
||||||
let _asCSV = bulkBlock.exportType.startsWith("csv");
|
|
||||||
|
|
||||||
let exportList = Object.values(blockedInstances).map((entry) => {
|
|
||||||
if (asJSON) {
|
|
||||||
return {
|
|
||||||
domain: entry.domain,
|
|
||||||
public_comment: entry.public_comment
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return entry.domain;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (bulkBlock.exportType == "json") {
|
|
||||||
return dispatch(adminActions.updateBulkBlockVal(["list", JSON.stringify(exportList)]));
|
|
||||||
} else if (bulkBlock.exportType == "json-download") {
|
|
||||||
return fileDownload(JSON.stringify(exportList), "block-export.json");
|
|
||||||
} else if (bulkBlock.exportType == "plain") {
|
|
||||||
return dispatch(adminActions.updateBulkBlockVal(["list", exportList.join("\n")]));
|
|
||||||
}
|
|
||||||
}).then(() => {
|
|
||||||
setStatus("Exported!");
|
|
||||||
}).catch((e) => {
|
|
||||||
setError(e.message);
|
|
||||||
setStatus("");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetBulk(e) {
|
|
||||||
if (e != undefined) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
return dispatch(adminActions.resetBulkBlockVal());
|
|
||||||
}
|
|
||||||
|
|
||||||
function disableInfoFields(props={}) {
|
|
||||||
if (bulkBlock.list[0] == "[") {
|
|
||||||
return {
|
|
||||||
...props,
|
|
||||||
disabled: true,
|
|
||||||
placeHolder: "Domain list is a JSON import, input disabled"
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return props;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bulk">
|
|
||||||
<h2>Import / Export <a onClick={resetBulk}>reset</a></h2>
|
|
||||||
<Bulk.TextArea
|
|
||||||
id="list"
|
|
||||||
name="Domains, one per line"
|
|
||||||
placeHolder={`google.com\nfacebook.com`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Bulk.TextArea
|
|
||||||
id="public_comment"
|
|
||||||
name="Public comment"
|
|
||||||
inputProps={disableInfoFields({rows: 3})}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Bulk.TextArea
|
|
||||||
id="private_comment"
|
|
||||||
name="Private comment"
|
|
||||||
inputProps={disableInfoFields({rows: 3})}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Bulk.Checkbox
|
|
||||||
id="obfuscate"
|
|
||||||
name="Obfuscate domains? "
|
|
||||||
inputProps={disableInfoFields()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="hidden">
|
|
||||||
<Bulk.File
|
|
||||||
id="json"
|
|
||||||
fileType="application/json"
|
|
||||||
withPreview={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="messagebutton">
|
|
||||||
<div>
|
|
||||||
<button type="submit" onClick={importBlocks}>Import</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button type="submit" onClick={exportBlocks}>Export</button>
|
|
||||||
|
|
||||||
<Bulk.Select id="exportType" name="Export type" options={
|
|
||||||
<>
|
|
||||||
<option value="plain">One per line in text field</option>
|
|
||||||
<option value="json">JSON in text field</option>
|
|
||||||
<option value="json-download">JSON file download</option>
|
|
||||||
<option disabled value="csv">CSV in text field (glitch-soc)</option>
|
|
||||||
<option disabled value="csv-download">CSV file download (glitch-soc)</option>
|
|
||||||
</>
|
|
||||||
}/>
|
|
||||||
</div>
|
|
||||||
<br/>
|
|
||||||
<div>
|
|
||||||
{errorMsg.length > 0 &&
|
|
||||||
<div className="error accent">{errorMsg}</div>
|
|
||||||
}
|
|
||||||
{statusMsg.length > 0 &&
|
|
||||||
<div className="accent">{statusMsg}</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function InstancePageWrapped() {
|
|
||||||
/* We wrap the component to generate formFields with a setter depending on the domain
|
|
||||||
if formFields() is used inside the same component that is re-rendered with their state,
|
|
||||||
inputs get re-created on every change, causing them to lose focus, and bad performance
|
|
||||||
*/
|
|
||||||
let [_match, {domain}] = useRoute(`${base}/:domain`);
|
|
||||||
|
|
||||||
if (domain == "view") { // from form field submission
|
|
||||||
let realDomain = (new URL(document.location)).searchParams.get("domain");
|
|
||||||
if (realDomain == undefined) {
|
|
||||||
return <Redirect to={base}/>;
|
|
||||||
} else {
|
|
||||||
domain = realDomain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function alterDomain([key, val]) {
|
|
||||||
return adminActions.updateDomainBlockVal([domain, key, val]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields = formFields(alterDomain, (state) => state.admin.newInstanceBlocks[domain]);
|
|
||||||
|
|
||||||
return <InstancePage domain={domain} Form={fields} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function InstancePage({domain, Form}) {
|
|
||||||
const dispatch = Redux.useDispatch();
|
|
||||||
const entry = Redux.useSelector(state => state.admin.newInstanceBlocks[domain]);
|
|
||||||
const [_location, setLocation] = useLocation();
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (entry == undefined) {
|
|
||||||
dispatch(api.admin.getEditableDomainBlock(domain));
|
|
||||||
}
|
|
||||||
}, [dispatch, domain, entry]);
|
|
||||||
|
|
||||||
const [errorMsg, setError] = React.useState("");
|
|
||||||
const [statusMsg, setStatus] = React.useState("");
|
|
||||||
|
|
||||||
if (entry == undefined) {
|
|
||||||
return <Loading/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateBlock = submit(
|
|
||||||
() => dispatch(api.admin.updateDomainBlock(domain)),
|
|
||||||
{setStatus, setError}
|
|
||||||
);
|
|
||||||
|
|
||||||
const removeBlock = submit(
|
|
||||||
() => dispatch(api.admin.removeDomainBlock(domain)),
|
|
||||||
{setStatus, setError, startStatus: "Removing", successStatus: "Removed!", onSuccess: () => {
|
|
||||||
setLocation(base);
|
|
||||||
}}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1><BackButton to={base}/> Federation settings for: {domain}</h1>
|
|
||||||
{entry.new
|
|
||||||
? "No stored block yet, you can add one below:"
|
|
||||||
: <b className="error">Editing domain blocks is not implemented yet, <a href="https://github.com/superseriousbusiness/gotosocial/issues/1198" target="_blank" rel="noopener noreferrer">check here for progress</a>.</b>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Form.TextArea
|
|
||||||
id="public_comment"
|
|
||||||
name="Public comment"
|
|
||||||
inputProps={{
|
|
||||||
disabled: !entry.new
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Form.TextArea
|
|
||||||
id="private_comment"
|
|
||||||
name="Private comment"
|
|
||||||
inputProps={{
|
|
||||||
disabled: !entry.new
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Form.Checkbox
|
|
||||||
id="obfuscate"
|
|
||||||
name="Obfuscate domain? "
|
|
||||||
inputProps={{
|
|
||||||
disabled: !entry.new
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="messagebutton">
|
|
||||||
{entry.new
|
|
||||||
? <button type="submit" onClick={updateBlock}>{entry.new ? "Add block" : "Save block"}</button>
|
|
||||||
: <button className="danger" onClick={removeBlock}>Remove block</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
{errorMsg.length > 0 &&
|
|
||||||
<div className="error accent">{errorMsg}</div>
|
|
||||||
}
|
|
||||||
{statusMsg.length > 0 &&
|
|
||||||
<div className="accent">{statusMsg}</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
const { useRoute, Redirect } = require("wouter");
|
||||||
|
|
||||||
|
const query = require("../../lib/query");
|
||||||
|
|
||||||
|
const { useTextInput, useBoolInput } = require("../../lib/form");
|
||||||
|
|
||||||
|
const useFormSubmit = require("../../lib/form/submit");
|
||||||
|
|
||||||
|
const { TextInput, Checkbox, TextArea } = require("../../components/form/inputs");
|
||||||
|
|
||||||
|
const Loading = require("../../components/loading");
|
||||||
|
const BackButton = require("../../components/back-button");
|
||||||
|
const MutationButton = require("../../components/form/mutation-button");
|
||||||
|
|
||||||
|
module.exports = function InstanceDetail({ baseUrl }) {
|
||||||
|
const { data: blockedInstances = [], isLoading } = query.useInstanceBlocksQuery();
|
||||||
|
|
||||||
|
let [_match, { domain }] = useRoute(`${baseUrl}/:domain`);
|
||||||
|
|
||||||
|
if (domain == "view") { // from form field submission
|
||||||
|
domain = (new URL(document.location)).searchParams.get("domain");
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingBlock = React.useMemo(() => {
|
||||||
|
return blockedInstances.find((block) => block.domain == domain);
|
||||||
|
}, [blockedInstances, domain]);
|
||||||
|
|
||||||
|
if (domain == undefined) {
|
||||||
|
return <Redirect to={baseUrl} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let infoContent = null;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
infoContent = <Loading />;
|
||||||
|
} else if (existingBlock == undefined) {
|
||||||
|
infoContent = <span>No stored block yet, you can add one below:</span>;
|
||||||
|
} else {
|
||||||
|
infoContent = (
|
||||||
|
<div className="info">
|
||||||
|
<i className="fa fa-fw fa-exclamation-triangle" aria-hidden="true"></i>
|
||||||
|
<b>Editing domain blocks isn't implemented yet, <a href="https://github.com/superseriousbusiness/gotosocial/issues/1198" target="_blank" rel="noopener noreferrer">check here for progress</a></b>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1><BackButton to={baseUrl} /> Federation settings for: {domain}</h1>
|
||||||
|
{infoContent}
|
||||||
|
<DomainBlockForm defaultDomain={domain} block={existingBlock} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function DomainBlockForm({ defaultDomain, block = {} }) {
|
||||||
|
const isExistingBlock = block.domain != undefined;
|
||||||
|
|
||||||
|
const disabledForm = isExistingBlock
|
||||||
|
? {
|
||||||
|
disabled: true,
|
||||||
|
title: "Domain suspensions currently cannot be edited."
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const form = {
|
||||||
|
domain: useTextInput("domain", { defaultValue: block.domain ?? defaultDomain }),
|
||||||
|
obfuscate: useBoolInput("obfuscate", { defaultValue: block.obfuscate }),
|
||||||
|
commentPrivate: useTextInput("private_comment", { defaultValue: block.private_comment }),
|
||||||
|
commentPublic: useTextInput("public_comment", { defaultValue: block.public_comment })
|
||||||
|
};
|
||||||
|
|
||||||
|
const [submitForm, addResult] = useFormSubmit(form, query.useAddInstanceBlockMutation(), { changedOnly: false });
|
||||||
|
|
||||||
|
const [removeBlock, removeResult] = query.useRemoveInstanceBlockMutation({ fixedCacheKey: block.id });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={submitForm}>
|
||||||
|
<TextInput
|
||||||
|
field={form.domain}
|
||||||
|
label="Domain"
|
||||||
|
placeholder="example.com"
|
||||||
|
{...disabledForm}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
field={form.obfuscate}
|
||||||
|
label="Obfuscate domain in public lists"
|
||||||
|
{...disabledForm}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
field={form.commentPrivate}
|
||||||
|
label="Private comment"
|
||||||
|
rows={3}
|
||||||
|
{...disabledForm}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
field={form.commentPublic}
|
||||||
|
label="Public comment"
|
||||||
|
rows={3}
|
||||||
|
{...disabledForm}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MutationButton
|
||||||
|
label="Suspend"
|
||||||
|
result={addResult}
|
||||||
|
{...disabledForm}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
isExistingBlock &&
|
||||||
|
<MutationButton
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeBlock(block.id)}
|
||||||
|
label="Remove"
|
||||||
|
result={removeResult}
|
||||||
|
className="button danger"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
|
||||||
|
const query = require("../../lib/query");
|
||||||
|
|
||||||
|
const {
|
||||||
|
useTextInput,
|
||||||
|
useBoolInput,
|
||||||
|
useFileInput
|
||||||
|
} = require("../../lib/form");
|
||||||
|
|
||||||
|
const useFormSubmit = require("../../lib/form/submit");
|
||||||
|
|
||||||
|
const {
|
||||||
|
TextInput,
|
||||||
|
TextArea,
|
||||||
|
Checkbox,
|
||||||
|
FileInput
|
||||||
|
} = require("../../components/form/inputs");
|
||||||
|
const FormWithData = require("../../lib/form/form-with-data");
|
||||||
|
|
||||||
|
module.exports = function ImportExport() {
|
||||||
|
return (
|
||||||
|
<div className="import-export">
|
||||||
|
<h2>Import / Export</h2>
|
||||||
|
<FormWithData
|
||||||
|
dataQuery={query.useInstanceBlocksQuery}
|
||||||
|
DataForm={ImportExportForm}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function ImportExportForm({ data: blockedInstances }) {
|
||||||
|
const form = {
|
||||||
|
list: useTextInput("list"),
|
||||||
|
obfuscate: useBoolInput("obfuscate"),
|
||||||
|
commentPrivate: useTextInput("private_comment"),
|
||||||
|
commentPublic: useTextInput("public_comment"),
|
||||||
|
json: useFileInput("json")
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form>
|
||||||
|
<TextArea
|
||||||
|
field={form.list}
|
||||||
|
label="Domains, one per line"
|
||||||
|
placeholder={`google.com\nfacebook.com`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
field={form.commentPrivate}
|
||||||
|
label="Private comment"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
field={form.commentPublic}
|
||||||
|
label="Public comment"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
field={form.obfuscate}
|
||||||
|
label="Obfuscate domain in public lists"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
|
@ -18,31 +18,26 @@
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const Promise = require("bluebird");
|
const React = require("react");
|
||||||
|
const { Switch, Route } = require("wouter");
|
||||||
|
|
||||||
module.exports = function submit(func, {
|
const baseUrl = `/settings/admin/federation`;
|
||||||
setStatus, setError,
|
|
||||||
startStatus="PATCHing", successStatus="Saved!",
|
const InstanceOverview = require("./overview");
|
||||||
onSuccess,
|
const InstanceDetail = require("./detail");
|
||||||
onError
|
|
||||||
}) {
|
module.exports = function Federation({ }) {
|
||||||
return function() {
|
return (
|
||||||
setStatus(startStatus);
|
<Switch>
|
||||||
setError("");
|
{/* <Route path={`${baseUrl}/import-export`}>
|
||||||
return Promise.try(() => {
|
<InstanceImportExport />
|
||||||
return func();
|
</Route> */}
|
||||||
}).then(() => {
|
|
||||||
setStatus(successStatus);
|
<Route path={`${baseUrl}/:domain`}>
|
||||||
if (onSuccess != undefined) {
|
<InstanceDetail baseUrl={baseUrl} />
|
||||||
return onSuccess();
|
</Route>
|
||||||
}
|
|
||||||
}).catch((e) => {
|
<InstanceOverview baseUrl={baseUrl} />
|
||||||
setError(e.message);
|
</Switch>
|
||||||
setStatus("");
|
);
|
||||||
console.error(e);
|
|
||||||
if (onError != undefined) {
|
|
||||||
onError(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
};
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
const { Link, useLocation } = require("wouter");
|
||||||
|
const { matchSorter } = require("match-sorter");
|
||||||
|
|
||||||
|
const { useTextInput } = require("../../lib/form");
|
||||||
|
|
||||||
|
const { TextInput } = require("../../components/form/inputs");
|
||||||
|
|
||||||
|
const query = require("../../lib/query");
|
||||||
|
|
||||||
|
const Loading = require("../../components/loading");
|
||||||
|
const ImportExport = require("./import-export");
|
||||||
|
|
||||||
|
module.exports = function InstanceOverview({ baseUrl }) {
|
||||||
|
const { data: blockedInstances = [], isLoading } = query.useInstanceBlocksQuery();
|
||||||
|
|
||||||
|
const [_location, setLocation] = useLocation();
|
||||||
|
|
||||||
|
const filterField = useTextInput("filter");
|
||||||
|
const filter = filterField.value;
|
||||||
|
|
||||||
|
const filteredInstances = React.useMemo(() => {
|
||||||
|
return matchSorter(Object.values(blockedInstances), filter, { keys: ["domain"] });
|
||||||
|
}, [blockedInstances, filter]);
|
||||||
|
|
||||||
|
let filtered = blockedInstances.length - filteredInstances.length;
|
||||||
|
|
||||||
|
function filterFormSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
setLocation(`${baseUrl}/${filter}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Federation</h1>
|
||||||
|
|
||||||
|
<div className="instance-list">
|
||||||
|
<h2>Suspended instances</h2>
|
||||||
|
<span>
|
||||||
|
Suspending a domain blocks all current and future accounts on that instance. Stored content will be removed,
|
||||||
|
and no more data is sent to the remote server.
|
||||||
|
</span>
|
||||||
|
<form className="filter" role="search" onSubmit={filterFormSubmit}>
|
||||||
|
<TextInput field={filterField} placeholder="example.com" label="Search or add domain suspension" />
|
||||||
|
<Link to={`${baseUrl}/${filter}`}><a className="button">Suspend</a></Link>
|
||||||
|
</form>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
{blockedInstances.length} blocked instance{blockedInstances.length != 1 ? "s" : ""} {filtered > 0 && `(${filtered} filtered)`}
|
||||||
|
</span>
|
||||||
|
<div className="list">
|
||||||
|
{filteredInstances.map((entry) => {
|
||||||
|
return (
|
||||||
|
<Link key={entry.domain} to={`${baseUrl}/${entry.domain}`}>
|
||||||
|
<a className="entry nounderline">
|
||||||
|
<span id="domain">
|
||||||
|
{entry.domain}
|
||||||
|
</span>
|
||||||
|
<span id="date">
|
||||||
|
{new Date(entry.created_at).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ImportExport />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -47,19 +47,19 @@ module.exports = function AdminSettings() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function AdminSettingsForm({data: instance}) {
|
function AdminSettingsForm({ data: instance }) {
|
||||||
const form = {
|
const form = {
|
||||||
title: useTextInput("title", {defaultValue: instance.title}),
|
title: useTextInput("title", { defaultValue: instance.title }),
|
||||||
thumbnail: useFileInput("thumbnail", {withPreview: true}),
|
thumbnail: useFileInput("thumbnail", { withPreview: true }),
|
||||||
thumbnailDesc: useTextInput("thumbnail_description", {defaultValue: instance.thumbnail_description}),
|
thumbnailDesc: useTextInput("thumbnail_description", { defaultValue: instance.thumbnail_description }),
|
||||||
shortDesc: useTextInput("short_description", {defaultValue: instance.short_description}),
|
shortDesc: useTextInput("short_description", { defaultValue: instance.short_description }),
|
||||||
description: useTextInput("description", {defaultValue: instance.description}),
|
description: useTextInput("description", { defaultValue: instance.description }),
|
||||||
contactUser: useTextInput("contact_username", {defaultValue: instance.contact_account?.username}),
|
contactUser: useTextInput("contact_username", { defaultValue: instance.contact_account?.username }),
|
||||||
contactEmail: useTextInput("contact_email", {defaultValue: instance.email}),
|
contactEmail: useTextInput("contact_email", { defaultValue: instance.email }),
|
||||||
terms: useTextInput("terms", {defaultValue: instance.terms})
|
terms: useTextInput("terms", { defaultValue: instance.terms })
|
||||||
};
|
};
|
||||||
|
|
||||||
const [result, submitForm] = useFormSubmit(form, query.useUpdateInstanceMutation());
|
const [submitForm, result] = useFormSubmit(form, query.useUpdateInstanceMutation());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={submitForm}>
|
<form onSubmit={submitForm}>
|
||||||
|
@ -117,7 +117,7 @@ function AdminSettingsForm({data: instance}) {
|
||||||
placeholder=""
|
placeholder=""
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MutationButton text="Save" result={result}/>
|
<MutationButton label="Save" result={result} />
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -20,24 +20,28 @@
|
||||||
|
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
|
|
||||||
module.exports = function MutateButton({text, result}) {
|
module.exports = function MutationButton({ label, result, disabled, ...inputProps }) {
|
||||||
let buttonText = text;
|
let iconClass = "";
|
||||||
|
|
||||||
|
console.log(label, result);
|
||||||
|
|
||||||
if (result.isLoading) {
|
if (result.isLoading) {
|
||||||
buttonText = "Processing...";
|
iconClass = "fa-spin fa-refresh";
|
||||||
|
} else if (result.isSuccess) {
|
||||||
|
iconClass = "fa-check fadeout";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<div>
|
return (<div>
|
||||||
{result.error &&
|
{result.error &&
|
||||||
<section className="error">{result.error.status}: {result.error.data.error}</section>
|
<section className="error">{result.error.status}: {result.error.data.error}</section>
|
||||||
}
|
}
|
||||||
<input
|
<button type="submit" disabled={result.isLoading || disabled} {...inputProps}>
|
||||||
className="button"
|
<i className={`fa fa-fw ${iconClass}`} aria-hidden="true"></i>
|
||||||
type="submit"
|
{result.isLoading
|
||||||
disabled={result.isLoading}
|
? "Processing..."
|
||||||
value={buttonText}
|
: label
|
||||||
/>
|
}
|
||||||
{result.isSuccess && "Success!"}
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
|
@ -46,7 +46,7 @@ const nav = {
|
||||||
adminOnly: true,
|
adminOnly: true,
|
||||||
"Instance Settings": require("./admin/settings.js"),
|
"Instance Settings": require("./admin/settings.js"),
|
||||||
"Actions": require("./admin/actions"),
|
"Actions": require("./admin/actions"),
|
||||||
"Federation": require("./admin/federation.js"),
|
"Federation": require("./admin/federation"),
|
||||||
},
|
},
|
||||||
"Custom Emoji": {
|
"Custom Emoji": {
|
||||||
adminOnly: true,
|
adminOnly: true,
|
||||||
|
@ -172,7 +172,7 @@ function App() {
|
||||||
function Main() {
|
function Main() {
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<PersistGate loading={<section><Loading/></section>} persistor={persistor}>
|
<PersistGate loading={<section><Loading /></section>} persistor={persistor}>
|
||||||
<App />
|
<App />
|
||||||
</PersistGate>
|
</PersistGate>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|
|
@ -21,11 +21,11 @@
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
const prettierBytes = require("prettier-bytes");
|
const prettierBytes = require("prettier-bytes");
|
||||||
|
|
||||||
module.exports = function useFileInput({name, _Name}, {
|
module.exports = function useFileInput({ name, _Name }, {
|
||||||
withPreview,
|
withPreview,
|
||||||
maxSize,
|
maxSize,
|
||||||
initialInfo = "no file selected"
|
initialInfo = "no file selected"
|
||||||
}) {
|
} = {}) {
|
||||||
const [file, setFile] = React.useState();
|
const [file, setFile] = React.useState();
|
||||||
const [imageURL, setImageURL] = React.useState();
|
const [imageURL, setImageURL] = React.useState();
|
||||||
const [info, setInfo] = React.useState();
|
const [info, setInfo] = React.useState();
|
||||||
|
@ -40,7 +40,7 @@ module.exports = function useFileInput({name, _Name}, {
|
||||||
if (withPreview) {
|
if (withPreview) {
|
||||||
setImageURL(URL.createObjectURL(file));
|
setImageURL(URL.createObjectURL(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
let size = prettierBytes(file.size);
|
let size = prettierBytes(file.size);
|
||||||
if (maxSize && file.size > maxSize) {
|
if (maxSize && file.size > maxSize) {
|
||||||
size = <span className="error-text">{size}</span>;
|
size = <span className="error-text">{size}</span>;
|
||||||
|
|
|
@ -20,9 +20,8 @@
|
||||||
|
|
||||||
const syncpipe = require("syncpipe");
|
const syncpipe = require("syncpipe");
|
||||||
|
|
||||||
module.exports = function useFormSubmit(form, [mutationQuery, result]) {
|
module.exports = function useFormSubmit(form, [mutationQuery, result], { changedOnly = true } = {}) {
|
||||||
return [
|
return [
|
||||||
result,
|
|
||||||
function submitForm(e) {
|
function submitForm(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -31,7 +30,7 @@ module.exports = function useFormSubmit(form, [mutationQuery, result]) {
|
||||||
const mutationData = syncpipe(form, [
|
const mutationData = syncpipe(form, [
|
||||||
(_) => Object.values(_),
|
(_) => Object.values(_),
|
||||||
(_) => _.map((field) => {
|
(_) => _.map((field) => {
|
||||||
if (field.hasChanged()) {
|
if (!changedOnly || field.hasChanged()) {
|
||||||
updatedFields.push(field);
|
updatedFields.push(field);
|
||||||
return [field.name, field.value];
|
return [field.name, field.value];
|
||||||
} else {
|
} else {
|
||||||
|
@ -42,9 +41,8 @@ module.exports = function useFormSubmit(form, [mutationQuery, result]) {
|
||||||
(_) => Object.fromEntries(_)
|
(_) => Object.fromEntries(_)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (updatedFields.length > 0) {
|
return mutationQuery(mutationData);
|
||||||
return mutationQuery(mutationData);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
result
|
||||||
];
|
];
|
||||||
};
|
};
|
|
@ -18,7 +18,11 @@
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const { updateCacheOnMutation } = require("./lib");
|
const {
|
||||||
|
replaceCacheOnMutation,
|
||||||
|
appendCacheOnMutation,
|
||||||
|
spliceCacheOnMutation
|
||||||
|
} = require("./lib");
|
||||||
const base = require("./base");
|
const base = require("./base");
|
||||||
|
|
||||||
const endpoints = (build) => ({
|
const endpoints = (build) => ({
|
||||||
|
@ -27,19 +31,46 @@ const endpoints = (build) => ({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: `/api/v1/instance`,
|
url: `/api/v1/instance`,
|
||||||
asForm: true,
|
asForm: true,
|
||||||
body: formData
|
body: formData,
|
||||||
|
discardEmpty: true
|
||||||
}),
|
}),
|
||||||
...updateCacheOnMutation("instance")
|
...replaceCacheOnMutation("instance")
|
||||||
}),
|
}),
|
||||||
mediaCleanup: build.mutation({
|
mediaCleanup: build.mutation({
|
||||||
query: (days) => ({
|
query: (days) => ({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `/api/v1/admin/media_cleanup`,
|
url: `/api/v1/admin/media_cleanup`,
|
||||||
params: {
|
params: {
|
||||||
remote_cache_days: days
|
remote_cache_days: days
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
instanceBlocks: build.query({
|
||||||
|
query: () => ({
|
||||||
|
url: `/api/v1/admin/domain_blocks`
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
addInstanceBlock: build.mutation({
|
||||||
|
query: (formData) => ({
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/v1/admin/domain_blocks`,
|
||||||
|
asForm: true,
|
||||||
|
body: formData,
|
||||||
|
discardEmpty: true
|
||||||
|
}),
|
||||||
|
...appendCacheOnMutation("instanceBlocks")
|
||||||
|
}),
|
||||||
|
removeInstanceBlock: build.mutation({
|
||||||
|
query: (id) => ({
|
||||||
|
method: "DELETE",
|
||||||
|
url: `/api/v1/admin/domain_blocks/${id}`,
|
||||||
|
}),
|
||||||
|
...spliceCacheOnMutation("instanceBlocks", {
|
||||||
|
findKey: (draft, newData) => {
|
||||||
|
return draft.findIndex((block) => block.id == newData.id);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = base.injectEndpoints({endpoints});
|
module.exports = base.injectEndpoints({ endpoints });
|
|
@ -24,12 +24,19 @@ const { convertToForm } = require("../api");
|
||||||
|
|
||||||
function instanceBasedQuery(args, api, extraOptions) {
|
function instanceBasedQuery(args, api, extraOptions) {
|
||||||
const state = api.getState();
|
const state = api.getState();
|
||||||
const {instance, token} = state.oauth;
|
const { instance, token } = state.oauth;
|
||||||
|
|
||||||
if (args.baseUrl == undefined) {
|
if (args.baseUrl == undefined) {
|
||||||
args.baseUrl = instance;
|
args.baseUrl = instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.discardEmpty) {
|
||||||
|
if (args.body == undefined || Object.keys(args.body).length == 0) {
|
||||||
|
return { data: null };
|
||||||
|
}
|
||||||
|
delete args.discardEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.asForm) {
|
if (args.asForm) {
|
||||||
delete args.asForm;
|
delete args.asForm;
|
||||||
args.body = convertToForm(args.body);
|
args.body = convertToForm(args.body);
|
||||||
|
|
|
@ -28,16 +28,37 @@ module.exports = {
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateCacheOnMutation(queryName, arg = undefined) {
|
replaceCacheOnMutation: makeCacheMutation((draft, newData) => {
|
||||||
// https://redux-toolkit.js.org/rtk-query/usage/manual-cache-updates#pessimistic-updates
|
Object.assign(draft, newData);
|
||||||
|
}),
|
||||||
|
appendCacheOnMutation: makeCacheMutation((draft, newData) => {
|
||||||
|
draft.push(newData);
|
||||||
|
}),
|
||||||
|
spliceCacheOnMutation: makeCacheMutation((draft, newData, key) => {
|
||||||
|
draft.splice(key, 1);
|
||||||
|
}),
|
||||||
|
updateCacheOnMutation: makeCacheMutation((draft, newData, key) => {
|
||||||
|
draft[key] = newData;
|
||||||
|
}),
|
||||||
|
removeFromCacheOnMutation: makeCacheMutation((draft, newData, key) => {
|
||||||
|
delete draft[key];
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://redux-toolkit.js.org/rtk-query/usage/manual-cache-updates#pessimistic-updates
|
||||||
|
function makeCacheMutation(action) {
|
||||||
|
return function cacheMutation(queryName, { key, findKey, arg } = {}) {
|
||||||
return {
|
return {
|
||||||
onQueryStarted: (_, { dispatch, queryFulfilled}) => {
|
onQueryStarted: (_, { dispatch, queryFulfilled }) => {
|
||||||
queryFulfilled.then(({data: newData}) => {
|
queryFulfilled.then(({ data: newData }) => {
|
||||||
dispatch(base.util.updateQueryData(queryName, arg, (draft) => {
|
dispatch(base.util.updateQueryData(queryName, arg, (draft) => {
|
||||||
Object.assign(draft, newData);
|
if (findKey != undefined) {
|
||||||
|
key = findKey(draft, newData);
|
||||||
|
}
|
||||||
|
action(draft, newData, key);
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
};
|
}
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const { updateCacheOnMutation } = require("./lib");
|
const { replaceCacheOnMutation } = require("./lib");
|
||||||
const base = require("./base");
|
const base = require("./base");
|
||||||
|
|
||||||
const endpoints = (build) => ({
|
const endpoints = (build) => ({
|
||||||
|
@ -32,9 +32,10 @@ const endpoints = (build) => ({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: `/api/v1/accounts/update_credentials`,
|
url: `/api/v1/accounts/update_credentials`,
|
||||||
asForm: true,
|
asForm: true,
|
||||||
body: formData
|
body: formData,
|
||||||
|
discardEmpty: true
|
||||||
}),
|
}),
|
||||||
...updateCacheOnMutation("verifyCredentials")
|
...replaceCacheOnMutation("verifyCredentials")
|
||||||
}),
|
}),
|
||||||
passwordChange: build.mutation({
|
passwordChange: build.mutation({
|
||||||
query: (data) => ({
|
query: (data) => ({
|
||||||
|
@ -45,4 +46,4 @@ const endpoints = (build) => ({
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = base.injectEndpoints({endpoints});
|
module.exports = base.injectEndpoints({ endpoints });
|
|
@ -218,7 +218,7 @@ section.with-sidebar > div, section.with-sidebar > form {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
input, textarea {
|
input, textarea, button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
@ -228,14 +228,6 @@ section.with-sidebar > div, section.with-sidebar > form {
|
||||||
width: initial;
|
width: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:read-only {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:invalid {
|
|
||||||
border-color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -364,7 +356,6 @@ span.form-info {
|
||||||
.list {
|
.list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-top: 0.5rem;
|
|
||||||
max-height: 40rem;
|
max-height: 40rem;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
|
@ -372,10 +363,19 @@ span.form-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
background: $settings-entry-bg;
|
background: $settings-entry-bg;
|
||||||
|
border: 0.1rem solid transparent;
|
||||||
|
|
||||||
|
&:nth-child(even) {
|
||||||
|
background: $settings-entry-alternate-bg;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: $settings-entry-hover-bg;
|
background: $settings-entry-hover-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:active, &:focus, &:hover {
|
||||||
|
border-color: $fg-accent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -383,15 +383,10 @@ span.form-info {
|
||||||
.filter {
|
.filter {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
input {
|
|
||||||
width: auto;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
padding: 0.3rem;
|
padding: 0.5rem;
|
||||||
margin: 0.2rem 0;
|
margin: 0.2rem 0;
|
||||||
|
|
||||||
#domain {
|
#domain {
|
||||||
|
@ -652,4 +647,43 @@ span.form-info {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
color: $info-fg;
|
||||||
|
background: $info-bg;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: $br;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-top: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $info-link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button .fa-fw {
|
||||||
|
margin-left: -1.28571429em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fadeout {
|
||||||
|
animation-name: fadeout;
|
||||||
|
animation-duration: 0.5s;
|
||||||
|
animation-delay: 2s;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeout {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -51,7 +51,7 @@ module.exports = function UserProfile() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function UserProfileForm({data: profile}) {
|
function UserProfileForm({ data: profile }) {
|
||||||
/*
|
/*
|
||||||
User profile update form keys
|
User profile update form keys
|
||||||
- bool bot
|
- bool bot
|
||||||
|
@ -65,18 +65,18 @@ function UserProfileForm({data: profile}) {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const form = {
|
const form = {
|
||||||
avatar: useFileInput("avatar", {withPreview: true}),
|
avatar: useFileInput("avatar", { withPreview: true }),
|
||||||
header: useFileInput("header", {withPreview: true}),
|
header: useFileInput("header", { withPreview: true }),
|
||||||
displayName: useTextInput("display_name", {defaultValue: profile.display_name}),
|
displayName: useTextInput("display_name", { defaultValue: profile.display_name }),
|
||||||
note: useTextInput("note", {defaultValue: profile.source?.note}),
|
note: useTextInput("note", { defaultValue: profile.source?.note }),
|
||||||
customCSS: useTextInput("custom_css", {defaultValue: profile.custom_css}),
|
customCSS: useTextInput("custom_css", { defaultValue: profile.custom_css }),
|
||||||
bot: useBoolInput("bot", {defaultValue: profile.bot}),
|
bot: useBoolInput("bot", { defaultValue: profile.bot }),
|
||||||
locked: useBoolInput("locked", {defaultValue: profile.locked}),
|
locked: useBoolInput("locked", { defaultValue: profile.locked }),
|
||||||
enableRSS: useBoolInput("enable_rss", {defaultValue: profile.enable_rss}),
|
enableRSS: useBoolInput("enable_rss", { defaultValue: profile.enable_rss }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const allowCustomCSS = Redux.useSelector(state => state.instances.current.configuration.accounts.allow_custom_css);
|
const allowCustomCSS = Redux.useSelector(state => state.instances.current.configuration.accounts.allow_custom_css);
|
||||||
const [result, submitForm] = useFormSubmit(form, query.useUpdateCredentialsMutation());
|
const [submitForm, result] = useFormSubmit(form, query.useUpdateCredentialsMutation());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="user-profile" onSubmit={submitForm}>
|
<form className="user-profile" onSubmit={submitForm}>
|
||||||
|
@ -125,7 +125,7 @@ function UserProfileForm({data: profile}) {
|
||||||
field={form.enableRSS}
|
field={form.enableRSS}
|
||||||
label="Enable RSS feed of Public posts"
|
label="Enable RSS feed of Public posts"
|
||||||
/>
|
/>
|
||||||
{ !allowCustomCSS ? null :
|
{!allowCustomCSS ? null :
|
||||||
<TextArea
|
<TextArea
|
||||||
field={form.customCSS}
|
field={form.customCSS}
|
||||||
label="Custom CSS"
|
label="Custom CSS"
|
||||||
|
@ -135,7 +135,7 @@ function UserProfileForm({data: profile}) {
|
||||||
<a href="https://docs.gotosocial.org/en/latest/user_guide/custom_css" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about custom profile CSS (opens in a new tab)</a>
|
<a href="https://docs.gotosocial.org/en/latest/user_guide/custom_css" target="_blank" className="moreinfolink" rel="noreferrer">Learn more about custom profile CSS (opens in a new tab)</a>
|
||||||
</TextArea>
|
</TextArea>
|
||||||
}
|
}
|
||||||
<MutationButton text="Save profile info" result={result}/>
|
<MutationButton label="Save profile info" result={result} />
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -48,7 +48,7 @@ module.exports = function UserSettings() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function UserSettingsForm({data: {source}}) {
|
function UserSettingsForm({ data: { source } }) {
|
||||||
/* form keys
|
/* form keys
|
||||||
- string source[privacy]
|
- string source[privacy]
|
||||||
- bool source[sensitive]
|
- bool source[sensitive]
|
||||||
|
@ -57,20 +57,20 @@ function UserSettingsForm({data: {source}}) {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const form = {
|
const form = {
|
||||||
defaultPrivacy: useTextInput("source[privacy]", {defaultValue: source.privacy ?? "unlisted"}),
|
defaultPrivacy: useTextInput("source[privacy]", { defaultValue: source.privacy ?? "unlisted" }),
|
||||||
isSensitive: useBoolInput("source[sensitive]", {defaultValue: source.sensitive}),
|
isSensitive: useBoolInput("source[sensitive]", { defaultValue: source.sensitive }),
|
||||||
language: useTextInput("source[language]", {defaultValue: source.language ?? "EN"}),
|
language: useTextInput("source[language]", { defaultValue: source.language ?? "EN" }),
|
||||||
format: useTextInput("source[status_format]", {defaultValue: source.status_format ?? "plain"}),
|
format: useTextInput("source[status_format]", { defaultValue: source.status_format ?? "plain" }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const [result, submitForm] = useFormSubmit(form, query.useUpdateCredentialsMutation());
|
const [submitForm, result] = useFormSubmit(form, query.useUpdateCredentialsMutation());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form className="user-settings" onSubmit={submitForm}>
|
<form className="user-settings" onSubmit={submitForm}>
|
||||||
<h1>Post settings</h1>
|
<h1>Post settings</h1>
|
||||||
<Select field={form.language} label="Default post language" options={
|
<Select field={form.language} label="Default post language" options={
|
||||||
<Languages/>
|
<Languages />
|
||||||
}>
|
}>
|
||||||
</Select>
|
</Select>
|
||||||
<Select field={form.defaultPrivacy} label="Default post privacy" options={
|
<Select field={form.defaultPrivacy} label="Default post privacy" options={
|
||||||
|
@ -95,10 +95,10 @@ function UserSettingsForm({data: {source}}) {
|
||||||
label="Mark my posts as sensitive by default"
|
label="Mark my posts as sensitive by default"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MutationButton text="Save settings" result={result}/>
|
<MutationButton label="Save settings" result={result} />
|
||||||
</form>
|
</form>
|
||||||
<div>
|
<div>
|
||||||
<PasswordChange/>
|
<PasswordChange />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -107,12 +107,14 @@ function UserSettingsForm({data: {source}}) {
|
||||||
function PasswordChange() {
|
function PasswordChange() {
|
||||||
const form = {
|
const form = {
|
||||||
oldPassword: useTextInput("old_password"),
|
oldPassword: useTextInput("old_password"),
|
||||||
newPassword: useTextInput("old_password", {validator(val) {
|
newPassword: useTextInput("old_password", {
|
||||||
if (val != "" && val == form.oldPassword.value) {
|
validator(val) {
|
||||||
return "New password same as old password";
|
if (val != "" && val == form.oldPassword.value) {
|
||||||
|
return "New password same as old password";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
return "";
|
})
|
||||||
}})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const verifyNewPassword = useTextInput("verifyNewPassword", {
|
const verifyNewPassword = useTextInput("verifyNewPassword", {
|
||||||
|
@ -124,15 +126,15 @@ function PasswordChange() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const [result, submitForm] = useFormSubmit(form, query.usePasswordChangeMutation());
|
const [submitForm, result] = useFormSubmit(form, query.usePasswordChangeMutation());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="change-password" onSubmit={submitForm}>
|
<form className="change-password" onSubmit={submitForm}>
|
||||||
<h1>Change password</h1>
|
<h1>Change password</h1>
|
||||||
<TextInput type="password" field={form.oldPassword} label="Current password"/>
|
<TextInput type="password" field={form.oldPassword} label="Current password" />
|
||||||
<TextInput type="password" field={form.newPassword} label="New password"/>
|
<TextInput type="password" field={form.newPassword} label="New password" />
|
||||||
<TextInput type="password" field={verifyNewPassword} label="Confirm new password"/>
|
<TextInput type="password" field={verifyNewPassword} label="Confirm new password" />
|
||||||
<MutationButton text="Change password" result={result}/>
|
<MutationButton label="Change password" result={result} />
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
Loading…
Reference in New Issue