This {type == "statuses" ? "toot" : "account"} uses the following custom emoji, select the ones you want to copy/disable:
diff --git a/web/source/settings/admin/federation/detail.js b/web/source/settings/admin/federation/detail.js
index eb33960a7..6d695a67e 100644
--- a/web/source/settings/admin/federation/detail.js
+++ b/web/source/settings/admin/federation/detail.js
@@ -34,7 +34,7 @@ const BackButton = require("../../components/back-button");
const MutationButton = require("../../components/form/mutation-button");
module.exports = function InstanceDetail({ baseUrl }) {
- const { data: blockedInstances = [], isLoading } = query.useInstanceBlocksQuery();
+ const { data: blockedInstances = {}, isLoading } = query.useInstanceBlocksQuery();
let [_match, { domain }] = useRoute(`${baseUrl}/:domain`);
@@ -43,7 +43,7 @@ module.exports = function InstanceDetail({ baseUrl }) {
}
const existingBlock = React.useMemo(() => {
- return blockedInstances.find((block) => block.domain == domain);
+ return blockedInstances[domain];
}, [blockedInstances, domain]);
if (domain == undefined) {
diff --git a/web/source/settings/admin/federation/import-export.js b/web/source/settings/admin/federation/import-export.js
index 1d70ff86d..ccaa36086 100644
--- a/web/source/settings/admin/federation/import-export.js
+++ b/web/source/settings/admin/federation/import-export.js
@@ -19,14 +19,15 @@
"use strict";
const React = require("react");
+const { Switch, Route, Redirect, useLocation } = require("wouter");
const query = require("../../lib/query");
-const processDomainList = require("../../lib/import-export");
const {
useTextInput,
useBoolInput,
- useFileInput
+ useRadioInput,
+ useCheckListInput
} = require("../../lib/form");
const useFormSubmit = require("../../lib/form/submit");
@@ -35,93 +36,263 @@ const {
TextInput,
TextArea,
Checkbox,
- FileInput,
- useCheckListInput
+ Select,
+ RadioGroup
} = require("../../components/form/inputs");
-const FormWithData = require("../../lib/form/form-with-data");
const CheckList = require("../../components/check-list");
const MutationButton = require("../../components/form/mutation-button");
+const isValidDomain = require("is-valid-domain");
+const FormWithData = require("../../lib/form/form-with-data");
+const { Error } = require("../../components/error");
+
+const baseUrl = "/settings/admin/federation/import-export";
module.exports = function ImportExport() {
- const [parsedList, setParsedList] = React.useState();
-
+ const [updateFromFile, setUpdateFromFile] = React.useState(false);
const form = {
domains: useTextInput("domains"),
- obfuscate: useBoolInput("obfuscate"),
- commentPrivate: useTextInput("private_comment"),
- commentPublic: useTextInput("public_comment"),
- // json: useFileInput("json")
+ exportType: useTextInput("exportType", { defaultValue: "plain", dontReset: true })
};
- function submitImport(e) {
- e.preventDefault();
+ const [submitParse, parseResult] = useFormSubmit(form, query.useProcessDomainListMutation());
+ const [submitExport, exportResult] = useFormSubmit(form, query.useExportDomainListMutation());
- Promise.try(() => {
- return processDomainList(form.domains.value);
- }).then((processed) => {
- setParsedList(processed);
- }).catch((e) => {
- console.error(e);
- });
+ function fileChanged(e) {
+ const reader = new FileReader();
+ reader.onload = function (read) {
+ form.domains.setter(read.target.result);
+ setUpdateFromFile(true);
+ };
+ reader.readAsText(e.target.files[0]);
+ }
+
+ const [_location, setLocation] = useLocation();
+
+ if (updateFromFile) {
+ setUpdateFromFile(false);
+ submitParse();
}
return (
-
-
Import / Export
-
- {
- parsedList
- ?
- :
- }
-
-
+
+
+ {!parseResult.isSuccess && }
+
+
+ {
+ parseResult.reset();
+ setLocation(baseUrl);
+ }}>
+ < back
+ Confirm import:
+
+
+
+
+
+ {parseResult.isSuccess && }
+ Import / Export suspended domains
+
+
+
+
+
);
};
-function ImportExportList({ list }) {
- const entryCheckList = useCheckListInput("selectedDomains", {
- entries: list,
- uniqueKey: "domain"
- });
+function ImportList({ list, data: blockedInstances }) {
+ const hasComment = React.useMemo(() => {
+ let hasPublic = false;
+ let hasPrivate = false;
+
+ list.some((entry) => {
+ if (entry.public_comment?.length > 0) {
+ hasPublic = true;
+ }
+
+ if (entry.private_comment?.length > 0) {
+ hasPrivate = true;
+ }
+
+ return hasPublic && hasPrivate;
+ });
+
+ if (hasPublic && hasPrivate) {
+ return { both: true };
+ } else if (hasPublic) {
+ return { type: "public_comment" };
+ } else if (hasPrivate) {
+ return { type: "private_comment" };
+ } else {
+ return {};
+ }
+ }, [list]);
+
+ const showComment = useTextInput("showComment", { defaultValue: hasComment.type ?? "public_comment" });
+ let commentName = "";
+ if (showComment.value == "public_comment") { commentName = "Public comment"; }
+ if (showComment.value == "private_comment") { commentName = "Private comment"; }
+
+ const form = {
+ domains: useCheckListInput("domains", {
+ entries: list,
+ uniqueKey: "domain"
+ }),
+ obfuscate: useBoolInput("obfuscate"),
+ privateComment: useTextInput("private_comment", {
+ defaultValue: `Imported on ${new Date().toLocaleString()}`
+ }),
+ privateCommentBehavior: useRadioInput("private_comment_behavior", {
+ defaultValue: "append",
+ options: {
+ append: "Append to",
+ replace: "Replace"
+ }
+ }),
+ publicComment: useTextInput("public_comment"),
+ publicCommentBehavior: useRadioInput("public_comment_behavior", {
+ defaultValue: "append",
+ options: {
+ append: "Append to",
+ replace: "Replace"
+ }
+ }),
+ };
+
+ const [importDomains, importResult] = useFormSubmit(form, query.useImportDomainListMutation(), { changedOnly: false });
return (
-
+ <>
+
+ {list.length} domain{list.length != 1 ? "s" : ""} in this list
+
+ {hasComment.both &&
+
+ Show public comments
+ Show private comments
+ >
+ } />
+ }
+
+
+ Domain
+
+ {commentName}
+ >
+ }
+ blockedInstances={blockedInstances}
+ commentType={showComment.value}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ >
);
}
-function ImportExportForm({ form, submitImport }) {
+function DomainEntry({ entry, onChange, blockedInstances, commentType }) {
+ const domainField = useTextInput("domain", {
+ defaultValue: entry.domain,
+ validator: (value) => {
+ return (entry.checked && !isValidDomain(value, { wildcard: true, allowUnicode: true }))
+ ? "Invalid domain"
+ : "";
+ }
+ });
+
+ React.useEffect(() => {
+ onChange({ valid: domainField.valid });
+ /* eslint-disable-next-line react-hooks/exhaustive-deps */
+ }, [domainField.valid]);
+
+ let icon = null;
+
+ if (blockedInstances[domainField.value] != undefined) {
+ icon = (
+ <>
+
+
Domain block already exists.
+ >
+ );
+ }
+
return (
-
-
+ {
+ domainField.onChange(e);
+ onChange({ domain: e.target.value, checked: true });
+ }}
/>
-
-
-
-
-
-
-
-
- {/* default form action */}
-
-
+
{icon}
+
{entry[commentType]}
+ >
);
}
\ No newline at end of file
diff --git a/web/source/settings/admin/federation/index.js b/web/source/settings/admin/federation/index.js
index 2c4cf7aaa..beaa6e1c5 100644
--- a/web/source/settings/admin/federation/index.js
+++ b/web/source/settings/admin/federation/index.js
@@ -30,7 +30,7 @@ const InstanceImportExport = require("./import-export");
module.exports = function Federation({ }) {
return (
-
+
diff --git a/web/source/settings/admin/federation/overview.js b/web/source/settings/admin/federation/overview.js
index d7dcb158b..d89e954f0 100644
--- a/web/source/settings/admin/federation/overview.js
+++ b/web/source/settings/admin/federation/overview.js
@@ -29,7 +29,6 @@ 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();
@@ -39,11 +38,15 @@ module.exports = function InstanceOverview({ baseUrl }) {
const filterField = useTextInput("filter");
const filter = filterField.value;
- const filteredInstances = React.useMemo(() => {
- return matchSorter(Object.values(blockedInstances), filter, { keys: ["domain"] });
- }, [blockedInstances, filter]);
+ const blockedInstancesList = React.useMemo(() => {
+ return Object.values(blockedInstances);
+ }, [blockedInstances]);
- let filtered = blockedInstances.length - filteredInstances.length;
+ const filteredInstances = React.useMemo(() => {
+ return matchSorter(blockedInstancesList, filter, { keys: ["domain"] });
+ }, [blockedInstancesList, filter]);
+
+ let filtered = blockedInstancesList.length - filteredInstances.length;
function filterFormSubmit(e) {
e.preventDefault();
@@ -70,7 +73,7 @@ module.exports = function InstanceOverview({ baseUrl }) {
- {blockedInstances.length} blocked instance{blockedInstances.length != 1 ? "s" : ""} {filtered > 0 && `(${filtered} filtered)`}
+ {blockedInstancesList.length} blocked instance{blockedInstancesList.length != 1 ? "s" : ""} {filtered > 0 && `(${filtered} filtered by search)`}
{filteredInstances.map((entry) => {
diff --git a/web/source/settings/components/check-list.jsx b/web/source/settings/components/check-list.jsx
index 4281c2110..0f5267dfc 100644
--- a/web/source/settings/components/check-list.jsx
+++ b/web/source/settings/components/check-list.jsx
@@ -20,7 +20,7 @@
const React = require("react");
-module.exports = function CheckList({ field, Component, ...componentProps }) {
+module.exports = function CheckList({ field, Component, header = " All", ...componentProps }) {
return (
@@ -29,7 +29,7 @@ module.exports = function CheckList({ field, Component, ...componentProps }) {
type="checkbox"
onChange={field.toggleAll.onChange}
checked={field.toggleAll.value === 1}
- /> All
+ /> {header}
{Object.values(field.value).map((entry) => (
+ {Object.entries(field.options).map(([value, radioLabel]) => (
+
+
+ {radioLabel}
+
+ ))}
+ {label}
+
+ );
+}
+
module.exports = {
TextInput,
TextArea,
FileInput,
Checkbox,
- Select
+ Select,
+ RadioGroup
};
\ No newline at end of file
diff --git a/web/source/settings/lib/form/form-with-data.jsx b/web/source/settings/lib/form/form-with-data.jsx
index 090f20e26..4593a50e7 100644
--- a/web/source/settings/lib/form/form-with-data.jsx
+++ b/web/source/settings/lib/form/form-with-data.jsx
@@ -6,8 +6,8 @@ const Loading = require("../../components/loading");
// 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, arg }) {
- const { data, isLoading } = dataQuery(arg);
+module.exports = function FormWithData({ dataQuery, DataForm, queryArg, ...formProps }) {
+ const { data, isLoading } = dataQuery(queryArg);
if (isLoading) {
return (
@@ -16,6 +16,6 @@ module.exports = function FormWithData({ dataQuery, DataForm, arg }) {
);
} else {
- return
;
+ return
;
}
};
\ No newline at end of file
diff --git a/web/source/settings/lib/form/index.js b/web/source/settings/lib/form/index.js
index 6abedbc07..6f6e8fb76 100644
--- a/web/source/settings/lib/form/index.js
+++ b/web/source/settings/lib/form/index.js
@@ -33,6 +33,7 @@ module.exports = {
useTextInput: makeHook(require("./text")),
useFileInput: makeHook(require("./file")),
useBoolInput: makeHook(require("./bool")),
+ useRadioInput: makeHook(require("./radio")),
useComboBoxInput: makeHook(require("./combo-box")),
useCheckListInput: makeHook(require("./check-list")),
useValue: function (name, value) {
diff --git a/web/source/settings/lib/query/admin/federation-bulk.js b/web/source/settings/lib/form/radio.jsx
similarity index 56%
rename from web/source/settings/lib/query/admin/federation-bulk.js
rename to web/source/settings/lib/form/radio.jsx
index e791128ba..a00eba693 100644
--- a/web/source/settings/lib/query/admin/federation-bulk.js
+++ b/web/source/settings/lib/form/radio.jsx
@@ -18,7 +18,34 @@
"use strict";
-module.exports = (build) => ({
- // importInstanceBlocks: build.mutation({
- // })
-});
\ No newline at end of file
+const React = require("react");
+
+module.exports = function useRadioInput({ name, Name }, { defaultValue, options } = {}) {
+ const [value, setValue] = React.useState(defaultValue);
+
+ function onChange(e) {
+ setValue(e.target.value);
+ }
+
+ 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,
+ options,
+ hasChanged: () => value != defaultValue
+ });
+};
\ No newline at end of file
diff --git a/web/source/settings/lib/form/submit.js b/web/source/settings/lib/form/submit.js
index 900ae079e..738ed1c12 100644
--- a/web/source/settings/lib/form/submit.js
+++ b/web/source/settings/lib/form/submit.js
@@ -33,6 +33,10 @@ module.exports = function useFormSubmit(form, [mutationQuery, result], { changed
} else {
action = e;
}
+
+ if (action == "") {
+ action = undefined;
+ }
setUsedAction(action);
// transform the field definitions into an object with just their values
let updatedFields = [];
@@ -42,6 +46,7 @@ module.exports = function useFormSubmit(form, [mutationQuery, result], { changed
if (field.selectedValues != undefined) {
let selected = field.selectedValues();
if (!changedOnly || selected.length > 0) {
+ updatedFields.push(field);
return [field.name, selected];
}
} else if (!changedOnly || field.hasChanged()) {
diff --git a/web/source/settings/lib/form/text.jsx b/web/source/settings/lib/form/text.jsx
index 097d850e7..b9f434013 100644
--- a/web/source/settings/lib/form/text.jsx
+++ b/web/source/settings/lib/form/text.jsx
@@ -20,7 +20,7 @@
const React = require("react");
-module.exports = function useTextInput({name, Name}, {validator, defaultValue=""} = {}) {
+module.exports = function useTextInput({ name, Name }, { validator, defaultValue = "", dontReset = false } = {}) {
const [text, setText] = React.useState(defaultValue);
const [valid, setValid] = React.useState(true);
const textRef = React.useRef(null);
@@ -31,7 +31,9 @@ module.exports = function useTextInput({name, Name}, {validator, defaultValue=""
}
function reset() {
- setText("");
+ if (!dontReset) {
+ setText(defaultValue);
+ }
}
React.useEffect(() => {
diff --git a/web/source/settings/lib/get-views.js b/web/source/settings/lib/get-views.js
index 6e7dbf3b6..f0d5433fb 100644
--- a/web/source/settings/lib/get-views.js
+++ b/web/source/settings/lib/get-views.js
@@ -64,7 +64,7 @@ module.exports = function getViews(struct) {
}
panelRouterEl.push((
-
+
{ }}>
{/* FIXME: implement onReset */}
diff --git a/web/source/settings/lib/import-export.js b/web/source/settings/lib/import-export.js
deleted file mode 100644
index 22e322db9..000000000
--- a/web/source/settings/lib/import-export.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- 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 .
-*/
-
-"use strict";
-
-const Promise = require("bluebird");
-const isValidDomain = require("is-valid-domain");
-
-function parseDomainList(list) {
- if (list[0] == "[") {
- return JSON.parse(list);
- } else {
- return list.split("\n").map((line) => {
- let trimmed = line.trim();
- return trimmed.length > 0
- ? { domain: trimmed }
- : null;
- }).filter((a) => a); // not `null`
- }
-}
-
-function validateDomainList(list) {
- list.forEach((entry) => {
- entry.valid = isValidDomain(entry.domain, { wildcard: true, allowUnicode: true });
- });
-
- return list;
-}
-
-function deduplicateDomainList(list) {
- let domains = new Set();
- return list.filter((entry) => {
- if (domains.has(entry.domain)) {
- return false;
- } else {
- domains.add(entry.domain);
- return true;
- }
- });
-}
-
-module.exports = function processDomainList(data) {
- return Promise.try(() => {
- return parseDomainList(data);
- }).then((parsed) => {
- return deduplicateDomainList(parsed);
- }).then((deduped) => {
- return validateDomainList(deduped);
- });
-};
\ No newline at end of file
diff --git a/web/source/settings/lib/query/admin/import-export.js b/web/source/settings/lib/query/admin/import-export.js
new file mode 100644
index 000000000..20c691d9a
--- /dev/null
+++ b/web/source/settings/lib/query/admin/import-export.js
@@ -0,0 +1,212 @@
+/*
+ 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 .
+*/
+
+"use strict";
+
+const Promise = require("bluebird");
+const isValidDomain = require("is-valid-domain");
+const fileDownload = require("js-file-download");
+
+const {
+ replaceCacheOnMutation,
+ domainListToObject,
+ unwrapRes
+} = require("../lib");
+
+function parseDomainList(list) {
+ if (list[0] == "[") {
+ return JSON.parse(list);
+ } else {
+ return list.split("\n").map((line) => {
+ let domain = line.trim();
+ let valid = true;
+ if (domain.startsWith("http")) {
+ try {
+ domain = new URL(domain).hostname;
+ } catch (e) {
+ valid = false;
+ }
+ }
+ return domain.length > 0
+ ? { domain, valid }
+ : null;
+ }).filter((a) => a); // not `null`
+ }
+}
+
+function validateDomainList(list) {
+ list.forEach((entry) => {
+ entry.valid = (entry.valid !== false) && isValidDomain(entry.domain, { wildcard: true, allowUnicode: true });
+ entry.checked = entry.valid;
+ });
+
+ return list;
+}
+
+function deduplicateDomainList(list) {
+ let domains = new Set();
+ return list.filter((entry) => {
+ if (domains.has(entry.domain)) {
+ return false;
+ } else {
+ domains.add(entry.domain);
+ return true;
+ }
+ });
+}
+
+module.exports = (build) => ({
+ processDomainList: build.mutation({
+ queryFn: (formData) => {
+ return Promise.try(() => {
+ if (formData.domains == undefined || formData.domains.length == 0) {
+ throw "No domains entered";
+ }
+ return parseDomainList(formData.domains);
+ }).then((parsed) => {
+ return deduplicateDomainList(parsed);
+ }).then((deduped) => {
+ return validateDomainList(deduped);
+ }).then((data) => {
+ return { data };
+ }).catch((e) => {
+ return { error: e.toString() };
+ });
+ }
+ }),
+ exportDomainList: build.mutation({
+ queryFn: (formData, api, _extraOpts, baseQuery) => {
+ return Promise.try(() => {
+ return baseQuery({
+ url: `/api/v1/admin/domain_blocks`
+ });
+ }).then(unwrapRes).then((blockedInstances) => {
+ return blockedInstances.map((entry) => {
+ if (formData.exportType == "json") {
+ return {
+ domain: entry.domain,
+ public_comment: entry.public_comment
+ };
+ } else {
+ return entry.domain;
+ }
+ });
+ }).then((exportList) => {
+ if (formData.exportType == "json") {
+ return JSON.stringify(exportList);
+ } else {
+ return exportList.join("\n");
+ }
+ }).then((exportAsString) => {
+ if (formData.action == "export") {
+ return {
+ data: exportAsString
+ };
+ } else if (formData.action == "export-file") {
+ let domain = new URL(api.getState().oauth.instance).host;
+ let date = new Date();
+ let mime;
+
+ let filename = [
+ domain,
+ "blocklist",
+ date.getFullYear(),
+ (date.getMonth() + 1).toString().padStart(2, "0"),
+ date.getDate().toString().padStart(2, "0"),
+ ].join("-");
+
+ if (formData.exportType == "json") {
+ filename += ".json";
+ mime = "application/json";
+ } else {
+ filename += ".txt";
+ mime = "text/plain";
+ }
+
+ fileDownload(exportAsString, filename, mime);
+ }
+ return { data: null };
+ }).catch((e) => {
+ return { error: e };
+ });
+ }
+ }),
+ importDomainList: build.mutation({
+ query: (formData) => {
+ const { domains } = formData;
+
+ // add/replace comments, obfuscation data
+ let process = entryProcessor(formData);
+ domains.forEach((entry) => {
+ process(entry);
+ });
+
+ return {
+ method: "POST",
+ url: `/api/v1/admin/domain_blocks?import=true`,
+ asForm: true,
+ discardEmpty: true,
+ body: {
+ domains: new Blob([JSON.stringify(domains)], { type: "application/json" })
+ }
+ };
+ },
+ transformResponse: domainListToObject,
+ ...replaceCacheOnMutation("instanceBlocks")
+ })
+});
+
+function entryProcessor(formData) {
+ let funcs = [];
+
+ ["private_comment", "public_comment"].forEach((type) => {
+ let text = formData[type].trim();
+
+ if (text.length > 0) {
+ let behavior = formData[`${type}_behavior`];
+
+ if (behavior == "append") {
+ funcs.push(function appendComment(entry) {
+ if (entry[type] == undefined) {
+ entry[type] = text;
+ } else {
+ entry[type] = [entry[type], text].join("\n");
+ }
+ });
+ } else if (behavior == "replace") {
+ funcs.push(function replaceComment(entry) {
+ entry[type] = text;
+ });
+ }
+ }
+ });
+
+ return function process(entry) {
+ funcs.forEach((func) => {
+ func(entry);
+ });
+
+ entry.obfuscate = formData.obfuscate;
+
+ Object.entries(entry).forEach(([key, val]) => {
+ if (val == undefined) {
+ delete entry[key];
+ }
+ });
+ };
+}
\ No newline at end of file
diff --git a/web/source/settings/lib/query/admin/index.js b/web/source/settings/lib/query/admin/index.js
index 2e97fee43..545bb754f 100644
--- a/web/source/settings/lib/query/admin/index.js
+++ b/web/source/settings/lib/query/admin/index.js
@@ -21,7 +21,8 @@
const {
replaceCacheOnMutation,
appendCacheOnMutation,
- spliceCacheOnMutation
+ removeFromCacheOnMutation,
+ domainListToObject
} = require("../lib");
const base = require("../base");
@@ -48,7 +49,8 @@ const endpoints = (build) => ({
instanceBlocks: build.query({
query: () => ({
url: `/api/v1/admin/domain_blocks`
- })
+ }),
+ transformResponse: domainListToObject
}),
addInstanceBlock: build.mutation({
query: (formData) => ({
@@ -65,13 +67,13 @@ const endpoints = (build) => ({
method: "DELETE",
url: `/api/v1/admin/domain_blocks/${id}`,
}),
- ...spliceCacheOnMutation("instanceBlocks", {
- findKey: (draft, newData) => {
- return draft.findIndex((block) => block.id == newData.id);
+ ...removeFromCacheOnMutation("instanceBlocks", {
+ findKey: (_draft, newData) => {
+ return newData.domain;
}
})
}),
- ...require("./federation-bulk")(build)
+ ...require("./import-export")(build)
});
module.exports = base.injectEndpoints({ endpoints });
\ No newline at end of file
diff --git a/web/source/settings/lib/query/lib.js b/web/source/settings/lib/query/lib.js
index 631019bb2..ec353575a 100644
--- a/web/source/settings/lib/query/lib.js
+++ b/web/source/settings/lib/query/lib.js
@@ -18,6 +18,7 @@
"use strict";
+const syncpipe = require("syncpipe");
const base = require("./base");
module.exports = {
@@ -28,26 +29,36 @@ module.exports = {
return res.data;
}
},
+ domainListToObject: (data) => {
+ // Turn flat Array into Object keyed by block's domain
+ return syncpipe(data, [
+ (_) => _.map((entry) => [entry.domain, entry]),
+ (_) => Object.fromEntries(_)
+ ]);
+ },
replaceCacheOnMutation: makeCacheMutation((draft, newData) => {
Object.assign(draft, newData);
}),
appendCacheOnMutation: makeCacheMutation((draft, newData) => {
draft.push(newData);
}),
- spliceCacheOnMutation: makeCacheMutation((draft, newData, key) => {
+ spliceCacheOnMutation: makeCacheMutation((draft, newData, { key }) => {
draft.splice(key, 1);
}),
- updateCacheOnMutation: makeCacheMutation((draft, newData, key) => {
+ updateCacheOnMutation: makeCacheMutation((draft, newData, { key }) => {
draft[key] = newData;
}),
- removeFromCacheOnMutation: makeCacheMutation((draft, newData, key) => {
+ removeFromCacheOnMutation: makeCacheMutation((draft, newData, { key }) => {
delete draft[key];
+ }),
+ editCacheOnMutation: makeCacheMutation((draft, newData, { update }) => {
+ update(draft, newData);
})
};
// https://redux-toolkit.js.org/rtk-query/usage/manual-cache-updates#pessimistic-updates
function makeCacheMutation(action) {
- return function cacheMutation(queryName, { key, findKey, arg } = {}) {
+ return function cacheMutation(queryName, { key, findKey, arg, ...opts } = {}) {
return {
onQueryStarted: (_, { dispatch, queryFulfilled }) => {
queryFulfilled.then(({ data: newData }) => {
@@ -55,7 +66,7 @@ function makeCacheMutation(action) {
if (findKey != undefined) {
key = findKey(draft, newData);
}
- action(draft, newData, key);
+ action(draft, newData, { key, ...opts });
}));
});
}
diff --git a/web/source/settings/style.css b/web/source/settings/style.css
index 185c9c17b..01dac14ed 100644
--- a/web/source/settings/style.css
+++ b/web/source/settings/style.css
@@ -220,14 +220,15 @@ section.with-sidebar > div, section.with-sidebar > form {
flex-direction: column;
gap: 1rem;
- input, textarea, button {
+ input, textarea {
width: 100%;
line-height: 1.5rem;
}
- .button-inline {
+ button {
width: auto;
align-self: flex-start;
+ line-height: 1.5rem;
}
input[type=checkbox] {
@@ -702,6 +703,10 @@ button.with-icon {
}
}
+button.with-padding {
+ padding: 0.5rem calc(0.5rem + $fa-fw);
+}
+
.loading-icon {
align-self: flex-start;
}
@@ -713,6 +718,43 @@ button.with-icon {
animation-fill-mode: forwards;
}
+.suspend-import-list {
+ .checkbox-list {
+ .header, .entry {
+ display: grid;
+ grid-template-columns: auto 25ch auto 1fr;
+ }
+ }
+
+ .entry {
+ #icon {
+ margin-left: -0.5rem;
+ align-self: center;
+ }
+
+ #icon .already-blocked {
+ color: $green1;
+ }
+
+ p {
+ align-self: center;
+ margin: 0;
+ }
+ }
+}
+
+.form-field.radio {
+ &, label {
+ display: flex;
+ gap: 0.5rem;
+ }
+
+ input {
+ width: auto;
+ place-self: center;
+ }
+}
+
@keyframes fadeout {
from {
opacity: 1;