diff --git a/web/source/.eslintrc.js b/web/source/.eslintrc.js
index 7615c76d..09160993 100644
--- a/web/source/.eslintrc.js
+++ b/web/source/.eslintrc.js
@@ -1,5 +1,27 @@
+/*
+ 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 .
+*/
+
"use strict";
module.exports = {
- "extends": ["@joepie91/eslint-config/react"]
+ "extends": ["@joepie91/eslint-config/react"],
+ "plugins": ["license-header"],
+ "rules": {
+ "license-header/header": ["error", ".license-header.js"]
+ }
};
\ No newline at end of file
diff --git a/web/source/settings/redux/reducers/temporary.js b/web/source/.license-header.js
similarity index 75%
rename from web/source/settings/redux/reducers/temporary.js
rename to web/source/.license-header.js
index 0155f097..41b9ad67 100644
--- a/web/source/settings/redux/reducers/temporary.js
+++ b/web/source/.license-header.js
@@ -15,18 +15,3 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
-
-"use strict";
-
-const {createSlice} = require("@reduxjs/toolkit");
-
-module.exports = createSlice({
- name: "temporary",
- initialState: {
- },
- reducers: {
- setStatus: function(state, {payload}) {
- state.status = payload;
- }
- }
-});
\ No newline at end of file
diff --git a/web/source/css/_colors.css b/web/source/css/_colors.css
index ca17f579..13a75c9f 100644
--- a/web/source/css/_colors.css
+++ b/web/source/css/_colors.css
@@ -47,7 +47,13 @@ $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) */
$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) */
-$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) */
+
+$green1: #94E749; /* Green for positive/confirmation, similar contrast (luminance) as $blue2 */
+
+$info-fg: $gray1;
+$info-bg: #b3ddff;
+$info-link: $error-link;
$fg: $white1;
$bg: $gray1;
@@ -92,6 +98,7 @@ $avatar-border: $orange2;
$input-bg: $gray4;
$input-disabled-bg: $gray2;
$input-border: $blue1;
+$input-error-border: $error3;
$input-focus-border: $blue3;
$settings-nav-bg: $bg-accent;
@@ -107,5 +114,6 @@ $settings-nav-bg-active: $gray2;
$error-fg: $error1;
$error-bg: $error2;
-$settings-entry-bg: $gray3;
+$settings-entry-bg: $gray2;
+$settings-entry-alternate-bg: $gray3;
$settings-entry-hover-bg: $gray4;
\ No newline at end of file
diff --git a/web/source/css/base.css b/web/source/css/base.css
index 340052b5..e0350577 100644
--- a/web/source/css/base.css
+++ b/web/source/css/base.css
@@ -311,12 +311,16 @@ input, select, textarea, .input {
font-size: 1rem;
padding: 0.3rem;
- &:focus {
+ &:focus, &:active {
border-color: $input-focus-border;
}
+ &:invalid {
+ border-color: $input-error-border;
+ }
+
&:disabled {
- background: $input-disabled-bg;
+ background: transparent;
}
}
diff --git a/web/source/index.js b/web/source/index.js
index 7ad57104..e4b2086d 100644
--- a/web/source/index.js
+++ b/web/source/index.js
@@ -32,7 +32,7 @@ const prodCfg = {
global: true,
exts: ".js"
}],
- ["@browserify/envify", {global: true}]
+ ["@browserify/envify", { global: true }]
]
};
@@ -66,6 +66,7 @@ skulk({
],
},
settings: {
+ debug: false,
entryFile: "settings",
outputFile: "settings.js",
prodCfg: prodCfg,
diff --git a/web/source/package.json b/web/source/package.json
index 7685df23..f9520bab 100644
--- a/web/source/package.json
+++ b/web/source/package.json
@@ -6,7 +6,7 @@
"author": "f0x",
"license": "AGPL-3.0",
"scripts": {
- "lint": "eslint .",
+ "lint": "eslint . --ext .js,.jsx",
"build": "node index.js",
"dev": "NODE_ENV=development node index.js"
},
@@ -14,7 +14,6 @@
"@reduxjs/toolkit": "^1.8.6",
"ariakit": "^2.0.0-next.41",
"bluebird": "^3.7.2",
- "dotty": "^0.1.2",
"is-valid-domain": "^0.1.6",
"js-file-download": "^0.4.12",
"langs": "^2.0.0",
@@ -44,6 +43,7 @@
"babelify": "^10.0.0",
"css-extract": "^2.0.0",
"eslint": "^8.26.0",
+ "eslint-plugin-license-header": "^0.6.0",
"eslint-plugin-react": "^7.31.10",
"eslint-plugin-react-hooks": "^4.6.0",
"factor-bundle": "^2.5.0",
diff --git a/web/source/settings/admin/actions.js b/web/source/settings/admin/actions.js
index 66caa179..b91d81e1 100644
--- a/web/source/settings/admin/actions.js
+++ b/web/source/settings/admin/actions.js
@@ -19,42 +19,43 @@
"use strict";
const React = require("react");
-const Redux = require("react-redux");
-const Submit = require("../components/submit");
+const query = require("../lib/query");
-const api = require("../lib/api");
-const submit = require("../lib/submit");
+const { useTextInput } = require("../lib/form");
+const { TextInput } = require("../components/form/inputs");
+
+const MutationButton = require("../components/form/mutation-button");
module.exports = function AdminActionPanel() {
- const dispatch = Redux.useDispatch();
+ const daysField = useTextInput("days", { defaultValue: 30 });
- const [days, setDays] = React.useState(30);
+ const [mediaCleanup, mediaCleanupResult] = query.useMediaCleanupMutation();
- const [errorMsg, setError] = React.useState("");
- const [statusMsg, setStatus] = React.useState("");
-
- const removeMedia = submit(
- () => dispatch(api.admin.mediaCleanup(days)),
- {setStatus, setError}
- );
+ function submitMediaCleanup(e) {
+ e.preventDefault();
+ mediaCleanup(daysField.value);
+ }
return (
<>
Admin Actions
-
+
+
+
+
>
);
};
\ No newline at end of file
diff --git a/web/source/settings/admin/emoji/category-select.jsx b/web/source/settings/admin/emoji/category-select.jsx
index d22534ea..a35b3f2e 100644
--- a/web/source/settings/admin/emoji/category-select.jsx
+++ b/web/source/settings/admin/emoji/category-select.jsx
@@ -1,19 +1,19 @@
/*
- GoToSocial
- Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
+ 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 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.
+ 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 .
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
*/
"use strict";
@@ -36,13 +36,15 @@ function useEmojiByCategory(emoji) {
), [emoji]);
}
-function CategorySelect({value, categoryState, setIsNew=() => {}, children}) {
+function CategorySelect({ field, children }) {
+ const { value, setIsNew } = field;
+
const {
data: emoji = [],
isLoading,
isSuccess,
error
- } = query.useGetAllEmojiQuery({filter: "domain:local"});
+ } = query.useGetAllEmojiQuery({ filter: "domain:local" });
const emojiByCategory = useEmojiByCategory(emoji);
@@ -52,7 +54,7 @@ function CategorySelect({value, categoryState, setIsNew=() => {}, children}) {
const categoryItems = React.useMemo(() => {
return syncpipe(emojiByCategory, [
(_) => Object.keys(_), // just emoji category names
- (_) => matchSorter(_, value, {threshold: matchSorter.rankings.NO_MATCH}), // sorted by complex algorithm
+ (_) => matchSorter(_, value, { threshold: matchSorter.rankings.NO_MATCH }), // sorted by complex algorithm
(_) => _.map((categoryName) => [ // map to input value, and selectable element with icon
categoryName,
<>
@@ -67,24 +69,24 @@ function CategorySelect({value, categoryState, setIsNew=() => {}, children}) {
if (value != undefined && isSuccess && value.trim().length > 0) {
setIsNew(!categories.has(value.trim()));
}
- }, [categories, value, setIsNew, isSuccess]);
+ }, [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 (
<>
- {categoryState.value = e.target.value;}}/>;
+ { field.value = e.target.value; }} />;
>
);
} else if (isLoading) {
- return ;
+ return ;
}
return (
);
diff --git a/web/source/settings/admin/emoji/local/detail.js b/web/source/settings/admin/emoji/local/detail.js
index cc3ce6a7..cecd3686 100644
--- a/web/source/settings/admin/emoji/local/detail.js
+++ b/web/source/settings/admin/emoji/local/detail.js
@@ -19,155 +19,128 @@
"use strict";
const React = require("react");
-
const { useRoute, Link, Redirect } = require("wouter");
-const { CategorySelect } = require("../category-select");
-const { useComboBoxInput, useFileInput } = require("../../../components/form");
-
const query = require("../../../lib/query");
+
+const { useComboBoxInput, useFileInput, useValue } = require("../../../lib/form");
+const { CategorySelect } = require("../category-select");
+
+const useFormSubmit = require("../../../lib/form/submit");
+
const FakeToot = require("../../../components/fake-toot");
+const FormWithData = require("../../../lib/form/form-with-data");
const Loading = require("../../../components/loading");
+const { FileInput } = require("../../../components/form/inputs");
+const MutationButton = require("../../../components/form/mutation-button");
+const { Error } = require("../../../components/error");
const base = "/settings/custom-emoji/local";
module.exports = function EmojiDetailRoute() {
let [_match, params] = useRoute(`${base}/:emojiId`);
if (params?.emojiId == undefined) {
- return ;
+ return ;
} else {
return (
+
>
);
-}
-
-function DeleteButton({id}) {
- // TODO: confirmation dialog?
- const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation();
-
- let text = "Delete";
- if (deleteResult.isLoading) {
- text = "Deleting...";
- }
-
- if (deleteResult.isSuccess) {
- return ;
- }
-
- return (
-
- );
}
\ No newline at end of file
diff --git a/web/source/settings/admin/emoji/local/index.js b/web/source/settings/admin/emoji/local/index.js
index 4160fe41..68cbbc47 100644
--- a/web/source/settings/admin/emoji/local/index.js
+++ b/web/source/settings/admin/emoji/local/index.js
@@ -19,7 +19,7 @@
"use strict";
const React = require("react");
-const {Switch, Route} = require("wouter");
+const { Switch, Route } = require("wouter");
const EmojiOverview = require("./overview");
const EmojiDetail = require("./detail");
@@ -28,13 +28,11 @@ const base = "/settings/custom-emoji/local";
module.exports = function CustomEmoji() {
return (
- <>
-
-
-
-
-
-
- >
+
+
+
+
+
+
);
};
diff --git a/web/source/settings/admin/emoji/local/new-emoji.js b/web/source/settings/admin/emoji/local/new-emoji.js
index 6701dbf5..cf1b5f77 100644
--- a/web/source/settings/admin/emoji/local/new-emoji.js
+++ b/web/source/settings/admin/emoji/local/new-emoji.js
@@ -18,101 +18,61 @@
"use strict";
-const Promise = require('bluebird');
const React = require("react");
-const FakeToot = require("../../../components/fake-toot");
-const MutateButton = require("../../../components/mutation-button");
+const query = require("../../../lib/query");
const {
- useTextInput,
useFileInput,
useComboBoxInput
-} = require("../../../components/form");
+} = require("../../../lib/form");
+const useShortcode = require("./use-shortcode");
+
+const useFormSubmit = require("../../../lib/form/submit");
+
+const {
+ TextInput, FileInput
+} = require("../../../components/form/inputs");
-const query = require("../../../lib/query");
const { CategorySelect } = require('../category-select');
+const FakeToot = require("../../../components/fake-toot");
+const MutationButton = require("../../../components/form/mutation-button");
-const shortcodeRegex = /^[a-z0-9_]+$/;
+module.exports = function NewEmojiForm() {
+ const shortcode = useShortcode();
-module.exports = function NewEmojiForm({ emoji }) {
- const emojiCodes = React.useMemo(() => {
- return new Set(emoji.map((e) => e.shortcode));
- }, [emoji]);
-
- const [addEmoji, result] = query.useAddEmojiMutation();
-
- const [onFileChange, resetFile, { image, imageURL, imageInfo }] = useFileInput("image", {
+ const image = useFileInput("image", {
withPreview: true,
- maxSize: 50 * 1024
+ maxSize: 50 * 1024 // TODO: get from instance api?
});
- const [onShortcodeChange, resetShortcode, { shortcode, setShortcode, shortcodeRef }] = useTextInput("shortcode", {
- validator: function validateShortcode(code) {
- // technically invalid, but hacky fix to prevent validation error on page load
- if (shortcode == "") {return "";}
+ const category = useComboBoxInput("category");
- if (emojiCodes.has(code)) {
- return "Shortcode already in use";
- }
-
- if (code.length < 2 || code.length > 30) {
- return "Shortcode must be between 2 and 30 characters";
- }
-
- if (code.toLowerCase() != code) {
- return "Shortcode must be lowercase";
- }
-
- if (!shortcodeRegex.test(code)) {
- return "Shortcode must only contain lowercase letters, numbers, and underscores";
- }
-
- return "";
- }
- });
-
- const [categoryState, resetCategory, { category }] = useComboBoxInput("category");
+ const [submitForm, result] = useFormSubmit({
+ shortcode, image, category
+ }, query.useAddEmojiMutation());
React.useEffect(() => {
- if (shortcode.length == 0) {
- if (image != undefined) {
- let [name, _ext] = image.name.split(".");
- setShortcode(name);
+ if (shortcode.value.length == 0) {
+ if (image.value != undefined) {
+ let [name, _ext] = image.value.name.split(".");
+ shortcode.setter(name);
}
}
- // we explicitly don't want to add 'shortcode' as a dependency here
- // because we only want this to update to the filename if the field is empty
- // at the moment the file is selected, not some time after when the field is emptied
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [image]);
- function uploadEmoji(e) {
- if (e) {
- e.preventDefault();
- }
+ /* We explicitly don't want to have 'shortcode' as a dependency here
+ because we only want to change the shortcode to the filename if the field is empty
+ at the moment the file is selected, not some time after when the field is emptied
+ */
+ /* eslint-disable-next-line react-hooks/exhaustive-deps */
+ }, [image.value]);
- Promise.try(() => {
- return addEmoji({
- image,
- shortcode,
- category
- }).unwrap();
- }).then(() => {
- resetFile();
- resetShortcode();
- resetCategory();
- }).catch((e) => {
- console.error("Emoji upload error:", e);
- });
- }
+ let emojiOrShortcode = `:${shortcode.value}:`;
- let emojiOrShortcode = `:${shortcode}:`;
-
- if (imageURL != undefined) {
+ if (image.previewValue != undefined) {
emojiOrShortcode = ;
@@ -126,42 +86,22 @@ module.exports = function NewEmojiForm({ emoji }) {
Look at this new custom emoji {emojiOrShortcode} isn't it cool?
-
);
diff --git a/web/source/settings/admin/emoji/local/overview.js b/web/source/settings/admin/emoji/local/overview.js
index ebfb8969..524cc928 100644
--- a/web/source/settings/admin/emoji/local/overview.js
+++ b/web/source/settings/admin/emoji/local/overview.js
@@ -1,25 +1,25 @@
/*
- GoToSocial
- Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
+ 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 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.
+ 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 .
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
*/
"use strict";
const React = require("react");
-const {Link} = require("wouter");
+const { Link } = require("wouter");
const NewEmojiForm = require("./new-emoji");
@@ -27,33 +27,31 @@ const query = require("../../../lib/query");
const { useEmojiByCategory } = require("../category-select");
const Loading = require("../../../components/loading");
-const base = "/settings/custom-emoji/local";
-
-module.exports = function EmojiOverview() {
+module.exports = function EmojiOverview({ baseUrl }) {
const {
data: emoji = [],
isLoading,
error
- } = query.useGetAllEmojiQuery({filter: "domain:local"});
+ } = query.useGetAllEmojiQuery({ filter: "domain:local" });
return (
<>
{entries.map((e) => {
return (
-
- {/* */}
+
-
+
);
diff --git a/web/source/settings/admin/emoji/local/use-shortcode.js b/web/source/settings/admin/emoji/local/use-shortcode.js
new file mode 100644
index 00000000..109914b8
--- /dev/null
+++ b/web/source/settings/admin/emoji/local/use-shortcode.js
@@ -0,0 +1,61 @@
+/*
+ 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 .
+*/
+
+"use strict";
+
+const React = require("react");
+
+const query = require("../../../lib/query");
+const { useTextInput } = require("../../../lib/form");
+
+const shortcodeRegex = /^[a-z0-9_]+$/;
+
+module.exports = function useShortcode() {
+ const {
+ data: emoji = []
+ } = query.useGetAllEmojiQuery({ filter: "domain:local" });
+
+ const emojiCodes = React.useMemo(() => {
+ return new Set(emoji.map((e) => e.shortcode));
+ }, [emoji]);
+
+ return useTextInput("shortcode", {
+ validator: function validateShortcode(code) {
+ // technically invalid, but hacky fix to prevent validation error on page load
+ if (code == "") { return ""; }
+
+ if (emojiCodes.has(code)) {
+ return "Shortcode already in use";
+ }
+
+ if (code.length < 2 || code.length > 30) {
+ return "Shortcode must be between 2 and 30 characters";
+ }
+
+ if (code.toLowerCase() != code) {
+ return "Shortcode must be lowercase";
+ }
+
+ if (!shortcodeRegex.test(code)) {
+ return "Shortcode must only contain lowercase letters, numbers, and underscores";
+ }
+
+ return "";
+ }
+ });
+};
\ No newline at end of file
diff --git a/web/source/settings/admin/emoji/remote/index.js b/web/source/settings/admin/emoji/remote/index.js
index fb1e0508..f3bb325e 100644
--- a/web/source/settings/admin/emoji/remote/index.js
+++ b/web/source/settings/admin/emoji/remote/index.js
@@ -31,7 +31,7 @@ module.exports = function RemoteEmoji() {
data: emoji = [],
isLoading,
error
- } = query.useGetAllEmojiQuery({filter: "domain:local"});
+ } = query.useGetAllEmojiQuery({ filter: "domain:local" });
const emojiCodes = React.useMemo(() => {
return new Set(emoji.map((e) => e.shortcode));
@@ -40,11 +40,11 @@ module.exports = function RemoteEmoji() {
return (
<>
+ One or multiple emoji failed to process:
+ {errors.map(([shortcode, err]) => (
+
+ {shortcode}: {err}
+
+ ))}
+
+ );
+}
+
+function EmojiEntry({ entry: emoji, localEmojiCodes, onChange }) {
+ const shortcodeField = useTextInput("shortcode", {
defaultValue: emoji.shortcode,
validator: function validateShortcode(code) {
- return (checked && localEmojiCodes.has(code))
+ return (emoji.checked && localEmojiCodes.has(code))
? "Shortcode already in use"
: "";
}
});
React.useEffect(() => {
- updateEmoji({ valid: shortcodeValid });
+ onChange({ valid: shortcodeField.valid });
/* eslint-disable-next-line react-hooks/exhaustive-deps */
- }, [shortcodeValid]);
+ }, [shortcodeField.valid]);
return (
-
+ >
);
}
\ No newline at end of file
diff --git a/web/source/settings/admin/federation.js b/web/source/settings/admin/federation.js
deleted file mode 100644
index b7658f44..00000000
--- a/web/source/settings/admin/federation.js
+++ /dev/null
@@ -1,394 +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 .
-*/
-
-"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";
-
-// const {
-// TextInput,
-// TextArea,
-// File
-// } = require("../components/form-fields").formFields(adminActions.setAdminSettingsVal, (state) => state.instances.adminSettings);
-
-module.exports = function AdminSettings() {
- const dispatch = Redux.useDispatch();
- // const instance = Redux.useSelector(state => state.instances.adminSettings);
- const loadedBlockedInstances = Redux.useSelector(state => state.admin.loadedBlockedInstances);
-
- React.useEffect(() => {
- if (!loadedBlockedInstances ) {
- Promise.try(() => {
- return dispatch(api.admin.fetchDomainBlocks());
- });
- }
- }, [dispatch, loadedBlockedInstances]);
-
- if (!loadedBlockedInstances) {
- return (
-
- );
-}
-
-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 ;
- } else {
- domain = realDomain;
- }
- }
-
- function alterDomain([key, val]) {
- return adminActions.updateDomainBlockVal([domain, key, val]);
- }
-
- const fields = formFields(alterDomain, (state) => state.admin.newInstanceBlocks[domain]);
-
- return ;
-}
-
-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 ;
- }
-
- 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 (
-
-
Federation settings for: {domain}
- {entry.new
- ? "No stored block yet, you can add one below:"
- : Editing domain blocks is not implemented yet, check here for progress.
- }
-
-
-
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/web/source/settings/admin/federation/detail.js b/web/source/settings/admin/federation/detail.js
new file mode 100644
index 00000000..7324a42a
--- /dev/null
+++ b/web/source/settings/admin/federation/detail.js
@@ -0,0 +1,146 @@
+/*
+ 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 .
+*/
+
+"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[domain];
+ }, [blockedInstances, domain]);
+
+ if (domain == undefined) {
+ return ;
+ }
+
+ let infoContent = null;
+
+ if (isLoading) {
+ infoContent = ;
+ } else if (existingBlock == undefined) {
+ infoContent = No stored block yet, you can add one below:;
+ } else {
+ infoContent = (
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/web/source/settings/components/submit.jsx b/web/source/settings/admin/federation/index.js
similarity index 59%
rename from web/source/settings/components/submit.jsx
rename to web/source/settings/admin/federation/index.js
index 2a1b864e..beaa6e1c 100644
--- a/web/source/settings/components/submit.jsx
+++ b/web/source/settings/admin/federation/index.js
@@ -19,17 +19,26 @@
"use strict";
const React = require("react");
+const { Switch, Route } = require("wouter");
-module.exports = function Submit({onClick, label, errorMsg, statusMsg}) {
+const baseUrl = `/settings/admin/federation`;
+
+const InstanceOverview = require("./overview");
+const InstanceDetail = require("./detail");
+const InstanceImportExport = require("./import-export");
+
+module.exports = function Federation({ }) {
return (
-
- { label }
- {errorMsg.length > 0 &&
-
{errorMsg}
- }
- {statusMsg.length > 0 &&
-
{statusMsg}
- }
-
+
+
+
+
+
+
+
+
+
+
+
);
-};
+};
\ No newline at end of file
diff --git a/web/source/settings/admin/federation/overview.js b/web/source/settings/admin/federation/overview.js
new file mode 100644
index 00000000..b655423a
--- /dev/null
+++ b/web/source/settings/admin/federation/overview.js
@@ -0,0 +1,100 @@
+/*
+ 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 .
+*/
+
+"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");
+
+module.exports = function InstanceOverview({ baseUrl }) {
+ const { data: blockedInstances = [], isLoading } = query.useInstanceBlocksQuery();
+
+ const [_location, setLocation] = useLocation();
+
+ const filterField = useTextInput("filter");
+ const filter = filterField.value;
+
+ const blockedInstancesList = React.useMemo(() => {
+ return Object.values(blockedInstances);
+ }, [blockedInstances]);
+
+ const filteredInstances = React.useMemo(() => {
+ return matchSorter(blockedInstancesList, filter, { keys: ["domain"] });
+ }, [blockedInstancesList, filter]);
+
+ let filtered = blockedInstancesList.length - filteredInstances.length;
+
+ function filterFormSubmit(e) {
+ e.preventDefault();
+ setLocation(`${baseUrl}/${filter}`);
+ }
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+ <>
+
Federation
+
+
+
Suspended instances
+
+ 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.
+ This extends to all subdomains as well, so blocking 'example.com' also includes 'social.example.com'.
+
+
+
);
-};
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/web/source/settings/components/authorization/index.jsx b/web/source/settings/components/authorization/index.jsx
new file mode 100644
index 00000000..8bcf68e0
--- /dev/null
+++ b/web/source/settings/components/authorization/index.jsx
@@ -0,0 +1,76 @@
+/*
+ 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 .
+*/
+
+"use strict";
+
+const React = require("react");
+const Redux = require("react-redux");
+
+const query = require("../../lib/query");
+
+const Login = require("./login");
+const Loading = require("../loading");
+const { Error } = require("../error");
+
+module.exports = function Authorization({ App }) {
+ const loginState = Redux.useSelector((state) => state.oauth.loginState);
+ const [hasStoredLogin] = React.useState(loginState != "none" && loginState != "logout");
+
+ const { isLoading, isSuccess, data: account, error } = query.useVerifyCredentialsQuery(undefined, {
+ skip: loginState == "none" || loginState == "logout"
+ });
+
+ let showLogin = true;
+ let content = null;
+
+ if (isLoading && hasStoredLogin) {
+ showLogin = false;
+
+ let loadingInfo;
+ if (loginState == "callback") {
+ loadingInfo = "Processing OAUTH callback.";
+ } else if (loginState == "login") {
+ loadingInfo = "Verifying stored login.";
+ }
+
+ content = (
+
+ {content}
+ {showLogin && }
+
+ );
+ }
+};
\ No newline at end of file
diff --git a/web/source/settings/components/authorization/login.jsx b/web/source/settings/components/authorization/login.jsx
new file mode 100644
index 00000000..3115c5da
--- /dev/null
+++ b/web/source/settings/components/authorization/login.jsx
@@ -0,0 +1,67 @@
+/*
+ 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 .
+*/
+
+"use strict";
+
+const React = require("react");
+
+const query = require("../../lib/query");
+const { useTextInput, useValue } = require("../../lib/form");
+const useFormSubmit = require("../../lib/form/submit");
+const { TextInput } = require("../form/inputs");
+const MutationButton = require("../form/mutation-button");
+const Loading = require("../loading");
+
+module.exports = function Login({ }) {
+ const form = {
+ instance: useTextInput("instance", {
+ defaultValue: window.location.origin
+ }),
+ scopes: useValue("scopes", "user admin")
+ };
+
+ const [formSubmit, result] = useFormSubmit(
+ form,
+ query.useAuthorizeFlowMutation(),
+ { changedOnly: false }
+ );
+
+ if (result.isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/web/source/settings/components/back-button.jsx b/web/source/settings/components/back-button.jsx
index d95f82a7..9e849dee 100644
--- a/web/source/settings/components/back-button.jsx
+++ b/web/source/settings/components/back-button.jsx
@@ -21,7 +21,7 @@
const React = require("react");
const { Link } = require("wouter");
-module.exports = function BackButton({to}) {
+module.exports = function BackButton({ to }) {
return (
< back
diff --git a/web/source/settings/components/check-list.jsx b/web/source/settings/components/check-list.jsx
new file mode 100644
index 00000000..1276d5db
--- /dev/null
+++ b/web/source/settings/components/check-list.jsx
@@ -0,0 +1,58 @@
+/*
+ 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 .
+*/
+
+"use strict";
+
+const React = require("react");
+
+module.exports = function CheckList({ field, Component, header = " All", ...componentProps }) {
+ return (
+
@@ -28,7 +28,7 @@ module.exports = function ErrorFallback({error, resetErrorBoundary}) {
GoToSocial issue tracker
{" or "}
Matrix support room.
- Include the details below:
+ Include the details below:
-
+
{account.display_name.trim().length > 0 ? account.display_name : account.username}@{account.username}
diff --git a/web/source/settings/components/form-fields.jsx b/web/source/settings/components/form-fields.jsx
deleted file mode 100644
index 7b393b3e..00000000
--- a/web/source/settings/components/form-fields.jsx
+++ /dev/null
@@ -1,167 +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 .
-*/
-
-"use strict";
-
-const React = require("react");
-const Redux = require("react-redux");
-const d = require("dotty");
-const prettierBytes = require("prettier-bytes");
-
-function eventListeners(dispatch, setter, obj) {
- return {
- onTextChange: function (key) {
- return function (e) {
- dispatch(setter([key, e.target.value]));
- };
- },
-
- onCheckChange: function (key) {
- return function (e) {
- dispatch(setter([key, e.target.checked]));
- };
- },
-
- onFileChange: function (key, withPreview) {
- return function (e) {
- let file = e.target.files[0];
- if (withPreview) {
- let old = d.get(obj, key);
- if (old != undefined) {
- URL.revokeObjectURL(old); // no error revoking a non-Object URL as provided by instance
- }
- let objectURL = URL.createObjectURL(file);
- dispatch(setter([key, objectURL]));
- }
- dispatch(setter([`${key}File`, file]));
- };
- }
- };
-}
-
-function get(state, id, defaultVal) {
- let value;
- if (id.includes(".")) {
- value = d.get(state, id);
- } else {
- value = state[id];
- }
- if (value == undefined) {
- value = defaultVal;
- }
- return value;
-}
-
-// function removeFile(name) {
-// return function(e) {
-// e.preventDefault();
-// dispatch(user.setProfileVal([name, ""]));
-// dispatch(user.setProfileVal([`${name}File`, ""]));
-// };
-// }
-
-module.exports = {
- formFields: function formFields(setter, selector) {
- function FormField({
- type, id, name, className="", placeHolder="", fileType="", children=null,
- options=null, inputProps={}, withPreview=true, showSize=false, maxSize=Infinity
- }) {
- const dispatch = Redux.useDispatch();
- let state = Redux.useSelector(selector);
- let {
- onTextChange,
- onCheckChange,
- onFileChange
- } = eventListeners(dispatch, setter, state);
-
- let field;
- let defaultLabel = true;
- if (type == "text") {
- field = ;
- } else if (type == "textarea") {
- field = ;
- } else if (type == "checkbox") {
- field = ;
- } else if (type == "select") {
- field = (
-
- );
- } else if (type == "file") {
- defaultLabel = false;
- let file = get(state, `${id}File`);
-
- let size = null;
- if (showSize && file) {
- size = `(${prettierBytes(file.size)})`;
-
- if (file.size > maxSize) {
- size = {size};
- }
- }
-
- field = (
- <>
-
-
- {file ? file.name : "no file selected"} {size}
-
- {/* remove */}
-
- >
- );
- } else {
- defaultLabel = false;
- field = `unsupported FormField ${type}, this is a developer error`;
- }
-
- let label = ;
- return (
-
- );
- }
-
- return {
- TextInput: function(props) {
- return ;
- },
-
- TextArea: function(props) {
- return ;
- },
-
- Checkbox: function(props) {
- return ;
- },
-
- Select: function(props) {
- return ;
- },
-
- File: function(props) {
- return ;
- },
- };
- },
-
- eventListeners
-};
\ No newline at end of file
diff --git a/web/source/settings/components/form/index.js b/web/source/settings/components/form/index.js
deleted file mode 100644
index a9f333e0..00000000
--- a/web/source/settings/components/form/index.js
+++ /dev/null
@@ -1,37 +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 .
-*/
-
-"use strict";
-
-function capitalizeFirst(str) {
- return str.slice(0,1).toUpperCase()+str.slice(1);
-}
-
-function makeHook(func) {
- return (name, ...args) => func({
- name,
- Name: capitalizeFirst(name)
- },
- ...args);
-}
-
-module.exports = {
- useTextInput: makeHook(require("./text")),
- useFileInput: makeHook(require("./file")),
- useComboBoxInput: makeHook(require("./combobox"))
-};
\ No newline at end of file
diff --git a/web/source/settings/components/form/inputs.jsx b/web/source/settings/components/form/inputs.jsx
new file mode 100644
index 00000000..eef375ee
--- /dev/null
+++ b/web/source/settings/components/form/inputs.jsx
@@ -0,0 +1,141 @@
+/*
+ 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 .
+*/
+
+"use strict";
+
+const React = require("react");
+
+function TextInput({ label, field, ...inputProps }) {
+ const { onChange, value, ref } = field;
+
+ return (
+
+ );
+}
+
+module.exports = {
+ TextInput,
+ TextArea,
+ FileInput,
+ Checkbox,
+ Select,
+ RadioGroup
+};
\ No newline at end of file
diff --git a/web/source/settings/components/form/mutation-button.jsx b/web/source/settings/components/form/mutation-button.jsx
new file mode 100644
index 00000000..97bcdf08
--- /dev/null
+++ b/web/source/settings/components/form/mutation-button.jsx
@@ -0,0 +1,49 @@
+/*
+ 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 .
+*/
+
+"use strict";
+
+const React = require("react");
+const { Error } = require("../error");
+
+module.exports = function MutationButton({ label, result, disabled, showError = true, className = "", ...inputProps }) {
+ let iconClass = "";
+ const targetsThisButton = result.action == inputProps.name; // can also both be undefined, which is correct
+
+ if (targetsThisButton) {
+ if (result.isLoading) {
+ iconClass = "fa-spin fa-refresh";
+ } else if (result.isSuccess) {
+ iconClass = "fa-check fadeout";
+ }
+ }
+
+ return (
+ );
+};
\ No newline at end of file
diff --git a/web/source/settings/components/loading.jsx b/web/source/settings/components/loading.jsx
index f5648285..a278e646 100644
--- a/web/source/settings/components/loading.jsx
+++ b/web/source/settings/components/loading.jsx
@@ -22,6 +22,6 @@ const React = require("react");
module.exports = function Loading() {
return (
-
+
);
};
\ No newline at end of file
diff --git a/web/source/settings/components/login.jsx b/web/source/settings/components/login.jsx
deleted file mode 100644
index 4774423f..00000000
--- a/web/source/settings/components/login.jsx
+++ /dev/null
@@ -1,102 +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 .
-*/
-
-"use strict";
-
-const Promise = require("bluebird");
-const React = require("react");
-const Redux = require("react-redux");
-
-const { setInstance } = require("../redux/reducers/oauth").actions;
-const api = require("../lib/api");
-
-module.exports = function Login({error}) {
- const dispatch = Redux.useDispatch();
- const [ instanceField, setInstanceField ] = React.useState("");
- const [ errorMsg, setErrorMsg ] = React.useState();
- const instanceFieldRef = React.useRef("");
-
- React.useEffect(() => {
- // check if current domain runs an instance
- let currentDomain = window.location.origin;
- Promise.try(() => {
- return dispatch(api.instance.fetchWithoutStore(currentDomain));
- }).then(() => {
- if (instanceFieldRef.current.length == 0) { // user hasn't started typing yet
- dispatch(setInstance(currentDomain));
- instanceFieldRef.current = currentDomain;
- setInstanceField(currentDomain);
- }
- }).catch((e) => {
- console.log("Current domain does not host a valid instance: ", e);
- });
- }, []);
-
- function tryInstance() {
- let domain = instanceFieldRef.current;
- Promise.try(() => {
- return dispatch(api.instance.fetchWithoutStore(domain)).catch((e) => {
- // TODO: clearer error messages for common errors
- console.log(e);
- throw e;
- });
- }).then(() => {
- dispatch(setInstance(domain));
-
- return dispatch(api.oauth.register()).catch((e) => {
- console.log(e);
- throw e;
- });
- }).then(() => {
- return dispatch(api.oauth.authorize()); // will send user off-page
- }).catch((e) => {
- setErrorMsg(
- <>
- {e.type}
- {e.message}
- >
- );
- });
- }
-
- function updateInstanceField(e) {
- if (e.key == "Enter") {
- tryInstance(instanceField);
- } else {
- setInstanceField(e.target.value);
- instanceFieldRef.current = e.target.value;
- }
- }
-
- return (
-
-
- {ErrorElement}
- {LogoutElement}
-
- );
- }
-
}
function Main() {
return (
- } persistor={persistor}>
-
+ } persistor={persistor}>
+
);
diff --git a/web/source/settings/lib/api/admin.js b/web/source/settings/lib/api/admin.js
deleted file mode 100644
index 848772db..00000000
--- a/web/source/settings/lib/api/admin.js
+++ /dev/null
@@ -1,168 +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 .
-*/
-
-"use strict";
-
-const Promise = require("bluebird");
-const isValidDomain = require("is-valid-domain");
-
-const instance = require("../../redux/reducers/instances").actions;
-const admin = require("../../redux/reducers/admin").actions;
-
-module.exports = function ({ apiCall, getChanges }) {
- const adminAPI = {
- updateInstance: function updateInstance() {
- return function (dispatch, getState) {
- return Promise.try(() => {
- const state = getState().instances.adminSettings;
-
- const update = getChanges(state, {
- formKeys: ["title", "short_description", "description", "contact_account.username", "email", "terms", "thumbnail_description"],
- renamedKeys: {
- "email": "contact_email",
- "contact_account.username": "contact_username"
- },
- fileKeys: ["thumbnail"]
- });
-
- return dispatch(apiCall("PATCH", "/api/v1/instance", update, "form"));
- }).then((data) => {
- return dispatch(instance.setInstanceInfo(data));
- });
- };
- },
-
- fetchDomainBlocks: function fetchDomainBlocks() {
- return function (dispatch, _getState) {
- return Promise.try(() => {
- return dispatch(apiCall("GET", "/api/v1/admin/domain_blocks"));
- }).then((data) => {
- return dispatch(admin.setBlockedInstances(data));
- });
- };
- },
-
- updateDomainBlock: function updateDomainBlock(domain) {
- return function (dispatch, getState) {
- return Promise.try(() => {
- const state = getState().admin.newInstanceBlocks[domain];
- const update = getChanges(state, {
- formKeys: ["domain", "obfuscate", "public_comment", "private_comment"],
- });
-
- return dispatch(apiCall("POST", "/api/v1/admin/domain_blocks", update, "form"));
- }).then((block) => {
- return Promise.all([
- dispatch(admin.newDomainBlock([domain, block])),
- dispatch(admin.setDomainBlock([domain, block]))
- ]);
- });
- };
- },
-
- getEditableDomainBlock: function getEditableDomainBlock(domain) {
- return function (dispatch, getState) {
- let data = getState().admin.blockedInstances[domain];
- return dispatch(admin.newDomainBlock([domain, data]));
- };
- },
-
- bulkDomainBlock: function bulkDomainBlock() {
- return function (dispatch, getState) {
- let invalidDomains = [];
- let success = 0;
-
- return Promise.try(() => {
- const state = getState().admin.bulkBlock;
- let list = state.list;
- let domains;
-
- let fields = getChanges(state, {
- formKeys: ["obfuscate", "public_comment", "private_comment"]
- });
-
- let defaultDate = new Date().toUTCString();
-
- if (list[0] == "[") {
- domains = JSON.parse(state.list);
- } else {
- domains = list.split("\n").map((line_) => {
- let line = line_.trim();
- if (line.length == 0) {
- return null;
- }
-
- if (!isValidDomain(line, {wildcard: true, allowUnicode: true})) {
- invalidDomains.push(line);
- return null;
- }
-
- return {
- domain: line,
- created_at: defaultDate,
- ...fields
- };
- }).filter((a) => a != null);
- }
-
- if (domains.length == 0) {
- return;
- }
-
- const update = {
- domains: new Blob([JSON.stringify(domains)], {type: "application/json"})
- };
-
- return dispatch(apiCall("POST", "/api/v1/admin/domain_blocks?import=true", update, "form"));
- }).then((blocks) => {
- if (blocks != undefined) {
- return Promise.each(blocks, (block) => {
- success += 1;
- return dispatch(admin.setDomainBlock([block.domain, block]));
- });
- }
- }).then(() => {
- return {
- success,
- invalidDomains
- };
- });
- };
- },
-
- removeDomainBlock: function removeDomainBlock(domain) {
- return function (dispatch, getState) {
- return Promise.try(() => {
- const id = getState().admin.blockedInstances[domain].id;
- return dispatch(apiCall("DELETE", `/api/v1/admin/domain_blocks/${id}`));
- }).then((removed) => {
- return dispatch(admin.removeDomainBlock(removed.domain));
- });
- };
- },
-
- mediaCleanup: function mediaCleanup(days) {
- return function (dispatch, _getState) {
- return Promise.try(() => {
- return dispatch(apiCall("POST", `/api/v1/admin/media_cleanup?remote_cache_days=${days}`));
- });
- };
- },
- };
- return adminAPI;
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/api/index.js b/web/source/settings/lib/api/index.js
deleted file mode 100644
index 89f12cc8..00000000
--- a/web/source/settings/lib/api/index.js
+++ /dev/null
@@ -1,193 +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 .
-*/
-
-"use strict";
-
-const Promise = require("bluebird");
-const { isPlainObject } = require("is-plain-object");
-const d = require("dotty");
-
-const { APIError, AuthenticationError } = require("../errors");
-const { setInstanceInfo, setNamedInstanceInfo } = require("../../redux/reducers/instances").actions;
-
-function apiCall(method, route, payload, type = "json") {
- return function (dispatch, getState) {
- const state = getState();
- let base = state.oauth.instance;
- let auth = state.oauth.token;
-
- return Promise.try(() => {
- let url = new URL(base);
- let [path, query] = route.split("?");
- url.pathname = path;
- if (query != undefined) {
- url.search = query;
- }
- let body;
-
- let headers = {
- "Accept": "application/json",
- };
-
- if (payload != undefined) {
- if (type == "json") {
- headers["Content-Type"] = "application/json";
- body = JSON.stringify(payload);
- } else if (type == "form") {
- body = convertToForm(payload);
- }
- }
-
- if (auth != undefined) {
- headers["Authorization"] = auth;
- }
-
- return fetch(url.toString(), {
- method,
- headers,
- body
- });
- }).then((res) => {
- // try parse json even with error
- let json = res.json().catch((e) => {
- throw new APIError(`JSON parsing error: ${e.message}`);
- });
-
- return Promise.all([res, json]);
- }).then(([res, json]) => {
- if (!res.ok) {
- if (auth != undefined && (res.status == 401 || res.status == 403)) {
- // stored access token is invalid
- throw new AuthenticationError("401: Authentication error", {json, status: res.status});
- } else {
- throw new APIError(json.error, { json });
- }
- } else {
- return json;
- }
- });
- };
-}
-
-/*
- Takes an object with (nested) keys, and transforms it into
- a FormData object to be sent over the API
-*/
-function convertToForm(payload) {
- const formData = new FormData();
- Object.entries(payload).forEach(([key, val]) => {
- if (isPlainObject(val)) {
- Object.entries(val).forEach(([key2, val2]) => {
- if (val2 != undefined) {
- formData.set(`${key}[${key2}]`, val2);
- }
- });
- } else {
- if (val != undefined) {
- formData.set(key, val);
- }
- }
- });
- return formData;
-}
-
-function getChanges(state, keys) {
- const { formKeys = [], fileKeys = [], renamedKeys = {} } = keys;
- const update = {};
-
- formKeys.forEach((key) => {
- let value = d.get(state, key);
- if (value == undefined) {
- return;
- }
- if (renamedKeys[key]) {
- key = renamedKeys[key];
- }
- d.put(update, key, value);
- });
-
- fileKeys.forEach((key) => {
- let file = d.get(state, `${key}File`);
- if (file != undefined) {
- if (renamedKeys[key]) {
- key = renamedKeys[key];
- }
- d.put(update, key, file);
- }
- });
-
- return update;
-}
-
-function getCurrentUrl() {
- let [pre, _past] = window.location.pathname.split("/settings");
- return `${window.location.origin}${pre}/settings`;
-}
-
-function fetchInstanceWithoutStore(domain) {
- return function (dispatch, getState) {
- return Promise.try(() => {
- let lookup = getState().instances.info[domain];
- if (lookup != undefined) {
- return lookup;
- }
-
- // apiCall expects to pull the domain from state,
- // but we don't want to store it there yet
- // so we mock the API here with our function argument
- let fakeState = {
- oauth: { instance: domain }
- };
-
- return apiCall("GET", "/api/v1/instance")(dispatch, () => fakeState);
- }).then((json) => {
- if (json && json.uri) { // TODO: validate instance json more?
- dispatch(setNamedInstanceInfo([domain, json]));
- return json;
- }
- });
- };
-}
-
-function fetchInstance() {
- return function (dispatch, _getState) {
- return Promise.try(() => {
- return dispatch(apiCall("GET", "/api/v1/instance"));
- }).then((json) => {
- if (json && json.uri) {
- dispatch(setInstanceInfo(json));
- return json;
- }
- });
- };
-}
-
-let submoduleArgs = { apiCall, getCurrentUrl, getChanges };
-
-module.exports = {
- instance: {
- fetchWithoutStore: fetchInstanceWithoutStore,
- fetch: fetchInstance
- },
- oauth: require("./oauth")(submoduleArgs),
- user: require("./user")(submoduleArgs),
- admin: require("./admin")(submoduleArgs),
- apiCall,
- convertToForm,
- getChanges
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/api/oauth.js b/web/source/settings/lib/api/oauth.js
deleted file mode 100644
index 68095cac..00000000
--- a/web/source/settings/lib/api/oauth.js
+++ /dev/null
@@ -1,127 +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 .
-*/
-
-"use strict";
-
-const Promise = require("bluebird");
-
-const { OAUTHError, AuthenticationError } = require("../errors");
-
-const oauth = require("../../redux/reducers/oauth").actions;
-const temporary = require("../../redux/reducers/temporary").actions;
-const admin = require("../../redux/reducers/admin").actions;
-
-module.exports = function oauthAPI({ apiCall, getCurrentUrl }) {
- return {
-
- register: function register(scopes = []) {
- return function (dispatch, _getState) {
- return Promise.try(() => {
- return dispatch(apiCall("POST", "/api/v1/apps", {
- client_name: "GoToSocial Settings",
- scopes: scopes.join(" "),
- redirect_uris: getCurrentUrl(),
- website: getCurrentUrl()
- }));
- }).then((json) => {
- json.scopes = scopes;
- dispatch(oauth.setRegistration(json));
- });
- };
- },
-
- authorize: function authorize() {
- return function (dispatch, getState) {
- let state = getState();
- let reg = state.oauth.registration;
- let base = new URL(state.oauth.instance);
-
- base.pathname = "/oauth/authorize";
- base.searchParams.set("client_id", reg.client_id);
- base.searchParams.set("redirect_uri", getCurrentUrl());
- base.searchParams.set("response_type", "code");
- base.searchParams.set("scope", reg.scopes.join(" "));
-
- dispatch(oauth.setLoginState("callback"));
- dispatch(temporary.setStatus("Redirecting to instance login..."));
-
- // send user to instance's login flow
- window.location.assign(base.href);
- };
- },
-
- tokenize: function tokenize(code) {
- return function (dispatch, getState) {
- let reg = getState().oauth.registration;
-
- return Promise.try(() => {
- if (reg == undefined || reg.client_id == undefined) {
- throw new OAUTHError("Callback code present, but no client registration is available from localStorage. \nNote: localStorage is unavailable in Private Browsing.");
- }
-
- return dispatch(apiCall("POST", "/oauth/token", {
- client_id: reg.client_id,
- client_secret: reg.client_secret,
- redirect_uri: getCurrentUrl(),
- grant_type: "authorization_code",
- code: code
- }));
- }).then((json) => {
- window.history.replaceState({}, document.title, window.location.pathname);
- return dispatch(oauth.login(json));
- });
- };
- },
-
- checkIfAdmin: function checkIfAdmin() {
- return function (dispatch, getState) {
- const state = getState();
- let stored = state.oauth.isAdmin;
- if (stored != undefined) {
- return stored;
- }
-
- // newer GoToSocial version will include a `role` in the Account data, check that first
- if (state.user.profile.role == "admin") {
- dispatch(oauth.setAdmin(true));
- return true;
- }
-
- // no role info, try fetching an admin-only route and see if we get an error
- return Promise.try(() => {
- return dispatch(apiCall("GET", "/api/v1/admin/domain_blocks"));
- }).then((data) => {
- return Promise.all([
- dispatch(oauth.setAdmin(true)),
- dispatch(admin.setBlockedInstances(data))
- ]);
- }).catch(AuthenticationError, () => {
- return dispatch(oauth.setAdmin(false));
- });
- };
- },
-
- logout: function logout() {
- return function (dispatch, _getState) {
- // TODO: GoToSocial does not have a logout API route yet
-
- return dispatch(oauth.remove());
- };
- }
- };
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/api/user.js b/web/source/settings/lib/api/user.js
deleted file mode 100644
index 41031d48..00000000
--- a/web/source/settings/lib/api/user.js
+++ /dev/null
@@ -1,67 +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 .
-*/
-
-"use strict";
-
-const Promise = require("bluebird");
-
-const user = require("../../redux/reducers/user").actions;
-
-module.exports = function ({ apiCall, getChanges }) {
- function updateCredentials(selector, keys) {
- return function (dispatch, getState) {
- return Promise.try(() => {
- const state = selector(getState());
-
- const update = getChanges(state, keys);
-
- return dispatch(apiCall("PATCH", "/api/v1/accounts/update_credentials", update, "form"));
- }).then((account) => {
- return dispatch(user.setAccount(account));
- });
- };
- }
-
- return {
- fetchAccount: function fetchAccount() {
- return function (dispatch, _getState) {
- return Promise.try(() => {
- return dispatch(apiCall("GET", "/api/v1/accounts/verify_credentials"));
- }).then((account) => {
- return dispatch(user.setAccount(account));
- });
- };
- },
-
- updateProfile: function updateProfile() {
- const formKeys = ["display_name", "locked", "source", "custom_css", "source.note", "enable_rss"];
- const renamedKeys = {
- "source.note": "note"
- };
- const fileKeys = ["header", "avatar"];
-
- return updateCredentials((state) => state.user.profile, {formKeys, renamedKeys, fileKeys});
- },
-
- updateSettings: function updateProfile() {
- const formKeys = ["source"];
-
- return updateCredentials((state) => state.user.settings, {formKeys});
- }
- };
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/errors.js b/web/source/settings/lib/errors.js
deleted file mode 100644
index 85302f18..00000000
--- a/web/source/settings/lib/errors.js
+++ /dev/null
@@ -1,27 +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 .
-*/
-
-"use strict";
-
-const createError = require("create-error");
-
-module.exports = {
- APIError: createError("APIError"),
- OAUTHError: createError("OAUTHError"),
- AuthenticationError: createError("AuthenticationError"),
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/form/bool.jsx b/web/source/settings/lib/form/bool.jsx
new file mode 100644
index 00000000..b124abd5
--- /dev/null
+++ b/web/source/settings/lib/form/bool.jsx
@@ -0,0 +1,50 @@
+/*
+ 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 .
+*/
+
+"use strict";
+
+const React = require("react");
+
+module.exports = function useBoolInput({ name, Name }, { defaultValue = false } = {}) {
+ const [value, setValue] = React.useState(defaultValue);
+
+ function onChange(e) {
+ setValue(e.target.checked);
+ }
+
+ function reset() {
+ setValue(defaultValue);
+ }
+
+ // Array / Object hybrid, for easier access in different contexts
+ return Object.assign([
+ onChange,
+ reset,
+ {
+ [name]: value,
+ [`set${Name}`]: setValue
+ }
+ ], {
+ name,
+ onChange,
+ reset,
+ value,
+ setter: setValue,
+ hasChanged: () => value != defaultValue
+ });
+};
\ No newline at end of file
diff --git a/web/source/settings/lib/form/check-list.jsx b/web/source/settings/lib/form/check-list.jsx
new file mode 100644
index 00000000..c1233273
--- /dev/null
+++ b/web/source/settings/lib/form/check-list.jsx
@@ -0,0 +1,147 @@
+/*
+ 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 .
+*/
+
+"use strict";
+
+const React = require("react");
+const syncpipe = require("syncpipe");
+
+function createState(entries, uniqueKey, oldState, defaultValue) {
+ return syncpipe(entries, [
+ (_) => _.map((entry) => {
+ let key = entry[uniqueKey];
+ return [
+ key,
+ {
+ ...entry,
+ key,
+ checked: oldState[key]?.checked ?? entry.checked ?? defaultValue
+ }
+ ];
+ }),
+ (_) => Object.fromEntries(_)
+ ]);
+}
+
+function updateAllState(state, newValue) {
+ return syncpipe(state, [
+ (_) => Object.values(_),
+ (_) => _.map((entry) => [entry.key, {
+ ...entry,
+ checked: newValue
+ }]),
+ (_) => Object.fromEntries(_)
+ ]);
+}
+
+function updateState(state, key, newValue) {
+ return {
+ ...state,
+ [key]: {
+ ...state[key],
+ ...newValue
+ }
+ };
+}
+
+module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "key", defaultValue = false }) {
+ const [state, setState] = React.useState({});
+
+ const [someSelected, setSomeSelected] = React.useState(false);
+ const [toggleAllState, setToggleAllState] = React.useState(0);
+ const toggleAllRef = React.useRef(null);
+
+ React.useEffect(() => {
+ /*
+ entries changed, update state,
+ re-using old state if available for key
+ */
+ setState(createState(entries, uniqueKey, state, defaultValue));
+
+ /* eslint-disable-next-line react-hooks/exhaustive-deps */
+ }, [entries]);
+
+ React.useEffect(() => {
+ /* Updates (un)check all checkbox, based on shortcode checkboxes
+ Can be 0 (not checked), 1 (checked) or 2 (indeterminate)
+ */
+ if (toggleAllRef.current == null) {
+ return;
+ }
+
+ let values = Object.values(state);
+ /* one or more boxes are checked */
+ let some = values.some((v) => v.checked);
+
+ let all = false;
+ if (some) {
+ /* there's not at least one unchecked box */
+ all = !values.some((v) => v.checked == false);
+ }
+
+ setSomeSelected(some);
+
+ if (some && !all) {
+ setToggleAllState(2);
+ toggleAllRef.current.indeterminate = true;
+ } else {
+ setToggleAllState(all ? 1 : 0);
+ toggleAllRef.current.indeterminate = false;
+ }
+ }, [state, toggleAllRef]);
+
+ function toggleAll(e) {
+ let selectAll = e.target.checked;
+
+ if (toggleAllState == 2) { // indeterminate
+ selectAll = false;
+ }
+
+ setState(updateAllState(state, selectAll));
+ setToggleAllState(selectAll);
+ }
+
+ function reset() {
+ setState(updateAllState(state, defaultValue));
+ }
+
+ function selectedValues() {
+ return syncpipe(state, [
+ (_) => Object.values(_),
+ (_) => _.filter((entry) => entry.checked)
+ ]);
+ }
+
+ return Object.assign([
+ state,
+ reset,
+ { name }
+ ], {
+ name,
+ value: state,
+ onChange: (key, newValue) => setState(updateState(state, key, newValue)),
+ selectedValues,
+ reset,
+ someSelected,
+ toggleAll: {
+ ref: toggleAllRef,
+ value: toggleAllState,
+ onChange: toggleAll
+ }
+ });
+};
\ No newline at end of file
diff --git a/web/source/settings/components/form/combobox.jsx b/web/source/settings/lib/form/combo-box.jsx
similarity index 71%
rename from web/source/settings/components/form/combobox.jsx
rename to web/source/settings/lib/form/combo-box.jsx
index aeee38fc..3e8cea44 100644
--- a/web/source/settings/components/form/combobox.jsx
+++ b/web/source/settings/lib/form/combo-box.jsx
@@ -18,9 +18,13 @@
"use strict";
+const React = require("react");
+
const { useComboboxState } = require("ariakit/combobox");
-module.exports = function useComboBoxInput({name, Name}, {validator, defaultValue} = {}) {
+module.exports = function useComboBoxInput({ name, Name }, { defaultValue } = {}) {
+ const [isNew, setIsNew] = React.useState(false);
+
const state = useComboboxState({
defaultValue,
gutter: 0,
@@ -31,11 +35,22 @@ module.exports = function useComboBoxInput({name, Name}, {validator, defaultValu
state.setValue("");
}
- return [
+ return Object.assign([
state,
reset,
{
[name]: state.value,
+ name,
+ [`${name}IsNew`]: isNew,
+ [`set${Name}IsNew`]: setIsNew
}
- ];
+ ], {
+ name,
+ state,
+ value: state.value,
+ hasChanged: () => state.value != defaultValue,
+ isNew,
+ setIsNew,
+ reset
+ });
};
\ No newline at end of file
diff --git a/web/source/settings/components/form/file.jsx b/web/source/settings/lib/form/file.jsx
similarity index 78%
rename from web/source/settings/components/form/file.jsx
rename to web/source/settings/lib/form/file.jsx
index 4dd0e516..85f23e27 100644
--- a/web/source/settings/components/form/file.jsx
+++ b/web/source/settings/lib/form/file.jsx
@@ -21,11 +21,11 @@
const React = require("react");
const prettierBytes = require("prettier-bytes");
-module.exports = function useFileInput({name, _Name}, {
+module.exports = function useFileInput({ name, _Name }, {
withPreview,
maxSize,
initialInfo = "no file selected"
-}) {
+} = {}) {
const [file, setFile] = React.useState();
const [imageURL, setImageURL] = React.useState();
const [info, setInfo] = React.useState();
@@ -40,7 +40,7 @@ module.exports = function useFileInput({name, _Name}, {
if (withPreview) {
setImageURL(URL.createObjectURL(file));
}
-
+
let size = prettierBytes(file.size);
if (maxSize && file.size > maxSize) {
size = {size};
@@ -61,18 +61,31 @@ module.exports = function useFileInput({name, _Name}, {
setInfo();
}
- return [
+ const infoComponent = (
+
+ {info
+ ? info
+ : initialInfo
+ }
+
+ );
+
+ // Array / Object hybrid, for easier access in different contexts
+ return Object.assign([
onChange,
reset,
{
[name]: file,
[`${name}URL`]: imageURL,
- [`${name}Info`]:
- {info
- ? info
- : initialInfo
- }
-
+ [`${name}Info`]: infoComponent,
}
- ];
+ ], {
+ onChange,
+ reset,
+ name,
+ value: file,
+ previewValue: imageURL,
+ hasChanged: () => file != undefined,
+ infoComponent
+ });
};
\ No newline at end of file
diff --git a/web/source/settings/components/mutation-button.jsx b/web/source/settings/lib/form/form-with-data.jsx
similarity index 61%
rename from web/source/settings/components/mutation-button.jsx
rename to web/source/settings/lib/form/form-with-data.jsx
index 2d0f2a2b..a383af50 100644
--- a/web/source/settings/components/mutation-button.jsx
+++ b/web/source/settings/lib/form/form-with-data.jsx
@@ -20,23 +20,20 @@
const React = require("react");
-module.exports = function MutateButton({text, result}) {
- let buttonText = text;
+const Loading = require("../../components/loading");
- if (result.isLoading) {
- buttonText = "Processing...";
+// Wrap Form component inside component that fires the RTK Query call,
+// so Form will only be rendered when data is available to generate form-fields for
+module.exports = function FormWithData({ dataQuery, DataForm, queryArg, ...formProps }) {
+ const { data, isLoading } = dataQuery(queryArg);
+
+ if (isLoading) {
+ return (
+